mirror of
https://github.com/strapi/strapi.git
synced 2025-08-28 10:45:51 +00:00
chore: update guided tour tracking events (#24192)
This commit is contained in:
parent
6a38f974ec
commit
c96edda856
@ -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 };
|
||||
|
@ -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)}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user