From 580285787daa75989931cdde320e2151c04a9c5e Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Oct 2022 21:05:05 -0400 Subject: [PATCH] Add create and click analytics events to 'next' create page Signed-off-by: Eric Peterson --- .changeset/analyze-next-software-creation.md | 5 + .../TemplateWizardPage/Stepper/Stepper.tsx | 37 +++++- .../TemplateWizardPage.test.tsx | 115 ++++++++++++++++++ .../TemplateWizardPage/TemplateWizardPage.tsx | 59 ++++----- 4 files changed, 182 insertions(+), 34 deletions(-) create mode 100644 .changeset/analyze-next-software-creation.md create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx diff --git a/.changeset/analyze-next-software-creation.md b/.changeset/analyze-next-software-creation.md new file mode 100644 index 0000000000..0a4ce20e9d --- /dev/null +++ b/.changeset/analyze-next-software-creation.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +The `create` and `click` analytics events are now also captured on the "next" version of the component creation page. diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index 511b020c3c..bed2515aaf 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -13,8 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useApiHolder } from '@backstage/core-plugin-api'; -import { JsonObject, JsonValue } from '@backstage/types'; +import { + useAnalytics, + useApiHolder, + useRouteRefParams, +} from '@backstage/core-plugin-api'; +import { JsonValue } from '@backstage/types'; import { Stepper as MuiStepper, Step as MuiStep, @@ -31,6 +35,7 @@ import { createAsyncValidators } from './createAsyncValidators'; import { useTemplateSchema } from './useTemplateSchema'; import { ReviewState } from './ReviewState'; import validator from '@rjsf/validator-ajv8'; +import { selectedTemplateRouteRef } from '../../../routes'; const useStyles = makeStyles(theme => ({ backButton: { @@ -59,10 +64,12 @@ export interface StepperProps { const Form = withTheme(require('@rjsf/material-ui-v5').Theme); export const Stepper = (props: StepperProps) => { + const { templateName } = useRouteRefParams(selectedTemplateRouteRef); + const analytics = useAnalytics(); const { steps } = useTemplateSchema(props.manifest); const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); - const [formState, setFormState] = useState({}); + const [formState, setFormState] = useState>({}); const [errors, setErrors] = useState< undefined | Record >(); @@ -90,7 +97,11 @@ export const Stepper = (props: StepperProps) => { setActiveStep(prevActiveStep => prevActiveStep - 1); }; - const handleNext = async ({ formData }: { formData: JsonObject }) => { + const handleNext = async ({ + formData, + }: { + formData: Record; + }) => { // TODO(blam): What do we do about loading states, does each field extension get a chance // to display it's own loading? Or should we grey out the entire form. setErrors(undefined); @@ -105,7 +116,11 @@ export const Stepper = (props: StepperProps) => { setErrors(returnedValidation); } else { setErrors(undefined); - setActiveStep(prevActiveStep => prevActiveStep + 1); + setActiveStep(prevActiveStep => { + const stepNum = prevActiveStep + 1; + analytics.captureEvent('click', `Next Step (${stepNum})`); + return stepNum; + }); } setFormState(current => ({ ...current, ...formData })); }; @@ -160,7 +175,17 @@ export const Stepper = (props: StepperProps) => { diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx new file mode 100644 index 0000000000..f36fae9aa3 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -0,0 +1,115 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ApiProvider } from '@backstage/core-app-api'; +import { analyticsApiRef } from '@backstage/core-plugin-api'; +import { + MockAnalyticsApi, + renderInTestApp, + TestApiRegistry, +} from '@backstage/test-utils'; +import { act, fireEvent } from '@testing-library/react'; +import React from 'react'; +import { scaffolderApiRef } from '../../api'; +import { nextRouteRef, rootRouteRef } from '../../routes'; +import { ScaffolderApi } from '../../types'; +import { TemplateWizardPage } from './TemplateWizardPage'; + +jest.mock('react-router-dom', () => { + return { + ...(jest.requireActual('react-router-dom') as any), + useParams: () => ({ + templateName: 'test', + }), + }; +}); + +const scaffolderApiMock: jest.Mocked = { + scaffold: jest.fn(), + getTemplateParameterSchema: jest.fn(), + getIntegrationsList: jest.fn(), + getTask: jest.fn(), + streamLogs: jest.fn(), + listActions: jest.fn(), + listTasks: jest.fn(), +}; + +const analyticsMock = new MockAnalyticsApi(); +const apis = TestApiRegistry.from( + [scaffolderApiRef, scaffolderApiMock], + [analyticsApiRef, analyticsMock], +); + +describe('TemplateWizardPage', () => { + it('captures expected analytics events', async () => { + scaffolderApiMock.scaffold.mockResolvedValue({ taskId: 'xyz' }); + scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ + steps: [ + { + title: 'Step 1', + schema: { + properties: { + name: { + type: 'string', + }, + }, + }, + }, + ], + title: 'React JSON Schema Form Test', + }); + + const { findByRole, getByRole } = await renderInTestApp( + + , + , + { + mountedRoutes: { + '/create': nextRouteRef, + '/create-legacy': rootRouteRef, + }, + }, + ); + + // Fill out the name field + fireEvent.change(getByRole('textbox', { name: 'name' }), { + target: { value: 'expected-name' }, + }); + + // Go to the final page + await act(async () => { + fireEvent.click(await findByRole('button', { name: 'Review' })); + }); + + // Create the software + await act(async () => { + fireEvent.click(await findByRole('button', { name: 'Create' })); + }); + + // The "Next Step" button should have fired an event + 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: 'create', + subject: 'expected-name', + context: { entityRef: 'template:default/test' }, + }); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index b650e4a676..28dc806214 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -26,6 +26,7 @@ import { NextFieldExtensionOptions } from '../../extensions'; import { Navigate, useNavigate } from 'react-router'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { + AnalyticsContext, errorApiRef, useApi, useRouteRef, @@ -111,34 +112,36 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { } return ( - -
- - {loading && } - {manifest && ( - + +
+ + {loading && } + {manifest && ( + + } + noPadding + titleTypographyProps={{ component: 'h2' }} + > + - } - noPadding - titleTypographyProps={{ component: 'h2' }} - > - - - )} - - + + )} + + + ); };