diff --git a/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Public.tsx b/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Public.tsx index 291978f97c..8516dcd342 100644 --- a/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Public.tsx +++ b/editor.planx.uk/src/@planx/components/FileUploadAndLabel/Public.tsx @@ -286,13 +286,13 @@ interface FileListItemProps { const InteractiveFileListItem = (props: FileListItemProps) => { const [open, setOpen] = React.useState(false); - const { trackHelpClick } = useAnalyticsTracking(); + const { trackEvent } = useAnalyticsTracking(); const { info, policyRef, howMeasured, definitionImg } = props.moreInformation || {}; const handleHelpClick = (metadata: HelpClickMetadata) => { setOpen(true); - trackHelpClick(metadata); // This returns a promise but we don't need to await for it + trackEvent({ event: "helpClick", metadata }); // This returns a promise but we don't need to await for it }; return ( diff --git a/editor.planx.uk/src/@planx/components/Notice/Public.tsx b/editor.planx.uk/src/@planx/components/Notice/Public.tsx index 4228161e44..ea615f698c 100644 --- a/editor.planx.uk/src/@planx/components/Notice/Public.tsx +++ b/editor.planx.uk/src/@planx/components/Notice/Public.tsx @@ -80,10 +80,14 @@ const NoticeComponent: React.FC = (props) => { ? () => props.handleSubmit?.() : undefined; - const { trackFlowDirectionChange } = useAnalyticsTracking(); + const { trackEvent } = useAnalyticsTracking(); const handleNoticeResetClick = () => { - trackFlowDirectionChange("reset"); + trackEvent({ + event: "flowDirectionChange", + metadata: null, + flowDirection: "reset", + }); props.resetPreview && props.resetPreview(); }; diff --git a/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx b/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx index fea229f249..34b010cf89 100644 --- a/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx +++ b/editor.planx.uk/src/@planx/components/PropertyInformation/Public.tsx @@ -187,10 +187,14 @@ function PropertyDetails(props: PropertyDetailsProps) { const { data, showPropertyTypeOverride, overrideAnswer } = props; const filteredData = data.filter((d) => Boolean(d.detail)); - const { trackBackwardsNavigation } = useAnalyticsTracking(); + const { trackEvent } = useAnalyticsTracking(); const handleOverrideAnswer = (fn: string) => { - trackBackwardsNavigation("change"); + trackEvent({ + event: "backwardsNavigation", + metadata: null, + initiator: "change", + }); overrideAnswer(fn); }; diff --git a/editor.planx.uk/src/@planx/components/Result/Public/ResultReason.tsx b/editor.planx.uk/src/@planx/components/Result/Public/ResultReason.tsx index cfa4401188..c3550b6a3b 100644 --- a/editor.planx.uk/src/@planx/components/Result/Public/ResultReason.tsx +++ b/editor.planx.uk/src/@planx/components/Result/Public/ResultReason.tsx @@ -139,15 +139,21 @@ const ResultReason: React.FC = ({ const hasMoreInfo = question.data.info ?? question.data.policyRef; - const ariaLabel = `${question.data.text}: Your answer was: ${response}. ${hasMoreInfo + const ariaLabel = `${question.data.text}: Your answer was: ${response}. ${ + hasMoreInfo ? "Click to expand for more information about this question." : "" - }`; + }`; - const { trackBackwardsNavigation } = useAnalyticsTracking(); + const { trackEvent } = useAnalyticsTracking(); const handleChangeAnswer = (id: string) => { - trackBackwardsNavigation("change", id); + trackEvent({ + event: "backwardsNavigation", + metadata: null, + initiator: "change", + nodeId: id, + }); changeAnswer(id); }; diff --git a/editor.planx.uk/src/@planx/components/Section/Public.tsx b/editor.planx.uk/src/@planx/components/Section/Public.tsx index 3eb8d22654..5e759034d2 100644 --- a/editor.planx.uk/src/@planx/components/Section/Public.tsx +++ b/editor.planx.uk/src/@planx/components/Section/Public.tsx @@ -135,14 +135,19 @@ export function SectionsOverviewList({ alteredSectionIds, }); - const { trackBackwardsNavigation } = useAnalyticsTracking(); + const { trackEvent } = useAnalyticsTracking(); const changeFirstAnswerInSection = (sectionId: string) => { const sectionIndex = flow._root.edges?.indexOf(sectionId); if (sectionIndex !== undefined) { const firstNodeInSection = flow._root.edges?.[sectionIndex + 1]; if (firstNodeInSection) { - trackBackwardsNavigation("change", firstNodeInSection); + trackEvent({ + event: "backwardsNavigation", + metadata: null, + initiator: "change", + nodeId: firstNodeInSection, + }); changeAnswer(firstNodeInSection); } } diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/QuestionHeader.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/QuestionHeader.tsx index 6fa9f7c59f..835a86c0a0 100644 --- a/editor.planx.uk/src/@planx/components/shared/Preview/QuestionHeader.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Preview/QuestionHeader.tsx @@ -61,11 +61,11 @@ const QuestionHeader: React.FC = ({ img, }) => { const [open, setOpen] = React.useState(false); - const { trackHelpClick } = useAnalyticsTracking(); + const { trackEvent } = useAnalyticsTracking(); const handleHelpClick = () => { setOpen(true); - trackHelpClick(); // This returns a promise but we don't need to await for it + trackEvent({ event: "helpClick", metadata: {} }); // This returns a promise but we don't need to await for it }; return ( @@ -124,9 +124,17 @@ const QuestionHeader: React.FC = ({ <> {definitionImg && ( - + )} - + ) : undefined} diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/SaveResumeButton.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/SaveResumeButton.tsx index efd7878976..589d50bedb 100644 --- a/editor.planx.uk/src/@planx/components/shared/Preview/SaveResumeButton.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Preview/SaveResumeButton.tsx @@ -17,11 +17,15 @@ const InnerContainer = styled(Box)(({ theme }) => ({ const SaveResumeButton: React.FC = () => { const saveToEmail = useStore((state) => state.saveToEmail); - const { trackFlowDirectionChange } = useAnalyticsTracking(); + const { trackEvent } = useAnalyticsTracking(); const handleClick = () => { if (saveToEmail) { - trackFlowDirectionChange("save"); + trackEvent({ + event: "flowDirectionChange", + metadata: null, + flowDirection: "save", + }); useStore.setState({ path: ApplicationPath.Save }); } else { useStore.setState({ path: ApplicationPath.Resume }); diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx index f25f1f67b8..94e3dbceb5 100644 --- a/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx @@ -239,7 +239,7 @@ function SummaryListsBySections(props: SummaryListsBySectionsProps) { // For applicable component types, display a list of their question & answers with a "change" link // ref https://design-system.service.gov.uk/components/summary-list/ function SummaryList(props: SummaryListProps) { - const { trackBackwardsNavigation } = useAnalyticsTracking(); + const { trackEvent } = useAnalyticsTracking(); const [isDialogOpen, setIsDialogOpen] = useState(false); const [nodeToChange, setNodeToChange] = useState( undefined, @@ -248,7 +248,12 @@ function SummaryList(props: SummaryListProps) { const handleCloseDialog = (isConfirmed: boolean) => { setIsDialogOpen(false); if (isConfirmed && nodeToChange) { - trackBackwardsNavigation("change", nodeToChange); + trackEvent({ + event: "backwardsNavigation", + metadata: null, + initiator: "change", + nodeId: nodeToChange, + }); props.changeAnswer(nodeToChange); } }; diff --git a/editor.planx.uk/src/components/Header.tsx b/editor.planx.uk/src/components/Header.tsx index e7e0f84bea..17831dad9e 100644 --- a/editor.planx.uk/src/components/Header.tsx +++ b/editor.planx.uk/src/components/Header.tsx @@ -320,7 +320,7 @@ const PublicToolbar: React.FC<{ theme.breakpoints.up("md"), ); - const { trackFlowDirectionChange } = useAnalyticsTracking(); + const { trackEvent } = useAnalyticsTracking(); const [isDialogOpen, setIsDialogOpen] = useState(false); const openConfirmationDialog = () => setIsDialogOpen(true); @@ -328,7 +328,11 @@ const PublicToolbar: React.FC<{ const handleRestart = (isConfirmed: boolean) => { setIsDialogOpen(false); if (isConfirmed) { - trackFlowDirectionChange("reset"); + trackEvent({ + event: "flowDirectionChange", + metadata: null, + flowDirection: "reset", + }); if (path === ApplicationPath.SingleSession) { clearLocalFlow(id); window.location.reload(); @@ -362,7 +366,14 @@ const PublicToolbar: React.FC<{ size="large" aria-describedby="restart-application-description" > - Open a dialog with the option to restart your application. If you chose to restart your application, this will delete your previous answers + + Open a dialog with the option to restart your application. + If you chose to restart your application, this will delete + your previous answers + )} diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx index 8d97d18305..c02b22c145 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx @@ -1,3 +1,4 @@ +import { DocumentNode } from "@apollo/client"; import { DEFAULT_FLAG_CATEGORY, Flag, @@ -85,19 +86,75 @@ type Metadata = let lastVisibleNodeAnalyticsLogId: number | undefined = undefined; +type EventData = + | HelpClick + | NextStepsClick + | BackwardsNavigation + | FlowDirectionChange + | InputErrors; + +/** + * Capture when a user clicks on the `More Information` i.e. the help on a + * question. The mutation directly applies "has_clicked_help: true" although + * this can accept metadata as a currently if a user select `info` on a file + * requirement that data can be stored in this field. + */ +type HelpClick = { + event: "helpClick"; + metadata: HelpClickMetadata; +}; + +/** + * A user gets to a `NextSteps` component. Track every time a user selects a + * link and appends it to the array. + */ +type NextStepsClick = { + event: "nextStepsClick"; + metadata: SelectedUrlsMetadata; +}; + +/** + * A user selects either `Back` of `Change` this event it triggered which if + * the `nodeId` is available the `metadata` will be updated with the type of + * backwards navigation i.e. `back` or `change` and the value is data about + * the node which will be navigated to. + */ +type BackwardsNavigation = { + event: "backwardsNavigation"; + metadata: null; + initiator: BackwardsNavigationInitiatorType; + nodeId?: string; +}; + +/** + * The flow direction when a new analytics session is created is + * - 'init': for the flow no longerStorage data is found + * - 'resume': for the flow localStorage data is found + * - 'forwards': breadcrumbs are being appended to i.e. forwards through flow + * - 'backwards': breadcrumbs are bing removed i.e. backwards through flow + * - 'reset': a user restarts by clicking the 'reset' button + * - 'save': a user ends a session by saving + */ +type FlowDirectionChange = { + event: "flowDirectionChange"; + metadata: null; + flowDirection: AnalyticsLogDirection; +}; + +/** + * Captures when a user encounters an error as caught by the ErrorWrapper + * appends the error message to an array of errors + */ +type InputErrors = { + event: "inputErrors"; + metadata: null; + error: string; +}; + const analyticsContext = createContext<{ createAnalytics: (type: AnalyticsType) => Promise; - trackHelpClick: (metadata?: HelpClickMetadata) => Promise; - trackNextStepsLinkClick: (metadata?: SelectedUrlsMetadata) => Promise; - trackFlowDirectionChange: ( - flowDirection: AnalyticsLogDirection, - ) => Promise; - trackBackwardsNavigation: ( - backwardsNavigationType: BackwardsNavigationInitiatorType, - nodeId?: string, - ) => Promise; + trackEvent: (eventData: EventData) => Promise; node: Store.node | null; - trackInputErrors: (error: string) => Promise; track: ( nodeId: string, direction?: AnalyticsLogDirection, @@ -105,12 +162,8 @@ const analyticsContext = createContext<{ ) => Promise; }>({ createAnalytics: () => Promise.resolve(), - trackHelpClick: () => Promise.resolve(), - trackNextStepsLinkClick: () => Promise.resolve(), - trackFlowDirectionChange: () => Promise.resolve(), - trackBackwardsNavigation: () => Promise.resolve(), + trackEvent: () => Promise.resolve(), node: null, - trackInputErrors: () => Promise.resolve(), track: () => Promise.resolve(), }); const { Provider } = analyticsContext; @@ -188,12 +241,8 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ @@ -289,11 +338,9 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ return !shouldTrackAnalytics || !lastVisibleNodeAnalyticsLogId; } - async function trackHelpClick(metadata?: HelpClickMetadata) { - if (shouldSkipTracking()) return; - + async function updateMetadata(mutation: DocumentNode, metadata: Metadata) { await publicClient.mutate({ - mutation: UPDATE_HAS_CLICKED_HELP, + mutation: mutation, variables: { id: lastVisibleNodeAnalyticsLogId, metadata, @@ -301,49 +348,55 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ }); } - async function trackNextStepsLinkClick(metadata?: SelectedUrlsMetadata) { + async function trackEvent(eventData: EventData) { if (shouldSkipTracking()) return; - - await publicClient.mutate({ - mutation: UPDATE_ANALYTICS_LOG_METADATA, - variables: { - id: lastVisibleNodeAnalyticsLogId, - metadata, - }, - }); - } - - async function trackFlowDirectionChange( - flowDirection: AnalyticsLogDirection, - ) { - if (shouldSkipTracking()) return; - - await publicClient.mutate({ - mutation: UPDATE_FLOW_DIRECTION, - variables: { - id: lastVisibleNodeAnalyticsLogId, - flow_direction: flowDirection, - }, - }); + const { event, metadata } = eventData; + switch (event) { + case "helpClick": + updateMetadata(UPDATE_HAS_CLICKED_HELP, metadata); + return; + case "nextStepsClick": + updateMetadata(UPDATE_ANALYTICS_LOG_METADATA, metadata); + return; + case "backwardsNavigation": { + const { initiator, nodeId } = eventData; + const metadata = generateBackwardsNavigationMetadata(initiator, nodeId); + updateMetadata(UPDATE_ANALYTICS_LOG_METADATA, metadata); + return; + } + case "flowDirectionChange": { + const { flowDirection } = eventData; + handleFlowDirectionChange(flowDirection); + return; + } + case "inputErrors": { + const { error } = eventData; + handleInputErrors(error); + return; + } + } } - async function trackBackwardsNavigation( + function generateBackwardsNavigationMetadata( initiator: BackwardsNavigationInitiatorType, nodeId?: string, ) { - if (shouldSkipTracking()) return; - const targetNodeMetadata = nodeId ? getTargetNodeDataFromFlow(nodeId) : {}; const metadata: BackwardsNavigationMetadata = initiator === "change" ? { change: targetNodeMetadata } : { back: targetNodeMetadata }; + return metadata; + } + async function handleFlowDirectionChange( + flowDirection: AnalyticsLogDirection, + ) { await publicClient.mutate({ - mutation: UPDATE_ANALYTICS_LOG_METADATA, + mutation: UPDATE_FLOW_DIRECTION, variables: { id: lastVisibleNodeAnalyticsLogId, - metadata, + flow_direction: flowDirection, }, }); } @@ -489,7 +542,7 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ /** * Capture user input errors caught by ErrorWrapper component */ - async function trackInputErrors(error: string) { + async function handleInputErrors(error: string) { if (shouldSkipTracking()) return; await publicClient.mutate({ diff --git a/editor.planx.uk/src/pages/Preview/Questions.tsx b/editor.planx.uk/src/pages/Preview/Questions.tsx index 4aaaaf490c..478fb92a4f 100644 --- a/editor.planx.uk/src/pages/Preview/Questions.tsx +++ b/editor.planx.uk/src/pages/Preview/Questions.tsx @@ -77,8 +77,7 @@ const Questions = ({ previewEnvironment }: QuestionsProps) => { state.getType, ]); const isStandalone = previewEnvironment === "standalone"; - const { createAnalytics, node, trackBackwardsNavigation } = - useAnalyticsTracking(); + const { createAnalytics, node, trackEvent } = useAnalyticsTracking(); const [gotFlow, setGotFlow] = useState(false); const isUsingLocalStorage = useStore((state) => state.path) === ApplicationPath.SingleSession; @@ -150,7 +149,12 @@ const Questions = ({ previewEnvironment }: QuestionsProps) => { const goBack = useCallback(() => { const previous = previousCard(node); if (previous) { - trackBackwardsNavigation("back", previous); + trackEvent({ + event: "backwardsNavigation", + metadata: null, + initiator: "back", + nodeId: previous, + }); record(previous); } }, [node?.id]); diff --git a/editor.planx.uk/src/ui/public/NextStepsList.tsx b/editor.planx.uk/src/ui/public/NextStepsList.tsx index 4e70d4a9aa..d6d5c90bfc 100644 --- a/editor.planx.uk/src/ui/public/NextStepsList.tsx +++ b/editor.planx.uk/src/ui/public/NextStepsList.tsx @@ -124,11 +124,14 @@ const Step = ({ title, description, url }: ListItemProps) => ( function NextStepsList(props: NextStepsListProps) { const [selectedUrls, setSelectedUrls] = useState([]); - const { trackNextStepsLinkClick } = useAnalyticsTracking(); + const { trackEvent } = useAnalyticsTracking(); const handleSelectingUrl = (newUrl: string) => { setSelectedUrls((prevSelectedUrls) => [...prevSelectedUrls, newUrl]); - trackNextStepsLinkClick({ selectedUrls: [...selectedUrls, newUrl] }); + trackEvent({ + event: "nextStepsClick", + metadata: { selectedUrls: [...selectedUrls, newUrl] }, + }); }; return ( diff --git a/editor.planx.uk/src/ui/shared/ErrorWrapper.tsx b/editor.planx.uk/src/ui/shared/ErrorWrapper.tsx index c71d037b4d..fc9cad7a03 100644 --- a/editor.planx.uk/src/ui/shared/ErrorWrapper.tsx +++ b/editor.planx.uk/src/ui/shared/ErrorWrapper.tsx @@ -45,11 +45,11 @@ export default function ErrorWrapper({ role = "alert", }: Props): FCReturn { const inputId = id ? `${ERROR_MESSAGE}-${id}` : undefined; - const { trackInputErrors } = useAnalyticsTracking(); + const { trackEvent } = useAnalyticsTracking(); useEffect(() => { - error && trackInputErrors(error); - }, [error, trackInputErrors]); + error && trackEvent({ event: "inputErrors", metadata: null, error }); + }, [error, trackEvent]); return (