From c96edda8568ebfabfc4d94eafaccb6ded7d1a21e Mon Sep 17 00:00:00 2001 From: markkaylor Date: Mon, 25 Aug 2025 10:35:19 +0200 Subject: [PATCH] chore: update guided tour tracking events (#24192) --- .../src/components/GuidedTour/Context.tsx | 32 ++++++++++++++--- .../src/components/GuidedTour/Overview.tsx | 34 +++++++++++++------ .../admin/admin/src/features/Tracking.tsx | 5 +-- .../tests/MarketplacePage.test.tsx | 12 ------- .../core/admin/admin/src/translations/en.json | 1 + 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/packages/core/admin/admin/src/components/GuidedTour/Context.tsx b/packages/core/admin/admin/src/components/GuidedTour/Context.tsx index 49f8d52a4a..da52964759 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Context.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Context.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { produce } from 'immer'; +import { useTracking } from '../../features/Tracking'; import { usePersistentState } from '../../hooks/usePersistentState'; import { createContext } from '../Context'; @@ -81,17 +82,23 @@ const getInitialTourState = (tours: Tours) => { }, {} as TourState); }; +const getCompletedTours = (tours: TourState): ValidTourName[] => { + return Object.keys(tours).filter( + (tourName) => tours[tourName as ValidTourName].isCompleted + ) as ValidTourName[]; +}; + +const areAllToursCompleted = (tours: TourState) => Object.values(tours).every((t) => t.isCompleted); + function reducer(state: State, action: Action): State { return produce(state, (draft) => { if (action.type === 'next_step') { const currentStep = draft.tours[action.payload].currentStep; const tourLength = guidedTours[action.payload]._meta.totalStepCount; - if (currentStep >= tourLength) return; - const nextStep = currentStep + 1; draft.tours[action.payload].currentStep = nextStep; - draft.tours[action.payload].isCompleted = nextStep === tourLength; + draft.tours[action.payload].isCompleted = nextStep >= tourLength; } if (action.type === 'previous_step') { @@ -141,6 +148,7 @@ const GuidedTourContext = ({ children: React.ReactNode; enabled?: boolean; }) => { + const { trackUsage } = useTracking(); const [storedTours, setStoredTours] = usePersistentState(STORAGE_KEY, { tours: getInitialTourState(guidedTours), enabled, @@ -154,6 +162,22 @@ const GuidedTourContext = ({ setStoredTours(state); }, [state, setStoredTours]); + // Derive all completed tours from state + const currentAllCompletedState = areAllToursCompleted(state.tours); + // Store completed state in ref to survive a re-render, + // when current state changes this will persist and be used for comparison + const previousAllCompletedStateRef = React.useRef(currentAllCompletedState); + React.useEffect(() => { + const previousAllCompletedState = previousAllCompletedStateRef.current; + // When the previous state was not complete but the current state is now complete, fire the event + if (!previousAllCompletedState && currentAllCompletedState) { + trackUsage('didCompleteGuidedTour', { name: 'all' }); + } + + // When the current state has all tours completed so will the previous state, the tracking event won't fire again + previousAllCompletedStateRef.current = currentAllCompletedState; + }, [currentAllCompletedState, trackUsage]); + return ( {children} @@ -162,4 +186,4 @@ const GuidedTourContext = ({ }; export type { Action, State, ValidTourName }; -export { GuidedTourContext, useGuidedTour, reducer }; +export { GuidedTourContext, useGuidedTour, reducer, getCompletedTours }; diff --git a/packages/core/admin/admin/src/components/GuidedTour/Overview.tsx b/packages/core/admin/admin/src/components/GuidedTour/Overview.tsx index 4a606252d6..2f5d928c28 100644 --- a/packages/core/admin/admin/src/components/GuidedTour/Overview.tsx +++ b/packages/core/admin/admin/src/components/GuidedTour/Overview.tsx @@ -10,7 +10,7 @@ import { useTracking } from '../../features/Tracking'; import { useGetGuidedTourMetaQuery } from '../../services/admin'; import { ConfirmDialog } from '../ConfirmDialog'; -import { type ValidTourName, useGuidedTour } from './Context'; +import { type ValidTourName, useGuidedTour, getCompletedTours } from './Context'; import { GUIDED_TOUR_REQUIRED_ACTIONS } from './utils/constants'; /* ------------------------------------------------------------------------------------------------- @@ -145,14 +145,14 @@ export const GuidedTourHomepageOverview = () => { const { formatMessage } = useIntl(); const { trackUsage } = useTracking(); - const tours = useGuidedTour('Overview', (s) => s.state.tours); + const tourState = useGuidedTour('Overview', (s) => s.state.tours); const dispatch = useGuidedTour('Overview', (s) => s.dispatch); const enabled = useGuidedTour('Overview', (s) => s.state.enabled); const completedActions = useGuidedTour('Overview', (s) => s.state.completedActions); const { data: guidedTourMeta } = useGetGuidedTourMetaQuery(); - const tourNames = Object.keys(tours) as ValidTourName[]; - const completedTours = tourNames.filter((tourName) => tours[tourName].isCompleted); + const tourNames = Object.keys(tourState) as ValidTourName[]; + const completedTours = getCompletedTours(tourState); const completionPercentage = tourNames.length > 0 ? Math.round((completedTours.length / tourNames.length) * 100) : 0; @@ -161,9 +161,13 @@ export const GuidedTourHomepageOverview = () => { dispatch({ type: 'skip_all_tours' }); }; - const handleClickStrapiCloud = (tourName: ValidTourName) => { - trackUsage('didCompleteGuidedTour', { name: tourName }); - dispatch({ type: 'skip_tour', payload: tourName }); + const handleStartTour = (tourName: ValidTourName) => { + trackUsage('didStartGuidedTour', { name: tourName, fromHomepage: true }); + + if (tourName === 'strapiCloud') { + trackUsage('didCompleteGuidedTour', { name: tourName }); + dispatch({ type: 'next_step', payload: tourName }); + } }; if (!guidedTourMeta?.data.isFirstSuperAdminUser || !enabled) { @@ -197,7 +201,15 @@ export const GuidedTourHomepageOverview = () => { paddingBottom={8} gap={2} > - {completionPercentage}% + + {formatMessage( + { + id: 'tours.overview.completed', + defaultMessage: '{completed}% completed', + }, + { completed: completionPercentage } + )} + @@ -229,7 +241,7 @@ export const GuidedTourHomepageOverview = () => { {TASK_CONTENT.map((task) => { const tourName = task.tourName as ValidTourName; - const tour = tours[tourName]; + const tour = tourState[tourName]; const isLinkDisabled = tourName !== 'contentTypeBuilder' && @@ -270,7 +282,7 @@ export const GuidedTourHomepageOverview = () => { isExternal disabled={isLinkDisabled} href={task.link.to} - onClick={() => handleClickStrapiCloud(task.tourName as ValidTourName)} + onClick={() => handleStartTour(task.tourName as ValidTourName)} > {formatMessage(task.link.label)} @@ -281,7 +293,7 @@ export const GuidedTourHomepageOverview = () => { to={task.link.to} tag={NavLink} onClick={() => - trackUsage('didStartGuidedTourFromHomepage', { name: tourName }) + trackUsage('didStartGuidedTour', { name: tourName, fromHomepage: true }) } > {formatMessage(task.link.label)} diff --git a/packages/core/admin/admin/src/features/Tracking.tsx b/packages/core/admin/admin/src/features/Tracking.tsx index 7db26837ca..dc6c0ac6b5 100644 --- a/packages/core/admin/admin/src/features/Tracking.tsx +++ b/packages/core/admin/admin/src/features/Tracking.tsx @@ -399,14 +399,15 @@ interface DidSkipGuidedTour { interface DidCompleteGuidedTour { name: 'didCompleteGuidedTour'; properties: { - name: keyof Tours; + name: keyof Tours | 'all'; }; } interface DidStartGuidedTour { - name: 'didStartGuidedTourFromHomepage'; + name: 'didStartGuidedTour'; properties: { name: keyof Tours; + fromHomepage?: boolean; }; } diff --git a/packages/core/admin/admin/src/pages/Marketplace/tests/MarketplacePage.test.tsx b/packages/core/admin/admin/src/pages/Marketplace/tests/MarketplacePage.test.tsx index d4f75645f3..797be05ac4 100644 --- a/packages/core/admin/admin/src/pages/Marketplace/tests/MarketplacePage.test.tsx +++ b/packages/core/admin/admin/src/pages/Marketplace/tests/MarketplacePage.test.tsx @@ -4,15 +4,10 @@ import { screen, within } from '@testing-library/react'; import { render as renderRTL, waitFor } from '@tests/utils'; import { useAppInfo } from '../../../features/AppInfo'; -import { useTracking } from '../../../features/Tracking'; import { MarketplacePage } from '../MarketplacePage'; jest.mock('../hooks/useNavigatorOnline'); -jest.mock('../../../features/Tracking', () => ({ - useTracking: jest.fn(() => ({ trackUsage: jest.fn() })), -})); - jest.mock('../../../features/AppInfo', () => ({ ...jest.requireActual('../../../features/AppInfo'), useAppInfo: jest.fn(() => ({ @@ -34,15 +29,8 @@ const waitForReload = async () => { describe('Marketplace page - layout', () => { it('renders the online layout', async () => { - const trackUsage = jest.fn(); - // @ts-expect-error - mock - useTracking.mockImplementationOnce(() => ({ trackUsage })); - const { queryByText, getByRole } = render(); await waitForReload(); - // Calls the tracking event - expect(trackUsage).toHaveBeenCalledWith('didGoToMarketplace'); - expect(trackUsage).toHaveBeenCalledTimes(1); expect(queryByText('You are offline')).not.toBeInTheDocument(); // Shows the sort button diff --git a/packages/core/admin/admin/src/translations/en.json b/packages/core/admin/admin/src/translations/en.json index cdc10ce254..f018f07f8b 100644 --- a/packages/core/admin/admin/src/translations/en.json +++ b/packages/core/admin/admin/src/translations/en.json @@ -859,6 +859,7 @@ "tours.profile.description": "You can reset the guided tour at any time.", "tours.profile.reset": "Reset guided tour", "tours.profile.notification.success.reset": "Guided tour reset", + "tours.overview.completed": "{completed}% completed", "widget.key-statistics.title": "Project statistics", "widget.key-statistics.list.admins": "Admins", "widget.key-statistics.list.apiTokens": "API Tokens",