future: Guided Tour Tracking (#23926)

Co-authored-by: Mark Kaylor <mark.kaylor@strapi.io>
This commit is contained in:
Simone 2025-07-16 15:29:21 +02:00 committed by GitHub
parent a970fea681
commit fc585c658c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 76 additions and 25 deletions

View File

@ -3,6 +3,7 @@ import * as React from 'react';
import { produce } from 'immer'; import { produce } from 'immer';
import { GetGuidedTourMeta } from '../../../../shared/contracts/admin'; import { GetGuidedTourMeta } from '../../../../shared/contracts/admin';
import { useTracking } from '../../features/Tracking';
import { usePersistentState } from '../../hooks/usePersistentState'; import { usePersistentState } from '../../hooks/usePersistentState';
import { createContext } from '../Context'; import { createContext } from '../Context';
@ -71,7 +72,6 @@ function reducer(state: State, action: Action): State {
} }
const STORAGE_KEY = 'STRAPI_GUIDED_TOUR'; const STORAGE_KEY = 'STRAPI_GUIDED_TOUR';
const UnstableGuidedTourContext = ({ const UnstableGuidedTourContext = ({
children, children,
enabled = true, enabled = true,

View File

@ -4,6 +4,7 @@ import { useIntl } from 'react-intl';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { styled, useTheme } from 'styled-components'; import { styled, useTheme } from 'styled-components';
import { useTracking } from '../../features/Tracking';
import { useGetGuidedTourMetaQuery } from '../../services/admin'; import { useGetGuidedTourMetaQuery } from '../../services/admin';
import { ConfirmDialog } from '../ConfirmDialog'; import { ConfirmDialog } from '../ConfirmDialog';
@ -144,6 +145,7 @@ export const UnstableGuidedTourOverview = () => {
const enabled = unstableUseGuidedTour('Overview', (s) => s.state.enabled); const enabled = unstableUseGuidedTour('Overview', (s) => s.state.enabled);
const { data: guidedTourMeta } = useGetGuidedTourMetaQuery(); const { data: guidedTourMeta } = useGetGuidedTourMetaQuery();
const tourNames = Object.keys(tours) as ValidTourName[]; const tourNames = Object.keys(tours) as ValidTourName[];
const { trackUsage } = useTracking();
const completedTours = tourNames.filter((tourName) => tours[tourName].isCompleted); const completedTours = tourNames.filter((tourName) => tours[tourName].isCompleted);
const completionPercentage = const completionPercentage =
@ -153,6 +155,16 @@ export const UnstableGuidedTourOverview = () => {
return null; return null;
} }
const handleConfirmDialog = () => {
trackUsage('didSkipGuidedTour', { name: 'all' });
dispatch({ type: 'skip_all_tours' });
};
const handleClickOnLink = (tourName: ValidTourName) => {
trackUsage('didCompleteGuidedTour', { name: tourName });
dispatch({ type: 'skip_tour', payload: tourName });
};
return ( return (
<Container tag="section" gap={0}> <Container tag="section" gap={0}>
{/* Greeting */} {/* Greeting */}
@ -192,7 +204,7 @@ export const UnstableGuidedTourOverview = () => {
})} })}
</Button> </Button>
</Dialog.Trigger> </Dialog.Trigger>
<ConfirmDialog onConfirm={() => dispatch({ type: 'skip_all_tours' })}> <ConfirmDialog onConfirm={handleConfirmDialog}>
{formatMessage({ {formatMessage({
id: 'tours.overview.close.description', id: 'tours.overview.close.description',
defaultMessage: 'Are you sure you want to close the guided tour?', defaultMessage: 'Are you sure you want to close the guided tour?',
@ -211,14 +223,11 @@ export const UnstableGuidedTourOverview = () => {
</Typography> </Typography>
<Box width="100%" borderColor="neutral150" marginTop={4} hasRadius> <Box width="100%" borderColor="neutral150" marginTop={4} hasRadius>
{TASK_CONTENT.map((task) => { {TASK_CONTENT.map((task) => {
const tour = tours[task.tourName as ValidTourName]; const tourName = task.tourName as ValidTourName;
const tour = tours[tourName];
return ( return (
<TourTaskContainer <TourTaskContainer key={tourName} alignItems="center" justifyContent="space-between">
key={task.tourName}
alignItems="center"
justifyContent="space-between"
>
{tour.isCompleted ? ( {tour.isCompleted ? (
<> <>
<Flex gap={2}> <Flex gap={2}>
@ -243,14 +252,19 @@ export const UnstableGuidedTourOverview = () => {
<Link <Link
isExternal isExternal
href={task.link.to} href={task.link.to}
onClick={() => onClick={() => handleClickOnLink(task.tourName as ValidTourName)}
dispatch({ type: 'skip_tour', payload: task.tourName as ValidTourName })
}
> >
{formatMessage(task.link.label)} {formatMessage(task.link.label)}
</Link> </Link>
) : ( ) : (
<Link endIcon={<ChevronRight />} to={task.link.to} tag={NavLink}> <Link
endIcon={<ChevronRight />}
to={task.link.to}
tag={NavLink}
onClick={() =>
trackUsage('didStartGuidedTourFromHomepage', { name: tourName })
}
>
{formatMessage(task.link.label)} {formatMessage(task.link.label)}
</Link> </Link>
)} )}

View File

@ -13,6 +13,8 @@ import { FormattedMessage, type MessageDescriptor } from 'react-intl';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { styled } from 'styled-components'; import { styled } from 'styled-components';
import { type EventWithoutProperties, useTracking } from '../../features/Tracking';
import { unstableUseGuidedTour, ValidTourName } from './Context'; import { unstableUseGuidedTour, ValidTourName } from './Context';
/* ------------------------------------------------------------------------------------------------- /* -------------------------------------------------------------------------------------------------
@ -137,7 +139,23 @@ const createStepComponents = (tourName: ValidTourName): Step => ({
), ),
Actions: ({ showStepCount = true, showSkip = false, to, children, ...flexProps }) => { Actions: ({ showStepCount = true, showSkip = false, to, children, ...flexProps }) => {
const { trackUsage } = useTracking();
const dispatch = unstableUseGuidedTour('GuidedTourPopover', (s) => s.dispatch); const dispatch = unstableUseGuidedTour('GuidedTourPopover', (s) => s.dispatch);
const state = unstableUseGuidedTour('GuidedTourPopover', (s) => s.state);
const currentStep = state.tours[tourName].currentStep + 1;
const actualTourLength = state.tours[tourName].length;
const handleSkipAction = () => {
trackUsage('didSkipGuidedTour', { name: tourName });
dispatch({ type: 'skip_tour', payload: tourName });
};
const handleNextStep = () => {
if (currentStep === actualTourLength) {
trackUsage('didCompleteGuidedTour', { name: tourName });
}
dispatch({ type: 'next_step', payload: tourName });
};
return ( return (
<ActionsContainer <ActionsContainer
@ -154,23 +172,16 @@ const createStepComponents = (tourName: ValidTourName): Step => ({
{showStepCount && <StepCount tourName={tourName} />} {showStepCount && <StepCount tourName={tourName} />}
<Flex gap={2}> <Flex gap={2}>
{showSkip && ( {showSkip && (
<Button <Button variant="tertiary" onClick={handleSkipAction}>
variant="tertiary"
onClick={() => dispatch({ type: 'skip_tour', payload: tourName })}
>
<FormattedMessage id="tours.skip" defaultMessage="Skip" /> <FormattedMessage id="tours.skip" defaultMessage="Skip" />
</Button> </Button>
)} )}
{to ? ( {to ? (
<LinkButton <LinkButton tag={NavLink} to={to} onClick={handleNextStep}>
tag={NavLink}
to={to}
onClick={() => dispatch({ type: 'next_step', payload: tourName })}
>
<FormattedMessage id="tours.next" defaultMessage="Next" /> <FormattedMessage id="tours.next" defaultMessage="Next" />
</LinkButton> </LinkButton>
) : ( ) : (
<Button onClick={() => dispatch({ type: 'next_step', payload: tourName })}> <Button onClick={handleNextStep}>
<FormattedMessage id="tours.next" defaultMessage="Next" /> <FormattedMessage id="tours.next" defaultMessage="Next" />
</Button> </Button>
)} )}

View File

@ -2,6 +2,7 @@ import * as React from 'react';
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { Tours } from '../components/UnstableGuidedTour/Tours';
import { useInitQuery, useTelemetryPropertiesQuery } from '../services/admin'; import { useInitQuery, useTelemetryPropertiesQuery } from '../services/admin';
import { useAppInfo } from './AppInfo'; import { useAppInfo } from './AppInfo';
@ -67,7 +68,7 @@ const TrackingProvider = ({ children }: TrackingProviderProps) => {
* Meanwhile those with properties have different property shapes corresponding to the specific * Meanwhile those with properties have different property shapes corresponding to the specific
* event so understanding which properties go with which event is very helpful. * event so understanding which properties go with which event is very helpful.
*/ */
interface EventWithoutProperties { export interface EventWithoutProperties {
name: name:
| 'changeComponentsOrder' | 'changeComponentsOrder'
| 'didAddComponentToDynamicZone' | 'didAddComponentToDynamicZone'
@ -161,7 +162,8 @@ interface EventWithoutProperties {
| 'willSaveContentType' | 'willSaveContentType'
| 'willSaveContentTypeLayout' | 'willSaveContentTypeLayout'
| 'didEditFieldNameOnContentType' | 'didEditFieldNameOnContentType'
| 'didCreateRelease'; | 'didCreateRelease'
| 'didLaunchGuidedtour';
properties?: never; properties?: never;
} }
@ -357,6 +359,27 @@ interface DidUpdateCTBSchema {
}; };
} }
interface DidSkipGuidedTour {
name: 'didSkipGuidedTour';
properties: {
name: keyof Tours | 'all';
};
}
interface DidCompleteGuidedTour {
name: 'didCompleteGuidedTour';
properties: {
name: keyof Tours;
};
}
interface DidStartGuidedTour {
name: 'didStartGuidedTourFromHomepage';
properties: {
name: keyof Tours;
};
}
type EventsWithProperties = type EventsWithProperties =
| CreateEntryEvents | CreateEntryEvents
| PublishEntryEvents | PublishEntryEvents
@ -379,7 +402,10 @@ type EventsWithProperties =
| WillNavigateEvent | WillNavigateEvent
| DidPublishRelease | DidPublishRelease
| MediaEvents | MediaEvents
| DidUpdateCTBSchema; | DidUpdateCTBSchema
| DidSkipGuidedTour
| DidCompleteGuidedTour
| DidStartGuidedTour;
export type TrackingEvent = EventWithoutProperties | EventsWithProperties; export type TrackingEvent = EventWithoutProperties | EventsWithProperties;
export interface UseTrackingReturn { export interface UseTrackingReturn {