Dashboard: Tweak interaction performance tracking (#108658)

* Dashboard: Tweak interaction tracking

* Bump scenes to canary (https://github.com/grafana/scenes/pull/1195)

* Update naming

* Use latest canary

* Update cenes, introduce singleton dashboard profiler, instrument v2 schema dashboard

* Fix an oooooqpsiiii

* Bmp scenes to 6.28.5

* Add documentation

* Add event start/end time stamps, update documentation. Bump scenes

* Docs

* Move file

* Typos
This commit is contained in:
Dominik Prokop
2025-07-30 15:23:48 +02:00
committed by GitHub
parent 20b08ccaab
commit bb45dc88ac
7 changed files with 276 additions and 59 deletions
+2 -2
View File
@@ -290,8 +290,8 @@
"@grafana/plugin-ui": "0.10.7",
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^6.27.2",
"@grafana/scenes-react": "^6.27.2",
"@grafana/scenes": "6.28.6",
"@grafana/scenes-react": "6.28.6",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -267,7 +267,7 @@ abstract class DashboardScenePageStateManagerBase<T>
const queryController = sceneGraph.getQueryController(dashboard);
trackDashboardSceneLoaded(dashboard, measure?.duration);
queryController?.startProfile('DashboardScene');
queryController?.startProfile('dashboard_view');
if (options.route !== DashboardRoutes.New) {
emitDashboardViewEvent({
@@ -51,6 +51,10 @@ import {
DeprecatedInternalId,
} from 'app/features/apiserver/types';
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import {
getDashboardInteractionCallback,
getDashboardSceneProfiler,
} from 'app/features/dashboard/services/DashboardProfiler';
import { DashboardMeta } from 'app/types/dashboard';
import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior';
@@ -157,6 +161,15 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
//createLayoutManager(dashboard);
const queryController = new behaviors.SceneQueryController(
{
enableProfiling:
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === metadata.name) !== -1,
onProfileComplete: getDashboardInteractionCallback(metadata.name, dashboard.title),
},
getDashboardSceneProfiler()
);
const dashboardScene = new DashboardScene(
{
description: dashboard.description,
@@ -184,7 +197,7 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
new behaviors.CursorSync({
sync: transformCursorSyncV2ToV1(dashboard.cursorSync),
}),
new behaviors.SceneQueryController(),
queryController,
registerDashboardMacro,
registerPanelInteractionsReporter,
new behaviors.LiveNowTimer({ enabled: dashboard.liveNow }),
@@ -1,7 +1,7 @@
import { uniqueId } from 'lodash';
import { DataFrameDTO, DataFrameJSON } from '@grafana/data';
import { config, logMeasurement, reportInteraction } from '@grafana/runtime';
import { config } from '@grafana/runtime';
import {
VizPanel,
SceneTimePicker,
@@ -17,12 +17,15 @@ import {
SceneGridItemLike,
SceneDataLayerProvider,
UserActionEvent,
SceneInteractionProfileEvent,
SceneObjectState,
} from '@grafana/scenes';
import { isWeekStart } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { K8S_V1_DASHBOARD_API_CONFIG } from 'app/features/dashboard/api/v1';
import {
getDashboardInteractionCallback,
getDashboardSceneProfiler,
} from 'app/features/dashboard/services/DashboardProfiler';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { DashboardDTO, DashboardDataDTO } from 'app/types/dashboard';
@@ -292,15 +295,20 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
}
: undefined;
const queryController = new behaviors.SceneQueryController(
{
enableProfiling:
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1,
onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title),
},
getDashboardSceneProfiler()
);
const behaviorList: SceneObjectState['$behaviors'] = [
new behaviors.CursorSync({
sync: oldModel.graphTooltip,
}),
new behaviors.SceneQueryController({
enableProfiling:
config.dashboardPerformanceMetrics.findIndex((uid) => uid === '*' || uid === oldModel.uid) !== -1,
onProfileComplete: getDashboardInteractionCallback(oldModel.uid, oldModel.title),
}),
queryController,
registerDashboardMacro,
registerPanelInteractionsReporter,
new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }),
@@ -499,40 +507,3 @@ export const convertOldSnapshotToScenesSnapshot = (panel: PanelModel) => {
panel.snapshotData = [];
}
};
function getDashboardInteractionCallback(uid: string, title: string) {
return (e: SceneInteractionProfileEvent) => {
let interactionType = '';
if (e.origin === 'SceneTimeRange') {
interactionType = 'time-range-change';
} else if (e.origin === 'SceneRefreshPicker') {
interactionType = 'refresh';
} else if (e.origin === 'DashboardScene') {
interactionType = 'view';
} else if (e.origin.indexOf('Variable') > -1) {
interactionType = 'variable-change';
}
reportInteraction('dashboard-render', {
interactionType,
duration: e.duration,
networkDuration: e.networkDuration,
totalJSHeapSize: e.totalJSHeapSize,
usedJSHeapSize: e.usedJSHeapSize,
jsHeapSizeLimit: e.jsHeapSizeLimit,
});
logMeasurement(
`dashboard.${interactionType}`,
{
duration: e.duration,
networkDuration: e.networkDuration,
totalJSHeapSize: e.totalJSHeapSize,
usedJSHeapSize: e.usedJSHeapSize,
jsHeapSizeLimit: e.jsHeapSizeLimit,
timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration,
},
{ dashboard: uid, title: title }
);
};
}
@@ -0,0 +1,34 @@
import { logMeasurement, reportInteraction } from '@grafana/runtime';
import { SceneInteractionProfileEvent, SceneRenderProfiler } from '@grafana/scenes';
let dashboardSceneProfiler: SceneRenderProfiler | undefined;
export function getDashboardSceneProfiler() {
if (!dashboardSceneProfiler) {
dashboardSceneProfiler = new SceneRenderProfiler();
}
return dashboardSceneProfiler;
}
export function getDashboardInteractionCallback(uid: string, title: string) {
return (e: SceneInteractionProfileEvent) => {
const payload = {
duration: e.duration,
networkDuration: e.networkDuration,
startTs: e.startTs,
endTs: e.endTs,
totalJSHeapSize: e.totalJSHeapSize,
usedJSHeapSize: e.usedJSHeapSize,
jsHeapSizeLimit: e.jsHeapSizeLimit,
timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration,
};
reportInteraction('dashboard_render', {
interactionType: e.origin,
uid,
...payload,
});
logMeasurement(`dashboard_render`, payload, { interactionType: e.origin, dashboard: uid, title: title });
};
}
@@ -0,0 +1,199 @@
# Grafana Dashboard Render Performance Metrics
This documentation describes the dashboard render performance metrics exposed from Grafana's frontend.
## Overview
The exposed dashboard performance metrics feature provides comprehensive tracking and profiling of dashboard interactions, allowing administrators and developers to analyze dashboard render performance, user interactions, and identify performance bottlenecks.
## Configuration
### Enabling Performance Metrics
Dashboard performance metrics are configured in the Grafana configuration file (`grafana.ini`) under the `[dashboards]` section:
```ini
[dashboards]
# Dashboards UIDs to report performance metrics for. * can be used to report metrics for all dashboards
dashboard_performance_metrics = *
```
**Configuration Options:**
- **`*`** - Enable profiling on all dashboards
- **`<comma-separated-list-of-dashboard-uid>`** - Enable profiling on specific dashboards only
- **`""` (empty)** - Disable performance metrics (default)
**Examples:**
```ini
# Enable for all dashboards
dashboard_performance_metrics = *
# Enable for specific dashboards
dashboard_performance_metrics = dashboard-uid-1,dashboard-uid-2,dashboard-uid-3
# Disable performance metrics
dashboard_performance_metrics =
```
## Tracked Interactions
The system tracks various dashboard interaction types automatically using the [`@grafana/scenes`](https://github.com/grafana/scenes) library. Each interaction is captured with a specific origin identifier that describes the type of user action performed. In Grafana, these interaction events are then reported as `dashboard_render` events with interaction type information included.
### Core Performance-Tracked Interactions
The following dashboard interaction types are tracked for dashboard render performance profiling:
| Interaction Type | Trigger | When Measured |
| ------------------------ | -------------------------- | -------------------------------------------------------- |
| `dashboard_view` | Dashboard view | When user loads or navigates to a dashboard |
| `refresh` | Manual/Auto refresh | When user clicks refresh button or auto-refresh triggers |
| `time_range_change` | Time picker changes | When user changes time range in time picker |
| `filter_added` | Ad-hoc filter addition | When user adds a new filter to the dashboard |
| `filter_removed` | Ad-hoc filter removal | When user removes a filter from the dashboard |
| `filter_changed` | Ad-hoc filter modification | When user changes filter values or operators |
| `filter_restored` | Ad-hoc filter restoration | When user restores a previously applied filter |
| `variable_value_changed` | Variable value changes | When user changes dashboard variable values |
| `scopes_changed` | Scopes modifications | When user modifies dashboard scopes |
The interactions mentioned above are reported to Echo service as well as sent to [Faro](https://grafana.com/docs/grafana-cloud/monitor-applications/frontend-observability/) as `dashboard_render` measurements:
```ts
const payload = {
duration: e.duration,
networkDuration: e.networkDuration,
startTs: e.startTs,
endTs: e.endTs,
totalJSHeapSize: e.totalJSHeapSize,
usedJSHeapSize: e.usedJSHeapSize,
jsHeapSizeLimit: e.jsHeapSizeLimit,
timeSinceBoot: performance.measure('time_since_boot', 'frontend_boot_js_done_time_seconds').duration,
};
reportInteraction('dashboard_render', {
interactionType: e.origin,
uid,
...payload,
});
logMeasurement(`dashboard_render`, payload, { interactionType: e.origin, dashboard: uid, title: title });
```
### Interaction Origin Mapping
The profiling system uses profiler event's `origin` directly as the `interactionType`, providing direct mapping between user actions and performance measurements.
## Profiling Implementation
### Profile Data Structure
Each interaction profile event captures:
```typescript
interface SceneInteractionProfileEvent {
origin: string; // Interaction type
duration: number; // Total interaction duration
networkDuration: number; // Network requests duration
totalJSHeapSize: number; // JavaScript heap size metrics
usedJSHeapSize: number; // Used JavaScript heap size
jsHeapSizeLimit: number; // JavaScript heap size limit
startTs: number; // Profile start timestamp
endTs: number; // Profile end timestamp
}
```
### Collected Metrics
For each tracked interaction, the system collects:
- **Dashboard Metadata**: UID, title
- **Performance Metrics**: Duration, network duration
- **Memory Metrics**: JavaScript heap usage statistics
- **Timing Information**: Time since boot, profile start and end timestamps
- **Interaction Context**: Type of user interaction
## Debugging and Development
### Enable Profiler Debug Logging
To observe profiling events in the browser console:
```javascript
localStorage.setItem('grafana.debug.scenes', 'true');
```
#### Console Output
When debug logging is enabled, you'll see console logs for each profiling event:
```
SceneRenderProfiler: Profile started: {origin: <NAME_OF_INTERACTION>, crumbs: Array(0)}
... // intermediate steps adding profile crumbs
SceneRenderProfiler: Stopped recording, total measured time (network included): 2123
```
### Enable Echo Service Debug Logging
To observe Echo events in the browser console:
```javascript
_debug.echo.enable();
```
#### Console Output
When Echo debug logging is enabled, you'll see console logs for each profiling event captured by Echo service:
```
[EchoSrv: interaction event]: {interactionName: 'dashboard_render', properties: {…}, meta: {…}}
```
### Browser Performance Profiler
Dashboard interactions can be recorded in the browser's performance profiler, where they appear as:
```
Dashboard Interaction <NAME_OF_INTERACTION>
```
## Analytics Integration
### Interaction Reporting
Performance data is integrated with Grafana's analytics system through:
- **`reportInteraction`**: Reports interaction events to Echo service with performance data
- **`logMeasurement`**: Records Faro's performance measurements with metadata
### Data Collection
The system reports the following data for each interaction:
```typescript
{
interactionType: string, // Type of interaction
uid: string, // Dashboard UID
duration: number, // Total duration
networkDuration: number, // Network time
startTs: number, // Profile start timestamp
endTs: number, // Profile end timestamp
totalJSHeapSize: number, // Memory metrics
usedJSHeapSize: number,
jsHeapSizeLimit: number,
timeSinceBoot: number // Time since frontend boot
}
```
## Implementation Details
The profiler is integrated into dashboard creation paths and uses a singleton pattern to share profiler instances across dashboard reloads. The performance tracking is implemented using the `SceneRenderProfiler` from the `@grafana/scenes` library.
## Related Documentation
- [PR #858 - Add SceneRenderProfiler to scenes](https://github.com/grafana/scenes/pull/858)
- [PR #99629 - Dashboard render performance metrics](https://github.com/grafana/grafana/pull/99629)
- [PR #108658 - Dashboard: Tweak interaction tracking](https://github.com/grafana/grafana/pull/108658)
- [PR #1195 - Enhance SceneRenderProfiler with additional interaction tracking](https://github.com/grafana/scenes/pull/1195)
- [PR #1198 - Make SceneRenderProfiler optional and injectable](https://github.com/grafana/scenes/pull/1198)
- [PR #1199 - SceneRenderProfiler: add start and end timestamps to profile events](https://github.com/grafana/scenes/pull/1199)
+11 -11
View File
@@ -3600,11 +3600,11 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes-react@npm:^6.27.2":
version: 6.28.2
resolution: "@grafana/scenes-react@npm:6.28.2"
"@grafana/scenes-react@npm:6.28.6":
version: 6.28.6
resolution: "@grafana/scenes-react@npm:6.28.6"
dependencies:
"@grafana/scenes": "npm:6.28.2"
"@grafana/scenes": "npm:6.28.6"
lru-cache: "npm:^10.2.2"
react-use: "npm:^17.4.0"
peerDependencies:
@@ -3616,13 +3616,13 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/c00730312828639f8a596c9fd9b0336ec57ca5cec624ac4f09a5fa0be93282d180451e3bcbf83c0209ff04486def25ff98d7b0ea6c370e91b8990112c3e903f2
checksum: 10/55dfe20a6454f218b7c5ab2dc728678885d48e53656d046c125b555b201891e121e0960e108229372c2d70336cfe3dade9b392ed001fef90aec57cbcc13f76ca
languageName: node
linkType: hard
"@grafana/scenes@npm:6.28.2, @grafana/scenes@npm:^6.27.2":
version: 6.28.2
resolution: "@grafana/scenes@npm:6.28.2"
"@grafana/scenes@npm:6.28.6":
version: 6.28.6
resolution: "@grafana/scenes@npm:6.28.6"
dependencies:
"@floating-ui/react": "npm:^0.26.16"
"@leeoniya/ufuzzy": "npm:^1.0.16"
@@ -3642,7 +3642,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/53370553ac4ac38d41ab1d782ee14cf05f483e35e5338406d619efb61e4c9881fd361c98ee116ae8bf40ab1a5fa2c82ca859fc519f6d532853c275e71949c654
checksum: 10/fcfcf663e2eb63ad25fad27ec50af8590b1a6c9e802927e8e457bf2f81d04cf971c2f12f456a04a502c929ec7a1d50ef7118baee095134b133ecc92b269a6907
languageName: node
linkType: hard
@@ -18187,8 +18187,8 @@ __metadata:
"@grafana/plugin-ui": "npm:0.10.7"
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/scenes": "npm:^6.27.2"
"@grafana/scenes-react": "npm:^6.27.2"
"@grafana/scenes": "npm:6.28.6"
"@grafana/scenes-react": "npm:6.28.6"
"@grafana/schema": "workspace:*"
"@grafana/sql": "workspace:*"
"@grafana/test-utils": "workspace:*"