mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-03 19:36:20 +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