Skip to content

Commit

Permalink
chore: Include currentDocument in funnel events
Browse files Browse the repository at this point in the history
  • Loading branch information
connorlanigan committed Sep 15, 2023
1 parent 6d19d70 commit 6c2b098
Show file tree
Hide file tree
Showing 15 changed files with 81 additions and 32 deletions.
4 changes: 3 additions & 1 deletion src/alert/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export { AlertProps };

const Alert = React.forwardRef(
({ type = 'info', visible = true, ...props }: AlertProps, ref: React.Ref<AlertProps.Ref>) => {
const baseComponentProps = useBaseComponent('Alert');
const baseComponentProps = useBaseComponent<HTMLDivElement>('Alert');

const { funnelInteractionId, submissionAttempt, funnelState, errorCount } = useFunnel();
const { stepNumber, stepNameSelector } = useFunnelStep();
Expand All @@ -40,10 +40,12 @@ const Alert = React.forwardRef(
stepName,
stepNameSelector,
subStepAllSelector: getSubStepAllSelector(),
currentDocument: baseComponentProps.__internalRootRef.current?.ownerDocument,
});
} else {
FunnelMetrics.funnelError({
funnelInteractionId,
currentDocument: baseComponentProps.__internalRootRef.current?.ownerDocument,
});
}
}
Expand Down
1 change: 1 addition & 0 deletions src/button/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const InternalButton = React.forwardRef(
subStepNameSelector,
elementSelector: getFunnelValueSelector(uniqueId),
subStepAllSelector: getSubStepAllSelector(),
currentDocument: buttonRef.current?.ownerDocument,
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/form-field/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export namespace FormFieldProps {
}
}

export interface InternalFormFieldProps extends FormFieldProps, InternalBaseComponentProps {
export interface InternalFormFieldProps extends FormFieldProps, InternalBaseComponentProps<HTMLDivElement> {
/**
* Visually hide the label.
*/
Expand Down
1 change: 1 addition & 0 deletions src/form-field/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export default function InternalFormField({
fieldErrorSelector: getFieldSlotSeletor(slotIds.error),
fieldLabelSelector: getFieldSlotSeletor(slotIds.label),
subStepAllSelector: getSubStepAllSelector(),
currentDocument: __internalRootRef?.current?.ownerDocument,
});
}

Expand Down
15 changes: 12 additions & 3 deletions src/form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,20 @@ const FormWithAnalytics = ({ variant = 'full-page', actions, ...props }: FormPro
};

export default function Form({ variant = 'full-page', ...props }: FormProps) {
const baseComponentProps = useBaseComponent('Form');
const baseComponentProps = useBaseComponent<HTMLDivElement>('Form');

return (
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={1} stepNameSelector={getFunnelNameSelector()}>
<AnalyticsFunnel
funnelType="single-page"
optionalStepNumbers={[]}
totalFunnelSteps={1}
elementRef={baseComponentProps.__internalRootRef}
>
<AnalyticsFunnelStep
stepNumber={1}
stepNameSelector={getFunnelNameSelector()}
elementRef={baseComponentProps.__internalRootRef}
>
<FormWithAnalytics variant={variant} {...props} {...baseComponentProps} />
</AnalyticsFunnelStep>
</AnalyticsFunnel>
Expand Down
9 changes: 6 additions & 3 deletions src/form/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useInternalI18n } from '../i18n/context';
import { useFunnel } from '../internal/analytics/hooks/use-funnel';
import { FunnelMetrics } from '../internal/analytics';

type InternalFormProps = FormProps & InternalBaseComponentProps;
export type InternalFormProps = FormProps & InternalBaseComponentProps<HTMLDivElement>;

export default function InternalForm({
children,
Expand All @@ -37,13 +37,16 @@ export default function InternalForm({
useEffect(() => {
if (funnelInteractionId && errorText) {
errorCount.current++;
FunnelMetrics.funnelError({ funnelInteractionId });
FunnelMetrics.funnelError({
funnelInteractionId,
currentDocument: __internalRootRef?.current?.ownerDocument,
});
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
errorCount.current--;
};
}
}, [funnelInteractionId, errorText, submissionAttempt, errorCount]);
}, [funnelInteractionId, errorText, submissionAttempt, errorCount, __internalRootRef]);

return (
<div {...baseProps} ref={__internalRootRef} className={clsx(styles.root, baseProps.className)}>
Expand Down
48 changes: 33 additions & 15 deletions src/internal/analytics/components/analytics-funnel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useContext, useEffect, useRef, useState } from 'react';
import React, { RefObject, useContext, useEffect, useRef, useState } from 'react';

import {
FunnelStepContext,
Expand Down Expand Up @@ -33,10 +33,15 @@ import { nodeBelongs } from '../../utils/node-belongs';

export const FUNNEL_VERSION = '1.2';

type AnalyticsFunnelProps = { children?: React.ReactNode; stepConfiguration?: StepConfiguration[] } & Pick<
FunnelProps,
'funnelType' | 'optionalStepNumbers' | 'totalFunnelSteps'
>;
type AnalyticsFunnelProps = {
children?: React.ReactNode;
stepConfiguration?: StepConfiguration[];
/**
* This ref is used to determine the current `document` of the events. It is not important
* on which exact element the ref is placed, as long as it's in the same document as the funnel.
*/
elementRef: RefObject<HTMLElement>;
} & Pick<FunnelProps, 'funnelType' | 'optionalStepNumbers' | 'totalFunnelSteps'>;

export const AnalyticsFunnel = (props: AnalyticsFunnelProps) => {
const { isInFunnel } = useFunnel();
Expand All @@ -54,17 +59,17 @@ export const AnalyticsFunnel = (props: AnalyticsFunnelProps) => {
};
export const CREATION_EDIT_FLOW_DONE_EVENT_NAME = 'awsui-creation-edit-flow-done';

const onFunnelCancelled = ({ funnelInteractionId }: { funnelInteractionId: string }) => {
FunnelMetrics.funnelCancelled({ funnelInteractionId });
const onFunnelCancelled = (props: Parameters<typeof FunnelMetrics['funnelCancelled']>[0]) => {
FunnelMetrics.funnelCancelled(props);
document.dispatchEvent(new Event(CREATION_EDIT_FLOW_DONE_EVENT_NAME));
};

const onFunnelComplete = ({ funnelInteractionId }: { funnelInteractionId: string }) => {
FunnelMetrics.funnelComplete({ funnelInteractionId });
const onFunnelComplete = (props: Parameters<typeof FunnelMetrics['funnelComplete']>[0]) => {
FunnelMetrics.funnelComplete(props);
document.dispatchEvent(new Event(CREATION_EDIT_FLOW_DONE_EVENT_NAME));
};

const InnerAnalyticsFunnel = ({ children, stepConfiguration, ...props }: AnalyticsFunnelProps) => {
const InnerAnalyticsFunnel = ({ children, stepConfiguration, elementRef, ...props }: AnalyticsFunnelProps) => {
const [funnelInteractionId, setFunnelInteractionId] = useState<string>('');
const [submissionAttempt, setSubmissionAttempt] = useState(0);
const isVisualRefresh = useVisualRefresh();
Expand All @@ -85,6 +90,8 @@ const InnerAnalyticsFunnel = ({ children, stepConfiguration, ...props }: Analyti
// The eslint-disable is required as we deliberately want this effect to run only once on mount and unmount,
// hence we do not provide any dependencies.
useEffect(() => {
const currentDocument = elementRef.current?.ownerDocument;

/*
We run this effect with a delay, in order to detect whether this funnel contains a Wizard.
If it does contain a Wizard, that Wizard should take precedence for handling the funnel, and
Expand All @@ -111,6 +118,7 @@ const InnerAnalyticsFunnel = ({ children, stepConfiguration, ...props }: Analyti
theme: isVisualRefresh ? 'vr' : 'classic',
funnelVersion: FUNNEL_VERSION,
stepConfiguration: stepConfiguration ?? singleStepFlowStepConfiguration,
currentDocument,
});

setFunnelInteractionId(funnelInteractionId);
Expand All @@ -128,14 +136,14 @@ const InnerAnalyticsFunnel = ({ children, stepConfiguration, ...props }: Analyti

if (funnelState.current === 'validating') {
// Finish the validation phase early.
onFunnelComplete({ funnelInteractionId });
onFunnelComplete({ funnelInteractionId, currentDocument });
funnelState.current = 'complete';
}

if (funnelState.current === 'complete') {
FunnelMetrics.funnelSuccessful({ funnelInteractionId });
FunnelMetrics.funnelSuccessful({ funnelInteractionId, currentDocument });
} else {
onFunnelCancelled({ funnelInteractionId });
onFunnelCancelled({ funnelInteractionId, currentDocument });
funnelState.current = 'cancelled';
}
};
Expand Down Expand Up @@ -171,7 +179,7 @@ const InnerAnalyticsFunnel = ({ children, stepConfiguration, ...props }: Analyti
/*
If no validation errors are rendered, we treat the funnel as complete.
*/
onFunnelComplete({ funnelInteractionId });
onFunnelComplete({ funnelInteractionId, currentDocument: elementRef.current?.ownerDocument });
funnelState.current = 'complete';
} else {
funnelState.current = 'default';
Expand Down Expand Up @@ -208,6 +216,11 @@ const InnerAnalyticsFunnel = ({ children, stepConfiguration, ...props }: Analyti

type AnalyticsFunnelStepProps = {
children?: React.ReactNode | ((props: FunnelStepContextValue) => React.ReactNode);
/**
* This ref is used to determine the current `document` of the events. It is not important
* on which exact element the ref is placed, as long as it's in the same document as the funnel.
*/
elementRef: RefObject<HTMLElement>;
} & Pick<FunnelStepProps, 'stepNumber' | 'stepNameSelector'>;

export const AnalyticsFunnelStep = (props: AnalyticsFunnelStepProps) => {
Expand Down Expand Up @@ -263,7 +276,7 @@ function useStepChangeListener(handler: (stepConfiguration: SubStepConfiguration
return stepChangeCallback;
}

const InnerAnalyticsFunnelStep = ({ children, stepNumber, stepNameSelector }: AnalyticsFunnelStepProps) => {
const InnerAnalyticsFunnelStep = ({ children, stepNumber, stepNameSelector, elementRef }: AnalyticsFunnelStepProps) => {
const { funnelInteractionId, funnelState, funnelType } = useFunnel();
const parentStep = useFunnelStep();
const parentStepExists = parentStep.isInStep;
Expand All @@ -287,6 +300,7 @@ const InnerAnalyticsFunnelStep = ({ children, stepNumber, stepNameSelector }: An
subStepAllSelector: getSubStepAllSelector(),
totalSubSteps: subStepCount.current,
subStepConfiguration,
currentDocument: elementRef.current?.ownerDocument,
});
});

Expand All @@ -307,6 +321,7 @@ const InnerAnalyticsFunnelStep = ({ children, stepNumber, stepNameSelector }: An
*/
return;
}
const currentDocument = elementRef.current?.ownerDocument;

const stepName = getNameFromSelector(stepNameSelector);

Expand All @@ -319,6 +334,7 @@ const InnerAnalyticsFunnelStep = ({ children, stepNumber, stepNameSelector }: An
subStepAllSelector: getSubStepAllSelector(),
totalSubSteps: subStepCount.current,
subStepConfiguration: getSubStepConfiguration(),
currentDocument,
});
}

Expand All @@ -333,6 +349,7 @@ const InnerAnalyticsFunnelStep = ({ children, stepNumber, stepNameSelector }: An
subStepAllSelector: getSubStepAllSelector(),
// eslint-disable-next-line react-hooks/exhaustive-deps
totalSubSteps: subStepCount.current,
currentDocument,
});
}
};
Expand All @@ -344,6 +361,7 @@ const InnerAnalyticsFunnelStep = ({ children, stepNumber, stepNameSelector }: An
parentStepExists,
funnelType,
parentStepFunnelInteractionId,
elementRef,
]);

const contextValue: FunnelStepContextValue = {
Expand Down
2 changes: 2 additions & 0 deletions src/internal/analytics/hooks/use-funnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const useFunnelSubStep = () => {
stepName,
stepNameSelector,
subStepAllSelector: getSubStepAllSelector(),
currentDocument: subStepRef.current?.ownerDocument,
});

/*
Expand Down Expand Up @@ -103,6 +104,7 @@ export const useFunnelSubStep = () => {
stepName,
stepNameSelector,
subStepAllSelector: getSubStepAllSelector(),
currentDocument: subStepRef.current?.ownerDocument,
});
}
};
Expand Down
2 changes: 2 additions & 0 deletions src/internal/analytics/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type FunnelType = 'single-page' | 'multi-page';
// Common properties for all funnels
export interface BaseFunnelProps {
funnelInteractionId: string;
currentDocument: Document | undefined;
}

export interface FunnelProps extends BaseFunnelProps {
Expand All @@ -23,6 +24,7 @@ export interface FunnelStartProps {
funnelVersion: string;
componentVersion: string;
theme: string;
currentDocument: Document | undefined;
}

// A function type for a generic funnel method
Expand Down
6 changes: 3 additions & 3 deletions src/internal/hooks/use-base-component/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { MutableRefObject } from 'react';
import { RefObject } from 'react';
import { useComponentMetadata } from '@cloudscape-design/component-toolkit/internal';
import { useTelemetry } from '../use-telemetry';
import { PACKAGE_VERSION } from '../../environment';
import useFocusVisible from '../focus-visible';

export interface InternalBaseComponentProps {
__internalRootRef?: MutableRefObject<any> | null;
export interface InternalBaseComponentProps<T = any> {
__internalRootRef?: RefObject<T> | null;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/link/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ const InternalLink = React.forwardRef(

const infoLinkLabelFromContext = useContext(InfoLinkLabelContext);

const linkRef = useRef<HTMLElement>(null);

const { funnelInteractionId } = useFunnel();
const { stepNumber, stepNameSelector } = useFunnelStep();
const { subStepSelector, subStepNameSelector } = useFunnelSubStep();
Expand All @@ -87,6 +89,7 @@ const InternalLink = React.forwardRef(
subStepNameSelector,
elementSelector: getFunnelValueSelector(uniqueId),
subStepAllSelector: getSubStepAllSelector(),
currentDocument: linkRef.current?.ownerDocument,
});
} else if (external) {
const stepName = getNameFromSelector(stepNameSelector);
Expand All @@ -102,6 +105,7 @@ const InternalLink = React.forwardRef(
subStepNameSelector,
elementSelector: getFunnelValueSelector(uniqueId),
subStepAllSelector: getSubStepAllSelector(),
currentDocument: linkRef.current?.ownerDocument,
});
}
};
Expand Down Expand Up @@ -131,7 +135,6 @@ const InternalLink = React.forwardRef(
}
};

const linkRef = useRef<HTMLElement>(null);
const isVisualRefresh = useVisualRefresh();
useForwardFocus(ref, linkRef);

Expand Down
9 changes: 7 additions & 2 deletions src/wizard/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { useRef, useEffect } from 'react';
import { useRef, useEffect, RefObject } from 'react';
import { FunnelMetrics } from '../internal/analytics';
import { WizardProps } from './interfaces';

export function useFunnelChangeEvent(funnelInteractionId: string | undefined, steps: WizardProps['steps']) {
export function useFunnelChangeEvent(
funnelInteractionId: string | undefined,
steps: WizardProps['steps'],
elementRef?: RefObject<HTMLElement>
) {
const listenForStepChanges = useRef(false);

useEffect(() => {
Expand All @@ -27,6 +31,7 @@ export function useFunnelChangeEvent(funnelInteractionId: string | undefined, st
FunnelMetrics.funnelChange({
funnelInteractionId,
stepConfiguration: getStepConfiguration(steps),
currentDocument: elementRef?.current?.ownerDocument,
});

// This dependency array does not include `steps`, because `steps` is not stable across renders.
Expand Down
3 changes: 2 additions & 1 deletion src/wizard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { WizardProps } from './interfaces';
import { useFunnel } from '../internal/analytics/hooks/use-funnel';

function Wizard({ isLoadingNextStep = false, allowSkipTo = false, ...props }: WizardProps) {
const baseComponentProps = useBaseComponent('Wizard');
const baseComponentProps = useBaseComponent<HTMLDivElement>('Wizard');
const { wizardCount } = useFunnel();
const externalProps = getExternalProps(props);

Expand All @@ -31,6 +31,7 @@ function Wizard({ isLoadingNextStep = false, allowSkipTo = false, ...props }: Wi
.filter(step => step !== -1)}
totalFunnelSteps={props.steps.length}
stepConfiguration={getStepConfiguration(props.steps)}
elementRef={baseComponentProps.__internalRootRef}
>
<InternalWizard
isLoadingNextStep={isLoadingNextStep}
Expand Down
Loading

0 comments on commit 6c2b098

Please sign in to comment.