Skip to content

Commit 53f6c4a

Browse files
authored
Send error events for redirect errors (#2973)
* refactor(analytics): group some constants together * refactor(analytics): send redirect error event * test: added test * refactor: remove `async` for `runQueue`
1 parent dfa8ed6 commit 53f6c4a

23 files changed

+218
-105
lines changed

.changeset/new-bags-crash.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@adyen/adyen-web': patch
3+
---
4+
5+
Send redirection error events to analytics.

packages/lib/src/components/Card/Card.Analytics.test.tsx

+9-10
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ let analyticsEventObject;
88

99
import {
1010
ANALYTICS_CONFIGURED_STR,
11-
ANALYTICS_EVENT_INFO,
12-
ANALYTICS_EVENT_LOG,
11+
ANALYTICS_EVENT,
1312
ANALYTICS_FOCUS_STR,
1413
ANALYTICS_RENDERED_STR,
1514
ANALYTICS_SUBMIT_STR,
@@ -47,7 +46,7 @@ describe('Card: calls that generate "info" analytics should produce objects with
4746

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

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

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

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

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

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

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

172171
expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
173-
event: ANALYTICS_EVENT_LOG,
172+
event: ANALYTICS_EVENT.log,
174173
data: {
175174
component: card.constructor['type'],
176175
type: ANALYTICS_SUBMIT_STR,

packages/lib/src/components/GooglePay/GooglePay.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import GooglePay from './GooglePay';
22
import GooglePayService from './GooglePayService';
33

44
import Analytics from '../../core/Analytics';
5-
import { ANALYTICS_EVENT_INFO, ANALYTICS_SELECTED_STR, NO_CHECKOUT_ATTEMPT_ID } from '../../core/Analytics/constants';
5+
import { ANALYTICS_EVENT, ANALYTICS_SELECTED_STR, NO_CHECKOUT_ATTEMPT_ID } from '../../core/Analytics/constants';
66

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

@@ -458,7 +458,7 @@ describe('GooglePay', () => {
458458
gpay.submit();
459459

460460
expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
461-
event: ANALYTICS_EVENT_INFO,
461+
event: ANALYTICS_EVENT.info,
462462
data: {
463463
component: gpay.props.type,
464464
type: ANALYTICS_SELECTED_STR,

packages/lib/src/components/Redirect/Redirect.test.tsx

+79-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { mount } from 'enzyme';
2+
import { render, waitFor, screen } from '@testing-library/preact';
23
import { h } from 'preact';
3-
import Redirect from './Redirect';
44
import RedirectShopper from './components/RedirectShopper';
55
import RedirectElement from './Redirect';
6+
import Analytics from '../../core/Analytics';
7+
import { RedirectConfiguration } from './types';
68

79
jest.mock('../../utils/detectInIframeInSameOrigin', () => {
810
return jest.fn().mockImplementation(() => {
@@ -13,7 +15,7 @@ jest.mock('../../utils/detectInIframeInSameOrigin', () => {
1315
describe('Redirect', () => {
1416
describe('isValid', () => {
1517
test('Is always valid', () => {
16-
const redirect = new Redirect(global.core, { type: 'redirect' });
18+
const redirect = new RedirectElement(global.core, { type: 'redirect' });
1719
expect(redirect.isValid).toBe(true);
1820
});
1921
});
@@ -57,3 +59,78 @@ describe('Redirect', () => {
5759
});
5860
});
5961
});
62+
63+
describe('Redirect error', () => {
64+
const oldWindowLocation = window.location;
65+
66+
beforeAll(() => {
67+
delete window.location;
68+
// @ts-ignore test only
69+
window.location = Object.defineProperties(
70+
{},
71+
{
72+
...Object.getOwnPropertyDescriptors(oldWindowLocation),
73+
assign: {
74+
configurable: true,
75+
value: jest.fn()
76+
}
77+
}
78+
);
79+
});
80+
81+
afterAll(() => {
82+
window.location = oldWindowLocation;
83+
});
84+
85+
test('should send an error event to the analytics module if beforeRedirect rejects', async () => {
86+
const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', bundleType: '' });
87+
analytics.sendAnalytics = jest.fn(() => {});
88+
const props: RedirectConfiguration = {
89+
url: 'test',
90+
method: 'POST',
91+
paymentMethodType: 'ideal',
92+
modules: { analytics },
93+
beforeRedirect: (_, reject) => {
94+
return reject();
95+
}
96+
};
97+
98+
const redirectElement = new RedirectElement(global.core, props);
99+
render(redirectElement.render());
100+
await waitFor(() => {
101+
expect(screen.getByTestId('redirect-shopper-form')).toBeInTheDocument();
102+
});
103+
104+
expect(analytics.sendAnalytics).toHaveBeenCalledWith(
105+
'ideal',
106+
{ code: '600', component: 'ideal', errorType: 'Redirect', type: 'error' },
107+
undefined
108+
);
109+
});
110+
111+
test('should send an error event to the analytics module if the redirection failed', async () => {
112+
(window.location.assign as jest.Mock).mockImplementation(() => {
113+
throw new Error('Mock error');
114+
});
115+
116+
const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', bundleType: '' });
117+
analytics.sendAnalytics = jest.fn(() => {});
118+
const props: RedirectConfiguration = {
119+
url: 'test',
120+
method: 'GET',
121+
paymentMethodType: 'ideal',
122+
modules: { analytics }
123+
};
124+
125+
const redirectElement = new RedirectElement(global.core, props);
126+
render(redirectElement.render());
127+
128+
await waitFor(() => {
129+
expect(analytics.sendAnalytics).toHaveBeenCalledWith(
130+
'ideal',
131+
{ code: '600', component: 'ideal', errorType: 'Redirect', type: 'error' },
132+
undefined
133+
);
134+
});
135+
});
136+
});

packages/lib/src/components/Redirect/Redirect.tsx

+18-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import RedirectButton from '../internal/RedirectButton';
66
import { TxVariants } from '../tx-variants';
77
import { RedirectConfiguration } from './types';
88
import collectBrowserInfo from '../../utils/browserInfo';
9+
import { ANALYTICS_ERROR_CODE, ANALYTICS_ERROR_TYPE, ANALYTICS_EVENT } from '../../core/Analytics/constants';
910

1011
class RedirectElement extends UIElement<RedirectConfiguration> {
1112
public static type = TxVariants.redirect;
@@ -23,6 +24,15 @@ class RedirectElement extends UIElement<RedirectConfiguration> {
2324
};
2425
}
2526

27+
private handleRedirectError = () => {
28+
super.submitAnalytics({
29+
component: this.props.paymentMethodType,
30+
type: ANALYTICS_EVENT.error,
31+
errorType: ANALYTICS_ERROR_TYPE.redirect,
32+
code: ANALYTICS_ERROR_CODE.redirect
33+
});
34+
};
35+
2636
get isValid() {
2737
return true;
2838
}
@@ -33,7 +43,14 @@ class RedirectElement extends UIElement<RedirectConfiguration> {
3343

3444
render() {
3545
if (this.props.url && this.props.method) {
36-
return <RedirectShopper url={this.props.url} {...this.props} onActionHandled={this.onActionHandled} />;
46+
return (
47+
<RedirectShopper
48+
url={this.props.url}
49+
{...this.props}
50+
onActionHandled={this.onActionHandled}
51+
onRedirectError={this.handleRedirectError}
52+
/>
53+
);
3754
}
3855

3956
if (this.props.showPayButton) {

packages/lib/src/components/Redirect/components/RedirectShopper/RedirectShopper.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ interface RedirectShopperProps {
1010
redirectFromTopWhenInIframe?: boolean;
1111
paymentMethodType?: string;
1212
onActionHandled?: (rtnObj: ActionHandledReturnObject) => void;
13+
onRedirectError?: () => void;
1314
}
1415

1516
class RedirectShopper extends Component<RedirectShopperProps> {
1617
private postForm;
1718
public static defaultProps = {
1819
beforeRedirect: resolve => resolve(),
20+
onRedirectError: () => {},
1921
method: 'GET'
2022
};
2123

@@ -49,14 +51,17 @@ class RedirectShopper extends Component<RedirectShopperProps> {
4951
})
5052
);
5153

52-
dispatchEvent.then(doRedirect).catch(() => {});
54+
dispatchEvent.then(doRedirect).catch(() => {
55+
this.props.onRedirectError();
56+
});
5357
}
5458

5559
render({ url, method, data = {} }) {
5660
if (method === 'POST') {
5761
return (
5862
<form
5963
method="post"
64+
data-testid="redirect-shopper-form"
6065
action={url}
6166
style={{ display: 'none' }}
6267
ref={ref => {

packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.test.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ThreeDS2Challenge } from './index';
22
import Analytics from '../../core/Analytics';
3-
import { ANALYTICS_API_ERROR, Analytics3DS2Errors, ANALYTICS_EVENT_ERROR, ANALYTICS_RENDERED_STR } from '../../core/Analytics/constants';
3+
import { ANALYTICS_ERROR_TYPE, Analytics3DS2Errors, ANALYTICS_RENDERED_STR, ANALYTICS_EVENT } from '../../core/Analytics/constants';
44
import { THREEDS2_CHALLENGE_ERROR, THREEDS2_ERROR } from './constants';
55

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

3333
expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
34-
event: ANALYTICS_EVENT_ERROR,
34+
event: ANALYTICS_EVENT.error,
3535
data: {
3636
component: challenge.constructor['type'],
3737
type: THREEDS2_ERROR,
38-
errorType: ANALYTICS_API_ERROR,
38+
errorType: ANALYTICS_ERROR_TYPE.apiError,
3939
message: `${THREEDS2_CHALLENGE_ERROR}: Missing 'paymentData' property from threeDS2 action`,
4040
code: Analytics3DS2Errors.ACTION_IS_MISSING_PAYMENT_DATA
4141
}

packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { hasOwnProperty } from '../../utils/hasOwnProperty';
77
import { TxVariants } from '../tx-variants';
88
import { ThreeDS2ChallengeConfiguration } from './types';
99
import AdyenCheckoutError, { API_ERROR } from '../../core/Errors/AdyenCheckoutError';
10-
import { ANALYTICS_API_ERROR, Analytics3DS2Errors, ANALYTICS_RENDERED_STR, Analytics3DS2Events } from '../../core/Analytics/constants';
10+
import { ANALYTICS_ERROR_TYPE, Analytics3DS2Errors, ANALYTICS_RENDERED_STR, Analytics3DS2Events } from '../../core/Analytics/constants';
1111
import { SendAnalyticsObject } from '../../core/Analytics/types';
1212
import { CoreProvider } from '../../core/Context/CoreProvider';
1313
import { ActionHandledReturnObject } from '../../types/global-types';
@@ -61,7 +61,7 @@ class ThreeDS2Challenge extends UIElement<ThreeDS2ChallengeConfiguration> {
6161
this.submitAnalytics({
6262
type: THREEDS2_ERROR,
6363
code: Analytics3DS2Errors.ACTION_IS_MISSING_PAYMENT_DATA,
64-
errorType: ANALYTICS_API_ERROR,
64+
errorType: ANALYTICS_ERROR_TYPE.apiError,
6565
message: `${THREEDS2_CHALLENGE_ERROR}: Missing 'paymentData' property from threeDS2 action`
6666
});
6767

packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.test.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ThreeDS2DeviceFingerprint } from './index';
22
import Analytics from '../../core/Analytics';
3-
import { ANALYTICS_API_ERROR, Analytics3DS2Errors, ANALYTICS_EVENT_ERROR, ANALYTICS_RENDERED_STR } from '../../core/Analytics/constants';
3+
import { Analytics3DS2Errors, ANALYTICS_RENDERED_STR, ANALYTICS_EVENT, ANALYTICS_ERROR_TYPE } from '../../core/Analytics/constants';
44
import { THREEDS2_ERROR, THREEDS2_FINGERPRINT_ERROR } from './constants';
55

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

3434
expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
35-
event: ANALYTICS_EVENT_ERROR,
35+
event: ANALYTICS_EVENT.error,
3636
data: {
3737
component: fingerprint.constructor['type'],
3838
type: THREEDS2_ERROR,
39-
errorType: ANALYTICS_API_ERROR,
39+
errorType: ANALYTICS_ERROR_TYPE.apiError,
4040
message: `${THREEDS2_FINGERPRINT_ERROR}: Missing 'paymentData' property from threeDS2 action`,
4141
code: Analytics3DS2Errors.ACTION_IS_MISSING_PAYMENT_DATA
4242
}

packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { existy } from '../../utils/commonUtils';
66
import { TxVariants } from '../tx-variants';
77
import { ThreeDS2DeviceFingerprintConfiguration } from './types';
88
import AdyenCheckoutError, { API_ERROR } from '../../core/Errors/AdyenCheckoutError';
9-
import { ANALYTICS_API_ERROR, Analytics3DS2Errors, ANALYTICS_RENDERED_STR, Analytics3DS2Events } from '../../core/Analytics/constants';
9+
import { ANALYTICS_ERROR_TYPE, Analytics3DS2Errors, ANALYTICS_RENDERED_STR, Analytics3DS2Events } from '../../core/Analytics/constants';
1010
import { SendAnalyticsObject } from '../../core/Analytics/types';
1111
import { THREEDS2_ERROR, THREEDS2_FINGERPRINT, THREEDS2_FINGERPRINT_ERROR, THREEDS2_FULL } from './constants';
1212
import { ActionHandledReturnObject } from '../../types/global-types';
@@ -53,7 +53,7 @@ class ThreeDS2DeviceFingerprint extends UIElement<ThreeDS2DeviceFingerprintConfi
5353
this.submitAnalytics({
5454
type: THREEDS2_ERROR,
5555
code: Analytics3DS2Errors.ACTION_IS_MISSING_PAYMENT_DATA,
56-
errorType: ANALYTICS_API_ERROR,
56+
errorType: ANALYTICS_ERROR_TYPE.apiError,
5757
message: `${THREEDS2_FINGERPRINT_ERROR}: Missing 'paymentData' property from threeDS2 action`
5858
});
5959

packages/lib/src/components/ThreeDS2/callSubmit3DS2Fingerprint.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { pick } from '../../utils/commonUtils';
33
import { ThreeDS2FingerprintResponse } from './types';
44
import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError';
55
import { THREEDS2_ERROR, THREEDS2_FINGERPRINT_SUBMIT } from './constants';
6-
import { ANALYTICS_API_ERROR, Analytics3DS2Errors, ANALYTICS_SDK_ERROR } from '../../core/Analytics/constants';
6+
import { ANALYTICS_ERROR_TYPE, Analytics3DS2Errors } from '../../core/Analytics/constants';
77
import { SendAnalyticsObject } from '../../core/Analytics/types';
88

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

@@ -62,7 +62,7 @@ export default function callSubmit3DS2Fingerprint({ data }): void {
6262
analyticsErrorObject = {
6363
type: THREEDS2_ERROR,
6464
code: Analytics3DS2Errors.NO_ACTION_FOR_CHALLENGE,
65-
errorType: ANALYTICS_API_ERROR,
65+
errorType: ANALYTICS_ERROR_TYPE.apiError,
6666
message: `${THREEDS2_FINGERPRINT_SUBMIT}: no action object in a response indicating a "challenge" flow`
6767
};
6868
this.submitAnalytics(analyticsErrorObject);
@@ -82,7 +82,7 @@ export default function callSubmit3DS2Fingerprint({ data }): void {
8282
analyticsErrorObject = {
8383
type: THREEDS2_ERROR,
8484
code: Analytics3DS2Errors.NO_COMPONENT_FOR_ACTION,
85-
errorType: ANALYTICS_SDK_ERROR,
85+
errorType: ANALYTICS_ERROR_TYPE.sdkError,
8686
message: `${THREEDS2_FINGERPRINT_SUBMIT}: no component defined to handle the action response`
8787
};
8888
this.submitAnalytics(analyticsErrorObject);

0 commit comments

Comments
 (0)