From eb47bbeef91dc3763fc1352953e36b1dc5a8c0eb Mon Sep 17 00:00:00 2001 From: Christos Date: Fri, 20 Sep 2024 16:55:45 +0300 Subject: [PATCH] Stepper: Handle `calypso_signup_actions_complete_step` tracking (#94528) --- client/landing/stepper/constants.ts | 6 +++- .../analytics/record-step-complete.ts | 20 ++++++++++++ .../hooks/use-step-route-tracking/index.tsx | 27 +++++++++++++++- .../components/step-route/test/index.tsx | 31 ++++++++++++++++++- .../declarative-flow/internals/index.tsx | 1 + 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 client/landing/stepper/declarative-flow/internals/analytics/record-step-complete.ts diff --git a/client/landing/stepper/constants.ts b/client/landing/stepper/constants.ts index 46345e3ba310ab..afbe9bc4de2285 100644 --- a/client/landing/stepper/constants.ts +++ b/client/landing/stepper/constants.ts @@ -25,6 +25,7 @@ export const STEPPER_TRACKS_EVENT_STEP_NAV_GO_BACK = 'calypso_signup_step_nav_ba export const STEPPER_TRACKS_EVENT_STEP_NAV_GO_NEXT = 'calypso_signup_step_nav_next'; export const STEPPER_TRACKS_EVENT_STEP_NAV_GO_TO = 'calypso_signup_step_nav_go_to'; export const STEPPER_TRACKS_EVENT_STEP_NAV_EXIT_FLOW = 'calypso_signup_step_nav_exit_flow'; +export const STEPPER_TRACKS_EVENT_STEP_COMPLETE = 'calypso_signup_actions_complete_step'; export const STEPPER_TRACKS_EVENTS_STEP_NAV = < const >[ STEPPER_TRACKS_EVENT_STEP_NAV_SUBMIT, @@ -34,4 +35,7 @@ export const STEPPER_TRACKS_EVENTS_STEP_NAV = < const >[ STEPPER_TRACKS_EVENT_STEP_NAV_EXIT_FLOW, ]; -export const STEPPER_TRACKS_EVENTS = < const >[ ...STEPPER_TRACKS_EVENTS_STEP_NAV ]; +export const STEPPER_TRACKS_EVENTS = < const >[ + ...STEPPER_TRACKS_EVENTS_STEP_NAV, + STEPPER_TRACKS_EVENT_STEP_COMPLETE, +]; diff --git a/client/landing/stepper/declarative-flow/internals/analytics/record-step-complete.ts b/client/landing/stepper/declarative-flow/internals/analytics/record-step-complete.ts new file mode 100644 index 00000000000000..1a09977b9ce45d --- /dev/null +++ b/client/landing/stepper/declarative-flow/internals/analytics/record-step-complete.ts @@ -0,0 +1,20 @@ +import { recordTracksEvent } from '@automattic/calypso-analytics'; +import { resolveDeviceTypeByViewPort } from '@automattic/viewport'; +import { STEPPER_TRACKS_EVENT_STEP_COMPLETE } from 'calypso/landing/stepper/constants'; + +export interface RecordStepCompleteProps { + flow: string; + step: string; + optionalProps?: Record< string, string | number | null >; +} + +const recordStepComplete = ( { flow, step, optionalProps }: RecordStepCompleteProps ) => { + recordTracksEvent( STEPPER_TRACKS_EVENT_STEP_COMPLETE, { + flow, + step, + device: resolveDeviceTypeByViewPort(), + ...optionalProps, + } ); +}; + +export default recordStepComplete; diff --git a/client/landing/stepper/declarative-flow/internals/components/step-route/hooks/use-step-route-tracking/index.tsx b/client/landing/stepper/declarative-flow/internals/components/step-route/hooks/use-step-route-tracking/index.tsx index 55f53cc9f731de..91571b0bf6fa4b 100644 --- a/client/landing/stepper/declarative-flow/internals/components/step-route/hooks/use-step-route-tracking/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/components/step-route/hooks/use-step-route-tracking/index.tsx @@ -2,9 +2,12 @@ import { SiteDetails } from '@automattic/data-stores'; import { isAnyHostingFlow } from '@automattic/onboarding'; -import { useEffect } from 'react'; +import { useEffect, useRef } from '@wordpress/element'; import { getStepOldSlug } from 'calypso/landing/stepper/declarative-flow/helpers/get-step-old-slug'; import { getAssemblerSource } from 'calypso/landing/stepper/declarative-flow/internals/analytics/record-design'; +import recordStepComplete, { + type RecordStepCompleteProps, +} from 'calypso/landing/stepper/declarative-flow/internals/analytics/record-step-complete'; import recordStepStart from 'calypso/landing/stepper/declarative-flow/internals/analytics/record-step-start'; import { useIntent } from 'calypso/landing/stepper/hooks/use-intent'; import { useSelectedDesign } from 'calypso/landing/stepper/hooks/use-selected-design'; @@ -46,6 +49,21 @@ export const useStepRouteTracking = ( { const { site, siteSlugOrId } = useSiteData(); const isRequestingSelectedSite = useIsRequestingSelectedSite( siteSlugOrId, site ); const hasRequestedSelectedSite = siteSlugOrId ? !! site && ! isRequestingSelectedSite : true; + const stepCompleteEventPropsRef = useRef< RecordStepCompleteProps | null >( null ); + + /** + * Cleanup effect to record step-complete event when `StepRoute` unmounts. + * This is to ensure that the event is recorded when the user navigates away from the step. + * We only record this if step-start event gets recorded and `stepCompleteEventPropsRef.current` is populated (as a result). + */ + useEffect( () => { + return () => { + if ( stepCompleteEventPropsRef.current ) { + recordStepComplete( stepCompleteEventPropsRef.current ); + } + }; + // IMPORTANT: Do not add dependencies to this effect, as it should only record when the component unmounts. + }, [] ); useEffect( () => { // We record the event only when the step is not empty. Additionally, we should not fire this event whenever the intent is changed @@ -67,6 +85,13 @@ export const useStepRouteTracking = ( { ...( flowVariantSlug && { flow_variant: flowVariantSlug } ), } ); + // Apply the props to record in the exit/step-complete event. We only record this if start event gets recorded. + stepCompleteEventPropsRef.current = { + flow: flowName, + step: stepSlug, + optionalProps: { intent }, + }; + const stepOldSlug = getStepOldSlug( stepSlug ); if ( stepOldSlug ) { diff --git a/client/landing/stepper/declarative-flow/internals/components/step-route/test/index.tsx b/client/landing/stepper/declarative-flow/internals/components/step-route/test/index.tsx index 7861be0e0f05e1..d6ad94515fbb2d 100644 --- a/client/landing/stepper/declarative-flow/internals/components/step-route/test/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/components/step-route/test/index.tsx @@ -6,6 +6,7 @@ import { waitFor } from '@testing-library/react'; import { addQueryArgs } from '@wordpress/url'; import React, { FC, Suspense } from 'react'; import { MemoryRouter } from 'react-router'; +import recordStepComplete from 'calypso/landing/stepper/declarative-flow/internals/analytics/record-step-complete'; import recordStepStart from 'calypso/landing/stepper/declarative-flow/internals/analytics/record-step-start'; import { useIntent } from 'calypso/landing/stepper/hooks/use-intent'; import { useSelectedDesign } from 'calypso/landing/stepper/hooks/use-selected-design'; @@ -17,16 +18,17 @@ import { import { isUserLoggedIn } from 'calypso/state/current-user/selectors'; import { renderWithProvider } from 'calypso/test-helpers/testing-library'; import StepRoute from '../'; -import type { NavigationControls } from '../../../types'; import type { Flow, StepperStep, StepProps, + NavigationControls, } from 'calypso/landing/stepper/declarative-flow/internals/types'; jest.mock( 'calypso/signup/storageUtils' ); jest.mock( 'calypso/state/current-user/selectors' ); jest.mock( 'calypso/landing/stepper/declarative-flow/internals/analytics/record-step-start' ); +jest.mock( 'calypso/landing/stepper/declarative-flow/internals/analytics/record-step-complete' ); jest.mock( 'calypso/landing/stepper/hooks/use-intent' ); jest.mock( 'calypso/landing/stepper/hooks/use-selected-design' ); jest.mock( 'calypso/lib/analytics/page-view' ); @@ -191,5 +193,32 @@ describe( 'StepRoute', () => { expect( recordStepStart ).not.toHaveBeenCalled(); expect( recordPageView ).not.toHaveBeenCalled(); } ); + + it( 'tracks step-complete when the step is unmounted and step-start was previously recorded', () => { + ( isUserLoggedIn as jest.Mock ).mockReturnValue( true ); + ( getSignupCompleteFlowNameAndClear as jest.Mock ).mockReturnValue( 'some-other-flow' ); + ( getSignupCompleteStepNameAndClear as jest.Mock ).mockReturnValue( 'some-other-step-slug' ); + const { unmount } = render( { step: regularStep } ); + + expect( recordStepComplete ).not.toHaveBeenCalled(); + unmount(); + expect( recordStepComplete ).toHaveBeenCalledWith( { + step: 'some-step-slug', + flow: 'some-flow', + optionalProps: { + intent: 'build', + }, + } ); + } ); + + it( 'skips tracking step-complete when the step is unmounted and step-start was not recorded', () => { + ( getSignupCompleteFlowNameAndClear as jest.Mock ).mockReturnValue( 'some-flow' ); + ( getSignupCompleteStepNameAndClear as jest.Mock ).mockReturnValue( 'some-step-slug' ); + const { unmount } = render( { step: regularStep } ); + + expect( recordStepStart ).not.toHaveBeenCalled(); + unmount(); + expect( recordStepComplete ).not.toHaveBeenCalled(); + } ); } ); } ); diff --git a/client/landing/stepper/declarative-flow/internals/index.tsx b/client/landing/stepper/declarative-flow/internals/index.tsx index 140751ef60fee7..f7ce8fdbd9f258 100644 --- a/client/landing/stepper/declarative-flow/internals/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/index.tsx @@ -209,6 +209,7 @@ export const FlowRenderer: React.FC< { flow: Flow } > = ( { flow } ) => { path={ `/${ flow.variantSlug ?? flow.name }/${ step.slug }/:lang?` } element={