mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 03:17:11 +00:00
future: Guided Tour Tracking (#23926)
Co-authored-by: Mark Kaylor <mark.kaylor@strapi.io>
This commit is contained in:
parent
a970fea681
commit
fc585c658c
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user