Dynamic Dashboards: Add more tracking (#114098)

This commit is contained in:
Ida Štambuk
2025-11-20 12:01:21 +01:00
committed by GitHub
parent 3c0d5745fe
commit 6e9a33e712
10 changed files with 82 additions and 7 deletions
@@ -21,6 +21,7 @@ import { BulkActionElement } from '../scene/types/BulkActionElement';
import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { DashboardInteractions } from '../utils/interactions';
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils';
import { MultiSelectedVizPanelsEditableElement } from './MultiSelectedVizPanelsEditableElement';
@@ -95,6 +96,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
public useEditPaneOptions = useEditPaneOptions.bind(this);
public onDelete() {
DashboardInteractions.panelActionClicked('duplicate', getPanelIdForVizPanel(this.panel), 'edit_pane');
const layout = dashboardSceneGraph.getLayoutManagerFor(this.panel);
layout.removePanel?.(this.panel);
}
@@ -116,11 +118,13 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
}
public onDuplicate() {
DashboardInteractions.panelActionClicked('duplicate', getPanelIdForVizPanel(this.panel), 'edit_pane');
const layout = dashboardSceneGraph.getLayoutManagerFor(this.panel);
layout.duplicatePanel?.(this.panel);
}
public onCopy() {
DashboardInteractions.panelActionClicked('copy', getPanelIdForVizPanel(this.panel), 'edit_pane');
const dashboard = getDashboardSceneFor(this.panel);
dashboard.copyPanel(this.panel);
}
@@ -147,7 +151,9 @@ const OpenPanelEditViz = ({ panel }: OpenPanelEditVizProps) => {
<Stack alignItems="center" width="100%">
<Button
onClick={() => {
locationService.partial({ editPanel: getPanelIdForVizPanel(panel) });
const panelId = getPanelIdForVizPanel(panel);
locationService.partial({ editPanel: panelId });
DashboardInteractions.panelActionClicked('configure', panelId, 'edit_pane');
}}
icon="sliders-v-alt"
fullWidth
@@ -95,6 +95,9 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
viewPanel: panel.getPathId(),
editPanel: undefined,
}),
onClick: () => {
DashboardInteractions.panelActionClicked('view', getPanelIdForVizPanel(panel), 'panel');
},
});
}
@@ -106,6 +109,9 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
iconClassName: 'edit',
shortcut: 'e',
href: getEditPanelUrl(getPanelIdForVizPanel(panel)),
onClick: () => {
DashboardInteractions.panelActionClicked('edit', getPanelIdForVizPanel(panel), 'panel');
},
});
}
@@ -187,6 +193,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
text: t('panel.header-menu.duplicate', `Duplicate`),
iconClassName: 'file-copy-alt',
onClick: () => {
DashboardInteractions.panelActionClicked('duplicate', getPanelIdForVizPanel(panel), 'panel');
dashboard.duplicatePanel(panel);
},
shortcut: 'p d',
@@ -198,6 +205,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
text: t('panel.header-menu.copy', `Copy`),
iconClassName: 'copy',
onClick: () => {
DashboardInteractions.panelActionClicked('copy', getPanelIdForVizPanel(panel), 'panel');
dashboard.copyPanel(panel);
},
});
@@ -359,6 +367,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
text: t('panel.header-menu.remove', `Remove`),
iconClassName: 'trash-alt',
onClick: () => {
DashboardInteractions.panelActionClicked('delete', getPanelIdForVizPanel(panel), 'panel');
onRemovePanel(dashboard, panel);
},
shortcut: 'p r',
@@ -6,6 +6,7 @@ import { locationService } from '@grafana/runtime';
import { sceneUtils } from '@grafana/scenes';
import { Box, Button, ButtonGroup, Dropdown, Menu, Stack } from '@grafana/ui';
import { DashboardInteractions } from '../utils/interactions';
import { findVizPanelByKey, getVizPanelKeyForPanelId } from '../utils/utils';
import { DashboardScene } from './DashboardScene';
@@ -22,6 +23,7 @@ function UnconfiguredPanelComp(props: PanelProps) {
const onConfigure = () => {
locationService.partial({ editPanel: props.id });
DashboardInteractions.panelActionClicked('configure', props.id, 'panel');
};
const onUseLibraryPanel = () => {
@@ -314,6 +314,9 @@ describe('DashboardSceneSerializer', () => {
{
type: 'text',
},
{
type: 'row',
},
{
type: 'text',
},
@@ -344,9 +347,10 @@ describe('DashboardSceneSerializer', () => {
uid: 'my-uid',
title: 'hello',
schemaVersion: DASHBOARD_SCHEMA_VERSION,
panels_count: 3,
panels_count: 4,
panel_type_text_count: 2,
panel_type_timeseries_count: 1,
panel_type_row_count: 1,
variable_type_query_count: 2,
variable_type_textbox_count: 1,
settings_nowdelay: undefined,
@@ -680,6 +684,7 @@ describe('DashboardSceneSerializer', () => {
uid: 'dashboard-test',
title: 'Cloudwatch ec2 new layout',
panels_count: 6,
rowCount: 2,
schemaVersion: DASHBOARD_SCHEMA_VERSION,
settings_nowdelay: undefined,
settings_livenow: true,
@@ -65,6 +65,7 @@ export interface DashboardTrackingInfo {
title?: string;
schemaVersion: number;
panels_count: number;
rowCount?: number;
settings_nowdelay?: number;
settings_livenow?: boolean;
}
@@ -455,11 +456,18 @@ export class V2DashboardSerializer
const variables =
'variables' in this.initialSaveModel! ? getV2SchemaVariables(this.initialSaveModel.variables) : [];
const rowCount =
'layout' in this.initialSaveModel
? this.initialSaveModel.layout.kind === 'RowsLayout'
? this.initialSaveModel.layout.spec.rows.length
: 0
: 0;
return {
schemaVersion: DASHBOARD_SCHEMA_VERSION,
uid: s.state.uid,
title: this.initialSaveModel.title,
panels_count: panelPluginIds.length || 0,
rowCount,
settings_nowdelay: undefined,
settings_livenow: !!this.initialSaveModel.liveNow,
...panels,
@@ -14,7 +14,13 @@ type DashboardLibraryTrackingInfo = {
export const DashboardInteractions = {
// Dashboard interactions:
dashboardInitialized: (
properties: { theme: undefined; duration: number | undefined; isScene: boolean } & Partial<DashboardTrackingInfo> &
properties: {
theme: undefined;
duration: number | undefined;
isScene: boolean;
hasEditPermissions?: boolean;
hasSavePermissions?: boolean;
} & Partial<DashboardTrackingInfo> &
Partial<DynamicDashboardsTrackingInformation> &
Partial<{ version_before_migration: number | undefined }>
) => {
@@ -28,11 +34,19 @@ export const DashboardInteractions = {
dashboardCreatedOrSaved: (
isNew: boolean | undefined,
properties:
| ({ name: string; url: string } & DashboardLibraryTrackingInfo)
| ({
name: string;
url: string;
uid: string;
numPanels: number;
numRows: number;
} & DashboardLibraryTrackingInfo)
| ({
name: string;
url: string;
numPanels: number;
numTabs: number;
numRows: number;
uid: string;
conditionalRenderRules: number;
autoLayoutCount: number;
@@ -73,6 +87,14 @@ export const DashboardInteractions = {
reportDashboardInteraction('add_variable_button_clicked', properties);
},
panelActionClicked(
item: 'configure' | 'edit' | 'copy' | 'duplicate' | 'delete' | 'view',
id: number,
source: 'panel' | 'edit_pane'
) {
reportDashboardInteraction('panel_action_clicked', { item, id, source });
},
// Dashboard edit item actions
// dashboards_edit_action_clicked: when user adds or removes an item in edit mode
// props: { item: string } - item is one of: add_panel, group_row, group_tab, ungroup, paste_panel, remove_row, remove_tab
@@ -49,6 +49,8 @@ describe('dashboard tracking', () => {
name: 'new dashboard',
url: 'new-url',
numPanels: 6,
numRows: 6,
numTabs: 4,
conditionalRenderRules: 3,
autoLayoutCount: 3,
customGridLayoutCount: 2,
@@ -69,6 +71,7 @@ describe('dashboard tracking', () => {
duration: 42,
isScene: true,
tabCount: 4,
rowCount: 2,
templateVariableCount: 2,
maxNestingLevel: 3,
panel_type_timeseries_count: 6,
@@ -86,6 +89,8 @@ describe('dashboard tracking', () => {
uid: 'dashboard-test',
variable_type_custom_count: 1,
variable_type_query_count: 1,
hasEditPermissions: true,
hasSavePermissions: true,
});
});
});
@@ -14,6 +14,8 @@ export function trackDashboardSceneLoaded(dashboard: DashboardScene, duration?:
theme: undefined,
duration,
isScene: true,
hasEditPermissions: dashboard.canEditDashboard(),
hasSavePermissions: Boolean(dashboard.state.meta.canSave),
...(dashboard.getTrackingInformation() ?? {}),
...(dynamicDashboardsTrackingInformation
? {
@@ -63,6 +65,8 @@ export function trackDashboardSceneCreatedOrSaved(
// Extract datasourceTypes from URL params (supports both community and provisioned dashboards) or dashboard panels
const datasourceTypes = getDatasourceTypes(dashboard);
const sceneDashboardTrackingInfo = dashboard.getTrackingInformation();
const dynamicDashboardsTrackingInformation = dashboard.getDynamicDashboardsTrackingInformation();
const dashboardLibraryProperties =
@@ -81,8 +85,10 @@ export function trackDashboardSceneCreatedOrSaved(
...initialProperties,
...(dynamicDashboardsTrackingInformation
? {
uid: dashboard.state.uid,
uid: dashboard.state.uid || '',
numPanels: dynamicDashboardsTrackingInformation.panelCount,
numTabs: dynamicDashboardsTrackingInformation.tabCount,
numRows: dynamicDashboardsTrackingInformation.rowCount,
conditionalRenderRules: dynamicDashboardsTrackingInformation.conditionalRenderRulesCount,
autoLayoutCount: dynamicDashboardsTrackingInformation.autoLayoutCount,
customGridLayoutCount: dynamicDashboardsTrackingInformation.customGridLayoutCount,
@@ -90,6 +96,9 @@ export function trackDashboardSceneCreatedOrSaved(
...dashboardLibraryProperties,
}
: {
uid: dashboard.state.uid || '',
numPanels: sceneDashboardTrackingInfo?.panels_count || 0,
numRows: sceneDashboardTrackingInfo?.rowCount || 0,
...dashboardLibraryProperties,
}),
});
@@ -65,7 +65,13 @@ export const useDashboardSave = (isCopy = false) => {
if (isCopy) {
DashboardInteractions.dashboardCopied({ name: dashboard.title || '', url: result.url });
} else {
trackDashboardCreatedOrSaved(!!dashboard.id, { name: dashboard.title, url: result.url });
trackDashboardCreatedOrSaved(!!dashboard.id, {
name: dashboard.title,
url: result.url,
uid: result.uid,
numPanels: dashboard.panels.filter((p) => p.type !== 'row').length,
numRows: dashboard.panels.filter((p) => p.type === 'row').length,
});
}
const currentPath = locationService.getLocation().pathname;
@@ -26,7 +26,10 @@ export function trackDashboardLoaded(dashboard: DashboardModel, duration?: numbe
});
}
export function trackDashboardCreatedOrSaved(isNew: boolean | undefined, trackingProps: { name: string; url: string }) {
export function trackDashboardCreatedOrSaved(
isNew: boolean | undefined,
trackingProps: { name: string; url: string; uid: string; numPanels: number; numRows: number }
) {
DashboardInteractions.dashboardCreatedOrSaved(isNew, trackingProps);
}