diff --git a/.changeset/blue-hotels-shake.md b/.changeset/blue-hotels-shake.md new file mode 100644 index 0000000000..57fb806377 --- /dev/null +++ b/.changeset/blue-hotels-shake.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +--- + +Capturing more event clicks for scaffolder diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json index 99b0e0b7cb..4e34545d6d 100644 --- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json +++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json @@ -141,19 +141,25 @@ }, "presentation": { "type": "object", - "description": "A way to redefine the labels for actionable buttons.", + "description": "A way to redefine the presentation of the scaffolder.", "properties": { - "backButtonText": { - "type": "string", - "description": "A button which return the user to one step back." - }, - "createButtonText": { - "type": "string", - "description": "A button which start the execution of the template." - }, - "reviewButtonText": { - "type": "string", - "description": "A button which open the review step to verify the input prior to start the execution." + "buttonLabels": { + "type": "object", + "description": "A way to redefine the labels for actionable buttons.", + "properties": { + "backButtonText": { + "type": "string", + "description": "A button which return the user to one step back." + }, + "createButtonText": { + "type": "string", + "description": "A button which start the execution of the template." + }, + "reviewButtonText": { + "type": "string", + "description": "A button which open the review step to verify the input prior to start the execution." + } + } } } }, diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 62effbd14a..33e0f5a2a0 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -35,9 +35,8 @@ import { type FormValidation, } from './createAsyncValidators'; import { ReviewState, type ReviewStateProps } from '../ReviewState'; -import { useTemplateSchema } from '../../hooks/useTemplateSchema'; +import { useTemplateSchema, useFormDataFromQuery } from '../../hooks'; import validator from '@rjsf/validator-ajv8'; -import { useFormDataFromQuery } from '../../hooks'; import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps'; import { hasErrors } from './utils'; import * as FieldOverrides from './FieldOverrides'; @@ -115,6 +114,13 @@ export const Stepper = (stepperProps: StepperProps) => { const [errors, setErrors] = useState(); const styles = useStyles(); + const backLabel = + presentation?.buttonLabels?.backButtonText ?? backButtonText; + const createLabel = + presentation?.buttonLabels?.createButtonText ?? createButtonText; + const reviewLabel = + presentation?.buttonLabels?.reviewButtonText ?? reviewButtonText; + const extensions = useMemo(() => { return Object.fromEntries( props.extensions.map(({ name, component }) => [name, component]), @@ -150,7 +156,8 @@ export const Stepper = (stepperProps: StepperProps) => { const handleCreate = useCallback(() => { props.onCreate(formState); - }, [props, formState]); + analytics.captureEvent('click', `${createLabel}`); + }, [props, formState, analytics, createLabel]); const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts }); @@ -175,19 +182,13 @@ export const Stepper = (stepperProps: StepperProps) => { setActiveStep(prevActiveStep => { const stepNum = prevActiveStep + 1; analytics.captureEvent('click', `Next Step (${stepNum})`); + analytics.captureEvent('click', `Next Step (${stepNum})`); return stepNum; }); } setFormState(current => ({ ...current, ...formData })); }; - const backLabel = - presentation?.buttonLabels?.backButtonText ?? backButtonText; - const createLabel = - presentation?.buttonLabels?.createButtonText ?? createButtonText; - const reviewLabel = - presentation?.buttonLabels?.reviewButtonText ?? reviewButtonText; - return ( <> {isValidating && } @@ -214,7 +215,7 @@ export const Stepper = (stepperProps: StepperProps) => { ); })} - Review + ${reviewLabel}
@@ -274,7 +275,7 @@ export const Stepper = (stepperProps: StepperProps) => { className={styles.backButton} disabled={activeStep < 1} > - Back + {backLabel} diff --git a/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx b/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx index 99d9235791..1dd6a48f64 100644 --- a/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/ContextMenu.tsx @@ -28,7 +28,7 @@ import Toc from '@material-ui/icons/Toc'; import ControlPointIcon from '@material-ui/icons/ControlPoint'; import MoreVert from '@material-ui/icons/MoreVert'; import React, { useState } from 'react'; -import { useApi } from '@backstage/core-plugin-api'; +import { useAnalytics, useApi } from '@backstage/core-plugin-api'; import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; type ContextMenuProps = { @@ -61,10 +61,12 @@ export const ContextMenu = (props: ContextMenuProps) => { const pageTheme = getPageTheme({ themeId: 'website' }); const classes = useStyles({ fontColor: pageTheme.fontColor }); const scaffolderApi = useApi(scaffolderApiRef); + const analytics = useAnalytics(); const [anchorEl, setAnchorEl] = useState(); const [{ status: cancelStatus }, { execute: cancel }] = useAsync(async () => { if (taskId) { + analytics.captureEvent('cancelled', 'Template has been cancelled'); await scaffolderApi.cancelTask(taskId); } }); diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx index 3e7a3b8f52..f50b76e14a 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.tsx @@ -26,7 +26,7 @@ import { useTaskEventStream, } from '@backstage/plugin-scaffolder-react'; import { selectedTemplateRouteRef } from '../../routes'; -import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { useAnalytics, useApi, useRouteRef } from '@backstage/core-plugin-api'; import qs from 'qs'; import { ContextMenu } from './ContextMenu'; import { @@ -66,6 +66,7 @@ export const OngoingTask = (props: { const { taskId } = useParams(); const templateRouteRef = useRouteRef(selectedTemplateRouteRef); const navigate = useNavigate(); + const analytics = useAnalytics(); const scaffolderApi = useApi(scaffolderApiRef); const taskStream = useTaskEventStream(taskId!); const classes = useStyles(); @@ -113,6 +114,8 @@ export const OngoingTask = (props: { return; } + analytics.captureEvent('click', `Task has been started over`); + navigate({ pathname: templateRouteRef({ namespace, @@ -121,6 +124,7 @@ export const OngoingTask = (props: { search: `?${qs.stringify({ formData: JSON.stringify(formData) })}`, }); }, [ + analytics, navigate, taskStream.task?.spec.parameters, taskStream.task?.spec.templateInfo?.entity?.metadata, @@ -130,6 +134,7 @@ export const OngoingTask = (props: { const [{ status: cancelStatus }, { execute: triggerCancel }] = useAsync( async () => { if (taskId) { + analytics.captureEvent('cancelled', 'Template has been cancelled'); await scaffolderApi.cancelTask(taskId); } }, diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx index 269ff81271..5f112cf37d 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -129,19 +129,30 @@ describe('TemplateWizardPage', () => { fireEvent.click(await findByRole('button', { name: 'Create' })); }); - // The "Next Step" button should have fired an event + // The "Next Step" button should have fired few events expect(analyticsMock.getEvents()[0]).toMatchObject({ action: 'click', subject: 'Next Step (1)', context: { entityRef: 'template:default/test' }, }); - // And the "Create" button should have fired an event expect(analyticsMock.getEvents()[1]).toMatchObject({ + action: 'click', + subject: 'Next Step (1)', + context: { entityRef: 'template:default/test' }, + }); + + // And the "Create" button should have fired few event + expect(analyticsMock.getEvents()[2]).toMatchObject({ action: 'create', subject: 'expected-name', context: { entityRef: 'template:default/test' }, - value: 120, + }); + + expect(analyticsMock.getEvents()[3]).toMatchObject({ + action: 'click', + subject: 'Create', + context: { entityRef: 'template:default/test' }, }); }); describe('scaffolder page context menu', () => {