From bb1d7d9070aeb01716eaf6679b54553781e40f30 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 8 Oct 2025 14:17:05 +0200 Subject: [PATCH] Provisioning: Add FE analytics (#112118) * Provisioning: Add FE analytics * Cleanup * More cleanup * Add missing workflow * Add more tracking --- .../provisioning/Config/ConfigForm.tsx | 10 +++++++- .../Repository/DeleteRepositoryButton.tsx | 10 ++++++++ .../Repository/RepositoryActions.tsx | 13 +++++++++- .../Repository/RepositoryCard.tsx | 14 ++++++++++- .../Repository/SyncRepository.tsx | 6 +++++ .../Shared/ConnectRepositoryButton.tsx | 4 ++- .../Wizard/ProvisioningWizard.tsx | 25 +++++++++++++++++-- .../BulkDeleteProvisionedResource.tsx | 13 +++++++++- .../BulkMoveProvisionedResource.tsx | 9 ++++++- .../DeleteProvisionedDashboardForm.tsx | 8 +++++- .../MoveProvisionedDashboardForm.tsx | 8 +++++- .../SaveProvisionedDashboardForm.tsx | 18 +++++++++++-- .../Folders/DeleteProvisionedFolderForm.tsx | 8 +++++- .../Folders/NewProvisionedFolderForm.tsx | 8 +++++- .../app/features/provisioning/utils/data.ts | 2 +- 15 files changed, 141 insertions(+), 15 deletions(-) diff --git a/public/app/features/provisioning/Config/ConfigForm.tsx b/public/app/features/provisioning/Config/ConfigForm.tsx index c0ba526533f..0ba33a4e5db 100644 --- a/public/app/features/provisioning/Config/ConfigForm.tsx +++ b/public/app/features/provisioning/Config/ConfigForm.tsx @@ -4,6 +4,7 @@ import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom-v5-compat'; import { t } from '@grafana/i18n'; +import { reportInteraction } from '@grafana/runtime'; import { Button, Checkbox, @@ -118,12 +119,19 @@ export function ConfigForm({ data }: ConfigFormProps) { useEffect(() => { if (request.isSuccess) { const formData = getValues(); + + reportInteraction('grafana_provisioning_repository_updated', { + repositoryName: repositoryName ?? 'unknown', + repositoryType: formData.type, + target: formData.sync?.target ?? 'unknown', + }); + reset(formData); setTimeout(() => { navigate('/admin/provisioning'); }, 300); } - }, [request.isSuccess, reset, getValues, navigate]); + }, [request.isSuccess, reset, getValues, navigate, repositoryName]); const onSubmit = async (form: RepositoryFormData) => { setIsLoading(true); diff --git a/public/app/features/provisioning/Repository/DeleteRepositoryButton.tsx b/public/app/features/provisioning/Repository/DeleteRepositoryButton.tsx index 51186523d51..23ea3943268 100644 --- a/public/app/features/provisioning/Repository/DeleteRepositoryButton.tsx +++ b/public/app/features/provisioning/Repository/DeleteRepositoryButton.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom-v5-compat'; import { t, Trans } from '@grafana/i18n'; +import { reportInteraction } from '@grafana/runtime'; import { Button, ConfirmModal, Dropdown, Icon, Menu, Stack } from '@grafana/ui'; import { Repository, @@ -44,6 +45,15 @@ export function DeleteRepositoryButton({ name, repository, redirectTo }: Props) }; await replaceRepository({ name, repository: updatedRepository }); } + + reportInteraction('grafana_provisioning_repository_deleted', { + repositoryName: name, + repositoryType: repository?.spec?.type ?? 'unknown', + deleteAction: selectedAction, + target: repository?.spec?.sync?.target ?? 'unknown', + workflows: repository?.spec?.workflows ?? [], + }); + deleteRepository({ name }); }, [deleteRepository, replaceRepository, name, selectedAction, repository]); diff --git a/public/app/features/provisioning/Repository/RepositoryActions.tsx b/public/app/features/provisioning/Repository/RepositoryActions.tsx index 70fa314bee1..edadb2b538e 100644 --- a/public/app/features/provisioning/Repository/RepositoryActions.tsx +++ b/public/app/features/provisioning/Repository/RepositoryActions.tsx @@ -1,4 +1,5 @@ import { t, Trans } from '@grafana/i18n'; +import { reportInteraction } from '@grafana/runtime'; import { Badge, Button, LinkButton, Stack } from '@grafana/ui'; import { Repository } from 'app/api/clients/provisioning/v0alpha1'; @@ -33,7 +34,17 @@ export function RepositoryActions({ repository }: RepositoryActionsProps) { )} - + { + reportInteraction('grafana_provisioning_repository_settings_opened', { + repositoryName: name, + repositoryType: repository.spec?.type ?? 'unknown', + }); + }} + > Settings diff --git a/public/app/features/provisioning/Repository/RepositoryCard.tsx b/public/app/features/provisioning/Repository/RepositoryCard.tsx index 545777b9f4f..8a8205e9a2f 100644 --- a/public/app/features/provisioning/Repository/RepositoryCard.tsx +++ b/public/app/features/provisioning/Repository/RepositoryCard.tsx @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; import { t, Trans } from '@grafana/i18n'; +import { reportInteraction } from '@grafana/runtime'; import { Stack, Text, TextLink, Icon, Card, LinkButton, Badge } from '@grafana/ui'; import { Repository, ResourceCount } from 'app/api/clients/provisioning/v0alpha1'; @@ -103,7 +104,18 @@ export function RepositoryCard({ repository }: Props) { View - + { + reportInteraction('grafana_provisioning_repository_settings_opened', { + repositoryName: name, + repositoryType: spec?.type ?? 'unknown', + }); + }} + > Settings diff --git a/public/app/features/provisioning/Repository/SyncRepository.tsx b/public/app/features/provisioning/Repository/SyncRepository.tsx index bbf4ff01fb4..36a62d44d1f 100644 --- a/public/app/features/provisioning/Repository/SyncRepository.tsx +++ b/public/app/features/provisioning/Repository/SyncRepository.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom-v5-compat'; import { Trans, t } from '@grafana/i18n'; +import { reportInteraction } from '@grafana/runtime'; import { Button, ConfirmModal } from '@grafana/ui'; import { Repository, useCreateRepositoryJobsMutation } from 'app/api/clients/provisioning/v0alpha1'; @@ -23,6 +24,11 @@ export function SyncRepository({ repository }: Props) { if (!name) { return; } + reportInteraction('grafana_provisioning_repository_pull_triggered', { + repositoryName: name, + repositoryType: repository.spec?.type ?? 'unknown', + target: repository.spec?.sync.target ?? 'unknown', + }); createJob({ name, jobSpec: { diff --git a/public/app/features/provisioning/Shared/ConnectRepositoryButton.tsx b/public/app/features/provisioning/Shared/ConnectRepositoryButton.tsx index e61b9f31e21..cdf80faec33 100644 --- a/public/app/features/provisioning/Shared/ConnectRepositoryButton.tsx +++ b/public/app/features/provisioning/Shared/ConnectRepositoryButton.tsx @@ -33,7 +33,9 @@ export function ConnectRepositoryButton({ items }: Props) { key={config.type} icon={config.icon} label={config.label} - onClick={() => navigate(`${CONNECT_URL}/${config.type}`)} + onClick={() => { + navigate(`${CONNECT_URL}/${config.type}`); + }} /> ); })} diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index 5eb4fd174ee..01b8c5436c3 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents, GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { getAppEvents, isFetchError } from '@grafana/runtime'; +import { getAppEvents, isFetchError, reportInteraction } from '@grafana/runtime'; import { Box, Button, ConfirmModal, Stack, Text, useStyles2 } from '@grafana/ui'; import { RepositoryViewList, useDeleteRepositoryMutation } from 'app/api/clients/provisioning/v0alpha1'; import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; @@ -14,7 +14,7 @@ import { getDefaultValues } from '../Config/defaults'; import { ProvisioningAlert } from '../Shared/ProvisioningAlert'; import { PROVISIONING_URL } from '../constants'; import { useCreateOrUpdateRepository } from '../hooks/useCreateOrUpdateRepository'; -import { dataToSpec } from '../utils/data'; +import { dataToSpec, getWorkflows } from '../utils/data'; import { getFormErrors } from '../utils/getFormErrors'; import { BootstrapStep } from './BootstrapStep'; @@ -176,6 +176,11 @@ export const ProvisioningWizard = memo(function ProvisioningWizard({ if (previousStepIndex >= 0) { const previousStep = steps[previousStepIndex]; + reportInteraction('grafana_provisioning_wizard_previous_clicked', { + fromStep: activeStep, + toStep: previousStep.id, + repositoryType: repoType, + }); setActiveStep(previousStep.id); // Remove current step from completed steps when going back setCompletedSteps((prev) => prev.filter((step) => step !== activeStep)); @@ -209,6 +214,10 @@ export const ProvisioningWizard = memo(function ProvisioningWizard({ const handleConfirmCancel = () => { setShowCancelConfirmation(false); + reportInteraction('grafana_provisioning_wizard_cancelled', { + cancelledAtStep: activeStep, + repositoryType: repoType, + }); handleRepositoryDeletion(repoName); }; @@ -254,6 +263,12 @@ export const ProvisioningWizard = memo(function ProvisioningWizard({ // Only navigate to provisioning URL if we're on the actual last step if (isLastStep) { + const formData = getValues(); + reportInteraction('grafana_provisioning_repository_created', { + repositoryType: repoType, + target: syncTarget, + workflowsEnabled: getWorkflows(formData.repository), + }); navigate(PROVISIONING_URL); } else { let nextStepIndex = currentStepIndex + 1; @@ -274,6 +289,12 @@ export const ProvisioningWizard = memo(function ProvisioningWizard({ return; } + reportInteraction('grafana_provisioning_wizard_step_completed', { + step: activeStep, + repositoryType: repoType, + target: syncTarget, + }); + setActiveStep(steps[nextStepIndex].id); setCompletedSteps((prev) => [...new Set([...prev, activeStep])]); setStepStatusInfo({ status: 'idle' }); diff --git a/public/app/features/provisioning/components/BulkActions/BulkDeleteProvisionedResource.tsx b/public/app/features/provisioning/components/BulkActions/BulkDeleteProvisionedResource.tsx index 5c0c6f5de32..ff3743fc45b 100644 --- a/public/app/features/provisioning/components/BulkActions/BulkDeleteProvisionedResource.tsx +++ b/public/app/features/provisioning/components/BulkActions/BulkDeleteProvisionedResource.tsx @@ -3,7 +3,7 @@ import { FormProvider, useForm } from 'react-hook-form'; import { AppEvents } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { getAppEvents } from '@grafana/runtime'; +import { getAppEvents, reportInteraction } from '@grafana/runtime'; import { Box, Button, Stack } from '@grafana/ui'; import { Job, RepositoryView } from 'app/api/clients/provisioning/v0alpha1'; import { DescendantCount } from 'app/features/browse-dashboards/components/BrowseActions/DescendantCount'; @@ -47,6 +47,17 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions const resources = collectSelectedItems(selectedItems); + const folderCount = Object.keys(selectedItems.folder || {}).length; + const dashboardCount = Object.keys(selectedItems.dashboard || {}).length; + reportInteraction('grafana_provisioning_bulk_delete_submitted', { + workflow: data.workflow, + repositoryName: repository.name ?? 'unknown', + repositoryType: repository.type ?? 'unknown', + resourceCount: resources.length, + folderCount, + dashboardCount, + }); + // Create the delete job spec const jobSpec: DeleteJobSpec = { action: 'delete', diff --git a/public/app/features/provisioning/components/BulkActions/BulkMoveProvisionedResource.tsx b/public/app/features/provisioning/components/BulkActions/BulkMoveProvisionedResource.tsx index 6e553ed760c..7f07a6184e7 100644 --- a/public/app/features/provisioning/components/BulkActions/BulkMoveProvisionedResource.tsx +++ b/public/app/features/provisioning/components/BulkActions/BulkMoveProvisionedResource.tsx @@ -4,7 +4,7 @@ import { FormProvider, useForm } from 'react-hook-form'; import { AppEvents } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { getAppEvents } from '@grafana/runtime'; +import { getAppEvents, reportInteraction } from '@grafana/runtime'; import { Box, Button, Field, Stack } from '@grafana/ui'; import { useGetFolderQuery } from 'app/api/clients/folder/v1beta1'; import { RepositoryView, Job } from 'app/api/clients/provisioning/v0alpha1'; @@ -87,6 +87,13 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions return; } + reportInteraction('grafana_provisioning_bulk_move_submitted', { + workflow: data.workflow, + repositoryName: repository.name ?? 'unknown', + repositoryType: repository.type ?? 'unknown', + resourceCount: resources.length, + }); + // Create the move job spec const jobSpec: MoveJobSpec = { action: 'move', diff --git a/public/app/features/provisioning/components/Dashboards/DeleteProvisionedDashboardForm.tsx b/public/app/features/provisioning/components/Dashboards/DeleteProvisionedDashboardForm.tsx index 0d41a378d61..743975947a0 100644 --- a/public/app/features/provisioning/components/Dashboards/DeleteProvisionedDashboardForm.tsx +++ b/public/app/features/provisioning/components/Dashboards/DeleteProvisionedDashboardForm.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { getAppEvents } from '@grafana/runtime'; +import { getAppEvents, reportInteraction } from '@grafana/runtime'; import { Button, Drawer, Stack } from '@grafana/ui'; import { Job, RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene'; @@ -80,6 +80,12 @@ export function DeleteProvisionedDashboardForm({ return; } + reportInteraction('grafana_provisioning_dashboard_delete_submitted', { + workflow, + repositoryName: repo, + repositoryType: repository?.type ?? 'unknown', + }); + // Branch workflow: use /files API for direct file operations if (workflow === 'branch') { const branchRef = ref; diff --git a/public/app/features/provisioning/components/Dashboards/MoveProvisionedDashboardForm.tsx b/public/app/features/provisioning/components/Dashboards/MoveProvisionedDashboardForm.tsx index 3968484ceb2..637d0861882 100644 --- a/public/app/features/provisioning/components/Dashboards/MoveProvisionedDashboardForm.tsx +++ b/public/app/features/provisioning/components/Dashboards/MoveProvisionedDashboardForm.tsx @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { getAppEvents } from '@grafana/runtime'; +import { getAppEvents, reportInteraction } from '@grafana/runtime'; import { Alert, Button, Drawer, Field, Input, Spinner, Stack } from '@grafana/ui'; import { useGetFolderQuery } from 'app/api/clients/folder/v1beta1'; import { @@ -123,6 +123,12 @@ export function MoveProvisionedDashboardForm({ return; } + reportInteraction('grafana_provisioning_dashboard_move_submitted', { + workflow, + repositoryName: repo, + repositoryType: repository?.type ?? 'unknown', + }); + // Branch workflow: use /files API for direct file operations if (workflow === 'branch') { if (!currentFileData?.resource?.file) { diff --git a/public/app/features/provisioning/components/Dashboards/SaveProvisionedDashboardForm.tsx b/public/app/features/provisioning/components/Dashboards/SaveProvisionedDashboardForm.tsx index 368076cafae..f278c2e436c 100644 --- a/public/app/features/provisioning/components/Dashboards/SaveProvisionedDashboardForm.tsx +++ b/public/app/features/provisioning/components/Dashboards/SaveProvisionedDashboardForm.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents, locationUtil } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { getAppEvents, locationService } from '@grafana/runtime'; +import { getAppEvents, locationService, reportInteraction } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import { Button, Field, Input, Stack, TextArea } from '@grafana/ui'; import { RepositoryView, Unstructured } from 'app/api/clients/provisioning/v0alpha1'; @@ -133,7 +133,15 @@ export function SaveProvisionedDashboardForm({ }); // Submit handler for saving the form data - const handleFormSubmit = async ({ title, description, repo, path, comment, ref }: ProvisionedDashboardFormData) => { + const handleFormSubmit = async ({ + title, + description, + repo, + path, + comment, + ref, + folder, + }: ProvisionedDashboardFormData) => { // Validate required fields if (!repo || !path) { console.error('Missing required fields for saving:', { repo, path }); @@ -155,6 +163,12 @@ export function SaveProvisionedDashboardForm({ copyTags: true, }); + reportInteraction('grafana_provisioning_dashboard_save_submitted', { + workflow, + repositoryName: repo, + repositoryType: repository?.type ?? 'unknown', + }); + createOrUpdateFile({ // Skip adding ref to the default branch request ref: ref === repository?.branch ? undefined : ref, diff --git a/public/app/features/provisioning/components/Folders/DeleteProvisionedFolderForm.tsx b/public/app/features/provisioning/components/Folders/DeleteProvisionedFolderForm.tsx index 2c76f5c877f..102cacbf7d4 100644 --- a/public/app/features/provisioning/components/Folders/DeleteProvisionedFolderForm.tsx +++ b/public/app/features/provisioning/components/Folders/DeleteProvisionedFolderForm.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { getAppEvents } from '@grafana/runtime'; +import { getAppEvents, reportInteraction } from '@grafana/runtime'; import { Box, Button, Stack } from '@grafana/ui'; import { Folder } from 'app/api/clients/folder/v1beta1'; import { Job, RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; @@ -61,6 +61,12 @@ function FormContent({ initialValues, parentFolder, repository, workflowOptions, return; } + reportInteraction('grafana_provisioning_folder_delete_submitted', { + workflow, + repositoryName: repo, + repositoryType: repository?.type ?? 'unknown', + }); + // Branch workflow: use /files API for direct file operations if (workflow === 'branch') { const branchRef = ref; diff --git a/public/app/features/provisioning/components/Folders/NewProvisionedFolderForm.tsx b/public/app/features/provisioning/components/Folders/NewProvisionedFolderForm.tsx index 10bcfcdcf66..3b9d62916a4 100644 --- a/public/app/features/provisioning/components/Folders/NewProvisionedFolderForm.tsx +++ b/public/app/features/provisioning/components/Folders/NewProvisionedFolderForm.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { getAppEvents } from '@grafana/runtime'; +import { getAppEvents, reportInteraction } from '@grafana/runtime'; import { Alert, Button, Field, Input, Stack } from '@grafana/ui'; import { Folder } from 'app/api/clients/folder/v1beta1'; import { RepositoryView, useCreateRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; @@ -115,6 +115,12 @@ function FormContent({ initialValues, repository, workflowOptions, folder, onDis ref = undefined; } + reportInteraction('grafana_provisioning_folder_create_submitted', { + workflow, + repositoryName: repoName, + repositoryType: repository?.type ?? 'unknown', + }); + create({ ref, name: repoName, diff --git a/public/app/features/provisioning/utils/data.ts b/public/app/features/provisioning/utils/data.ts index 452a86d7e46..930e943d83b 100644 --- a/public/app/features/provisioning/utils/data.ts +++ b/public/app/features/provisioning/utils/data.ts @@ -2,7 +2,7 @@ import { RepositorySpec } from 'app/api/clients/provisioning/v0alpha1'; import { RepositoryFormData } from '../types'; -const getWorkflows = (data: RepositoryFormData): RepositorySpec['workflows'] => { +export const getWorkflows = (data: RepositoryFormData): RepositorySpec['workflows'] => { if (data.readOnly) { return []; }