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 { GetGuidedTourMeta } from '../../../../shared/contracts/admin';
import { useTracking } from '../../features/Tracking';
import { usePersistentState } from '../../hooks/usePersistentState';
import { createContext } from '../Context';
@ -71,7 +72,6 @@ function reducer(state: State, action: Action): State {
}
const STORAGE_KEY = 'STRAPI_GUIDED_TOUR';
const UnstableGuidedTourContext = ({
children,
enabled = true,

View File

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

View File

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

View File

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