Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send error events for redirect errors #2973

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/new-bags-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adyen/adyen-web': patch
---

Send redirection error events to analytics.
19 changes: 9 additions & 10 deletions packages/lib/src/components/Card/Card.Analytics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ let analyticsEventObject;

import {
ANALYTICS_CONFIGURED_STR,
ANALYTICS_EVENT_INFO,
ANALYTICS_EVENT_LOG,
ANALYTICS_EVENT,
ANALYTICS_FOCUS_STR,
ANALYTICS_RENDERED_STR,
ANALYTICS_SUBMIT_STR,
Expand Down Expand Up @@ -47,7 +46,7 @@ describe('Card: calls that generate "info" analytics should produce objects with

// With configData removed inspect what's left
expect(analyticsEventObject).toEqual({
event: ANALYTICS_EVENT_INFO,
event: ANALYTICS_EVENT.info,
data: { component: card.constructor['type'], type: ANALYTICS_RENDERED_STR }
});
});
Expand All @@ -65,7 +64,7 @@ describe('Card: calls that generate "info" analytics should produce objects with

// With configData removed inspect what's left
expect(analyticsEventObject).toEqual({
event: ANALYTICS_EVENT_INFO,
event: ANALYTICS_EVENT.info,
data: { component: card.constructor['type'], type: ANALYTICS_RENDERED_STR, isStoredPaymentMethod: true, brand: 'mc' }
});
});
Expand All @@ -76,7 +75,7 @@ describe('Card: calls that generate "info" analytics should produce objects with
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
event: ANALYTICS_EVENT.info,
data: { component: card.constructor['type'], type: ANALYTICS_CONFIGURED_STR }
});
});
Expand All @@ -89,7 +88,7 @@ describe('Card: calls that generate "info" analytics should produce objects with
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
event: ANALYTICS_EVENT.info,
data: { component: card.constructor['type'], type: ANALYTICS_CONFIGURED_STR, isStoredPaymentMethod: true, brand: 'mc' }
});
});
Expand All @@ -109,7 +108,7 @@ describe('Card: calls that generate "info" analytics should produce objects with
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
event: ANALYTICS_EVENT.info,
data: { component: card.constructor['type'], type: ANALYTICS_FOCUS_STR, target: 'card_number' }
});
});
Expand All @@ -129,7 +128,7 @@ describe('Card: calls that generate "info" analytics should produce objects with
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
event: ANALYTICS_EVENT.info,
data: { component: card.constructor['type'], type: ANALYTICS_UNFOCUS_STR, target: 'card_number' }
});
});
Expand All @@ -141,7 +140,7 @@ describe('Card: calls that generate "info" analytics should produce objects with
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
event: ANALYTICS_EVENT.info,
data: {
component: card.constructor['type'],
type: ANALYTICS_VALIDATION_ERROR_STR,
Expand Down Expand Up @@ -170,7 +169,7 @@ describe('Card: calls that generate "log" analytics should produce objects with
card.submitAnalytics({ type: ANALYTICS_SUBMIT_STR });

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_LOG,
event: ANALYTICS_EVENT.log,
data: {
component: card.constructor['type'],
type: ANALYTICS_SUBMIT_STR,
Expand Down
4 changes: 2 additions & 2 deletions packages/lib/src/components/GooglePay/GooglePay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import GooglePay from './GooglePay';
import GooglePayService from './GooglePayService';

import Analytics from '../../core/Analytics';
import { ANALYTICS_EVENT_INFO, ANALYTICS_SELECTED_STR, NO_CHECKOUT_ATTEMPT_ID } from '../../core/Analytics/constants';
import { ANALYTICS_EVENT, ANALYTICS_SELECTED_STR, NO_CHECKOUT_ATTEMPT_ID } from '../../core/Analytics/constants';

const analyticsModule = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', bundleType: 'umd' });

Expand Down Expand Up @@ -458,7 +458,7 @@ describe('GooglePay', () => {
gpay.submit();

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
event: ANALYTICS_EVENT.info,
data: {
component: gpay.props.type,
type: ANALYTICS_SELECTED_STR,
Expand Down
81 changes: 79 additions & 2 deletions packages/lib/src/components/Redirect/Redirect.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { mount } from 'enzyme';
import { render, waitFor, screen } from '@testing-library/preact';
import { h } from 'preact';
import Redirect from './Redirect';
import RedirectShopper from './components/RedirectShopper';
import RedirectElement from './Redirect';
import Analytics from '../../core/Analytics';
import { RedirectConfiguration } from './types';

jest.mock('../../utils/detectInIframeInSameOrigin', () => {
return jest.fn().mockImplementation(() => {
Expand All @@ -13,7 +15,7 @@ jest.mock('../../utils/detectInIframeInSameOrigin', () => {
describe('Redirect', () => {
describe('isValid', () => {
test('Is always valid', () => {
const redirect = new Redirect(global.core, { type: 'redirect' });
const redirect = new RedirectElement(global.core, { type: 'redirect' });
expect(redirect.isValid).toBe(true);
});
});
Expand Down Expand Up @@ -57,3 +59,78 @@ describe('Redirect', () => {
});
});
});

describe('Redirect error', () => {
const oldWindowLocation = window.location;

beforeAll(() => {
delete window.location;
// @ts-ignore test only
window.location = Object.defineProperties(
{},
{
...Object.getOwnPropertyDescriptors(oldWindowLocation),
assign: {
configurable: true,
value: jest.fn()
}
}
);
});

afterAll(() => {
window.location = oldWindowLocation;
});

test('should send an error event to the analytics module if beforeRedirect rejects', async () => {
const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', bundleType: '' });
analytics.sendAnalytics = jest.fn(() => {});
const props: RedirectConfiguration = {
url: 'test',
method: 'POST',
paymentMethodType: 'ideal',
modules: { analytics },
beforeRedirect: (_, reject) => {
return reject();
}
};

const redirectElement = new RedirectElement(global.core, props);
render(redirectElement.render());
await waitFor(() => {
expect(screen.getByTestId('redirect-shopper-form')).toBeInTheDocument();
});

expect(analytics.sendAnalytics).toHaveBeenCalledWith(
'ideal',
{ code: '600', component: 'ideal', errorType: 'Redirect', type: 'error' },
undefined
);
});

test('should send an error event to the analytics module if the redirection failed', async () => {
(window.location.assign as jest.Mock).mockImplementation(() => {
throw new Error('Mock error');
});

const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', bundleType: '' });
analytics.sendAnalytics = jest.fn(() => {});
const props: RedirectConfiguration = {
url: 'test',
method: 'GET',
paymentMethodType: 'ideal',
modules: { analytics }
};

const redirectElement = new RedirectElement(global.core, props);
render(redirectElement.render());

await waitFor(() => {
expect(analytics.sendAnalytics).toHaveBeenCalledWith(
'ideal',
{ code: '600', component: 'ideal', errorType: 'Redirect', type: 'error' },
undefined
);
});
});
});
19 changes: 18 additions & 1 deletion packages/lib/src/components/Redirect/Redirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import RedirectButton from '../internal/RedirectButton';
import { TxVariants } from '../tx-variants';
import { RedirectConfiguration } from './types';
import collectBrowserInfo from '../../utils/browserInfo';
import { ANALYTICS_ERROR_CODE, ANALYTICS_ERROR_TYPE, ANALYTICS_EVENT } from '../../core/Analytics/constants';

class RedirectElement extends UIElement<RedirectConfiguration> {
public static type = TxVariants.redirect;
Expand All @@ -23,6 +24,15 @@ class RedirectElement extends UIElement<RedirectConfiguration> {
};
}

private handleRedirectError = () => {
super.submitAnalytics({
component: this.props.paymentMethodType,
type: ANALYTICS_EVENT.error,
errorType: ANALYTICS_ERROR_TYPE.redirect,
code: ANALYTICS_ERROR_CODE.redirect
});
};

get isValid() {
return true;
}
Expand All @@ -33,7 +43,14 @@ class RedirectElement extends UIElement<RedirectConfiguration> {

render() {
if (this.props.url && this.props.method) {
return <RedirectShopper url={this.props.url} {...this.props} onActionHandled={this.onActionHandled} />;
return (
<RedirectShopper
url={this.props.url}
{...this.props}
onActionHandled={this.onActionHandled}
onRedirectError={this.handleRedirectError}
/>
);
}

if (this.props.showPayButton) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ interface RedirectShopperProps {
redirectFromTopWhenInIframe?: boolean;
paymentMethodType?: string;
onActionHandled?: (rtnObj: ActionHandledReturnObject) => void;
onRedirectError?: () => void;
}

class RedirectShopper extends Component<RedirectShopperProps> {
private postForm;
public static defaultProps = {
beforeRedirect: resolve => resolve(),
onRedirectError: () => {},
method: 'GET'
};

Expand Down Expand Up @@ -49,14 +51,17 @@ class RedirectShopper extends Component<RedirectShopperProps> {
})
);

dispatchEvent.then(doRedirect).catch(() => {});
dispatchEvent.then(doRedirect).catch(() => {
this.props.onRedirectError();
});
}

render({ url, method, data = {} }) {
if (method === 'POST') {
return (
<form
method="post"
data-testid="redirect-shopper-form"
action={url}
style={{ display: 'none' }}
ref={ref => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ThreeDS2Challenge } from './index';
import Analytics from '../../core/Analytics';
import { ANALYTICS_API_ERROR, Analytics3DS2Errors, ANALYTICS_EVENT_ERROR, ANALYTICS_RENDERED_STR } from '../../core/Analytics/constants';
import { ANALYTICS_ERROR_TYPE, Analytics3DS2Errors, ANALYTICS_RENDERED_STR, ANALYTICS_EVENT } from '../../core/Analytics/constants';
import { THREEDS2_CHALLENGE_ERROR, THREEDS2_ERROR } from './constants';

const analyticsModule = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', bundleType: 'umd' });
Expand Down Expand Up @@ -31,11 +31,11 @@ describe('ThreeDS2Challenge: calls that generate analytics should produce object
const view = challenge.render();

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_ERROR,
event: ANALYTICS_EVENT.error,
data: {
component: challenge.constructor['type'],
type: THREEDS2_ERROR,
errorType: ANALYTICS_API_ERROR,
errorType: ANALYTICS_ERROR_TYPE.apiError,
message: `${THREEDS2_CHALLENGE_ERROR}: Missing 'paymentData' property from threeDS2 action`,
code: Analytics3DS2Errors.ACTION_IS_MISSING_PAYMENT_DATA
}
Expand Down
4 changes: 2 additions & 2 deletions packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { hasOwnProperty } from '../../utils/hasOwnProperty';
import { TxVariants } from '../tx-variants';
import { ThreeDS2ChallengeConfiguration } from './types';
import AdyenCheckoutError, { API_ERROR } from '../../core/Errors/AdyenCheckoutError';
import { ANALYTICS_API_ERROR, Analytics3DS2Errors, ANALYTICS_RENDERED_STR, Analytics3DS2Events } from '../../core/Analytics/constants';
import { ANALYTICS_ERROR_TYPE, Analytics3DS2Errors, ANALYTICS_RENDERED_STR, Analytics3DS2Events } from '../../core/Analytics/constants';
import { SendAnalyticsObject } from '../../core/Analytics/types';
import { CoreProvider } from '../../core/Context/CoreProvider';
import { ActionHandledReturnObject } from '../../types/global-types';
Expand Down Expand Up @@ -61,7 +61,7 @@ class ThreeDS2Challenge extends UIElement<ThreeDS2ChallengeConfiguration> {
this.submitAnalytics({
type: THREEDS2_ERROR,
code: Analytics3DS2Errors.ACTION_IS_MISSING_PAYMENT_DATA,
errorType: ANALYTICS_API_ERROR,
errorType: ANALYTICS_ERROR_TYPE.apiError,
message: `${THREEDS2_CHALLENGE_ERROR}: Missing 'paymentData' property from threeDS2 action`
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ThreeDS2DeviceFingerprint } from './index';
import Analytics from '../../core/Analytics';
import { ANALYTICS_API_ERROR, Analytics3DS2Errors, ANALYTICS_EVENT_ERROR, ANALYTICS_RENDERED_STR } from '../../core/Analytics/constants';
import { Analytics3DS2Errors, ANALYTICS_RENDERED_STR, ANALYTICS_EVENT, ANALYTICS_ERROR_TYPE } from '../../core/Analytics/constants';
import { THREEDS2_ERROR, THREEDS2_FINGERPRINT_ERROR } from './constants';

const analyticsModule = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', bundleType: 'umd' });
Expand Down Expand Up @@ -32,11 +32,11 @@ describe('ThreeDS2DeviceFingerprint: calls that generate analytics should produc
const view = fingerprint.render();

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_ERROR,
event: ANALYTICS_EVENT.error,
data: {
component: fingerprint.constructor['type'],
type: THREEDS2_ERROR,
errorType: ANALYTICS_API_ERROR,
errorType: ANALYTICS_ERROR_TYPE.apiError,
message: `${THREEDS2_FINGERPRINT_ERROR}: Missing 'paymentData' property from threeDS2 action`,
code: Analytics3DS2Errors.ACTION_IS_MISSING_PAYMENT_DATA
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { existy } from '../../utils/commonUtils';
import { TxVariants } from '../tx-variants';
import { ThreeDS2DeviceFingerprintConfiguration } from './types';
import AdyenCheckoutError, { API_ERROR } from '../../core/Errors/AdyenCheckoutError';
import { ANALYTICS_API_ERROR, Analytics3DS2Errors, ANALYTICS_RENDERED_STR, Analytics3DS2Events } from '../../core/Analytics/constants';
import { ANALYTICS_ERROR_TYPE, Analytics3DS2Errors, ANALYTICS_RENDERED_STR, Analytics3DS2Events } from '../../core/Analytics/constants';
import { SendAnalyticsObject } from '../../core/Analytics/types';
import { THREEDS2_ERROR, THREEDS2_FINGERPRINT, THREEDS2_FINGERPRINT_ERROR, THREEDS2_FULL } from './constants';
import { ActionHandledReturnObject } from '../../types/global-types';
Expand Down Expand Up @@ -53,7 +53,7 @@ class ThreeDS2DeviceFingerprint extends UIElement<ThreeDS2DeviceFingerprintConfi
this.submitAnalytics({
type: THREEDS2_ERROR,
code: Analytics3DS2Errors.ACTION_IS_MISSING_PAYMENT_DATA,
errorType: ANALYTICS_API_ERROR,
errorType: ANALYTICS_ERROR_TYPE.apiError,
message: `${THREEDS2_FINGERPRINT_ERROR}: Missing 'paymentData' property from threeDS2 action`
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { pick } from '../../utils/commonUtils';
import { ThreeDS2FingerprintResponse } from './types';
import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError';
import { THREEDS2_ERROR, THREEDS2_FINGERPRINT_SUBMIT } from './constants';
import { ANALYTICS_API_ERROR, Analytics3DS2Errors, ANALYTICS_SDK_ERROR } from '../../core/Analytics/constants';
import { ANALYTICS_ERROR_TYPE, Analytics3DS2Errors } from '../../core/Analytics/constants';
import { SendAnalyticsObject } from '../../core/Analytics/types';

/**
Expand Down Expand Up @@ -39,7 +39,7 @@ export default function callSubmit3DS2Fingerprint({ data }): void {
analyticsErrorObject = {
type: THREEDS2_ERROR,
code: Analytics3DS2Errors.NO_DETAILS_FOR_FRICTIONLESS_OR_REFUSED,
errorType: ANALYTICS_API_ERROR,
errorType: ANALYTICS_ERROR_TYPE.apiError,
message: `${THREEDS2_FINGERPRINT_SUBMIT}: no details object in a response indicating either a "frictionless" flow, or a "refused" response`
};

Expand All @@ -62,7 +62,7 @@ export default function callSubmit3DS2Fingerprint({ data }): void {
analyticsErrorObject = {
type: THREEDS2_ERROR,
code: Analytics3DS2Errors.NO_ACTION_FOR_CHALLENGE,
errorType: ANALYTICS_API_ERROR,
errorType: ANALYTICS_ERROR_TYPE.apiError,
message: `${THREEDS2_FINGERPRINT_SUBMIT}: no action object in a response indicating a "challenge" flow`
};
this.submitAnalytics(analyticsErrorObject);
Expand All @@ -82,7 +82,7 @@ export default function callSubmit3DS2Fingerprint({ data }): void {
analyticsErrorObject = {
type: THREEDS2_ERROR,
code: Analytics3DS2Errors.NO_COMPONENT_FOR_ACTION,
errorType: ANALYTICS_SDK_ERROR,
errorType: ANALYTICS_ERROR_TYPE.sdkError,
message: `${THREEDS2_FINGERPRINT_SUBMIT}: no component defined to handle the action response`
};
this.submitAnalytics(analyticsErrorObject);
Expand Down
Loading
Loading