Skip to content

Commit

Permalink
feat: Emit error events for Alerts (#1328)
Browse files Browse the repository at this point in the history
  • Loading branch information
connorlanigan authored Jul 17, 2023
1 parent eda0501 commit 97e4f0c
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 1 deletion.
55 changes: 55 additions & 0 deletions pages/funnel-analytics/with-error-alert-in-wizard.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from 'react';
import Wizard, { WizardProps } from '~components/wizard';

import { i18nStrings } from '../wizard/common';
import Alert from '~components/alert';
import Container from '~components/container';
import Header from '~components/header';

export default function WizardPage() {
const [errorMode, setErrorMode] = useState(0);

const steps: WizardProps.Step[] = [
{
title: 'Step 1',
content: (
<div>
<div>Content 1</div>
<div>{errorMode === 1 && <Alert type="error">This is an error on the step level</Alert>}</div>
<div>
{errorMode === 2 && (
<Container header={<Header>A container around the alert</Header>}>
<Alert type="error">This is an error on the substep level</Alert>
</Container>
)}
</div>
<div>
{errorMode === 3 && (
<>
<Alert type="error">This is an error on the step level</Alert>
<Container header={<Header>A container around the alert</Header>}>
<Alert type="error">This is an error on the substep level</Alert>
</Container>
</>
)}
</div>
</div>
),
},
{
title: 'Step 2',
content: <div>Content 2</div>,
},
];

return (
<Wizard
steps={steps}
i18nStrings={i18nStrings}
activeStepIndex={0}
onNavigate={() => setErrorMode((errorMode + 1) % 4)}
/>
);
}
183 changes: 183 additions & 0 deletions src/alert/__tests__/alert-analytics.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { act, render } from '@testing-library/react';

import { FunnelMetrics } from '../../../lib/components/internal/analytics';
import { useFunnel } from '../../../lib/components/internal/analytics/hooks/use-funnel';
import Alert from '../../../lib/components/alert';

import {
AnalyticsFunnel,
AnalyticsFunnelStep,
AnalyticsFunnelSubStep,
} from '../../../lib/components/internal/analytics/components/analytics-funnel';

import { mockFunnelMetrics } from '../../internal/analytics/__tests__/mocks';

describe('Alert Analytics', () => {
beforeEach(() => {
jest.clearAllMocks();
mockFunnelMetrics();
});

test('sends funnelSubStepError metric when the alert is placed inside a substep', () => {
render(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
<AnalyticsFunnelSubStep>
<Alert type="error">This is the error text</Alert>
</AnalyticsFunnelSubStep>
</AnalyticsFunnelStep>
</AnalyticsFunnel>
);

expect(FunnelMetrics.funnelError).not.toHaveBeenCalled();

expect(FunnelMetrics.funnelSubStepError).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelSubStepError).toHaveBeenCalledWith(
expect.objectContaining({
funnelInteractionId: 'mocked-funnel-id',
subStepSelector: expect.any(String),
stepNameSelector: expect.any(String),
subStepAllSelector: expect.any(String),
subStepNameSelector: expect.any(String),
})
);
});

test('sends funnelError metric when the alert is placed inside a step', () => {
render(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
<Alert type="error">This is the error text</Alert>
</AnalyticsFunnelStep>
</AnalyticsFunnel>
);

expect(FunnelMetrics.funnelSubStepError).not.toHaveBeenCalled();

expect(FunnelMetrics.funnelError).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelError).toHaveBeenCalledWith(
expect.objectContaining({
funnelInteractionId: 'mocked-funnel-id',
})
);
});

test('does not send any error metric when the alert is invisible', () => {
render(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
<AnalyticsFunnelSubStep>
<Alert type="error" visible={false}>
This is the error text
</Alert>
</AnalyticsFunnelSubStep>
</AnalyticsFunnelStep>
</AnalyticsFunnel>
);

expect(FunnelMetrics.funnelSubStepError).not.toHaveBeenCalled();
expect(FunnelMetrics.funnelError).not.toHaveBeenCalled();
});

test('does not send any error metrics for non-error alerts', () => {
render(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
<AnalyticsFunnelSubStep>
<Alert>Default</Alert>
<Alert type="info">Info</Alert>
<Alert type="success">Success</Alert>
<Alert type="warning">Warning</Alert>
</AnalyticsFunnelSubStep>
</AnalyticsFunnelStep>
</AnalyticsFunnel>
);

expect(FunnelMetrics.funnelSubStepError).not.toHaveBeenCalled();
expect(FunnelMetrics.funnelError).not.toHaveBeenCalled();
});

test('sends a funnelSubStepError metric when there is an error and the user attempts to submit the form', () => {
let funnelNextOrSubmitAttempt: undefined | (() => void) = undefined;

const ChildComponent = () => {
funnelNextOrSubmitAttempt = useFunnel().funnelNextOrSubmitAttempt;
return <></>;
};

const jsx = (
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
<AnalyticsFunnelSubStep>
<Alert type="error">This is the error text</Alert>
</AnalyticsFunnelSubStep>

<ChildComponent />
</AnalyticsFunnelStep>
</AnalyticsFunnel>
);

const { rerender } = render(jsx);
expect(FunnelMetrics.funnelSubStepError).toHaveBeenCalledTimes(1);

act(() => funnelNextOrSubmitAttempt!());
rerender(jsx);

expect(FunnelMetrics.funnelSubStepError).toHaveBeenCalledTimes(2);
});

test('does not send any error metrics when outside of a funnel context', () => {
render(<Alert type="error">This is the error text</Alert>);
expect(FunnelMetrics.funnelSubStepError).not.toHaveBeenCalled();
expect(FunnelMetrics.funnelError).not.toHaveBeenCalled();
});

test('does not send multiple funnelSubStepError metrics on rerender', () => {
const { rerender } = render(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
<AnalyticsFunnelSubStep>
<Alert type="error">This is the error text</Alert>
</AnalyticsFunnelSubStep>
</AnalyticsFunnelStep>
</AnalyticsFunnel>
);

rerender(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
<AnalyticsFunnelSubStep>
<Alert type="error">This is the error text</Alert>
</AnalyticsFunnelSubStep>
</AnalyticsFunnelStep>
</AnalyticsFunnel>
);

expect(FunnelMetrics.funnelSubStepError).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelError).not.toHaveBeenCalled();
});

test('does not send multiple funnelError metrics on rerender', () => {
const { rerender } = render(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
<Alert type="error">This is the error text</Alert>
</AnalyticsFunnelStep>
</AnalyticsFunnel>
);

rerender(
<AnalyticsFunnel funnelType="single-page" optionalStepNumbers={[]} totalFunnelSteps={1}>
<AnalyticsFunnelStep stepNumber={2} stepNameSelector=".step-name-selector">
<Alert type="error">This is the error text</Alert>
</AnalyticsFunnelStep>
</AnalyticsFunnel>
);

expect(FunnelMetrics.funnelError).toHaveBeenCalledTimes(1);
expect(FunnelMetrics.funnelSubStepError).not.toHaveBeenCalled();
});
});
43 changes: 42 additions & 1 deletion src/alert/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,56 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import React, { useEffect } from 'react';
import { applyDisplayName } from '../internal/utils/apply-display-name';
import { AlertProps } from './interfaces';
import InternalAlert from './internal';
import useBaseComponent from '../internal/hooks/use-base-component';
import { FunnelMetrics } from '../internal/analytics';
import { useFunnel, useFunnelStep, useFunnelSubStep } from '../internal/analytics/hooks/use-funnel';
import { getNameFromSelector, getSubStepAllSelector } from '../internal/analytics/selectors';

export { AlertProps };

export default function Alert({ type = 'info', visible = true, ...props }: AlertProps) {
const baseComponentProps = useBaseComponent('Alert');

const { funnelInteractionId, submissionAttempt, funnelState, errorCount } = useFunnel();
const { stepNumber, stepNameSelector } = useFunnelStep();
const { subStepSelector, subStepNameSelector } = useFunnelSubStep();

useEffect(() => {
if (funnelInteractionId && visible && type === 'error' && funnelState.current !== 'complete') {
const stepName = getNameFromSelector(stepNameSelector);
const subStepName = getNameFromSelector(subStepNameSelector);

errorCount.current++;

if (subStepSelector) {
FunnelMetrics.funnelSubStepError({
funnelInteractionId,
subStepSelector,
subStepName,
subStepNameSelector,
stepNumber,
stepName,
stepNameSelector,
subStepAllSelector: getSubStepAllSelector(),
});
} else {
FunnelMetrics.funnelError({
funnelInteractionId,
});
}

return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
errorCount.current--;
};
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [funnelInteractionId, visible, submissionAttempt, errorCount]);

return <InternalAlert type={type} visible={visible} {...props} {...baseComponentProps} />;
}

Expand Down

0 comments on commit 97e4f0c

Please sign in to comment.