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

Feature/using checkoutanalytics #2234

Merged
merged 116 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
0083e12
First use of checkoutanlytics endpoint (for logging onSubmit & create…
sponglord Jun 21, 2023
b717e2c
Added comments and fixed type
sponglord Jun 22, 2023
480845d
Commenting out "target" property until API accepts it
sponglord Jun 22, 2023
e1c245d
Fixing unit tests
sponglord Jun 22, 2023
1047fee
Fixing sonarcloud gripe
sponglord Jun 22, 2023
90788b7
Tightening up types
sponglord Jun 23, 2023
5455aa3
Adding test for event queue
sponglord Jun 23, 2023
2124f2b
Updated test description
sponglord Jun 23, 2023
40a3d14
Enhancing test
sponglord Jun 23, 2023
0f931cc
Adding collect-id unit test
sponglord Jun 26, 2023
466e597
Adding TelemetryEvent type
sponglord Jun 26, 2023
392ccf5
Increasing test coverage
sponglord Jun 26, 2023
144d4a3
Adding test
sponglord Jun 26, 2023
9c9dc37
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Jun 26, 2023
7fc2bb7
Adding Storage.test
sponglord Jun 26, 2023
a6f3e70
Further checks to ensure checkoutanalytics calls fail silently
sponglord Jun 28, 2023
6f82f73
Aligning fallback storage solution with normal, window based, Storage…
sponglord Jun 28, 2023
4b8689f
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Jun 28, 2023
066c1c9
Some adjustments now actual endpoint is accepting requests
sponglord Jun 29, 2023
f969557
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Jun 29, 2023
027786e
Analytics path read from constant and passed to relevant components
sponglord Jun 30, 2023
5c5d93c
Removing unused components
sponglord Jun 30, 2023
8fe248e
Fixed unit test
sponglord Jun 30, 2023
ee9f4cc
Added comments on the shape of objects for the /checkoutanalytics end…
sponglord Jun 30, 2023
86897ee
Unit test for when checkoutanalytics url is wrong
sponglord Jun 30, 2023
5a88231
Remove constant for mismatching threeDSServerTransID (it is no longer…
sponglord Jun 30, 2023
9b3a453
Use await when calling collectId
sponglord Jul 3, 2023
113ca9b
Remove analyticsContext from CoreOptions
sponglord Jul 3, 2023
9b8a495
Adding createAnalyticsAction function in Analytics to make it simpler…
sponglord Jul 3, 2023
9b35a88
Renaming CAEventsQueue to EventsQueue
sponglord Jul 3, 2023
6b7f5e2
Clarifying the "modular" nature of Analytics and EventsQueue
sponglord Jul 3, 2023
9ac36bc
Fixing return type
sponglord Jul 4, 2023
34e34c2
Pass in containerWidth with initial analytics call
sponglord Jul 4, 2023
39c48b5
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Jul 5, 2023
660bfa4
Drop addAnalyticsAction from Analytics module. Tidy up Analytics' types
sponglord Jul 5, 2023
cc08d2c
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Aug 1, 2023
9af0f62
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Aug 8, 2023
6647c78
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Aug 21, 2023
c04a45a
Switch to v3 of the endpoint
sponglord Aug 21, 2023
5ff8daf
Fix unit test (set new analytics version)
sponglord Aug 21, 2023
3a0fb28
Changes reflecting discussion on final API tweaks
sponglord Aug 24, 2023
4a01cf6
Create analytics-action for when PM selected or mounted. Send in sess…
sponglord Aug 25, 2023
0bb61bf
Commented out logs and removed unused code
sponglord Aug 25, 2023
4c7364c
Moved initial analytics setup to UIElement now that it (potentially) …
sponglord Aug 25, 2023
1d203c1
Added type
sponglord Aug 25, 2023
a22d1a5
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Aug 25, 2023
3bd2dbe
Fixed unit test
sponglord Aug 25, 2023
30f3f08
Temporarily don't pass isStoredPaymentMethod (waiting for API fix)
sponglord Sep 1, 2023
cf4846e
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Oct 6, 2023
8f9dd4c
Adding isStoredPaymentMethod to analytics event action now the b/e su…
sponglord Oct 9, 2023
02e30bf
Adding getter for Analytics' enabled prop so 'do-not-track' can be ad…
sponglord Oct 9, 2023
da70aeb
fixing e2e tests
sponglord Oct 9, 2023
198d266
remove .only from e2e test
sponglord Oct 16, 2023
aaf3ca8
Only load analytics pixel once
sponglord Dec 6, 2023
b7d004d
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Dec 7, 2023
936a2fc
comments added
sponglord Dec 7, 2023
f57b949
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Dec 20, 2023
8107b4c
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Jan 4, 2024
921c54c
Aligning Dropin & comps so that a 'mounted' event is always sent, fro…
sponglord Jan 4, 2024
8443c14
Renamed initial Analytics call from "send" to "setUp"
sponglord Jan 4, 2024
de111c1
Create timer to send events after a set period of time
sponglord Jan 4, 2024
32f7e26
Setup analytics after render has been called. Make call to submit the…
sponglord Jan 8, 2024
5291ff3
DropinComponent sends 'rendered' analytics-action
sponglord Jan 9, 2024
5006b79
For clarity UIElement uses switch in submitAnalytics function
sponglord Jan 9, 2024
eadc2ae
Added comment about Dropin also being able to pass a list of paymentM…
sponglord Jan 9, 2024
73c2b1e
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Jan 9, 2024
b8f83e8
Use single "rendered" event to describe component mounting, dropin pm…
sponglord Jan 12, 2024
ecec6c5
Changing analytics terminology - generic term is events with specific…
sponglord Jan 12, 2024
c213f5e
Changing analytics terminology - info events are collected into an an…
sponglord Jan 12, 2024
0bcf663
Fixing unit tests
sponglord Jan 12, 2024
c46949c
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Jan 15, 2024
658b79d
Also debounce errors
sponglord Jan 15, 2024
5cb067e
Fixed unit test
sponglord Jan 15, 2024
c94e89d
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Jan 16, 2024
e348e6f
Extends feature/using_checkoutanalytics. First draft: adding focus/bl…
sponglord Jan 17, 2024
e88e4e9
Second draft: adding error analytics events for Credit card fields
sponglord Jan 17, 2024
7daa146
Second draft: adding error analytics events for Credit card fields
sponglord Jan 17, 2024
6922725
Second draft: adding error analytics events for Credit card fields
sponglord Jan 18, 2024
d779ea9
Using UIElement.submitAnalytics as a gateway for all analytics events…
sponglord Jan 18, 2024
80c1259
Added comment about onFocus/onBlur callbacks now working for non-SFs
sponglord Jan 18, 2024
691ead3
Changed constant ANALYTICS_UNFOCUS_STR to have value "unfocus"
sponglord Jan 18, 2024
1aab30d
Non dropdown fields for Address also have focus/blur analytics
sponglord Jan 18, 2024
630ae5e
Reduce the analytics info event timer to 5 secs when in development mode
sponglord Jan 18, 2024
b96f011
Aligning some analytics values with what the endpoint expects
sponglord Jan 18, 2024
9d2533f
Fixing typo
sponglord Jan 18, 2024
c9ad485
Clauses added so unit tests pass
sponglord Jan 18, 2024
b2145c3
Adding analytics for when an instant PM button is pressed
sponglord Jan 18, 2024
2b1d742
Fixed TS error
sponglord Jan 18, 2024
950de66
Removing unused code
sponglord Jan 19, 2024
63937f0
Keep object sent to onFocus & onBlur callbacks in the form expected b…
sponglord Jan 19, 2024
fff6f0a
Moving logic to create different types of Analytics events into the A…
sponglord Jan 22, 2024
1edfaa2
Improving types
sponglord Jan 22, 2024
4c71fea
Merge pull request #2521 from Adyen/feature/using_checkoutanalytics_mvp
sponglord Jan 22, 2024
2e56871
Redeclare BaseElement.this._node *before* we render (was causing an i…
sponglord Jan 24, 2024
c2a0306
Checkoutanalytics mvp with 3DS2 events (#2531)
sponglord Jan 30, 2024
077e23b
Feature/using checkoutanalytics mvp iDeal (#2549)
sponglord Feb 14, 2024
b7493e8
Feature/using checkoutanalytics mvp qr codes (#2550)
sponglord Feb 14, 2024
dd9bbad
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Feb 15, 2024
83df0e2
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Feb 20, 2024
32864c9
Add platform prop to all subsequent analytics calls
sponglord Feb 20, 2024
ef9bf6b
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Feb 20, 2024
673f8d8
All events have a unique id
sponglord Feb 20, 2024
9b51bfc
Fixing type
sponglord Feb 20, 2024
61720dd
Fixing unit tests
sponglord Feb 20, 2024
59ff4dc
Fixing issue where analytics calls were still being sent despite chec…
sponglord Feb 23, 2024
a05436e
Added some tsconfig & linting exclusions
sponglord Feb 23, 2024
93a3af4
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Feb 29, 2024
934704b
Failed attempt to retrieve checkoutAttemptId returns rejected promise
sponglord Feb 29, 2024
08a4f0d
Refactoring Analytics.test to run in a way that more accurately mimic…
sponglord Feb 29, 2024
41f619d
Feature/using checkoutanalytics mapping validation errors (#2583)
sponglord Mar 4, 2024
daa05bc
Changing logic that dictates when we jump focus from PAN to expiryDate
sponglord Mar 4, 2024
c79bd9c
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Mar 7, 2024
f52cc3e
Feature/using checkoutanalytics_with_base64 refactor (#2586)
sponglord Mar 8, 2024
7897208
Merge branch 'main' into feature/using_checkoutanalytics
sponglord Mar 8, 2024
6abdc19
Updating card rules for Discover and Diners, just for transparency on…
sponglord Mar 8, 2024
f1f2e2c
Fixing e2e tests
sponglord Mar 8, 2024
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/selfish-socks-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adyen/adyen-web': minor
---

Starting using /checkoutanalytics endpoint to retrieve "checkoutAttemptId" log "submit" and "action-handled" events
4 changes: 1 addition & 3 deletions packages/e2e/tests/vouchers/boleto/boleto.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ const mockData = {
houseNumberOrName: '123',
city: 'Sao Paulo',
postalCode: '11111555',
stateOrProvince: 'SP',
firstName: 'N/A',
lastName: 'N/A'
stateOrProvince: 'SP'
},
shopperEmail: '[email protected]'
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import classNames from 'classnames';
import styles from '../AchInput.module.scss';
import Field from '../../../../internal/FormFields/Field';
import DataSfSpan from '../../../../Card/components/CardInput/components/DataSfSpan';
import { capitalizeFirstLetter } from '../../../../../utils/Formatters/formatters';

const AchSFInput = ({ id, dataInfo, className = '', label, focused, filled, errorMessage = '', isValid = false, onFocusField, dir }) => {
const capitalisedId = id.charAt(0).toUpperCase() + id.slice(1);
const capitalisedId = capitalizeFirstLetter(id);
const encryptedIdStr = `encrypted${capitalisedId}`;

return (
Expand Down
18 changes: 14 additions & 4 deletions packages/lib/src/components/ApplePay/ApplePay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { preparePaymentRequest } from './payment-request';
import { resolveSupportedVersion, mapBrands } from './utils';
import { ApplePayElementProps, ApplePayElementData, ApplePaySessionRequest, OnAuthorizedCallback } from './types';
import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError';
import { ANALYTICS_INSTANT_PAYMENT_BUTTON, ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants';
import { DecodeObject } from '../types';

const latestSupportedVersion = 14;

Expand Down Expand Up @@ -53,6 +55,11 @@ class ApplePayElement extends UIElement<ApplePayElementProps> {
}

submit() {
// Analytics
if (this.props.isInstantPayment) {
this.submitAnalytics({ type: ANALYTICS_SELECTED_STR, target: ANALYTICS_INSTANT_PAYMENT_BUTTON });
}

return this.startSession(this.props.onAuthorized);
}

Expand Down Expand Up @@ -104,10 +111,13 @@ class ApplePayElement extends UIElement<ApplePayElementProps> {

try {
const response = await httpPost(options, request);
const decodedData = base64.decode(response.data);
if (!decodedData) reject('Could not decode Apple Pay session');
const session = JSON.parse(decodedData as string);
resolve(session);
const decodedData: DecodeObject = base64.decode(response.data);
if (!decodedData.success) {
reject('Could not decode Apple Pay session');
} else {
const session = JSON.parse(decodedData.data);
resolve(session);
}
} catch (e) {
reject('Could not get Apple Pay session');
}
Expand Down
46 changes: 35 additions & 11 deletions packages/lib/src/components/BaseElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Core from '../core';
import { BaseElementProps, PaymentData } from './types';
import { RiskData } from '../core/RiskModule/RiskModule';
import { Resources } from '../core/Context/Resources';
import { AnalyticsInitialEvent, SendAnalyticsObject } from '../core/Analytics/types';
import { ANALYTICS_RENDERED_STR } from '../core/Analytics/constants';

class BaseElement<P extends BaseElementProps> {
public readonly _id = `${this.constructor['type']}-${uuid()}`;
Expand Down Expand Up @@ -46,6 +48,16 @@ class BaseElement<P extends BaseElementProps> {
return {};
}

/* eslint-disable-next-line */
protected setUpAnalytics(setUpAnalyticsObj: AnalyticsInitialEvent) {
return null;
}

/* eslint-disable-next-line */
protected submitAnalytics(analyticsObj?: SendAnalyticsObject) {
return null;
}

protected setState(newState: object): void {
this.state = { ...this.state, ...newState };
}
Expand All @@ -56,8 +68,8 @@ class BaseElement<P extends BaseElementProps> {
*/
get data(): PaymentData | RiskData {
const clientData = getProp(this.props, 'modules.risk.data');
const useAnalytics = !!getProp(this.props, 'modules.analytics.props.enabled');
const checkoutAttemptId = useAnalytics ? getProp(this.props, 'modules.analytics.checkoutAttemptId') : 'do-not-track';
const useAnalytics = !!getProp(this.props, 'modules.analytics.getEnabled')?.();
const checkoutAttemptId = useAnalytics ? getProp(this.props, 'modules.analytics.getCheckoutAttemptId')?.() : 'do-not-track';
const order = this.state.order || this.props.order;

const componentData = this.formatData();
Expand Down Expand Up @@ -98,24 +110,36 @@ class BaseElement<P extends BaseElementProps> {
throw new Error('Component could not mount. Root node was not found.');
}

const setupAnalytics = !this._node;

if (this._node) {
this.unmount(); // new, if this._node exists then we are "remounting" so we first need to unmount if it's not already been done
} else {
// Set up analytics, once
if (this.props.modules && this.props.modules.analytics && !this.props.isDropin) {
this.props.modules.analytics.send({
containerWidth: this._node && this._node.offsetWidth,
component: this.constructor['analyticsType'] ?? this.constructor['type'],
flavor: 'components'
});
}
}

this._node = node;

this._component = this.render();

render(this._component, node);

// Set up analytics (once, since this._node is currently undefined) now that we have mounted and rendered
if (setupAnalytics) {
if (this.props.modules && this.props.modules.analytics) {
this.setUpAnalytics({
containerWidth: node && (node as HTMLElement).offsetWidth,
component: !this.props.isDropin ? this.constructor['analyticsType'] ?? this.constructor['type'] : 'dropin',
flavor: !this.props.isDropin ? 'components' : 'dropin'
}).then(() => {
// Once the initial analytics set up call has been made...
// ...create an analytics event declaring that the component has been rendered
// (The dropin will do this itself from DropinComponent once the PM list has rendered)
if (!this.props.isDropin) {
this.submitAnalytics({ type: ANALYTICS_RENDERED_STR });
}
});
}
}

return this;
}

Expand Down
162 changes: 162 additions & 0 deletions packages/lib/src/components/Card/Card.Analytics.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { CardElement } from './Card';
import Analytics from '../../core/Analytics';

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

let card;

import {
ANALYTICS_CONFIGURED_STR,
ANALYTICS_EVENT_INFO,
ANALYTICS_EVENT_LOG,
ANALYTICS_FOCUS_STR,
ANALYTICS_RENDERED_STR,
ANALYTICS_SUBMIT_STR,
ANALYTICS_UNFOCUS_STR,
ANALYTICS_VALIDATION_ERROR_STR
} from '../../core/Analytics/constants';

describe('Card: calls that generate "info" analytics should produce objects with the expected shapes ', () => {
beforeEach(() => {
console.log = jest.fn(() => {});

card = new CardElement({
modules: {
analytics: analyticsModule
}
});

analyticsModule.createAnalyticsEvent = jest.fn(() => null);
});

test('Analytics should produce an "info" event, of type "rendered", for a card PM', () => {
card.submitAnalytics({
type: ANALYTICS_RENDERED_STR
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
data: { component: card.constructor['type'], type: ANALYTICS_RENDERED_STR }
});
});

test('Analytics should produce an "info" event, of type "rendered", for a storedCard PM', () => {
card.submitAnalytics({
type: ANALYTICS_RENDERED_STR,
isStoredPaymentMethod: true,
brand: 'mc'
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
data: { component: card.constructor['type'], type: ANALYTICS_RENDERED_STR, isStoredPaymentMethod: true, brand: 'mc' }
});
});

test('Analytics should produce an "info" event, of type "configured", for a card PM', () => {
card.submitAnalytics({
type: ANALYTICS_CONFIGURED_STR
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
data: { component: card.constructor['type'], type: ANALYTICS_CONFIGURED_STR }
});
});

test('Analytics should produce an "info" event, of type "configured", for a storedCard PM', () => {
card.submitAnalytics({
type: ANALYTICS_CONFIGURED_STR,
isStoredPaymentMethod: true,
brand: 'mc'
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
data: { component: card.constructor['type'], type: ANALYTICS_CONFIGURED_STR, isStoredPaymentMethod: true, brand: 'mc' }
});
});

test('Analytics should produce an "info" event, of type "focus" with the target value correctly formed', () => {
card.onFocus({
fieldType: 'encryptedCardNumber',
event: {
action: 'focus',
focus: true,
numChars: 0,
fieldType: 'encryptedCardNumber',
rootNode: {},
type: 'card',
currentFocusObject: 'encryptedCardNumber'
}
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
data: { component: card.constructor['type'], type: ANALYTICS_FOCUS_STR, target: 'card_number' }
});
});

test('Analytics should produce an "info" event, of type "unfocus" with the target value correctly formed', () => {
card.onBlur({
fieldType: 'encryptedCardNumber',
event: {
action: 'focus',
focus: false,
numChars: 1,
fieldType: 'encryptedCardNumber',
rootNode: {},
type: 'card',
currentFocusObject: null
}
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
data: { component: card.constructor['type'], type: ANALYTICS_UNFOCUS_STR, target: 'card_number' }
});
});

test('Analytics should produce an "info" event, of type "validationError", with the expected properties', () => {
card.onErrorAnalytics({
fieldType: 'encryptedCardNumber',
errorCode: 'error.va.sf-cc-num.04'
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_INFO,
data: {
component: card.constructor['type'],
type: ANALYTICS_VALIDATION_ERROR_STR,
target: 'card_number',
validationErrorCode: 'error.va.sf-cc-num.04',
validationErrorMessage: 'card-number-not-filled-correctly'
}
});
});
});

describe('Card: calls that generate "log" analytics should produce objects with the expected shapes ', () => {
beforeEach(() => {
console.log = jest.fn(() => {});

card = new CardElement({
modules: {
analytics: analyticsModule
}
});
});

test('Analytics should produce an "log" event, of type "submit", for a card PM', () => {
card.submitAnalytics({ type: ANALYTICS_SUBMIT_STR });

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
event: ANALYTICS_EVENT_LOG,
data: {
component: card.constructor['type'],
type: ANALYTICS_SUBMIT_STR,
message: 'Shopper clicked pay'
}
});
});
});
Loading
Loading