diff --git a/packages/grafana-data/src/types/action.ts b/packages/grafana-data/src/types/action.ts index 4c90cce13ae..f248e22f024 100644 --- a/packages/grafana-data/src/types/action.ts +++ b/packages/grafana-data/src/types/action.ts @@ -25,6 +25,7 @@ export interface Action { */ export interface ActionModel { title: string; + type?: ActionType; onClick: (event: any, origin?: any, actionVars?: ActionVariableInput) => void; confirmation: (actionVars?: ActionVariableInput) => ReactNode; oneClick?: boolean; diff --git a/public/app/features/actions/analytics.ts b/public/app/features/actions/analytics.ts new file mode 100644 index 00000000000..bc0fe03d2e4 --- /dev/null +++ b/public/app/features/actions/analytics.ts @@ -0,0 +1,12 @@ +import { reportInteraction } from '@grafana/runtime'; + +export function reportActionTrigger(actionType: string, isOneClick: boolean, visualizationType: string) { + reportInteraction('dashboards_action_interaction', { + event: 'dashboards_action_interaction', + action: 'trigger_action', + action_type: actionType, + visualization_type: visualizationType, + trigger_type: isOneClick ? 'one_click' : 'button', + context: 'panel_runtime', + }); +} diff --git a/public/app/features/actions/utils.ts b/public/app/features/actions/utils.ts index d8ee0f85169..011185c0244 100644 --- a/public/app/features/actions/utils.ts +++ b/public/app/features/actions/utils.ts @@ -24,6 +24,8 @@ import { createAbsoluteUrl, RelativeUrl } from '../alerting/unified/utils/url'; import { getTimeSrv } from '../dashboard/services/TimeSrv'; import { getNextRequestId } from '../query/state/PanelQueryRunner'; +import { reportActionTrigger } from './analytics'; + /** @internal */ export const isInfinityActionWithAuth = (action: Action): boolean => { return (grafanaConfig.featureToggles.vizActionsAuth ?? false) && action.type === ActionType.Infinity; @@ -59,7 +61,8 @@ export const getActions = ( fieldScopedVars: ScopedVars, replaceVariables: InterpolateFunction, actions: Action[], - config: ValueLinkConfig + config: ValueLinkConfig, + visualizationType?: string ): Array> => { if (!actions || actions.length === 0) { return []; @@ -89,6 +92,7 @@ export const getActions = ( const actionModel: ActionModel = { title: replaceVariables(action.title, actionScopedVars), + type: action.type, confirmation: (actionVars?: ActionVariableInput) => genReplaceActionVars( boundReplaceVariables, @@ -96,6 +100,10 @@ export const getActions = ( actionVars )(action.confirmation || `Are you sure you want to ${action.title}?`), onClick: (evt: MouseEvent, origin: Field, actionVars?: ActionVariableInput) => { + if (visualizationType) { + reportActionTrigger(action.type, action.oneClick ?? false, visualizationType); + } + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions let request = {} as BackendSrvRequest; if (isInfinityActionWithAuth(action)) { diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 1ac1969d750..2286c7f6ada 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -35,6 +35,7 @@ import { removeStyles, } from 'app/plugins/panel/canvas/utils'; +import { reportActionTrigger } from '../../actions/analytics'; import { getActions, getActionsDefaultField, isInfinityActionWithAuth } from '../../actions/utils'; import { CanvasElementItem, CanvasElementOptions } from '../element'; import { canvasElementRegistry } from '../registry'; @@ -1049,6 +1050,9 @@ export class ElementState implements LayerElement { onConfirm={() => { this.showActionConfirmation = false; action.onClick(new MouseEvent('click'), null, this.actionVars); + if (action.type) { + reportActionTrigger(action.type, true, 'canvas'); + } this.forceUpdate(); }} onDismiss={() => { @@ -1104,7 +1108,12 @@ export class ElementState implements LayerElement { tabIndex={0} style={{ userSelect: 'none' }} > - + {this.showActionConfirmation && this.renderActionsConfirmModal(this.getPrimaryAction())} {this.showActionVarsModal && this.renderVariablesInputModal(this.getPrimaryAction())} diff --git a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx index e001a796df0..91f7e31421e 100644 --- a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx @@ -127,7 +127,8 @@ export const CanvasTooltip = ({ scene }: Props) => { scopedVars, scene.panel.props.replaceVariables!, element.options.actions ?? [], - config + config, + 'canvas' ); actionsModel.forEach((action) => { diff --git a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx index ace26bb2917..480d36a5a6a 100644 --- a/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapTooltip.tsx @@ -325,7 +325,9 @@ const HeatmapHoverCell = ({ links = getDataLinks(linksField, xValueIdx); } - actions = canExecuteActions ? getFieldActions(data.series!, linksField, replaceVariables, xValueIdx) : []; + actions = canExecuteActions + ? getFieldActions(data.series!, linksField, replaceVariables, xValueIdx, 'heatmap') + : []; } footer = ; diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx index 87ad10e5adc..6324f8d0ca1 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip.tsx @@ -1,6 +1,6 @@ import { ReactNode } from 'react'; -import { FieldType, TimeRange } from '@grafana/data'; +import { FieldType, TimeRange, usePluginContext } from '@grafana/data'; import { SortOrder } from '@grafana/schema/dist/esm/common/common.gen'; import { TooltipDisplayMode } from '@grafana/ui'; import { @@ -36,6 +36,7 @@ export const StateTimelineTooltip = ({ replaceVariables, dataLinks, }: StateTimelineTooltipProps) => { + const pluginContext = usePluginContext(); const xField = series.fields[0]; const dataIdx = seriesIdx != null ? dataIdxs[seriesIdx] : dataIdxs.find((idx) => idx != null); @@ -78,8 +79,9 @@ export const StateTimelineTooltip = ({ const hasOneClickLink = dataLinks.some((dataLink) => dataLink.oneClick === true); if (isPinned || hasOneClickLink) { + const visualizationType = pluginContext?.meta?.id ?? 'state-timeline'; const dataIdx = dataIdxs[seriesIdx]!; - const actions = getFieldActions(series, field, replaceVariables!, dataIdx); + const actions = getFieldActions(series, field, replaceVariables!, dataIdx, visualizationType); footer = ; } diff --git a/public/app/plugins/panel/status-history/utils.ts b/public/app/plugins/panel/status-history/utils.ts index 0f6f252be72..e8d270b0370 100644 --- a/public/app/plugins/panel/status-history/utils.ts +++ b/public/app/plugins/panel/status-history/utils.ts @@ -26,16 +26,25 @@ export const getFieldActions = ( dataFrame: DataFrame, field: Field, replaceVars: InterpolateFunction, - rowIndex: number + rowIndex: number, + visualizationType?: string ) => { const actions: Array> = []; if (field.state?.scopedVars) { const actionLookup = new Set(); - const actionsModel = getActions(dataFrame, field, field.state.scopedVars, replaceVars, field.config.actions ?? [], { - valueRowIndex: rowIndex, - }); + const actionsModel = getActions( + dataFrame, + field, + field.state.scopedVars, + replaceVars, + field.config.actions ?? [], + { + valueRowIndex: rowIndex, + }, + visualizationType + ); actionsModel.forEach((action) => { const key = `${action.title}`; diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index 657ff634694..5784ff7dafa 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -180,7 +180,8 @@ const getCellActions = ( field.state!.scopedVars!, replaceVariables ?? replaceVars, field.config.actions ?? [], - { valueRowIndex: rowIndex } + { valueRowIndex: rowIndex }, + 'table' ); if (actions.length === 1) { diff --git a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx index 9ef7e387051..2663b8f1acb 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesTooltip.tsx @@ -1,7 +1,15 @@ import { ReactNode } from 'react'; -import { DataFrame, Field, FieldType, formattedValueToString, InterpolateFunction, LinkModel } from '@grafana/data'; -import { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/common/common.gen'; +import { + DataFrame, + Field, + FieldType, + formattedValueToString, + InterpolateFunction, + LinkModel, + usePluginContext, +} from '@grafana/data'; +import { SortOrder, TooltipDisplayMode } from '@grafana/schema'; import { VizTooltipContent, VizTooltipFooter, @@ -63,6 +71,7 @@ export const TimeSeriesTooltip = ({ canExecuteActions, compareDiffMs, }: TimeSeriesTooltipProps) => { + const pluginContext = usePluginContext(); const xField = series.fields[0]; let xVal = xField.values[dataIdxs[0]!]; @@ -91,8 +100,11 @@ export const TimeSeriesTooltip = ({ const hasOneClickLink = dataLinks.some((dataLink) => dataLink.oneClick === true); if (isPinned || hasOneClickLink) { + const visualizationType = pluginContext?.meta?.id ?? 'timeseries'; const dataIdx = dataIdxs[seriesIdx]!; - const actions = canExecuteActions ? getFieldActions(series, field, replaceVariables, dataIdx) : []; + const actions = canExecuteActions + ? getFieldActions(series, field, replaceVariables, dataIdx, visualizationType) + : []; footer = ( diff --git a/public/app/plugins/panel/xychart/XYChartTooltip.tsx b/public/app/plugins/panel/xychart/XYChartTooltip.tsx index 1d176b5d82f..b7a9f04c513 100644 --- a/public/app/plugins/panel/xychart/XYChartTooltip.tsx +++ b/public/app/plugins/panel/xychart/XYChartTooltip.tsx @@ -132,7 +132,9 @@ export const XYChartTooltip = ({ if (isPinned || hasOneClickLink) { const yFieldFrame = data.find((frame) => frame.fields.includes(yField))!; - const actions = canExecuteActions ? getFieldActions(yFieldFrame, yField, replaceVariables, rowIndex) : []; + const actions = canExecuteActions + ? getFieldActions(yFieldFrame, yField, replaceVariables, rowIndex, 'xychart') + : []; footer = ; }