mirror of
https://github.com/strapi/strapi.git
synced 2025-08-29 11:15:55 +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 { produce } from 'immer';
|
||||||
|
|
||||||
|
import { useTracking } from '../../features/Tracking';
|
||||||
import { usePersistentState } from '../../hooks/usePersistentState';
|
import { usePersistentState } from '../../hooks/usePersistentState';
|
||||||
import { createContext } from '../Context';
|
import { createContext } from '../Context';
|
||||||
|
|
||||||
@ -81,17 +82,23 @@ const getInitialTourState = (tours: Tours) => {
|
|||||||
}, {} as TourState);
|
}, {} 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 {
|
function reducer(state: State, action: Action): State {
|
||||||
return produce(state, (draft) => {
|
return produce(state, (draft) => {
|
||||||
if (action.type === 'next_step') {
|
if (action.type === 'next_step') {
|
||||||
const currentStep = draft.tours[action.payload].currentStep;
|
const currentStep = draft.tours[action.payload].currentStep;
|
||||||
const tourLength = guidedTours[action.payload]._meta.totalStepCount;
|
const tourLength = guidedTours[action.payload]._meta.totalStepCount;
|
||||||
|
|
||||||
if (currentStep >= tourLength) return;
|
|
||||||
|
|
||||||
const nextStep = currentStep + 1;
|
const nextStep = currentStep + 1;
|
||||||
draft.tours[action.payload].currentStep = nextStep;
|
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') {
|
if (action.type === 'previous_step') {
|
||||||
@ -141,6 +148,7 @@ const GuidedTourContext = ({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { trackUsage } = useTracking();
|
||||||
const [storedTours, setStoredTours] = usePersistentState<State>(STORAGE_KEY, {
|
const [storedTours, setStoredTours] = usePersistentState<State>(STORAGE_KEY, {
|
||||||
tours: getInitialTourState(guidedTours),
|
tours: getInitialTourState(guidedTours),
|
||||||
enabled,
|
enabled,
|
||||||
@ -154,6 +162,22 @@ const GuidedTourContext = ({
|
|||||||
setStoredTours(state);
|
setStoredTours(state);
|
||||||
}, [state, setStoredTours]);
|
}, [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 (
|
return (
|
||||||
<GuidedTourProviderImpl state={state} dispatch={dispatch}>
|
<GuidedTourProviderImpl state={state} dispatch={dispatch}>
|
||||||
{children}
|
{children}
|
||||||
@ -162,4 +186,4 @@ const GuidedTourContext = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type { Action, State, ValidTourName };
|
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 { useGetGuidedTourMetaQuery } from '../../services/admin';
|
||||||
import { ConfirmDialog } from '../ConfirmDialog';
|
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';
|
import { GUIDED_TOUR_REQUIRED_ACTIONS } from './utils/constants';
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------------------------------
|
/* -------------------------------------------------------------------------------------------------
|
||||||
@ -145,14 +145,14 @@ export const GuidedTourHomepageOverview = () => {
|
|||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { trackUsage } = useTracking();
|
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 dispatch = useGuidedTour('Overview', (s) => s.dispatch);
|
||||||
const enabled = useGuidedTour('Overview', (s) => s.state.enabled);
|
const enabled = useGuidedTour('Overview', (s) => s.state.enabled);
|
||||||
const completedActions = useGuidedTour('Overview', (s) => s.state.completedActions);
|
const completedActions = useGuidedTour('Overview', (s) => s.state.completedActions);
|
||||||
const { data: guidedTourMeta } = useGetGuidedTourMetaQuery();
|
const { data: guidedTourMeta } = useGetGuidedTourMetaQuery();
|
||||||
|
|
||||||
const tourNames = Object.keys(tours) as ValidTourName[];
|
const tourNames = Object.keys(tourState) as ValidTourName[];
|
||||||
const completedTours = tourNames.filter((tourName) => tours[tourName].isCompleted);
|
const completedTours = getCompletedTours(tourState);
|
||||||
const completionPercentage =
|
const completionPercentage =
|
||||||
tourNames.length > 0 ? Math.round((completedTours.length / tourNames.length) * 100) : 0;
|
tourNames.length > 0 ? Math.round((completedTours.length / tourNames.length) * 100) : 0;
|
||||||
|
|
||||||
@ -161,9 +161,13 @@ export const GuidedTourHomepageOverview = () => {
|
|||||||
dispatch({ type: 'skip_all_tours' });
|
dispatch({ type: 'skip_all_tours' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickStrapiCloud = (tourName: ValidTourName) => {
|
const handleStartTour = (tourName: ValidTourName) => {
|
||||||
|
trackUsage('didStartGuidedTour', { name: tourName, fromHomepage: true });
|
||||||
|
|
||||||
|
if (tourName === 'strapiCloud') {
|
||||||
trackUsage('didCompleteGuidedTour', { name: tourName });
|
trackUsage('didCompleteGuidedTour', { name: tourName });
|
||||||
dispatch({ type: 'skip_tour', payload: tourName });
|
dispatch({ type: 'next_step', payload: tourName });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!guidedTourMeta?.data.isFirstSuperAdminUser || !enabled) {
|
if (!guidedTourMeta?.data.isFirstSuperAdminUser || !enabled) {
|
||||||
@ -197,7 +201,15 @@ export const GuidedTourHomepageOverview = () => {
|
|||||||
paddingBottom={8}
|
paddingBottom={8}
|
||||||
gap={2}
|
gap={2}
|
||||||
>
|
>
|
||||||
<Typography variant="pi">{completionPercentage}%</Typography>
|
<Typography variant="pi">
|
||||||
|
{formatMessage(
|
||||||
|
{
|
||||||
|
id: 'tours.overview.completed',
|
||||||
|
defaultMessage: '{completed}% completed',
|
||||||
|
},
|
||||||
|
{ completed: completionPercentage }
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
<StyledProgressBar value={completionPercentage} />
|
<StyledProgressBar value={completionPercentage} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<Dialog.Root>
|
<Dialog.Root>
|
||||||
@ -229,7 +241,7 @@ export const GuidedTourHomepageOverview = () => {
|
|||||||
<Box tag="ul" width="100%" borderColor="neutral150" marginTop={4} hasRadius>
|
<Box tag="ul" width="100%" borderColor="neutral150" marginTop={4} hasRadius>
|
||||||
{TASK_CONTENT.map((task) => {
|
{TASK_CONTENT.map((task) => {
|
||||||
const tourName = task.tourName as ValidTourName;
|
const tourName = task.tourName as ValidTourName;
|
||||||
const tour = tours[tourName];
|
const tour = tourState[tourName];
|
||||||
|
|
||||||
const isLinkDisabled =
|
const isLinkDisabled =
|
||||||
tourName !== 'contentTypeBuilder' &&
|
tourName !== 'contentTypeBuilder' &&
|
||||||
@ -270,7 +282,7 @@ export const GuidedTourHomepageOverview = () => {
|
|||||||
isExternal
|
isExternal
|
||||||
disabled={isLinkDisabled}
|
disabled={isLinkDisabled}
|
||||||
href={task.link.to}
|
href={task.link.to}
|
||||||
onClick={() => handleClickStrapiCloud(task.tourName as ValidTourName)}
|
onClick={() => handleStartTour(task.tourName as ValidTourName)}
|
||||||
>
|
>
|
||||||
{formatMessage(task.link.label)}
|
{formatMessage(task.link.label)}
|
||||||
</Link>
|
</Link>
|
||||||
@ -281,7 +293,7 @@ export const GuidedTourHomepageOverview = () => {
|
|||||||
to={task.link.to}
|
to={task.link.to}
|
||||||
tag={NavLink}
|
tag={NavLink}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
trackUsage('didStartGuidedTourFromHomepage', { name: tourName })
|
trackUsage('didStartGuidedTour', { name: tourName, fromHomepage: true })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{formatMessage(task.link.label)}
|
{formatMessage(task.link.label)}
|
||||||
|
@ -399,14 +399,15 @@ interface DidSkipGuidedTour {
|
|||||||
interface DidCompleteGuidedTour {
|
interface DidCompleteGuidedTour {
|
||||||
name: 'didCompleteGuidedTour';
|
name: 'didCompleteGuidedTour';
|
||||||
properties: {
|
properties: {
|
||||||
name: keyof Tours;
|
name: keyof Tours | 'all';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DidStartGuidedTour {
|
interface DidStartGuidedTour {
|
||||||
name: 'didStartGuidedTourFromHomepage';
|
name: 'didStartGuidedTour';
|
||||||
properties: {
|
properties: {
|
||||||
name: keyof Tours;
|
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 { render as renderRTL, waitFor } from '@tests/utils';
|
||||||
|
|
||||||
import { useAppInfo } from '../../../features/AppInfo';
|
import { useAppInfo } from '../../../features/AppInfo';
|
||||||
import { useTracking } from '../../../features/Tracking';
|
|
||||||
import { MarketplacePage } from '../MarketplacePage';
|
import { MarketplacePage } from '../MarketplacePage';
|
||||||
|
|
||||||
jest.mock('../hooks/useNavigatorOnline');
|
jest.mock('../hooks/useNavigatorOnline');
|
||||||
|
|
||||||
jest.mock('../../../features/Tracking', () => ({
|
|
||||||
useTracking: jest.fn(() => ({ trackUsage: jest.fn() })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('../../../features/AppInfo', () => ({
|
jest.mock('../../../features/AppInfo', () => ({
|
||||||
...jest.requireActual('../../../features/AppInfo'),
|
...jest.requireActual('../../../features/AppInfo'),
|
||||||
useAppInfo: jest.fn(() => ({
|
useAppInfo: jest.fn(() => ({
|
||||||
@ -34,15 +29,8 @@ const waitForReload = async () => {
|
|||||||
|
|
||||||
describe('Marketplace page - layout', () => {
|
describe('Marketplace page - layout', () => {
|
||||||
it('renders the online layout', async () => {
|
it('renders the online layout', async () => {
|
||||||
const trackUsage = jest.fn();
|
|
||||||
// @ts-expect-error - mock
|
|
||||||
useTracking.mockImplementationOnce(() => ({ trackUsage }));
|
|
||||||
|
|
||||||
const { queryByText, getByRole } = render();
|
const { queryByText, getByRole } = render();
|
||||||
await waitForReload();
|
await waitForReload();
|
||||||
// Calls the tracking event
|
|
||||||
expect(trackUsage).toHaveBeenCalledWith('didGoToMarketplace');
|
|
||||||
expect(trackUsage).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(queryByText('You are offline')).not.toBeInTheDocument();
|
expect(queryByText('You are offline')).not.toBeInTheDocument();
|
||||||
// Shows the sort button
|
// Shows the sort button
|
||||||
|
@ -859,6 +859,7 @@
|
|||||||
"tours.profile.description": "You can reset the guided tour at any time.",
|
"tours.profile.description": "You can reset the guided tour at any time.",
|
||||||
"tours.profile.reset": "Reset guided tour",
|
"tours.profile.reset": "Reset guided tour",
|
||||||
"tours.profile.notification.success.reset": "Guided tour reset",
|
"tours.profile.notification.success.reset": "Guided tour reset",
|
||||||
|
"tours.overview.completed": "{completed}% completed",
|
||||||
"widget.key-statistics.title": "Project statistics",
|
"widget.key-statistics.title": "Project statistics",
|
||||||
"widget.key-statistics.list.admins": "Admins",
|
"widget.key-statistics.list.admins": "Admins",
|
||||||
"widget.key-statistics.list.apiTokens": "API Tokens",
|
"widget.key-statistics.list.apiTokens": "API Tokens",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user