chore: update guided tour tracking events (#24192)

This commit is contained in:
markkaylor 2025-08-25 10:35:19 +02:00 committed by GitHub
parent 6a38f974ec
commit c96edda856
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 55 additions and 29 deletions

View File

@ -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<State>(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 (
<GuidedTourProviderImpl state={state} dispatch={dispatch}>
{children}
@ -162,4 +186,4 @@ const GuidedTourContext = ({
};
export type { Action, State, ValidTourName };
export { GuidedTourContext, useGuidedTour, reducer };
export { GuidedTourContext, useGuidedTour, reducer, getCompletedTours };

View File

@ -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}
>
<Typography variant="pi">{completionPercentage}%</Typography>
<Typography variant="pi">
{formatMessage(
{
id: 'tours.overview.completed',
defaultMessage: '{completed}% completed',
},
{ completed: completionPercentage }
)}
</Typography>
<StyledProgressBar value={completionPercentage} />
</Flex>
<Dialog.Root>
@ -229,7 +241,7 @@ export const GuidedTourHomepageOverview = () => {
<Box tag="ul" width="100%" borderColor="neutral150" marginTop={4} hasRadius>
{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)}
</Link>
@ -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)}

View File

@ -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;
};
}

View File

@ -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

View File

@ -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",