grafana/public/app/features/dashboard/services/DashboardAnalyticsAggregato...

384 lines
12 KiB
TypeScript

import { logMeasurement, reportInteraction } from '@grafana/runtime';
import {
type ScenePerformanceObserver,
type DashboardInteractionStartData,
type DashboardInteractionMilestoneData,
type DashboardInteractionCompleteData,
type PanelPerformanceData,
type QueryPerformanceData,
} from '@grafana/scenes';
import {
registerPerformanceObserver,
getPerformanceMemory,
writePerformanceGroupStart,
writePerformanceGroupLog,
writePerformanceGroupEnd,
} from './performanceUtils';
/**
* Panel metrics structure for analytics
*/
interface PanelAnalyticsMetrics {
panelId: string;
panelKey: string;
pluginId: string;
pluginVersion?: string;
totalQueryTime: number;
totalFieldConfigTime: number;
totalTransformationTime: number;
totalRenderTime: number;
pluginLoadTime: number;
queryOperations: Array<{
duration: number;
timestamp: number;
queryType?: string;
seriesCount?: number;
dataPointsCount?: number;
}>;
fieldConfigOperations: Array<{
duration: number;
timestamp: number;
}>;
transformationOperations: Array<{
duration: number;
timestamp: number;
transformationId?: string;
success?: boolean;
outputSeriesCount?: number;
}>;
renderOperations: Array<{
duration: number;
timestamp: number;
}>;
}
/**
* Aggregates Scene performance events into analytics-ready panel metrics
*/
export class DashboardAnalyticsAggregator implements ScenePerformanceObserver {
private panelMetrics = new Map<string, PanelAnalyticsMetrics>();
private dashboardUID = '';
private dashboardTitle = '';
public initialize(uid: string, title: string) {
// Clear previous dashboard data and set new context
this.panelMetrics.clear();
this.dashboardUID = uid;
this.dashboardTitle = title;
}
public destroy() {
// Clear dashboard context
this.panelMetrics.clear();
this.dashboardUID = '';
this.dashboardTitle = '';
}
/**
* Clear all collected metrics (called on dashboard interaction start)
*/
public clearMetrics() {
this.panelMetrics.clear();
}
/**
* Get aggregated panel metrics for analytics
*/
public getPanelMetrics(): PanelAnalyticsMetrics[] {
return Array.from(this.panelMetrics.values());
}
// Dashboard-level events (we don't need to track these for panel analytics)
onDashboardInteractionStart = (data: DashboardInteractionStartData): void => {
// Clear metrics when new dashboard interaction starts
this.clearMetrics();
};
onDashboardInteractionMilestone = (_data: DashboardInteractionMilestoneData): void => {
// No action needed for milestones in analytics
};
onDashboardInteractionComplete = (data: DashboardInteractionCompleteData): void => {
// Send analytics report for dashboard interaction completion
this.sendAnalyticsReport(data);
};
// Panel-level events
onPanelOperationStart = (data: PanelPerformanceData): void => {
// Start events don't need aggregation, just ensure panel exists
this.ensurePanelExists(data.panelKey, data.panelId, data.pluginId, data.pluginVersion);
};
onPanelOperationComplete = (data: PanelPerformanceData): void => {
// Aggregate panel metrics without verbose logging (handled by ScenePerformanceLogger)
const panel = this.panelMetrics.get(data.panelKey);
if (!panel) {
console.warn('Panel not found for operation completion:', data.panelKey);
return;
}
const duration = data.duration || 0;
switch (data.operation) {
case 'fieldConfig':
panel.totalFieldConfigTime += duration;
panel.fieldConfigOperations.push({
duration,
timestamp: data.timestamp,
});
break;
case 'transform':
panel.totalTransformationTime += duration;
panel.transformationOperations.push({
duration,
timestamp: data.timestamp,
transformationId: data.metadata.transformationId,
success: data.metadata.success,
});
break;
case 'query':
panel.totalQueryTime += duration;
panel.queryOperations.push({
duration,
timestamp: data.timestamp,
queryType: data.metadata.queryType,
});
break;
case 'render':
panel.totalRenderTime += duration;
panel.renderOperations.push({
duration,
timestamp: data.timestamp,
});
break;
case 'plugin-load':
panel.pluginLoadTime += duration;
break;
}
};
// Query-level events
onQueryStart = (_data: QueryPerformanceData): void => {
// Non-panel queries (annotations, variables, datasources) don't need aggregation
// These are infrastructure queries that don't belong to specific panels
// Logging handled by ScenePerformanceLogger to avoid duplication
};
onQueryComplete = (_data: QueryPerformanceData): void => {
// Non-panel queries (annotations, variables, datasources) don't need panel aggregation
// These are infrastructure queries that don't belong to specific panels
// Logging handled by ScenePerformanceLogger to avoid duplication
};
/**
* Ensure a panel exists in our tracking map
*/
private ensurePanelExists(
panelKey: string,
panelId: string,
pluginId: string,
pluginVersion?: string
): PanelAnalyticsMetrics {
let panel = this.panelMetrics.get(panelKey);
if (!panel) {
panel = {
panelId,
panelKey,
pluginId,
pluginVersion,
totalQueryTime: 0,
totalFieldConfigTime: 0,
totalTransformationTime: 0,
totalRenderTime: 0,
pluginLoadTime: 0,
queryOperations: [],
fieldConfigOperations: [],
transformationOperations: [],
renderOperations: [],
};
this.panelMetrics.set(panelKey, panel);
}
return panel;
}
/**
* Send analytics report for dashboard interactions
*/
private sendAnalyticsReport(data: DashboardInteractionCompleteData): void {
const payload = {
duration: data.duration || 0,
networkDuration: data.networkDuration || 0,
startTs: data.timestamp,
endTs: data.timestamp + (data.duration || 0),
timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration,
longFramesCount: data.longFramesCount,
longFramesTotalTime: data.longFramesTotalTime,
...getPerformanceMemory(),
};
const panelMetrics = this.getPanelMetrics();
this.logDashboardAnalyticsEvent(data, payload, panelMetrics);
reportInteraction('dashboard_render', {
interactionType: data.interactionType,
uid: this.dashboardUID,
...payload,
});
logMeasurement('dashboard_render', payload, {
interactionType: data.interactionType,
dashboard: this.dashboardUID,
title: this.dashboardTitle,
});
}
/**
* Log dashboard analytics event with panel metrics and performance insights
*/
private logDashboardAnalyticsEvent(
data: DashboardInteractionCompleteData,
payload: Record<string, unknown>,
panelMetrics: PanelAnalyticsMetrics[] | null
): void {
const panelCount = panelMetrics?.length || 0;
const panelSummary = panelCount ? `${panelCount} panels analyzed` : 'No panel metrics';
// Main analytics summary
const slowPanelCount =
panelMetrics?.filter(
(p) =>
p.totalQueryTime + p.totalTransformationTime + p.totalRenderTime + p.totalFieldConfigTime + p.pluginLoadTime >
100
).length || 0;
writePerformanceGroupStart(
'DAA',
`[ANALYTICS] ${data.interactionType} | ${panelSummary}${slowPanelCount > 0 ? ` | ${slowPanelCount} slow panels ⚠️` : ''}`
);
// Dashboard overview
writePerformanceGroupLog('DAA', '📊 Dashboard (ms):', {
duration: Math.round((data.duration || 0) * 10) / 10,
network: Math.round((data.networkDuration || 0) * 10) / 10,
interactionType: data.interactionType,
slowPanels: slowPanelCount,
});
// Analytics payload
writePerformanceGroupLog('DAA', '📈 Analytics payload:', payload);
// Individual collapsible panel logs with detailed breakdown
if (panelMetrics && panelMetrics.length > 0) {
panelMetrics.forEach((panel) => {
const totalPanelTime =
panel.totalQueryTime +
panel.totalTransformationTime +
panel.totalRenderTime +
panel.totalFieldConfigTime +
panel.pluginLoadTime;
const isSlowPanel = totalPanelTime > 100;
const slowWarning = isSlowPanel ? ' ⚠️ SLOW' : '';
writePerformanceGroupStart(
'DAA',
`🎨 Panel ${panel.pluginId}-${panel.panelId}: ${totalPanelTime.toFixed(1)}ms total${slowWarning}`
);
writePerformanceGroupLog('DAA', '🔧 Plugin:', {
id: panel.pluginId,
version: panel.pluginVersion || 'unknown',
panelId: panel.panelId,
panelKey: panel.panelKey,
});
writePerformanceGroupLog('DAA', '⚡ Performance (ms):', {
totalTime: Math.round(totalPanelTime * 10) / 10, // Round to 1 decimal
isSlowPanel: isSlowPanel,
breakdown: {
query: Math.round(panel.totalQueryTime * 10) / 10,
transform: Math.round(panel.totalTransformationTime * 10) / 10,
render: Math.round(panel.totalRenderTime * 10) / 10,
fieldConfig: Math.round(panel.totalFieldConfigTime * 10) / 10,
pluginLoad: Math.round(panel.pluginLoadTime * 10) / 10,
},
});
if (panel.queryOperations.length > 0) {
writePerformanceGroupLog('DAA', '📊 Queries:', {
count: panel.queryOperations.length,
details: panel.queryOperations.map((op, index) => ({
operation: index + 1,
duration: Math.round(op.duration * 10) / 10,
timestamp: op.timestamp,
queryType: op.queryType || 'unknown',
})),
});
}
if (panel.transformationOperations.length > 0) {
writePerformanceGroupLog('DAA', '🔄 Transformations:', {
count: panel.transformationOperations.length,
details: panel.transformationOperations.map((op, index) => ({
operation: index + 1,
duration: Math.round(op.duration * 10) / 10,
timestamp: op.timestamp,
transformationId: op.transformationId || 'unknown',
success: op.success !== false,
})),
});
}
if (panel.renderOperations.length > 0) {
writePerformanceGroupLog('DAA', '🎨 Renders:', {
count: panel.renderOperations.length,
details: panel.renderOperations.map((op, index) => ({
operation: index + 1,
duration: Math.round(op.duration * 10) / 10,
timestamp: op.timestamp,
})),
});
}
if (panel.fieldConfigOperations.length > 0) {
writePerformanceGroupLog('DAA', '⚙️ FieldConfigs:', {
count: panel.fieldConfigOperations.length,
details: panel.fieldConfigOperations.map((op, index) => ({
operation: index + 1,
duration: Math.round(op.duration * 10) / 10,
timestamp: op.timestamp,
})),
});
}
writePerformanceGroupEnd();
});
}
writePerformanceGroupEnd();
}
}
// Global singleton instance with lazy initialization
let dashboardAnalyticsAggregator: DashboardAnalyticsAggregator | null = null;
export function initializeDashboardAnalyticsAggregator(): DashboardAnalyticsAggregator {
if (!dashboardAnalyticsAggregator) {
dashboardAnalyticsAggregator = new DashboardAnalyticsAggregator();
// Register as global performance observer
registerPerformanceObserver(dashboardAnalyticsAggregator, 'DAA');
}
return dashboardAnalyticsAggregator;
}
export function getDashboardAnalyticsAggregator(): DashboardAnalyticsAggregator {
return initializeDashboardAnalyticsAggregator();
}