Provisioning: Add FE analytics (#112118)

* Provisioning: Add FE analytics

* Cleanup

* More cleanup

* Add missing workflow

* Add more tracking
This commit is contained in:
Alex Khomenko
2025-10-08 14:17:05 +02:00
committed by GitHub
parent 53180d5a39
commit bb1d7d9070
15 changed files with 141 additions and 15 deletions
@@ -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);
@@ -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]);
@@ -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) {
</Button>
)}
<SyncRepository repository={repository} />
<LinkButton variant="secondary" icon="cog" href={`${PROVISIONING_URL}/${name}/edit`}>
<LinkButton
variant="secondary"
icon="cog"
href={`${PROVISIONING_URL}/${name}/edit`}
onClick={() => {
reportInteraction('grafana_provisioning_repository_settings_opened', {
repositoryName: name,
repositoryType: repository.spec?.type ?? 'unknown',
});
}}
>
<Trans i18nKey="provisioning.repository-actions.settings">Settings</Trans>
</LinkButton>
</Stack>
@@ -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) {
<Trans i18nKey="provisioning.repository-card.view">View</Trans>
</LinkButton>
<SyncRepository repository={repository} />
<LinkButton variant="secondary" icon="cog" href={`${PROVISIONING_URL}/${name}/edit`} size="md">
<LinkButton
variant="secondary"
icon="cog"
href={`${PROVISIONING_URL}/${name}/edit`}
size="md"
onClick={() => {
reportInteraction('grafana_provisioning_repository_settings_opened', {
repositoryName: name,
repositoryType: spec?.type ?? 'unknown',
});
}}
>
<Trans i18nKey="provisioning.repository-card.settings">Settings</Trans>
</LinkButton>
</Stack>
@@ -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: {
@@ -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}`);
}}
/>
);
})}
@@ -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' });
@@ -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',
@@ -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',
@@ -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;
@@ -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) {
@@ -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,
@@ -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;
@@ -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,
@@ -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 [];
}