From 0083e1216eba125e86889fe73a44f00846c0d99e Mon Sep 17 00:00:00 2001 From: nicholas Date: Wed, 21 Jun 2023 17:40:52 +0200 Subject: [PATCH 01/94] First use of checkoutanlytics endpoint (for logging onSubmit & createFromAction events) --- .../AchInput/components/AchSFInput.tsx | 3 +- packages/lib/src/components/BaseElement.ts | 2 +- packages/lib/src/components/UIElement.tsx | 37 +++++- packages/lib/src/components/types.ts | 11 +- packages/lib/src/core/Analytics/Analytics.ts | 108 +++++++++--------- .../src/core/Analytics/CAEventsQueue.test.ts | 31 +++++ .../lib/src/core/Analytics/CAEventsQueue.ts | 69 +++++++++++ packages/lib/src/core/Analytics/constants.ts | 27 +++++ packages/lib/src/core/Analytics/types.ts | 21 +++- packages/lib/src/core/Analytics/utils.ts | 28 +++++ .../lib/src/core/Environment/Environment.ts | 15 +++ packages/lib/src/core/Environment/index.ts | 2 +- .../PaymentAction/actionTypes.ts | 1 + .../src/core/Services/analytics/collect-id.ts | 39 +++++-- .../lib/src/core/Services/analytics/types.ts | 7 +- packages/lib/src/core/Services/http.ts | 2 +- packages/lib/src/core/core.ts | 24 +++- packages/lib/src/core/types.ts | 5 + .../lib/src/utils/Formatters/formatters.ts | 4 + 19 files changed, 361 insertions(+), 75 deletions(-) create mode 100644 packages/lib/src/core/Analytics/CAEventsQueue.test.ts create mode 100644 packages/lib/src/core/Analytics/CAEventsQueue.ts create mode 100644 packages/lib/src/core/Analytics/constants.ts create mode 100644 packages/lib/src/core/Analytics/utils.ts diff --git a/packages/lib/src/components/Ach/components/AchInput/components/AchSFInput.tsx b/packages/lib/src/components/Ach/components/AchInput/components/AchSFInput.tsx index 0ed3a6ab6b..b4f3163af4 100644 --- a/packages/lib/src/components/Ach/components/AchInput/components/AchSFInput.tsx +++ b/packages/lib/src/components/Ach/components/AchInput/components/AchSFInput.tsx @@ -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 ( diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index aaef172fab..7d705f8a0d 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -56,7 +56,7 @@ class BaseElement

{ */ get data(): PaymentData | RiskData { const clientData = getProp(this.props, 'modules.risk.data'); - const checkoutAttemptId = getProp(this.props, 'modules.analytics.checkoutAttemptId'); + const checkoutAttemptId = getProp(this.props, 'modules.analytics.getCheckoutAttemptId')(); const order = this.state.order || this.props.order; const componentData = this.formatData(); diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 40353fa100..f37d4f9d91 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -11,6 +11,9 @@ import { hasOwnProperty } from '../utils/hasOwnProperty'; import DropinElement from './Dropin'; import { CoreOptions } from '../core/types'; import Core from '../core'; +import { AnalyticsObject } from '../core/Analytics/types'; +import { createAnalyticsObject } from '../core/Analytics/utils'; +import { ANALYTICS_SUBMIT_STR } from '../core/Analytics/constants'; export class UIElement

extends BaseElement

implements IUIElement { protected componentRef: any; @@ -45,6 +48,25 @@ export class UIElement

extends BaseElement

im return state; } + /* eslint-disable-next-line */ + protected submitAnalytics(obj = null) { + // Call analytics endpoint + let component = this.elementRef._id.substring(0, this.elementRef._id.indexOf('-')); + if (component === 'dropin') { + const subCompID = this.elementRef['dropinRef'].state.activePaymentMethod._id; + component = `${component}-${subCompID.substring(0, subCompID.indexOf('-'))}`; + } + + const aObj: AnalyticsObject = createAnalyticsObject({ + class: 'log', + component, + type: ANALYTICS_SUBMIT_STR, + target: 'pay_button' + }); + + this.props.modules.analytics.addAnalyticsAction('log', aObj); + } + private onSubmit(): void { //TODO: refactor this, instant payment methods are part of Dropin logic not UIElement if (this.props.isInstantPayment) { @@ -57,10 +79,14 @@ export class UIElement

extends BaseElement

im } if (this.props.onSubmit) { - // Classic flow + /** Classic flow */ + // Call analytics endpoint + this.submitAnalytics(); + + // Call onSubmit handler this.props.onSubmit({ data: this.data, isValid: this.isValid }, this.elementRef); } else if (this._parentInstance.session) { - // Session flow + /** Session flow */ // wrap beforeSubmit callback in a promise const beforeSubmitEvent = this.props.beforeSubmit ? new Promise((resolve, reject) => @@ -72,7 +98,12 @@ export class UIElement

extends BaseElement

im : Promise.resolve(this.data); beforeSubmitEvent - .then(data => this.submitPayment(data)) + .then(data => { + // Call analytics endpoint + this.submitAnalytics(); + // Submit payment + return this.submitPayment(data); + }) .catch(() => { // set state as ready to submit if the merchant cancels the action this.elementRef.setStatus('ready'); diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index 9d0cd164c7..208213c392 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -3,12 +3,12 @@ import { Order, PaymentAction, PaymentAmount, PaymentAmountExtended } from '../t import Language from '../language/Language'; import UIElement from './UIElement'; import Core from '../core'; -import Analytics from '../core/Analytics'; import RiskElement from '../core/RiskModule'; import { PayButtonProps } from './internal/PayButton/PayButton'; import Session from '../core/CheckoutSession'; import { SRPanel } from '../core/Errors/SRPanel'; import { Resources } from '../core/Context/Resources'; +import { AnalyticsConfig, AnalyticsObject } from '../core/Analytics/types'; export interface PaymentMethodData { paymentMethod: { @@ -63,12 +63,19 @@ export interface RawPaymentResponse extends PaymentResponse { [key: string]: any; } +export interface AnalyticsModule { + send: (a: AnalyticsConfig) => void; + addAnalyticsAction: (s: string, o: AnalyticsObject) => void; + sendAnalyticsActions: () => Promise; + getCheckoutAttemptId: () => string; +} + export interface BaseElementProps { _parentInstance?: Core; order?: Order; modules?: { srPanel?: SRPanel; - analytics?: Analytics; + analytics?: AnalyticsModule; resources?: Resources; risk?: RiskElement; }; diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index dd6cdab131..45061b5184 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -1,74 +1,80 @@ import logEvent from '../Services/analytics/log-event'; -import postTelemetry from '../Services/analytics/post-telemetry'; import collectId from '../Services/analytics/collect-id'; -import EventsQueue from './EventsQueue'; import { CoreOptions } from '../types'; +import CAEventsQueue, { EQObject } from './CAEventsQueue'; +import { ANALYTICS_ACTION, AnalyticsConfig, AnalyticsObject } from './types'; +import { ANALYTICS_ACTION_ERROR, ANALYTICS_ACTION_LOG } from './constants'; +import { debounce } from '../../components/internal/Address/utils'; +import { AnalyticsModule } from '../../components/types'; -export type AnalyticsProps = Pick; +export type AnalyticsProps = Pick; -class Analytics { - private static defaultProps = { +let _checkoutAttemptId = null; + +const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analyticsContext }: AnalyticsProps) => { + const defaultProps = { enabled: true, telemetry: true, - checkoutAttemptId: null, - experiments: [] + checkoutAttemptId: null }; - public checkoutAttemptId: string = null; - public props; - private readonly logEvent; - private readonly logTelemetry; - private readonly queue = new EventsQueue(); - public readonly collectId; - - constructor({ loadingContext, locale, clientKey, analytics, amount }: AnalyticsProps) { - this.props = { ...Analytics.defaultProps, ...analytics }; - - this.logEvent = logEvent({ loadingContext, locale }); - this.logTelemetry = postTelemetry({ loadingContext, locale, clientKey, amount }); - this.collectId = collectId({ loadingContext, clientKey, experiments: this.props.experiments }); + const props = { ...defaultProps, ...analytics }; - const { telemetry, enabled } = this.props; - if (telemetry === true && enabled === true) { - if (this.props.checkoutAttemptId) { - // handle prefilled checkoutAttemptId - this.checkoutAttemptId = this.props.checkoutAttemptId; - this.queue.run(this.checkoutAttemptId); - } + const { telemetry, enabled } = props; + if (telemetry === true && enabled === true) { + if (props.checkoutAttemptId) { + // handle prefilled checkoutAttemptId // TODO is this still something that ever happens? + _checkoutAttemptId = props.checkoutAttemptId; } } - send(event) { - const { enabled, payload, telemetry } = this.props; + const _logEvent = logEvent({ loadingContext, locale }); + const _collectId = collectId({ analyticsContext, clientKey, locale, amount }); + const _caEventsQueue: EQObject = CAEventsQueue({ analyticsContext, clientKey }); + + const analyticsObj: AnalyticsModule = { + send: (config: AnalyticsConfig) => { + const { enabled, payload, telemetry } = props; // TODO what is payload, is it ever used? - if (enabled === true) { - if (telemetry === true && !this.checkoutAttemptId) { - // fetch a new checkoutAttemptId if none is already available - this.collectId() - .then(checkoutAttemptId => { - this.checkoutAttemptId = checkoutAttemptId; - this.queue.run(this.checkoutAttemptId); - }) - .catch(e => { - console.warn(`Fetching checkoutAttemptId failed.${e ? ` Error=${e}` : ''}`); - }); + if (enabled === true) { + if (telemetry === true && !_checkoutAttemptId) { + // fetch a new checkoutAttemptId if none is already available + _collectId({ ...config, ...(payload && { ...payload }) }) + .then(checkoutAttemptId => { + console.log('### Analytics::checkoutAttemptId:: ', checkoutAttemptId); + _checkoutAttemptId = checkoutAttemptId; + }) + .catch(e => { + console.warn(`Fetching checkoutAttemptId failed.${e ? ` Error=${e}` : ''}`); + }); + } + // Log pixel // TODO once we stop using the pixel we can stop requiring both "enabled" & "telemetry" config options + _logEvent(config); } + }, - if (telemetry === true) { - const telemetryTask = checkoutAttemptId => - this.logTelemetry({ ...event, ...(payload && { ...payload }), checkoutAttemptId }).catch(() => {}); + // used in BaseElement + getCheckoutAttemptId: (): string => _checkoutAttemptId, - this.queue.add(telemetryTask); + addAnalyticsAction: (type: ANALYTICS_ACTION, obj: AnalyticsObject) => { + _caEventsQueue.add(`${type}s`, obj); - if (this.checkoutAttemptId) { - this.queue.run(this.checkoutAttemptId); - } + // errors get sent straight away, logs almost do (with a debounce), events are stored until an error or log comes along + if (type === ANALYTICS_ACTION_LOG || type === ANALYTICS_ACTION_ERROR) { + const debounceFn = type === ANALYTICS_ACTION_ERROR ? fn => fn : debounce; + debounceFn(analyticsObj.sendAnalyticsActions)(); } + }, - // Log pixel - this.logEvent(event); + sendAnalyticsActions: () => { + if (_checkoutAttemptId) { + return _caEventsQueue.run(_checkoutAttemptId); + } + return Promise.resolve(null); } - } -} + }; + + return analyticsObj; +}; export default Analytics; diff --git a/packages/lib/src/core/Analytics/CAEventsQueue.test.ts b/packages/lib/src/core/Analytics/CAEventsQueue.test.ts new file mode 100644 index 0000000000..e38541be1a --- /dev/null +++ b/packages/lib/src/core/Analytics/CAEventsQueue.test.ts @@ -0,0 +1,31 @@ +import CAEventsQueue from './CAEventsQueue'; + +describe('CAEventsQueue', () => { + const queue = CAEventsQueue({ analyticsContext: 'http://mydomain.com', clientKey: 'fsdjkh' }); + + test('adds log to the queue', () => { + const task1 = { foo: 'bar' }; + queue.add('logs', task1); + expect(queue.getQueue().logs.length).toBe(1); + }); + + test('adds event to the queue', () => { + const task1 = { foo: 'bar' }; + queue.add('events', task1); + expect(queue.getQueue().events.length).toBe(1); + }); + + test('adds error to the queue', () => { + const task1 = { foo: 'bar' }; + queue.add('errors', task1); + expect(queue.getQueue().errors.length).toBe(1); + }); + + test('run flushes the queue', () => { + queue.run('checkoutAttemptId'); + + expect(queue.getQueue().logs.length).toBe(0); + expect(queue.getQueue().events.length).toBe(0); + expect(queue.getQueue().errors.length).toBe(0); + }); +}); diff --git a/packages/lib/src/core/Analytics/CAEventsQueue.ts b/packages/lib/src/core/Analytics/CAEventsQueue.ts new file mode 100644 index 0000000000..c497ba08bc --- /dev/null +++ b/packages/lib/src/core/Analytics/CAEventsQueue.ts @@ -0,0 +1,69 @@ +import { HttpOptions, httpPost } from '../Services/http'; +import { AnalyticsObject } from './types'; + +interface CAActions { + channel: 'Web'; + events: AnalyticsObject[]; + errors: AnalyticsObject[]; + logs: AnalyticsObject[]; +} + +export interface EQObject { + add: (t, a) => void; + run: (id) => Promise; + getQueue: () => CAActions; + _runQueue: (id) => Promise; +} + +const CAEventsQueue = ({ analyticsContext, clientKey }) => { + const caActions: CAActions = { + channel: 'Web', + events: [], + errors: [], + logs: [] + }; + + const eqObject: EQObject = { + add: (type, actionObj) => { + caActions[type].push(actionObj); + }, + + run: checkoutAttemptId => { + const promise = eqObject._runQueue(checkoutAttemptId); + + caActions.events = []; + caActions.errors = []; + caActions.logs = []; + + return promise; + }, + + // Expose getter for testing purposes + getQueue: () => caActions, + + _runQueue: (checkoutAttemptId): Promise => { + if (!caActions.events.length && !caActions.logs.length && !caActions.errors.length) { + return Promise.resolve(null); + } + + const options: HttpOptions = { + errorLevel: 'silent' as const, + loadingContext: analyticsContext, + path: `v2/analytics/${checkoutAttemptId}?clientKey=${clientKey}` + }; + + const promise = httpPost(options, caActions) + .then(() => { + console.log('### CAEventsQueue::send:: success'); + return undefined; + }) + .catch(() => {}); + + return promise; + } + }; + + return eqObject; +}; + +export default CAEventsQueue; diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts new file mode 100644 index 0000000000..bff91e637d --- /dev/null +++ b/packages/lib/src/core/Analytics/constants.ts @@ -0,0 +1,27 @@ +export const ANALYTICS_ACTION_LOG = 'log'; +export const ANALYTICS_ACTION_ERROR = 'error'; +export const ANALYTICS_ACTION_EVENT = 'event'; + +export const ANALYTICS_ACTION_STR = 'Action'; +export const ANALYTICS_SUBMIT_STR = 'Submit'; + +export const ANALYTICS_IMPLEMENTATION_ERROR = 'ImplementationError'; +export const ANALYTICS_API_ERROR = 'APIError'; + +export const ANALYTICS_ERROR_CODE_ACTION_IS_MISSING_PAYMENT_DATA = 'web_700'; // Missing 'paymentData' property from threeDS2 action +export const ANALYTICS_ERROR_CODE_ACTION_IS_MISSING_TOKEN = 'web_701'; // Missing 'token' property from threeDS2 action` +export const ANALYTICS_ERROR_CODE_TOKEN_IS_MISSING_THREEDSMETHODURL = 'web_702'; // Decoded token is missing a valid threeDSMethodURL property + +/** + * Decoded token is missing one or more of the following properties: + * fingerprint: (threeDSMethodNotificationURL | postMessageDomain | threeDSServerTransID) + * challenge: (acsTransID | messageVersion | threeDSServerTransID) + */ +export const ANALYTICS_ERROR_CODE_TOKEN_IS_MISSING_OTHER_PROPS = 'web_703'; + +export const ANALYTICS_ERROR_CODE_TOKEN_DECODE_OR_PARSING_FAILED = 'web_704'; // token decoding or parsing has failed. ('not base64', 'malformed URI sequence' or 'Could not JSON parse token') +export const ANALYTICS_ERROR_CODE_3DS2_TIMEOUT = 'web_705'; // 3DS2 process has timed out + +export const ANALYTICS_ERROR_CODE_TOKEN_IS_MISSING_ACSURL = 'web_800'; // Decoded token is missing a valid acsURL property +export const ANALYTICS_ERROR_CODE_NO_TRANSSTATUS = 'web_801'; // Challenge has resulted in an error (no transStatus could be retrieved by the backend) +export const ANALYTICS_ERROR_CODE_MISMATCHING_TRANS_IDS = 'web_802'; // threeDSServerTransID: ids do not match diff --git a/packages/lib/src/core/Analytics/types.ts b/packages/lib/src/core/Analytics/types.ts index fa982dd227..134baab438 100644 --- a/packages/lib/src/core/Analytics/types.ts +++ b/packages/lib/src/core/Analytics/types.ts @@ -26,7 +26,26 @@ export interface AnalyticsOptions { payload?: any; /** - * List of experiments to be sent in the collectId call + * List of experiments to be sent in the collectId call // TODO - still used? */ experiments?: Experiment[]; } + +export interface AnalyticsObject { + timestamp: string; + component: string; + code?: string; + errorType?: string; + message?: string; + type?: string; + subtype?: string; +} + +export type ANALYTICS_ACTION = 'log' | 'error' | 'event'; + +export type AnalyticsConfig = { + containerWidth: number; + component: string; + flavor: string; + paymentMethods?: any[]; +}; diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts new file mode 100644 index 0000000000..2678f705a8 --- /dev/null +++ b/packages/lib/src/core/Analytics/utils.ts @@ -0,0 +1,28 @@ +import { AnalyticsObject } from './types'; +import { ANALYTICS_ACTION_STR, ANALYTICS_SUBMIT_STR } from './constants'; + +export const getUTCTimestamp = () => Date.now(); + +/** + * All objects for the /checkoutanalytics endpoint have base props: + * "timestamp" & "component" + * + * Error objects have, in addition to the base props: + * "code", "errorType" & "message" + * + * Log objects have, in addition to the base props: + * "type" & "message" (and maybe an "action" & "subtype") + * + * Event objects have, in addition to the base props: + * "type" & "target" + */ +export const createAnalyticsObject = (aObj): AnalyticsObject => ({ + timestamp: String(getUTCTimestamp()), + component: aObj.component, + ...(aObj.class === 'error' && { code: aObj.code, errorType: aObj.errorType }), // only added if we have an error object + ...((aObj.class === 'error' || (aObj.class === 'log' && aObj.type !== ANALYTICS_SUBMIT_STR)) && { message: aObj.message }), // only added if we have an error, or log object (that's not logging a submit/pay button press) + ...(aObj.class === 'log' && { type: aObj.type }), // only added if we have a log object + ...(aObj.class === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type + ...(aObj.class === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type + ...(aObj.class === 'event' && { type: aObj.type, target: aObj.target }) // only added if we have an event object +}); diff --git a/packages/lib/src/core/Environment/Environment.ts b/packages/lib/src/core/Environment/Environment.ts index df44d12ec2..4bba870e65 100644 --- a/packages/lib/src/core/Environment/Environment.ts +++ b/packages/lib/src/core/Environment/Environment.ts @@ -28,3 +28,18 @@ export const resolveCDNEnvironment = (env: string = FALLBACK_CDN_CONTEXT) => { return environments[env] || environments[env.toLowerCase()] || env; }; + +export const FALLBACK_ANALYTICS_CONTEXT = 'https://checkoutanalytics-live.adyen.com/checkoutanalytics/'; + +export const resolveAnalyticsEnvironment = (env: string = FALLBACK_ANALYTICS_CONTEXT) => { + const environments = { + test: 'https://checkoutanalytics-test.adyen.com/checkoutanalytics/', + live: 'https://checkoutanalytics-live.adyen.com/checkoutanalytics/', + 'live-us': 'https://checkoutanalytics-live-us.adyen.com/checkoutanalytics/', + 'live-au': 'https://checkoutanalytics-live-au.adyen.com/checkoutanalytics/', + 'live-apse': 'https://checkoutanalytics-live-apse.adyen.com/checkoutanalytics/', + 'live-in': 'https://checkoutanalytics-live-in.adyen.com/checkoutanalytics/' + }; + + return environments[env] || environments[env.toLowerCase()] || env; +}; diff --git a/packages/lib/src/core/Environment/index.ts b/packages/lib/src/core/Environment/index.ts index fca1af85ce..ddab592fd1 100644 --- a/packages/lib/src/core/Environment/index.ts +++ b/packages/lib/src/core/Environment/index.ts @@ -1 +1 @@ -export { resolveCDNEnvironment, resolveEnvironment } from './Environment'; +export { resolveCDNEnvironment, resolveEnvironment, resolveAnalyticsEnvironment } from './Environment'; diff --git a/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts b/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts index 877a3f5dd3..b392cb112e 100644 --- a/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts +++ b/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts @@ -67,6 +67,7 @@ const actionTypes = { _parentInstance: props._parentInstance, paymentMethodType: props.paymentMethodType, challengeWindowSize: props.challengeWindowSize, // always pass challengeWindowSize in case it's been set directly in the handleAction config object + analytics: props.modules.analytics, // Props unique to a particular flow ...get3DS2FlowProps(action.subtype, props) diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index 97a8d542b3..98ffd1bd8d 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -15,31 +15,45 @@ function confirmSessionDurationIsMaxFifteenMinutes(checkoutAttemptIdSession: Che } /** - * Log event to Adyen - * @param config - ready to be serialized and included in the body of request - * @returns a function returning a promise containing the response of the call + * Send an event to Adyen with some basic telemetry info and receive a checkoutAttemptId in response + * @param config - object containing values needed to calculate the url for the request; and also some that need to be serialized and included in the body of request + * @returns a function returning a promise containing the response of the call (an object containing a checkoutAttemptId property) */ -const collectId = ({ loadingContext, clientKey, experiments }: CollectIdProps) => { +// const collectId = ({ analyticsContext, clientKey, locale, amount }: CollectId2Props) => { +const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { let promise; const options = { errorLevel: 'silent' as const, - loadingContext: loadingContext, - path: `v2/analytics/id?clientKey=${clientKey}` + loadingContext: analyticsContext, + path: `v2/analytics?clientKey=${clientKey}` }; - return (): Promise => { + return (event): Promise => { + const telemetryEvent = { + // amount, // TODO will be supported in the future + version: process.env.VERSION, + channel: 'Web', + locale, + flavor: 'components', + referrer: window.location.href, + screenWidth: window.screen.width, + ...event + }; + if (promise) return promise; if (!clientKey) return Promise.reject(); const storage = new Storage('checkout-attempt-id', 'sessionStorage'); const checkoutAttemptIdSession = storage.get(); + // In some cases, e.g. where the merchant has redirected the shopper and then returned them to checkout, we still have a valid checkoutAttemptId + // so there is no need for the re-initialised Checkout to generate another one if (confirmSessionDurationIsMaxFifteenMinutes(checkoutAttemptIdSession)) { return Promise.resolve(checkoutAttemptIdSession.id); } - promise = httpPost(options, { experiments }) + promise = httpPost(options, telemetryEvent) .then(conversion => { if (conversion.id) { storage.set({ id: conversion.id, timestamp: Date.now() }); @@ -47,7 +61,14 @@ const collectId = ({ loadingContext, clientKey, experiments }: CollectIdProps) = } return undefined; }) - .catch(() => {}); + .catch(() => { + // TODO - temporarily faking it + console.log('### collect-id2:::: FAILED'); + const id = '64d673ff-36d3-4b32-999b-49e215f6b9891687261360764E7D99B01E11BF4C4B83CF7C7F49C5E75F23B2381E2ACBEE8E03E221E3BC95998'; + storage.set({ id: id, timestamp: Date.now() }); + return id; + // TODO - end + }); return promise; }; diff --git a/packages/lib/src/core/Services/analytics/types.ts b/packages/lib/src/core/Services/analytics/types.ts index ca24f6ad9b..c648e12219 100644 --- a/packages/lib/src/core/Services/analytics/types.ts +++ b/packages/lib/src/core/Services/analytics/types.ts @@ -1,4 +1,4 @@ -import { Experiment } from '../../Analytics/types'; +import { PaymentAmount } from '../../../types'; type CheckoutAttemptIdSession = { id: string; @@ -7,8 +7,9 @@ type CheckoutAttemptIdSession = { type CollectIdProps = { clientKey: string; - loadingContext: string; - experiments: Experiment[]; + analyticsContext: string; + locale: string; + amount: PaymentAmount; }; export { CheckoutAttemptIdSession, CollectIdProps }; diff --git a/packages/lib/src/core/Services/http.ts b/packages/lib/src/core/Services/http.ts index 9780151aae..a6f141accb 100644 --- a/packages/lib/src/core/Services/http.ts +++ b/packages/lib/src/core/Services/http.ts @@ -2,7 +2,7 @@ import fetch from './fetch'; import { FALLBACK_CONTEXT } from '../config'; import AdyenCheckoutError from '../Errors/AdyenCheckoutError'; -interface HttpOptions { +export interface HttpOptions { accept?: string; contentType?: string; errorMessage?: string; diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index 45fc0fd82f..5eebaadf09 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -4,7 +4,7 @@ import RiskModule from './RiskModule'; import paymentMethods, { getComponentConfiguration } from '../components'; import PaymentMethodsResponse from './ProcessResponse/PaymentMethodsResponse'; import getComponentForAction from './ProcessResponse/PaymentAction'; -import { resolveEnvironment, resolveCDNEnvironment } from './Environment'; +import { resolveEnvironment, resolveCDNEnvironment, resolveAnalyticsEnvironment } from './Environment'; import Analytics from './Analytics'; import { PaymentAction } from '../types'; import { CoreOptions } from './types'; @@ -14,6 +14,10 @@ import Session from './CheckoutSession'; import { hasOwnProperty } from '../utils/hasOwnProperty'; import { Resources } from './Context/Resources'; import { SRPanel } from './Errors/SRPanel'; +import { createAnalyticsObject } from './Analytics/utils'; +import { ANALYTICS_ACTION_STR } from './Analytics/constants'; +import { AnalyticsObject } from './Analytics/types'; +import { capitalizeFirstLetter } from '../utils/Formatters/formatters'; class Core { public session: Session; @@ -25,6 +29,7 @@ class Core { public loadingContext?: string; public cdnContext?: string; + public analyticsContext?: string; public static readonly version = { version: process.env.VERSION, @@ -42,6 +47,7 @@ class Core { this.loadingContext = resolveEnvironment(this.options.environment); this.cdnContext = resolveCDNEnvironment(this.options.resourceEnvironment || this.options.environment); + this.analyticsContext = resolveAnalyticsEnvironment(this.options.environment); const clientKeyType = this.options.clientKey?.substr(0, 4); if ((clientKeyType === 'test' || clientKeyType === 'live') && !this.loadingContext.includes(clientKeyType)) { @@ -143,6 +149,18 @@ class Core { } if (action.type) { + // Call analytics endpoint + const aObj: AnalyticsObject = createAnalyticsObject({ + class: 'log', + component: `${action.type}${action.subtype}`, + type: ANALYTICS_ACTION_STR, + subtype: capitalizeFirstLetter(action.type), + message: `${action.type}${action.subtype} is initiating` + }); + + this.modules.analytics.addAnalyticsAction('log', aObj); + + // Create a component based on the action const actionTypeConfiguration = getComponentConfiguration(action.type, this.options.paymentMethodsConfiguration); const props = { @@ -222,6 +240,7 @@ class Core { session: this.session, loadingContext: this.loadingContext, cdnContext: this.cdnContext, + analyticsContext: this.analyticsContext, createFromAction: this.createFromAction, _parentInstance: this }; @@ -362,8 +381,9 @@ class Core { this.modules = Object.freeze({ risk: new RiskModule({ ...this.options, loadingContext: this.loadingContext }), - analytics: new Analytics({ + analytics: Analytics({ loadingContext: this.loadingContext, + analyticsContext: this.analyticsContext, clientKey: this.options.clientKey, locale: this.options.locale, analytics: this.options.analytics, diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index 21781005d0..bebb228e5e 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -154,6 +154,11 @@ export interface CoreOptions { * @internal */ loadingContext?: string; + + /** + * @internal + */ + analyticsContext?: string; } export type PaymentMethodsConfiguration = diff --git a/packages/lib/src/utils/Formatters/formatters.ts b/packages/lib/src/utils/Formatters/formatters.ts index 269114c00d..bf6fa5e74d 100644 --- a/packages/lib/src/utils/Formatters/formatters.ts +++ b/packages/lib/src/utils/Formatters/formatters.ts @@ -4,3 +4,7 @@ import { FormatterFn } from './types'; export const digitsOnlyFormatter: FormatterFn = (value: string) => { return value.replace(/[^0-9]/g, ''); }; + +export const capitalizeFirstLetter = (str: string): string => { + return str.charAt(0).toUpperCase() + str.slice(1); +}; From b717e2c4be8c8a9619f5e30a48cbfde31072b67a Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 22 Jun 2023 11:34:33 +0200 Subject: [PATCH 02/94] Added comments and fixed type --- .../lib/src/components/Card/components/CardInput/types.ts | 5 ++--- .../lib/src/components/Dropin/components/DropinComponent.tsx | 2 +- packages/lib/src/core/Services/analytics/collect-id.ts | 3 ++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/components/Card/components/CardInput/types.ts b/packages/lib/src/components/Card/components/CardInput/types.ts index 8264edd98a..c81bfc4344 100644 --- a/packages/lib/src/components/Card/components/CardInput/types.ts +++ b/packages/lib/src/components/Card/components/CardInput/types.ts @@ -9,9 +9,8 @@ import { AddressSchema } from '../../../internal/Address/types'; import { CbObjOnError, StylesObject } from '../../../internal/SecuredFields/lib/types'; import { Resources } from '../../../../core/Context/Resources'; import { SRPanel } from '../../../../core/Errors/SRPanel'; -import Analytics from '../../../../core/Analytics'; import RiskElement from '../../../../core/RiskModule'; -import { ComponentMethodsRef } from '../../../types'; +import { AnalyticsModule, ComponentMethodsRef } from '../../../types'; import { DisclaimerMsgObject } from '../../../internal/DisclaimerMessage/DisclaimerMessage'; import { OnAddressLookupType } from '../../../internal/Address/components/AddressSearch'; @@ -90,7 +89,7 @@ export interface CardInputProps { minimumExpiryDate?: string; modules?: { srPanel: SRPanel; - analytics: Analytics; + analytics: AnalyticsModule; risk: RiskElement; resources: Resources; }; diff --git a/packages/lib/src/components/Dropin/components/DropinComponent.tsx b/packages/lib/src/components/Dropin/components/DropinComponent.tsx index a358325022..9514e05444 100644 --- a/packages/lib/src/components/Dropin/components/DropinComponent.tsx +++ b/packages/lib/src/components/Dropin/components/DropinComponent.tsx @@ -34,7 +34,7 @@ export class DropinComponent extends Component e.props.type), + // paymentMethods: elements.map(e => e.props.type), // TODO not supported in the initial request to checkoutanalytics component: 'dropin', flavor: 'dropin' }); diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index 98ffd1bd8d..1587a487b0 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -19,7 +19,7 @@ function confirmSessionDurationIsMaxFifteenMinutes(checkoutAttemptIdSession: Che * @param config - object containing values needed to calculate the url for the request; and also some that need to be serialized and included in the body of request * @returns a function returning a promise containing the response of the call (an object containing a checkoutAttemptId property) */ -// const collectId = ({ analyticsContext, clientKey, locale, amount }: CollectId2Props) => { +// const collectId = ({ analyticsContext, clientKey, locale, amount }: CollectId2Props) => { // TODO - amount will be supported in the future const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { let promise; @@ -69,6 +69,7 @@ const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { return id; // TODO - end }); + // .catch(() => {}); return promise; }; From 480845deaebe672499a3657dc837fa312970b933 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 22 Jun 2023 18:03:41 +0200 Subject: [PATCH 03/94] Commenting out "target" property until API accepts it --- packages/lib/src/core/Analytics/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index 2678f705a8..e73918d62b 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -23,6 +23,6 @@ export const createAnalyticsObject = (aObj): AnalyticsObject => ({ ...((aObj.class === 'error' || (aObj.class === 'log' && aObj.type !== ANALYTICS_SUBMIT_STR)) && { message: aObj.message }), // only added if we have an error, or log object (that's not logging a submit/pay button press) ...(aObj.class === 'log' && { type: aObj.type }), // only added if we have a log object ...(aObj.class === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type - ...(aObj.class === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type + // ...(aObj.class === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type // TODO should be allowed but for some reason API won't accept it ...(aObj.class === 'event' && { type: aObj.type, target: aObj.target }) // only added if we have an event object }); From e1c245df64e60d23337578b7d88d326ec191bd1e Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 22 Jun 2023 19:12:10 +0200 Subject: [PATCH 04/94] Fixing unit tests --- packages/lib/src/components/BaseElement.ts | 2 +- packages/lib/src/components/UIElement.tsx | 4 +- .../lib/src/core/Analytics/Analytics.test.ts | 73 +++++++++---------- packages/lib/src/core/Analytics/Analytics.ts | 2 +- .../PaymentAction/actionTypes.ts | 2 +- 5 files changed, 39 insertions(+), 44 deletions(-) diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index 7d705f8a0d..052521a271 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -56,7 +56,7 @@ class BaseElement

{ */ get data(): PaymentData | RiskData { const clientData = getProp(this.props, 'modules.risk.data'); - const checkoutAttemptId = getProp(this.props, 'modules.analytics.getCheckoutAttemptId')(); + const checkoutAttemptId = getProp(this.props, 'modules.analytics.getCheckoutAttemptId')?.(); const order = this.state.order || this.props.order; const componentData = this.formatData(); diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index f37d4f9d91..089159e2c5 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -51,7 +51,7 @@ export class UIElement

extends BaseElement

im /* eslint-disable-next-line */ protected submitAnalytics(obj = null) { // Call analytics endpoint - let component = this.elementRef._id.substring(0, this.elementRef._id.indexOf('-')); + let component = this.elementRef._id?.substring(0, this.elementRef._id.indexOf('-')); if (component === 'dropin') { const subCompID = this.elementRef['dropinRef'].state.activePaymentMethod._id; component = `${component}-${subCompID.substring(0, subCompID.indexOf('-'))}`; @@ -64,7 +64,7 @@ export class UIElement

extends BaseElement

im target: 'pay_button' }); - this.props.modules.analytics.addAnalyticsAction('log', aObj); + this.props.modules?.analytics.addAnalyticsAction('log', aObj); } private onSubmit(): void { diff --git a/packages/lib/src/core/Analytics/Analytics.test.ts b/packages/lib/src/core/Analytics/Analytics.test.ts index 6989662cb6..d0f6489f28 100644 --- a/packages/lib/src/core/Analytics/Analytics.test.ts +++ b/packages/lib/src/core/Analytics/Analytics.test.ts @@ -1,85 +1,80 @@ import Analytics from './Analytics'; import collectId from '../Services/analytics/collect-id'; -import postTelemetry from '../Services/analytics/post-telemetry'; +import logEvent from '../Services/analytics/log-event'; import { PaymentAmountExtended } from '../../types'; jest.mock('../Services/analytics/collect-id'); -jest.mock('../Services/analytics/post-telemetry'); +jest.mock('../Services/analytics/log-event'); const mockedCollectId = collectId as jest.Mock; -const mockedPostTelemetry = postTelemetry as jest.Mock; +const mockedLogEvent = logEvent as jest.Mock; let amount: PaymentAmountExtended; describe('Analytics', () => { const collectIdPromiseMock = jest.fn(() => Promise.resolve('123456')); - const logTelemetryPromiseMock = jest.fn(request => Promise.resolve(request)); + const logEventPromiseMock = jest.fn(request => Promise.resolve(request)); + + const event = { + containerWidth: 100, + component: 'card', + flavor: 'components' + }; beforeEach(() => { mockedCollectId.mockReset(); mockedCollectId.mockImplementation(() => collectIdPromiseMock); collectIdPromiseMock.mockClear(); - mockedPostTelemetry.mockReset(); - mockedPostTelemetry.mockImplementation(() => logTelemetryPromiseMock); - logTelemetryPromiseMock.mockClear(); + mockedLogEvent.mockReset(); + mockedLogEvent.mockImplementation(() => logEventPromiseMock); + logEventPromiseMock.mockClear(); amount = { value: 50000, currency: 'USD' }; }); test('Creates an Analytics module with defaultProps', () => { - const analytics = new Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); - expect(analytics.props.enabled).toBe(true); - expect(analytics.props.telemetry).toBe(true); + const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); + expect(analytics.send).not.toBe(null); + expect(analytics.getCheckoutAttemptId).not.toBe(null); + expect(analytics.addAnalyticsAction).not.toBe(null); + expect(analytics.sendAnalyticsActions).not.toBe(null); expect(collectIdPromiseMock).toHaveLength(0); }); - test('Calls the collectId endpoint by default (telemetry enabled)', () => { - const analytics = new Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); + test('Should not fire any calls if analytics is disabled', () => { + const analytics = Analytics({ analytics: { enabled: false }, loadingContext: '', locale: '', clientKey: '', amount }); + + analytics.send(event); expect(collectIdPromiseMock).not.toHaveBeenCalled(); - analytics.send({}); - expect(collectIdPromiseMock).toHaveBeenCalled(); + expect(logEventPromiseMock).not.toHaveBeenCalled(); }); - test('Will not call the collectId endpoint if telemetry is disabled', () => { - const analytics = new Analytics({ analytics: { telemetry: false }, loadingContext: '', locale: '', clientKey: '', amount }); + test('Will not call the collectId endpoint if telemetry is disabled, but will call the logEvent (analytics pixel)', () => { + const analytics = Analytics({ analytics: { telemetry: false }, loadingContext: '', locale: '', clientKey: '', amount }); expect(collectIdPromiseMock).not.toHaveBeenCalled(); - analytics.send({}); + analytics.send(event); expect(collectIdPromiseMock).not.toHaveBeenCalled(); + + expect(logEventPromiseMock).toHaveBeenCalledWith({ ...event }); }); - test('Sends an event', async () => { - const event = { - eventData: 'test' - }; - const analytics = new Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); + test('Calls the collectId endpoint by default, adding expected fields', async () => { + const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); analytics.send(event); expect(collectIdPromiseMock).toHaveBeenCalled(); await Promise.resolve(); // wait for the next tick - expect(logTelemetryPromiseMock).toHaveBeenCalledWith({ ...event, checkoutAttemptId: '123456' }); + expect(collectIdPromiseMock).toHaveBeenCalledWith({ ...event }); }); - test('Adds the fields in the payload', async () => { + test('A second attempt to call "send" should fail (since we already have a checkoutAttemptId)', async () => { const payload = { payloadData: 'test' }; - const event = { - eventData: 'test' - }; - const analytics = new Analytics({ analytics: { payload }, loadingContext: '', locale: '', clientKey: '', amount }); + const analytics = Analytics({ analytics: { payload }, loadingContext: '', locale: '', clientKey: '', amount }); analytics.send(event); - expect(collectIdPromiseMock).toHaveBeenCalled(); - await Promise.resolve(); // wait for the next tick - expect(logTelemetryPromiseMock).toHaveBeenCalledWith({ ...payload, ...event, checkoutAttemptId: '123456' }); - }); - - test('Should not fire any calls if analytics is disabled', () => { - const analytics = new Analytics({ analytics: { enabled: false }, loadingContext: '', locale: '', clientKey: '', amount }); - - analytics.send({}); - expect(collectIdPromiseMock).not.toHaveBeenCalled(); - expect(logTelemetryPromiseMock).not.toHaveBeenCalled(); + expect(collectIdPromiseMock).toHaveLength(0); }); }); diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 45061b5184..b89303fe93 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -41,7 +41,7 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy // fetch a new checkoutAttemptId if none is already available _collectId({ ...config, ...(payload && { ...payload }) }) .then(checkoutAttemptId => { - console.log('### Analytics::checkoutAttemptId:: ', checkoutAttemptId); + console.log('### Analytics::setting checkoutAttemptId:: ', checkoutAttemptId); _checkoutAttemptId = checkoutAttemptId; }) .catch(e => { diff --git a/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts b/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts index b392cb112e..299ec3e89c 100644 --- a/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts +++ b/packages/lib/src/core/ProcessResponse/PaymentAction/actionTypes.ts @@ -67,7 +67,7 @@ const actionTypes = { _parentInstance: props._parentInstance, paymentMethodType: props.paymentMethodType, challengeWindowSize: props.challengeWindowSize, // always pass challengeWindowSize in case it's been set directly in the handleAction config object - analytics: props.modules.analytics, + analytics: props.modules?.analytics, // Props unique to a particular flow ...get3DS2FlowProps(action.subtype, props) From 1047feeb0c17c7d3311791a94dd1d2dc306ce9ea Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 22 Jun 2023 19:26:14 +0200 Subject: [PATCH 05/94] Fixing sonarcloud gripe --- packages/lib/src/core/Analytics/CAEventsQueue.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/core/Analytics/CAEventsQueue.test.ts b/packages/lib/src/core/Analytics/CAEventsQueue.test.ts index e38541be1a..214715e246 100644 --- a/packages/lib/src/core/Analytics/CAEventsQueue.test.ts +++ b/packages/lib/src/core/Analytics/CAEventsQueue.test.ts @@ -1,7 +1,7 @@ import CAEventsQueue from './CAEventsQueue'; describe('CAEventsQueue', () => { - const queue = CAEventsQueue({ analyticsContext: 'http://mydomain.com', clientKey: 'fsdjkh' }); + const queue = CAEventsQueue({ analyticsContext: 'https://mydomain.com', clientKey: 'fsdjkh' }); test('adds log to the queue', () => { const task1 = { foo: 'bar' }; From 90788b7c329b73ed7ba23abd722fbfdc477e5270 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 23 Jun 2023 11:31:43 +0200 Subject: [PATCH 06/94] Tightening up types --- packages/lib/src/components/types.ts | 4 +-- .../lib/src/core/Analytics/Analytics.test.ts | 3 +- packages/lib/src/core/Analytics/Analytics.ts | 9 +++--- .../lib/src/core/Analytics/CAEventsQueue.ts | 8 +++--- packages/lib/src/core/Analytics/types.ts | 14 +++++++++- .../src/core/Services/analytics/collect-id.ts | 2 +- .../src/core/Services/analytics/log-event.ts | 28 +++++++++++-------- .../lib/src/core/Services/analytics/types.ts | 13 ++++----- 8 files changed, 47 insertions(+), 34 deletions(-) diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index 208213c392..4fc1f84bdc 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -8,7 +8,7 @@ import { PayButtonProps } from './internal/PayButton/PayButton'; import Session from '../core/CheckoutSession'; import { SRPanel } from '../core/Errors/SRPanel'; import { Resources } from '../core/Context/Resources'; -import { AnalyticsConfig, AnalyticsObject } from '../core/Analytics/types'; +import { AnalyticsInitialEvent, AnalyticsObject } from '../core/Analytics/types'; export interface PaymentMethodData { paymentMethod: { @@ -64,7 +64,7 @@ export interface RawPaymentResponse extends PaymentResponse { } export interface AnalyticsModule { - send: (a: AnalyticsConfig) => void; + send: (a: AnalyticsInitialEvent) => void; addAnalyticsAction: (s: string, o: AnalyticsObject) => void; sendAnalyticsActions: () => Promise; getCheckoutAttemptId: () => string; diff --git a/packages/lib/src/core/Analytics/Analytics.test.ts b/packages/lib/src/core/Analytics/Analytics.test.ts index d0f6489f28..22676cd2ee 100644 --- a/packages/lib/src/core/Analytics/Analytics.test.ts +++ b/packages/lib/src/core/Analytics/Analytics.test.ts @@ -13,7 +13,7 @@ let amount: PaymentAmountExtended; describe('Analytics', () => { const collectIdPromiseMock = jest.fn(() => Promise.resolve('123456')); - const logEventPromiseMock = jest.fn(request => Promise.resolve(request)); + const logEventPromiseMock = jest.fn(() => Promise.resolve(null)); const event = { containerWidth: 100, @@ -25,6 +25,7 @@ describe('Analytics', () => { mockedCollectId.mockReset(); mockedCollectId.mockImplementation(() => collectIdPromiseMock); collectIdPromiseMock.mockClear(); + mockedLogEvent.mockReset(); mockedLogEvent.mockImplementation(() => logEventPromiseMock); logEventPromiseMock.mockClear(); diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index b89303fe93..f8968df188 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -2,7 +2,7 @@ import logEvent from '../Services/analytics/log-event'; import collectId from '../Services/analytics/collect-id'; import { CoreOptions } from '../types'; import CAEventsQueue, { EQObject } from './CAEventsQueue'; -import { ANALYTICS_ACTION, AnalyticsConfig, AnalyticsObject } from './types'; +import { ANALYTICS_ACTION, AnalyticsInitialEvent, AnalyticsObject } from './types'; import { ANALYTICS_ACTION_ERROR, ANALYTICS_ACTION_LOG } from './constants'; import { debounce } from '../../components/internal/Address/utils'; import { AnalyticsModule } from '../../components/types'; @@ -33,13 +33,13 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy const _caEventsQueue: EQObject = CAEventsQueue({ analyticsContext, clientKey }); const analyticsObj: AnalyticsModule = { - send: (config: AnalyticsConfig) => { + send: (initialEvent: AnalyticsInitialEvent) => { const { enabled, payload, telemetry } = props; // TODO what is payload, is it ever used? if (enabled === true) { if (telemetry === true && !_checkoutAttemptId) { // fetch a new checkoutAttemptId if none is already available - _collectId({ ...config, ...(payload && { ...payload }) }) + _collectId({ ...initialEvent, ...(payload && { ...payload }) }) .then(checkoutAttemptId => { console.log('### Analytics::setting checkoutAttemptId:: ', checkoutAttemptId); _checkoutAttemptId = checkoutAttemptId; @@ -49,11 +49,10 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy }); } // Log pixel // TODO once we stop using the pixel we can stop requiring both "enabled" & "telemetry" config options - _logEvent(config); + _logEvent(initialEvent); } }, - // used in BaseElement getCheckoutAttemptId: (): string => _checkoutAttemptId, addAnalyticsAction: (type: ANALYTICS_ACTION, obj: AnalyticsObject) => { diff --git a/packages/lib/src/core/Analytics/CAEventsQueue.ts b/packages/lib/src/core/Analytics/CAEventsQueue.ts index c497ba08bc..a954fce869 100644 --- a/packages/lib/src/core/Analytics/CAEventsQueue.ts +++ b/packages/lib/src/core/Analytics/CAEventsQueue.ts @@ -1,5 +1,5 @@ import { HttpOptions, httpPost } from '../Services/http'; -import { AnalyticsObject } from './types'; +import { AnalyticsObject, EventQueueProps } from './types'; interface CAActions { channel: 'Web'; @@ -15,7 +15,7 @@ export interface EQObject { _runQueue: (id) => Promise; } -const CAEventsQueue = ({ analyticsContext, clientKey }) => { +const CAEventsQueue = ({ analyticsContext, clientKey }: EventQueueProps) => { const caActions: CAActions = { channel: 'Web', events: [], @@ -28,7 +28,7 @@ const CAEventsQueue = ({ analyticsContext, clientKey }) => { caActions[type].push(actionObj); }, - run: checkoutAttemptId => { + run: (checkoutAttemptId: string) => { const promise = eqObject._runQueue(checkoutAttemptId); caActions.events = []; @@ -41,7 +41,7 @@ const CAEventsQueue = ({ analyticsContext, clientKey }) => { // Expose getter for testing purposes getQueue: () => caActions, - _runQueue: (checkoutAttemptId): Promise => { + _runQueue: (checkoutAttemptId: string): Promise => { if (!caActions.events.length && !caActions.logs.length && !caActions.errors.length) { return Promise.resolve(null); } diff --git a/packages/lib/src/core/Analytics/types.ts b/packages/lib/src/core/Analytics/types.ts index 134baab438..14cf63e44a 100644 --- a/packages/lib/src/core/Analytics/types.ts +++ b/packages/lib/src/core/Analytics/types.ts @@ -1,3 +1,5 @@ +import { PaymentAmount } from '../../types'; + export interface Experiment { controlGroup: boolean; experimentId: string; @@ -43,9 +45,19 @@ export interface AnalyticsObject { export type ANALYTICS_ACTION = 'log' | 'error' | 'event'; -export type AnalyticsConfig = { +export type AnalyticsInitialEvent = { containerWidth: number; component: string; flavor: string; paymentMethods?: any[]; }; + +export type AnalyticsConfig = { + analyticsContext?: string; + clientKey?: string; + locale?: string; + amount?: PaymentAmount; + loadingContext?: string; +}; + +export type EventQueueProps = Pick; diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index 1587a487b0..9dbdb03901 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -19,7 +19,7 @@ function confirmSessionDurationIsMaxFifteenMinutes(checkoutAttemptIdSession: Che * @param config - object containing values needed to calculate the url for the request; and also some that need to be serialized and included in the body of request * @returns a function returning a promise containing the response of the call (an object containing a checkoutAttemptId property) */ -// const collectId = ({ analyticsContext, clientKey, locale, amount }: CollectId2Props) => { // TODO - amount will be supported in the future +// const collectId = ({ analyticsContext, clientKey, locale, amount }: CollectIdProps) => { // TODO - amount will be supported in the future const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { let promise; diff --git a/packages/lib/src/core/Services/analytics/log-event.ts b/packages/lib/src/core/Services/analytics/log-event.ts index 5cd8618a4d..e753836747 100644 --- a/packages/lib/src/core/Services/analytics/log-event.ts +++ b/packages/lib/src/core/Services/analytics/log-event.ts @@ -1,22 +1,26 @@ +import { LogEventProps } from './types'; + /** * Log event to Adyen * @param config - ready to be serialized and included in the request * @returns A log event function */ -const logEvent = config => event => { - const params = { - version: process.env.VERSION, - payload_version: 1, - platform: 'web', - locale: config.locale, - ...event - }; +const logEvent = (config: LogEventProps) => { + return initialEvent => { + const params = { + version: process.env.VERSION, + payload_version: 1, + platform: 'web', + locale: config.locale, + ...initialEvent + }; - const queryString = Object.keys(params) - .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) - .join('&'); + const queryString = Object.keys(params) + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) + .join('&'); - new Image().src = `${config.loadingContext}images/analytics.png?${queryString}`; + new Image().src = `${config.loadingContext}images/analytics.png?${queryString}`; + }; }; export default logEvent; diff --git a/packages/lib/src/core/Services/analytics/types.ts b/packages/lib/src/core/Services/analytics/types.ts index c648e12219..c1af63e323 100644 --- a/packages/lib/src/core/Services/analytics/types.ts +++ b/packages/lib/src/core/Services/analytics/types.ts @@ -1,15 +1,12 @@ -import { PaymentAmount } from '../../../types'; +import { AnalyticsConfig } from '../../Analytics/types'; type CheckoutAttemptIdSession = { id: string; timestamp: number; }; -type CollectIdProps = { - clientKey: string; - analyticsContext: string; - locale: string; - amount: PaymentAmount; -}; +type CollectIdProps = Pick; + +type LogEventProps = Pick; -export { CheckoutAttemptIdSession, CollectIdProps }; +export { CheckoutAttemptIdSession, CollectIdProps, LogEventProps }; From 5455aa3aa0fa779af9d4e44de0d9096258a43a91 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 23 Jun 2023 13:39:20 +0200 Subject: [PATCH 07/94] Adding test for event queue --- packages/lib/src/components/types.ts | 2 + .../lib/src/core/Analytics/Analytics.test.ts | 99 ++++++++++++++++--- packages/lib/src/core/Analytics/Analytics.ts | 5 +- packages/lib/src/core/Analytics/types.ts | 1 + .../src/core/Services/analytics/collect-id.ts | 2 +- 5 files changed, 96 insertions(+), 13 deletions(-) diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index 4fc1f84bdc..2097ae4967 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -9,6 +9,7 @@ import Session from '../core/CheckoutSession'; import { SRPanel } from '../core/Errors/SRPanel'; import { Resources } from '../core/Context/Resources'; import { AnalyticsInitialEvent, AnalyticsObject } from '../core/Analytics/types'; +import { EQObject } from '../core/Analytics/CAEventsQueue'; export interface PaymentMethodData { paymentMethod: { @@ -68,6 +69,7 @@ export interface AnalyticsModule { addAnalyticsAction: (s: string, o: AnalyticsObject) => void; sendAnalyticsActions: () => Promise; getCheckoutAttemptId: () => string; + getEventsQueue: () => EQObject; } export interface BaseElementProps { diff --git a/packages/lib/src/core/Analytics/Analytics.test.ts b/packages/lib/src/core/Analytics/Analytics.test.ts index 22676cd2ee..f45f962937 100644 --- a/packages/lib/src/core/Analytics/Analytics.test.ts +++ b/packages/lib/src/core/Analytics/Analytics.test.ts @@ -1,7 +1,10 @@ import Analytics from './Analytics'; import collectId from '../Services/analytics/collect-id'; import logEvent from '../Services/analytics/log-event'; -import { PaymentAmountExtended } from '../../types'; +import { PaymentAmount } from '../../types'; +import { createAnalyticsObject } from './utils'; +import wait from '../../utils/wait'; +import { DEFAULT_DEBOUNCE_TIME_MS } from '../../components/internal/Address/utils'; jest.mock('../Services/analytics/collect-id'); jest.mock('../Services/analytics/log-event'); @@ -9,18 +12,25 @@ jest.mock('../Services/analytics/log-event'); const mockedCollectId = collectId as jest.Mock; const mockedLogEvent = logEvent as jest.Mock; -let amount: PaymentAmountExtended; +const amount: PaymentAmount = { value: 50000, currency: 'USD' }; -describe('Analytics', () => { +const event = { + containerWidth: 100, + component: 'card', + flavor: 'components' +}; + +const analyticsEventObj = { + class: 'event', + component: 'cardComponent', + type: 'Focus', + target: 'PAN input' +}; + +describe('Analytics initialisation', () => { const collectIdPromiseMock = jest.fn(() => Promise.resolve('123456')); const logEventPromiseMock = jest.fn(() => Promise.resolve(null)); - const event = { - containerWidth: 100, - component: 'card', - flavor: 'components' - }; - beforeEach(() => { mockedCollectId.mockReset(); mockedCollectId.mockImplementation(() => collectIdPromiseMock); @@ -29,8 +39,6 @@ describe('Analytics', () => { mockedLogEvent.mockReset(); mockedLogEvent.mockImplementation(() => logEventPromiseMock); logEventPromiseMock.mockClear(); - - amount = { value: 50000, currency: 'USD' }; }); test('Creates an Analytics module with defaultProps', () => { @@ -78,4 +86,73 @@ describe('Analytics', () => { expect(collectIdPromiseMock).toHaveLength(0); }); + + test('Analytics events queue sends event object', async () => { + const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); + + const aObj = createAnalyticsObject(analyticsEventObj); + + expect(aObj.timestamp).not.toBe(undefined); + expect(aObj.target).toEqual('PAN input'); + expect(aObj.type).toEqual('Focus'); + + // no message prop for events + expect(aObj.message).toBe(undefined); + + analytics.addAnalyticsAction('event', aObj); + + // event object should not be sent immediately + expect(analytics.getEventsQueue().getQueue().events.length).toBe(1); + }); + + test('Analytics events queue sends error object', async () => { + const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); + + const evObj = createAnalyticsObject(analyticsEventObj); + analytics.addAnalyticsAction('event', evObj); + + expect(analytics.getEventsQueue().getQueue().events.length).toBe(1); + + const aObj = createAnalyticsObject({ + class: 'error', + component: 'threeDS2Fingerprint', + code: 'web_704', + errorType: 'APIError', + message: 'threeDS2Fingerprint Missing paymentData property from threeDS2 action' + }); + + expect(aObj.timestamp).not.toBe(undefined); + expect(aObj.component).toEqual('threeDS2Fingerprint'); + expect(aObj.errorType).toEqual('APIError'); + expect(aObj.message).not.toBe(undefined); + + analytics.addAnalyticsAction('error', aObj); + + // error object should be sent immediately, sending any events as well + expect(analytics.getEventsQueue().getQueue().errors.length).toBe(0); + expect(analytics.getEventsQueue().getQueue().events.length).toBe(0); + }); + + test('Analytics events queue sends log object', async () => { + const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); + + const aObj = createAnalyticsObject({ + class: 'log', + component: 'scheme', + type: 'Submit' + }); + + expect(aObj.timestamp).not.toBe(undefined); + expect(aObj.component).toEqual('scheme'); + expect(aObj.type).toEqual('Submit'); + + // no message prop for a log with type 'Submit' + expect(aObj.message).toBe(undefined); + + analytics.addAnalyticsAction('log', aObj); + + // log object should be sent almost immediately (after a debounce interval) + await wait(DEFAULT_DEBOUNCE_TIME_MS); + expect(analytics.getEventsQueue().getQueue().logs.length).toBe(0); + }); }); diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index f8968df188..560e4ed8c0 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -70,7 +70,10 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy return _caEventsQueue.run(_checkoutAttemptId); } return Promise.resolve(null); - } + }, + + // Expose getter for testing purposes + getEventsQueue: () => _caEventsQueue }; return analyticsObj; diff --git a/packages/lib/src/core/Analytics/types.ts b/packages/lib/src/core/Analytics/types.ts index 14cf63e44a..3237729788 100644 --- a/packages/lib/src/core/Analytics/types.ts +++ b/packages/lib/src/core/Analytics/types.ts @@ -41,6 +41,7 @@ export interface AnalyticsObject { message?: string; type?: string; subtype?: string; + target?: string; } export type ANALYTICS_ACTION = 'log' | 'error' | 'event'; diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index 9dbdb03901..78924b7bc7 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -62,7 +62,7 @@ const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { return undefined; }) .catch(() => { - // TODO - temporarily faking it + // TODO - temporarily faking it - so we get a checkoutAttemptId which will allow subsequents request to be made (they'll fail, but at least we see them in the console) console.log('### collect-id2:::: FAILED'); const id = '64d673ff-36d3-4b32-999b-49e215f6b9891687261360764E7D99B01E11BF4C4B83CF7C7F49C5E75F23B2381E2ACBEE8E03E221E3BC95998'; storage.set({ id: id, timestamp: Date.now() }); From 2124f2bdf2581c93cd1a8acba8ef2b948fe5fcf5 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 23 Jun 2023 13:53:51 +0200 Subject: [PATCH 08/94] Updated test description --- packages/lib/src/core/Analytics/Analytics.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/core/Analytics/Analytics.test.ts b/packages/lib/src/core/Analytics/Analytics.test.ts index f45f962937..1238aa75c7 100644 --- a/packages/lib/src/core/Analytics/Analytics.test.ts +++ b/packages/lib/src/core/Analytics/Analytics.test.ts @@ -27,7 +27,7 @@ const analyticsEventObj = { target: 'PAN input' }; -describe('Analytics initialisation', () => { +describe('Analytics initialisation and event queue', () => { const collectIdPromiseMock = jest.fn(() => Promise.resolve('123456')); const logEventPromiseMock = jest.fn(() => Promise.resolve(null)); From 40a3d14e85b3565f60a9dcfbcbf2e160ce77ea3b Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 23 Jun 2023 14:56:57 +0200 Subject: [PATCH 09/94] Enhancing test --- packages/lib/src/core/Analytics/Analytics.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/core/Analytics/Analytics.test.ts b/packages/lib/src/core/Analytics/Analytics.test.ts index 1238aa75c7..691a6a05cc 100644 --- a/packages/lib/src/core/Analytics/Analytics.test.ts +++ b/packages/lib/src/core/Analytics/Analytics.test.ts @@ -14,6 +14,8 @@ const mockedLogEvent = logEvent as jest.Mock; const amount: PaymentAmount = { value: 50000, currency: 'USD' }; +const mockCheckoutAttemptId = '123456'; + const event = { containerWidth: 100, component: 'card', @@ -28,7 +30,7 @@ const analyticsEventObj = { }; describe('Analytics initialisation and event queue', () => { - const collectIdPromiseMock = jest.fn(() => Promise.resolve('123456')); + const collectIdPromiseMock = jest.fn(() => Promise.resolve(mockCheckoutAttemptId)); const logEventPromiseMock = jest.fn(() => Promise.resolve(null)); beforeEach(() => { @@ -74,6 +76,8 @@ describe('Analytics initialisation and event queue', () => { expect(collectIdPromiseMock).toHaveBeenCalled(); await Promise.resolve(); // wait for the next tick expect(collectIdPromiseMock).toHaveBeenCalledWith({ ...event }); + + expect(analytics.getCheckoutAttemptId()).toEqual(mockCheckoutAttemptId); }); test('A second attempt to call "send" should fail (since we already have a checkoutAttemptId)', async () => { From 0f931cc0e3ce9e9a61708edb32ca68c72db3dff6 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 26 Jun 2023 11:26:30 +0200 Subject: [PATCH 10/94] Adding collect-id unit test --- .../Services/analytics/collect-id.test.ts | 58 +++++++++++++++++++ .../lib/src/core/Services/analytics/types.ts | 8 +-- 2 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 packages/lib/src/core/Services/analytics/collect-id.test.ts diff --git a/packages/lib/src/core/Services/analytics/collect-id.test.ts b/packages/lib/src/core/Services/analytics/collect-id.test.ts new file mode 100644 index 0000000000..eaa24e4ef7 --- /dev/null +++ b/packages/lib/src/core/Services/analytics/collect-id.test.ts @@ -0,0 +1,58 @@ +import { httpPost } from '../http'; +import collectId from './collect-id'; + +jest.mock('../http', () => ({ + // ...jest.requireActual('../http'), + httpPost: jest.fn(() => new Promise(() => {})) +})); + +// jest.mock('../http'); +// const mockedHttp = httpPost as jest.Mock; +// const httpPromiseMock = jest.fn(() => new Promise((resolve, reject) => {})); + +beforeEach(() => { + process.env.VERSION = 'x.x.x'; + // mockedHttp.mockImplementation(() => httpPromiseMock); +}); + +test('should send proper data to http service', () => { + const configuration = { + analyticsContext: 'https://checkoutanalytics-test.adyen.com/checkoutanalytics/', + locale: 'en-US', + clientKey: 'xxxx-yyyy', + amount: { + value: 10000, + currency: 'USD' + } + }; + + const customEvent = { + flavor: 'components', + containerWidth: 600, + component: 'scheme' + }; + + const log = collectId(configuration); + + log(customEvent); + + expect(httpPost).toHaveBeenCalledTimes(1); + expect(httpPost).toHaveBeenCalledWith( + { + errorLevel: 'silent', + loadingContext: 'https://checkoutanalytics-test.adyen.com/checkoutanalytics/', + path: 'v2/analytics?clientKey=xxxx-yyyy' + }, + { + // amount: configuration.amount,// TODO will be supported in the future + channel: 'Web', + locale: configuration.locale, + referrer: 'http://localhost/', + screenWidth: 0, + version: 'x.x.x', + flavor: customEvent.flavor, + containerWidth: customEvent.containerWidth, + component: customEvent.component + } + ); +}); diff --git a/packages/lib/src/core/Services/analytics/types.ts b/packages/lib/src/core/Services/analytics/types.ts index c1af63e323..492be8aa7e 100644 --- a/packages/lib/src/core/Services/analytics/types.ts +++ b/packages/lib/src/core/Services/analytics/types.ts @@ -1,12 +1,10 @@ import { AnalyticsConfig } from '../../Analytics/types'; -type CheckoutAttemptIdSession = { +export type CheckoutAttemptIdSession = { id: string; timestamp: number; }; -type CollectIdProps = Pick; +export type CollectIdProps = Pick; -type LogEventProps = Pick; - -export { CheckoutAttemptIdSession, CollectIdProps, LogEventProps }; +export type LogEventProps = Pick; From 466e5972899ed23ef6726aa287176c0667644445 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 26 Jun 2023 11:47:42 +0200 Subject: [PATCH 11/94] Adding TelemetryEvent type --- .../src/core/Services/analytics/collect-id.test.ts | 6 ------ .../lib/src/core/Services/analytics/collect-id.ts | 5 ++--- packages/lib/src/core/Services/analytics/types.ts | 13 +++++++++++++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/lib/src/core/Services/analytics/collect-id.test.ts b/packages/lib/src/core/Services/analytics/collect-id.test.ts index eaa24e4ef7..4fec0faca3 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.test.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.test.ts @@ -2,17 +2,11 @@ import { httpPost } from '../http'; import collectId from './collect-id'; jest.mock('../http', () => ({ - // ...jest.requireActual('../http'), httpPost: jest.fn(() => new Promise(() => {})) })); -// jest.mock('../http'); -// const mockedHttp = httpPost as jest.Mock; -// const httpPromiseMock = jest.fn(() => new Promise((resolve, reject) => {})); - beforeEach(() => { process.env.VERSION = 'x.x.x'; - // mockedHttp.mockImplementation(() => httpPromiseMock); }); test('should send proper data to http service', () => { diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index 78924b7bc7..d666d93218 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -1,6 +1,6 @@ import { httpPost } from '../http'; import Storage from '../../../utils/Storage'; -import { CheckoutAttemptIdSession, CollectIdProps } from './types'; +import { CheckoutAttemptIdSession, CollectIdProps, TelemetryEvent } from './types'; /** * If the checkout attempt ID was stored more than fifteen minutes ago, then we should request a new ID. @@ -30,12 +30,11 @@ const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { }; return (event): Promise => { - const telemetryEvent = { + const telemetryEvent: TelemetryEvent = { // amount, // TODO will be supported in the future version: process.env.VERSION, channel: 'Web', locale, - flavor: 'components', referrer: window.location.href, screenWidth: window.screen.width, ...event diff --git a/packages/lib/src/core/Services/analytics/types.ts b/packages/lib/src/core/Services/analytics/types.ts index 492be8aa7e..ebbfa5b7e0 100644 --- a/packages/lib/src/core/Services/analytics/types.ts +++ b/packages/lib/src/core/Services/analytics/types.ts @@ -1,4 +1,5 @@ import { AnalyticsConfig } from '../../Analytics/types'; +import { PaymentAmount } from '../../../types'; export type CheckoutAttemptIdSession = { id: string; @@ -8,3 +9,15 @@ export type CheckoutAttemptIdSession = { export type CollectIdProps = Pick; export type LogEventProps = Pick; + +export type TelemetryEvent = { + version: string; + channel: 'Web'; + locale: string; + referrer: string; + screenWidth: number; + containerWidth: number; + component: string; + flavor: string; + amount?: PaymentAmount; +}; From 392ccf5cabcfc8b922039edc2b72c152bd9e5c2a Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 26 Jun 2023 13:41:28 +0200 Subject: [PATCH 12/94] Increasing test coverage --- .../src/core/Services/analytics/collect-id.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/core/Services/analytics/collect-id.test.ts b/packages/lib/src/core/Services/analytics/collect-id.test.ts index 4fec0faca3..05771c96a3 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.test.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.test.ts @@ -2,7 +2,12 @@ import { httpPost } from '../http'; import collectId from './collect-id'; jest.mock('../http', () => ({ - httpPost: jest.fn(() => new Promise(() => {})) + httpPost: jest.fn( + () => + new Promise(resolve => { + resolve({ id: 'mockCheckoutAttemptId' }); + }) + ) })); beforeEach(() => { @@ -28,7 +33,9 @@ test('should send proper data to http service', () => { const log = collectId(configuration); - log(customEvent); + log(customEvent).then(val => { + expect(val).toEqual('mockCheckoutAttemptId'); + }); expect(httpPost).toHaveBeenCalledTimes(1); expect(httpPost).toHaveBeenCalledWith( From 144d4a3622038d2360d73c7d0f92664a7152a5a7 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 26 Jun 2023 15:23:55 +0200 Subject: [PATCH 13/94] Adding test --- .../Services/analytics/collect-id.test.ts | 27 ++++++++++++++++++- .../src/core/Services/analytics/collect-id.ts | 2 +- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/core/Services/analytics/collect-id.test.ts b/packages/lib/src/core/Services/analytics/collect-id.test.ts index 05771c96a3..9c543b582a 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.test.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.test.ts @@ -14,7 +14,25 @@ beforeEach(() => { process.env.VERSION = 'x.x.x'; }); -test('should send proper data to http service', () => { +test('Should lead to a rejected promise since no clientKey is provided', () => { + const configuration = { + analyticsContext: 'https://checkoutanalytics-test.adyen.com/checkoutanalytics/', + locale: 'en-US', + amount: { + value: 10000, + currency: 'USD' + } + }; + + const log = collectId(configuration); + log({}) + .then() + .catch(e => { + expect(e).toEqual('no-client-key'); + }); +}); + +test('Should send expected data to http service', () => { const configuration = { analyticsContext: 'https://checkoutanalytics-test.adyen.com/checkoutanalytics/', locale: 'en-US', @@ -56,4 +74,11 @@ test('should send proper data to http service', () => { component: customEvent.component } ); + + // A second attempt should return the previous promise and not lead to a new http call + const log2 = log(customEvent); + log2.then(val => { + expect(val).toEqual('mockCheckoutAttemptId'); + }); + expect(httpPost).toHaveBeenCalledTimes(1); }); diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index d666d93218..a941bf125f 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -41,7 +41,7 @@ const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { }; if (promise) return promise; - if (!clientKey) return Promise.reject(); + if (!clientKey) return Promise.reject('no-client-key'); const storage = new Storage('checkout-attempt-id', 'sessionStorage'); const checkoutAttemptIdSession = storage.get(); From 7fc2bb74174d69633bcdd1bf3a9456182aff8f16 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 26 Jun 2023 17:28:46 +0200 Subject: [PATCH 14/94] Adding Storage.test --- packages/lib/src/utils/Storage.test.ts | 60 ++++++++++++++++++++++++++ packages/lib/src/utils/Storage.ts | 3 ++ 2 files changed, 63 insertions(+) create mode 100644 packages/lib/src/utils/Storage.test.ts diff --git a/packages/lib/src/utils/Storage.test.ts b/packages/lib/src/utils/Storage.test.ts new file mode 100644 index 0000000000..b1da616d02 --- /dev/null +++ b/packages/lib/src/utils/Storage.test.ts @@ -0,0 +1,60 @@ +import Storage from './Storage'; + +describe('Storage implementation - normal, window-based, storage', () => { + test('Should add, retrieve and remove item from storage', () => { + const storage = new Storage('checkout-attempt-id', 'localStorage'); + + storage.set({ id: 'mockId' }); + + let idObj: any = storage.get(); + + expect(idObj.id).toEqual('mockId'); + + storage.remove(); + idObj = storage.get(); + + expect(idObj).toBe(null); + }); +}); + +describe('Storage implementation - storage fallback', () => { + let windowSpy; + + beforeEach(() => { + windowSpy = jest.spyOn(window, 'window', 'get'); + }); + + test('Should add, retrieve and remove item from storage', () => { + windowSpy.mockImplementation(() => ({ + localStorage: null + })); + + const storage = new Storage('checkout-attempt-id', 'localStorage'); + + storage.set({ id: 'mockId' }); + + expect(storage.storage.key('adyen-checkout__checkout-attempt-id')).toEqual(0); + expect(storage.storage.length).toEqual(1); + + let idObj: any = storage.get(); + + expect(idObj.id).toEqual('mockId'); + + storage.remove(); + + idObj = storage.get(); + + expect(idObj).toBe(null); + + storage.set({ id: 'mockId2' }); + idObj = storage.get(); + + expect(idObj.id).toEqual('mockId2'); + + storage.storage.clear(); + + idObj = storage.get(); + + expect(idObj).toBe(null); + }); +}); diff --git a/packages/lib/src/utils/Storage.ts b/packages/lib/src/utils/Storage.ts index c7a4209d50..7af8b8bdfd 100644 --- a/packages/lib/src/utils/Storage.ts +++ b/packages/lib/src/utils/Storage.ts @@ -34,6 +34,9 @@ class Storage { constructor(key: string, storageType: 'sessionStorage' | 'localStorage') { try { this.storage = storageType ? window[storageType] : window.localStorage; + if (!this.storage) { + throw new Error('storage does not exist'); + } } catch (e) { this.storage = new NonPersistentStorage(); } From a6f3e70538fa81ab3bc75d4ade274d83018d10fb Mon Sep 17 00:00:00 2001 From: nicholas Date: Wed, 28 Jun 2023 12:20:24 +0200 Subject: [PATCH 15/94] Further checks to ensure checkoutanalytics calls fail silently --- packages/lib/src/core/Analytics/Analytics.ts | 3 ++- packages/lib/src/core/Analytics/CAEventsQueue.ts | 5 ++++- .../lib/src/core/Services/analytics/collect-id.ts | 12 +++++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 560e4ed8c0..98412d4e7e 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -45,7 +45,8 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy _checkoutAttemptId = checkoutAttemptId; }) .catch(e => { - console.warn(`Fetching checkoutAttemptId failed.${e ? ` Error=${e}` : ''}`); + // Caught at collectId level. We do not expect this catch block to ever fire, but... just in case... + console.debug(`Fetching checkoutAttemptId failed.${e ? ` Error=${e}` : ''}`); }); } // Log pixel // TODO once we stop using the pixel we can stop requiring both "enabled" & "telemetry" config options diff --git a/packages/lib/src/core/Analytics/CAEventsQueue.ts b/packages/lib/src/core/Analytics/CAEventsQueue.ts index a954fce869..7fcefdd94e 100644 --- a/packages/lib/src/core/Analytics/CAEventsQueue.ts +++ b/packages/lib/src/core/Analytics/CAEventsQueue.ts @@ -57,7 +57,10 @@ const CAEventsQueue = ({ analyticsContext, clientKey }: EventQueueProps) => { console.log('### CAEventsQueue::send:: success'); return undefined; }) - .catch(() => {}); + .catch(() => { + // Caught, silently, at http level. We do not expect this catch block to ever fire, but... just in case... + console.debug('### CAEventsQueue:::: send has failed'); + }); return promise; } diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index a941bf125f..a10108244f 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -24,7 +24,7 @@ const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { let promise; const options = { - errorLevel: 'silent' as const, + errorLevel: 'fatal' as const, // ensure our catch block is called loadingContext: analyticsContext, path: `v2/analytics?clientKey=${clientKey}` }; @@ -54,21 +54,23 @@ const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { promise = httpPost(options, telemetryEvent) .then(conversion => { - if (conversion.id) { + if (conversion?.id) { storage.set({ id: conversion.id, timestamp: Date.now() }); return conversion.id; } return undefined; }) .catch(() => { - // TODO - temporarily faking it - so we get a checkoutAttemptId which will allow subsequents request to be made (they'll fail, but at least we see them in the console) - console.log('### collect-id2:::: FAILED'); + console.debug( + 'WARNING: Failed to retrieve "checkoutAttemptId". Consequently, analytics will not be available for this payment. The payment process, however, will not be affected.' + ); + + // TODO - temporarily faking it - generate a checkoutAttemptId which will allow subsequents request to be made (they'll fail, but at least we see them in the console) const id = '64d673ff-36d3-4b32-999b-49e215f6b9891687261360764E7D99B01E11BF4C4B83CF7C7F49C5E75F23B2381E2ACBEE8E03E221E3BC95998'; storage.set({ id: id, timestamp: Date.now() }); return id; // TODO - end }); - // .catch(() => {}); return promise; }; From 6f82f73bfc6a817c1692b68b8780bc08110f41ad Mon Sep 17 00:00:00 2001 From: nicholas Date: Wed, 28 Jun 2023 13:07:28 +0200 Subject: [PATCH 16/94] Aligning fallback storage solution with normal, window based, Storage API --- .../Services/analytics/collect-id.test.ts | 2 +- packages/lib/src/utils/Storage.test.ts | 20 ++++++++++++++++--- packages/lib/src/utils/Storage.ts | 16 +++++++++++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/lib/src/core/Services/analytics/collect-id.test.ts b/packages/lib/src/core/Services/analytics/collect-id.test.ts index 9c543b582a..be9b669357 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.test.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.test.ts @@ -58,7 +58,7 @@ test('Should send expected data to http service', () => { expect(httpPost).toHaveBeenCalledTimes(1); expect(httpPost).toHaveBeenCalledWith( { - errorLevel: 'silent', + errorLevel: 'fatal', loadingContext: 'https://checkoutanalytics-test.adyen.com/checkoutanalytics/', path: 'v2/analytics?clientKey=xxxx-yyyy' }, diff --git a/packages/lib/src/utils/Storage.test.ts b/packages/lib/src/utils/Storage.test.ts index b1da616d02..f8551efd93 100644 --- a/packages/lib/src/utils/Storage.test.ts +++ b/packages/lib/src/utils/Storage.test.ts @@ -6,6 +6,9 @@ describe('Storage implementation - normal, window-based, storage', () => { storage.set({ id: 'mockId' }); + expect(storage.keyByIndex(0)).toEqual('adyen-checkout__checkout-attempt-id'); + expect(storage.length).toEqual(1); + let idObj: any = storage.get(); expect(idObj.id).toEqual('mockId'); @@ -14,6 +17,17 @@ describe('Storage implementation - normal, window-based, storage', () => { idObj = storage.get(); expect(idObj).toBe(null); + + storage.set({ id: 'mockId2' }); + idObj = storage.get(); + + expect(idObj.id).toEqual('mockId2'); + + storage.clear(); + + idObj = storage.get(); + + expect(idObj).toBe(null); }); }); @@ -33,8 +47,8 @@ describe('Storage implementation - storage fallback', () => { storage.set({ id: 'mockId' }); - expect(storage.storage.key('adyen-checkout__checkout-attempt-id')).toEqual(0); - expect(storage.storage.length).toEqual(1); + expect(storage.keyByIndex(0)).toEqual('adyen-checkout__checkout-attempt-id'); + expect(storage.length).toEqual(1); let idObj: any = storage.get(); @@ -51,7 +65,7 @@ describe('Storage implementation - storage fallback', () => { expect(idObj.id).toEqual('mockId2'); - storage.storage.clear(); + storage.clear(); idObj = storage.get(); diff --git a/packages/lib/src/utils/Storage.ts b/packages/lib/src/utils/Storage.ts index 7af8b8bdfd..3d51ac2988 100644 --- a/packages/lib/src/utils/Storage.ts +++ b/packages/lib/src/utils/Storage.ts @@ -9,8 +9,8 @@ class NonPersistentStorage { return Object.keys(this.storage).length; } - key(keyName) { - return Object.keys(this.storage).indexOf(keyName); + key(index) { + return Object.keys(this.storage)[index]; } getItem(keyName) { return this.storage[keyName] || null; @@ -58,6 +58,18 @@ class Storage { public remove() { this.storage.removeItem(this.key); } + + public clear() { + this.storage.clear(); + } + + public keyByIndex(index) { + return this.storage.key(index); + } + + get length() { + return this.storage.length; + } } export default Storage; From 066c1c99e44c38b9a8f1e67d0dc10ed04fdd3c23 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 29 Jun 2023 17:03:50 +0200 Subject: [PATCH 17/94] Some adjustments now actual endpoint is accepting requests --- .changeset/selfish-socks-fix.md | 5 +++++ packages/lib/src/core/Analytics/Analytics.ts | 1 - packages/lib/src/core/Analytics/CAEventsQueue.ts | 2 +- packages/lib/src/core/Analytics/utils.ts | 2 +- .../src/core/Services/analytics/collect-id.test.ts | 2 +- .../lib/src/core/Services/analytics/collect-id.ts | 12 +++--------- 6 files changed, 11 insertions(+), 13 deletions(-) create mode 100644 .changeset/selfish-socks-fix.md diff --git a/.changeset/selfish-socks-fix.md b/.changeset/selfish-socks-fix.md new file mode 100644 index 0000000000..695fa49771 --- /dev/null +++ b/.changeset/selfish-socks-fix.md @@ -0,0 +1,5 @@ +--- +'@adyen/adyen-web': minor +--- + +Starting using /checkoutanalytics endpoint to retrieve "checkoutAttemptId" log "submit" and "action-handled" events diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 98412d4e7e..18e18f4690 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -41,7 +41,6 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy // fetch a new checkoutAttemptId if none is already available _collectId({ ...initialEvent, ...(payload && { ...payload }) }) .then(checkoutAttemptId => { - console.log('### Analytics::setting checkoutAttemptId:: ', checkoutAttemptId); _checkoutAttemptId = checkoutAttemptId; }) .catch(e => { diff --git a/packages/lib/src/core/Analytics/CAEventsQueue.ts b/packages/lib/src/core/Analytics/CAEventsQueue.ts index 7fcefdd94e..3f52a9d826 100644 --- a/packages/lib/src/core/Analytics/CAEventsQueue.ts +++ b/packages/lib/src/core/Analytics/CAEventsQueue.ts @@ -54,7 +54,7 @@ const CAEventsQueue = ({ analyticsContext, clientKey }: EventQueueProps) => { const promise = httpPost(options, caActions) .then(() => { - console.log('### CAEventsQueue::send:: success'); + // Succeed, silently return undefined; }) .catch(() => { diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index e73918d62b..fb8f3a974f 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -23,6 +23,6 @@ export const createAnalyticsObject = (aObj): AnalyticsObject => ({ ...((aObj.class === 'error' || (aObj.class === 'log' && aObj.type !== ANALYTICS_SUBMIT_STR)) && { message: aObj.message }), // only added if we have an error, or log object (that's not logging a submit/pay button press) ...(aObj.class === 'log' && { type: aObj.type }), // only added if we have a log object ...(aObj.class === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type - // ...(aObj.class === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type // TODO should be allowed but for some reason API won't accept it + ...(aObj.class === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type // TODO should be allowed but for some reason API won't accept it ...(aObj.class === 'event' && { type: aObj.type, target: aObj.target }) // only added if we have an event object }); diff --git a/packages/lib/src/core/Services/analytics/collect-id.test.ts b/packages/lib/src/core/Services/analytics/collect-id.test.ts index be9b669357..a9644960e8 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.test.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.test.ts @@ -5,7 +5,7 @@ jest.mock('../http', () => ({ httpPost: jest.fn( () => new Promise(resolve => { - resolve({ id: 'mockCheckoutAttemptId' }); + resolve({ checkoutAttemptId: 'mockCheckoutAttemptId' }); }) ) })); diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index a10108244f..a44c5bab4d 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -54,9 +54,9 @@ const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { promise = httpPost(options, telemetryEvent) .then(conversion => { - if (conversion?.id) { - storage.set({ id: conversion.id, timestamp: Date.now() }); - return conversion.id; + if (conversion?.checkoutAttemptId) { + storage.set({ id: conversion.checkoutAttemptId, timestamp: Date.now() }); + return conversion.checkoutAttemptId; } return undefined; }) @@ -64,12 +64,6 @@ const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { console.debug( 'WARNING: Failed to retrieve "checkoutAttemptId". Consequently, analytics will not be available for this payment. The payment process, however, will not be affected.' ); - - // TODO - temporarily faking it - generate a checkoutAttemptId which will allow subsequents request to be made (they'll fail, but at least we see them in the console) - const id = '64d673ff-36d3-4b32-999b-49e215f6b9891687261360764E7D99B01E11BF4C4B83CF7C7F49C5E75F23B2381E2ACBEE8E03E221E3BC95998'; - storage.set({ id: id, timestamp: Date.now() }); - return id; - // TODO - end }); return promise; From 027786ea98d71b7605f409ae97bfd042b3f72fa1 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 30 Jun 2023 11:10:16 +0200 Subject: [PATCH 18/94] Analytics path read from constant and passed to relevant components --- .../src/components/Dropin/components/DropinComponent.tsx | 2 +- packages/lib/src/core/Analytics/Analytics.ts | 6 +++--- packages/lib/src/core/Analytics/CAEventsQueue.ts | 4 ++-- packages/lib/src/core/Analytics/constants.ts | 2 ++ packages/lib/src/core/Analytics/types.ts | 2 +- packages/lib/src/core/Services/analytics/collect-id.ts | 4 ++-- packages/lib/src/core/Services/analytics/types.ts | 2 +- 7 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/lib/src/components/Dropin/components/DropinComponent.tsx b/packages/lib/src/components/Dropin/components/DropinComponent.tsx index 9514e05444..15a7bca0a7 100644 --- a/packages/lib/src/components/Dropin/components/DropinComponent.tsx +++ b/packages/lib/src/components/Dropin/components/DropinComponent.tsx @@ -34,7 +34,7 @@ export class DropinComponent extends Component e.props.type), // TODO not supported in the initial request to checkoutanalytics + // paymentMethods: elements.map(e => e.props.type), // TODO will be supported in the initial request to checkoutanalytics component: 'dropin', flavor: 'dropin' }); diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 18e18f4690..69f44c08c9 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -3,7 +3,7 @@ import collectId from '../Services/analytics/collect-id'; import { CoreOptions } from '../types'; import CAEventsQueue, { EQObject } from './CAEventsQueue'; import { ANALYTICS_ACTION, AnalyticsInitialEvent, AnalyticsObject } from './types'; -import { ANALYTICS_ACTION_ERROR, ANALYTICS_ACTION_LOG } from './constants'; +import { ANALYTICS_ACTION_ERROR, ANALYTICS_ACTION_LOG, ANALYTICS_PATH } from './constants'; import { debounce } from '../../components/internal/Address/utils'; import { AnalyticsModule } from '../../components/types'; @@ -29,8 +29,8 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy } const _logEvent = logEvent({ loadingContext, locale }); - const _collectId = collectId({ analyticsContext, clientKey, locale, amount }); - const _caEventsQueue: EQObject = CAEventsQueue({ analyticsContext, clientKey }); + const _collectId = collectId({ analyticsContext, clientKey, locale, amount, analyticsPath: ANALYTICS_PATH }); + const _caEventsQueue: EQObject = CAEventsQueue({ analyticsContext, clientKey, analyticsPath: ANALYTICS_PATH }); const analyticsObj: AnalyticsModule = { send: (initialEvent: AnalyticsInitialEvent) => { diff --git a/packages/lib/src/core/Analytics/CAEventsQueue.ts b/packages/lib/src/core/Analytics/CAEventsQueue.ts index 3f52a9d826..e96b15c82a 100644 --- a/packages/lib/src/core/Analytics/CAEventsQueue.ts +++ b/packages/lib/src/core/Analytics/CAEventsQueue.ts @@ -15,7 +15,7 @@ export interface EQObject { _runQueue: (id) => Promise; } -const CAEventsQueue = ({ analyticsContext, clientKey }: EventQueueProps) => { +const CAEventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueueProps) => { const caActions: CAActions = { channel: 'Web', events: [], @@ -49,7 +49,7 @@ const CAEventsQueue = ({ analyticsContext, clientKey }: EventQueueProps) => { const options: HttpOptions = { errorLevel: 'silent' as const, loadingContext: analyticsContext, - path: `v2/analytics/${checkoutAttemptId}?clientKey=${clientKey}` + path: `${analyticsPath}/${checkoutAttemptId}?clientKey=${clientKey}` }; const promise = httpPost(options, caActions) diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index bff91e637d..aa65c88d1e 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -1,3 +1,5 @@ +export const ANALYTICS_PATH = 'v2/analytics'; + export const ANALYTICS_ACTION_LOG = 'log'; export const ANALYTICS_ACTION_ERROR = 'error'; export const ANALYTICS_ACTION_EVENT = 'event'; diff --git a/packages/lib/src/core/Analytics/types.ts b/packages/lib/src/core/Analytics/types.ts index 3237729788..4b3c1d8e0d 100644 --- a/packages/lib/src/core/Analytics/types.ts +++ b/packages/lib/src/core/Analytics/types.ts @@ -61,4 +61,4 @@ export type AnalyticsConfig = { loadingContext?: string; }; -export type EventQueueProps = Pick; +export type EventQueueProps = Pick & { analyticsPath: string }; diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index a44c5bab4d..f45302d161 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -20,13 +20,13 @@ function confirmSessionDurationIsMaxFifteenMinutes(checkoutAttemptIdSession: Che * @returns a function returning a promise containing the response of the call (an object containing a checkoutAttemptId property) */ // const collectId = ({ analyticsContext, clientKey, locale, amount }: CollectIdProps) => { // TODO - amount will be supported in the future -const collectId = ({ analyticsContext, clientKey, locale }: CollectIdProps) => { +const collectId = ({ analyticsContext, clientKey, locale, analyticsPath }: CollectIdProps) => { let promise; const options = { errorLevel: 'fatal' as const, // ensure our catch block is called loadingContext: analyticsContext, - path: `v2/analytics?clientKey=${clientKey}` + path: `${analyticsPath}?clientKey=${clientKey}` }; return (event): Promise => { diff --git a/packages/lib/src/core/Services/analytics/types.ts b/packages/lib/src/core/Services/analytics/types.ts index ebbfa5b7e0..75a1f85a07 100644 --- a/packages/lib/src/core/Services/analytics/types.ts +++ b/packages/lib/src/core/Services/analytics/types.ts @@ -6,7 +6,7 @@ export type CheckoutAttemptIdSession = { timestamp: number; }; -export type CollectIdProps = Pick; +export type CollectIdProps = Pick & { analyticsPath: string }; export type LogEventProps = Pick; From 5c5d93c09b9e1680834713791e10f536969575bc Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 30 Jun 2023 11:22:02 +0200 Subject: [PATCH 19/94] Removing unused components --- .../src/core/Analytics/EventsQueue.test.ts | 23 -------- .../lib/src/core/Analytics/EventsQueue.ts | 18 ------- .../Services/analytics/post-telemetry.test.ts | 54 ------------------- .../core/Services/analytics/post-telemetry.ts | 44 --------------- 4 files changed, 139 deletions(-) delete mode 100644 packages/lib/src/core/Analytics/EventsQueue.test.ts delete mode 100644 packages/lib/src/core/Analytics/EventsQueue.ts delete mode 100644 packages/lib/src/core/Services/analytics/post-telemetry.test.ts delete mode 100644 packages/lib/src/core/Services/analytics/post-telemetry.ts diff --git a/packages/lib/src/core/Analytics/EventsQueue.test.ts b/packages/lib/src/core/Analytics/EventsQueue.test.ts deleted file mode 100644 index 3d12b704cc..0000000000 --- a/packages/lib/src/core/Analytics/EventsQueue.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { default as EventsQueue } from './EventsQueue'; - -describe('EventsQueue', () => { - let queue; - - beforeEach(() => { - queue = new EventsQueue(); - }); - - test('adds events to the queue', () => { - const task1 = () => jest.fn(); - queue.add(task1); - expect(queue.events.length).toBe(1); - }); - - test('run flushes the queue', () => { - const task1 = () => jest.fn(); - queue.add(task1); - expect(queue.events.length).toBe(1); - queue.run(); - expect(queue.events.length).toBe(0); - }); -}); diff --git a/packages/lib/src/core/Analytics/EventsQueue.ts b/packages/lib/src/core/Analytics/EventsQueue.ts deleted file mode 100644 index 051d761fd6..0000000000 --- a/packages/lib/src/core/Analytics/EventsQueue.ts +++ /dev/null @@ -1,18 +0,0 @@ -type EventItem = (checkoutAttemptId: string) => Promise; - -class EventsQueue { - public events: EventItem[] = []; - - add(event) { - this.events.push(event); - } - - run(checkoutAttemptId?: string) { - const promises = this.events.map(e => e(checkoutAttemptId)); - this.events = []; - - return Promise.all(promises); - } -} - -export default EventsQueue; diff --git a/packages/lib/src/core/Services/analytics/post-telemetry.test.ts b/packages/lib/src/core/Services/analytics/post-telemetry.test.ts deleted file mode 100644 index 2867fc8321..0000000000 --- a/packages/lib/src/core/Services/analytics/post-telemetry.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { httpPost } from '../http'; -import logTelemetry from './post-telemetry'; - -jest.mock('../http', () => ({ - ...jest.requireActual('../http'), - httpPost: jest.fn() -})); - -beforeEach(() => { - process.env.VERSION = 'x.x.x'; -}); - -test('should send proper data to http service', () => { - const configuration = { - loadingContext: 'https://checkoutshopper-test.adyen.com/checkoutshopper/', - locale: 'en-US', - clientKey: 'xxxx-yyyy', - amount: { - value: 10000, - currency: 'USD' - } - }; - - const customEvent = { - eventType: 'mouse-click' - }; - - const log = logTelemetry(configuration); - - log(customEvent); - - expect(httpPost).toHaveBeenCalledTimes(1); - expect(httpPost).toHaveBeenCalledWith( - { - errorLevel: 'silent', - loadingContext: 'https://checkoutshopper-test.adyen.com/checkoutshopper/', - path: 'v2/analytics/log?clientKey=xxxx-yyyy' - }, - { - amount: { - currency: 'USD', - value: 10000 - }, - channel: 'Web', - eventType: 'mouse-click', - flavor: 'components', - locale: 'en-US', - referrer: 'http://localhost/', - screenWidth: 0, - userAgent: 'Mozilla/5.0 (linux) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/20.0.3', - version: 'x.x.x' - } - ); -}); diff --git a/packages/lib/src/core/Services/analytics/post-telemetry.ts b/packages/lib/src/core/Services/analytics/post-telemetry.ts deleted file mode 100644 index ccd3a3a0d2..0000000000 --- a/packages/lib/src/core/Services/analytics/post-telemetry.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { httpPost } from '../http'; -import { PaymentAmount } from '../../../types'; - -type LogTelemetryConfig = { - loadingContext: string; - locale: string; - clientKey: string; - amount: PaymentAmount; -}; - -/** - * Log event to Adyen - * @param config - - */ -const logTelemetry = (config: LogTelemetryConfig) => (event: any) => { - if (!config.clientKey) { - return Promise.reject(); - } - - const options = { - errorLevel: 'silent' as const, - loadingContext: config.loadingContext, - path: `v2/analytics/log?clientKey=${config.clientKey}` - }; - - const telemetryEvent = { - amount: { - value: config.amount?.value || 0, - currency: config.amount?.currency - }, - version: process.env.VERSION, - channel: 'Web', - locale: config.locale, - flavor: 'components', - userAgent: navigator.userAgent, - referrer: window.location.href, - screenWidth: window.screen.width, - ...event - }; - - return httpPost(options, telemetryEvent); -}; - -export default logTelemetry; From 8fe248e63db9f68eedec19d07711fe38db9caf48 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 30 Jun 2023 11:44:08 +0200 Subject: [PATCH 20/94] Fixed unit test --- .../lib/src/core/Services/analytics/collect-id.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/core/Services/analytics/collect-id.test.ts b/packages/lib/src/core/Services/analytics/collect-id.test.ts index a9644960e8..338e351b0e 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.test.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.test.ts @@ -1,5 +1,6 @@ import { httpPost } from '../http'; import collectId from './collect-id'; +import { ANALYTICS_PATH } from '../../Analytics/constants'; jest.mock('../http', () => ({ httpPost: jest.fn( @@ -21,7 +22,8 @@ test('Should lead to a rejected promise since no clientKey is provided', () => { amount: { value: 10000, currency: 'USD' - } + }, + analyticsPath: ANALYTICS_PATH }; const log = collectId(configuration); @@ -40,7 +42,8 @@ test('Should send expected data to http service', () => { amount: { value: 10000, currency: 'USD' - } + }, + analyticsPath: ANALYTICS_PATH }; const customEvent = { From ee9f4ccf6e435d65342638d44d9121cc1fd0e092 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 30 Jun 2023 12:31:54 +0200 Subject: [PATCH 21/94] Added comments on the shape of objects for the /checkoutanalytics endpoint --- packages/lib/src/core/Analytics/utils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index fb8f3a974f..3696193f7a 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -5,13 +5,15 @@ export const getUTCTimestamp = () => Date.now(); /** * All objects for the /checkoutanalytics endpoint have base props: - * "timestamp" & "component" + * "timestamp" & "component" (and an optional "metadata" object of key-value pairs) * * Error objects have, in addition to the base props: * "code", "errorType" & "message" * * Log objects have, in addition to the base props: - * "type" & "message" (and maybe an "action" & "subtype") + * "type" & "target" (e.g. when onSubmit is called after a pay button click), or, + * "type" & "subtype" & "message" (e.g. when an action is handled), or, + * "type" & "message" (e.g. logging during the 3DS2 process) * * Event objects have, in addition to the base props: * "type" & "target" @@ -23,6 +25,6 @@ export const createAnalyticsObject = (aObj): AnalyticsObject => ({ ...((aObj.class === 'error' || (aObj.class === 'log' && aObj.type !== ANALYTICS_SUBMIT_STR)) && { message: aObj.message }), // only added if we have an error, or log object (that's not logging a submit/pay button press) ...(aObj.class === 'log' && { type: aObj.type }), // only added if we have a log object ...(aObj.class === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type - ...(aObj.class === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type // TODO should be allowed but for some reason API won't accept it + ...(aObj.class === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type ...(aObj.class === 'event' && { type: aObj.type, target: aObj.target }) // only added if we have an event object }); From 86897eef5ce57883f2b94cbe15495e78b888cf63 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 30 Jun 2023 14:56:26 +0200 Subject: [PATCH 22/94] Unit test for when checkoutanalytics url is wrong --- .../Services/analytics/collect-id.test.ts | 74 ++++++++++++------- .../src/core/Services/analytics/collect-id.ts | 8 +- 2 files changed, 51 insertions(+), 31 deletions(-) diff --git a/packages/lib/src/core/Services/analytics/collect-id.test.ts b/packages/lib/src/core/Services/analytics/collect-id.test.ts index 338e351b0e..c9c2966609 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.test.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.test.ts @@ -1,32 +1,35 @@ import { httpPost } from '../http'; -import collectId from './collect-id'; +import collectId, { FAILURE_MSG } from './collect-id'; import { ANALYTICS_PATH } from '../../Analytics/constants'; -jest.mock('../http', () => ({ - httpPost: jest.fn( - () => - new Promise(resolve => { - resolve({ checkoutAttemptId: 'mockCheckoutAttemptId' }); - }) - ) -})); +jest.mock('../http'); + +const mockedHttpPost = httpPost as jest.Mock; + +const httpPromiseSuccessMock = jest.fn(() => Promise.resolve({ checkoutAttemptId: 'mockCheckoutAttemptId' })); + +const httpPromiseFailMock = jest.fn(() => Promise.reject(' url incorrect')); + +const BASE_CONFIGURATION = { + analyticsContext: 'https://checkoutanalytics-test.adyen.com/checkoutanalytics/', + locale: 'en-US', + amount: { + value: 10000, + currency: 'USD' + }, + analyticsPath: ANALYTICS_PATH +}; beforeEach(() => { process.env.VERSION = 'x.x.x'; + + mockedHttpPost.mockReset(); + mockedHttpPost.mockImplementation(httpPromiseSuccessMock); + httpPromiseSuccessMock.mockClear(); }); test('Should lead to a rejected promise since no clientKey is provided', () => { - const configuration = { - analyticsContext: 'https://checkoutanalytics-test.adyen.com/checkoutanalytics/', - locale: 'en-US', - amount: { - value: 10000, - currency: 'USD' - }, - analyticsPath: ANALYTICS_PATH - }; - - const log = collectId(configuration); + const log = collectId(BASE_CONFIGURATION); log({}) .then() .catch(e => { @@ -34,16 +37,31 @@ test('Should lead to a rejected promise since no clientKey is provided', () => { }); }); -test('Should send expected data to http service', () => { +test('Should fail since path is incorrect', () => { + mockedHttpPost.mockReset(); + mockedHttpPost.mockImplementation(httpPromiseFailMock); + httpPromiseFailMock.mockClear(); + const configuration = { - analyticsContext: 'https://checkoutanalytics-test.adyen.com/checkoutanalytics/', - locale: 'en-US', + ...BASE_CONFIGURATION, clientKey: 'xxxx-yyyy', - amount: { - value: 10000, - currency: 'USD' - }, - analyticsPath: ANALYTICS_PATH + analyticsPath: 'v99/analytics' + }; + + const log = collectId(configuration); + log({}) + .then(val => { + expect(val).toEqual(FAILURE_MSG); + }) + .catch(() => {}); + + expect(httpPost).toHaveBeenCalledTimes(1); +}); + +test('Should send expected data to http service', () => { + const configuration = { + ...BASE_CONFIGURATION, + clientKey: 'xxxx-yyyy' }; const customEvent = { diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index f45302d161..62a2d145d9 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -2,6 +2,9 @@ import { httpPost } from '../http'; import Storage from '../../../utils/Storage'; import { CheckoutAttemptIdSession, CollectIdProps, TelemetryEvent } from './types'; +export const FAILURE_MSG = + 'WARNING: Failed to retrieve "checkoutAttemptId". Consequently, analytics will not be available for this payment. The payment process, however, will not be affected.'; + /** * If the checkout attempt ID was stored more than fifteen minutes ago, then we should request a new ID. * More here: COWEB-1099 @@ -61,9 +64,8 @@ const collectId = ({ analyticsContext, clientKey, locale, analyticsPath }: Colle return undefined; }) .catch(() => { - console.debug( - 'WARNING: Failed to retrieve "checkoutAttemptId". Consequently, analytics will not be available for this payment. The payment process, however, will not be affected.' - ); + console.debug(FAILURE_MSG); + return FAILURE_MSG; }); return promise; From 5a88231ea0de7bb175f13dc2597fd05111df1cae Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 30 Jun 2023 15:38:14 +0200 Subject: [PATCH 23/94] Remove constant for mismatching threeDSServerTransID (it is no longer a situation we catch) --- packages/lib/src/core/Analytics/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index aa65c88d1e..5cb17db9cb 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -26,4 +26,3 @@ export const ANALYTICS_ERROR_CODE_3DS2_TIMEOUT = 'web_705'; // 3DS2 process has export const ANALYTICS_ERROR_CODE_TOKEN_IS_MISSING_ACSURL = 'web_800'; // Decoded token is missing a valid acsURL property export const ANALYTICS_ERROR_CODE_NO_TRANSSTATUS = 'web_801'; // Challenge has resulted in an error (no transStatus could be retrieved by the backend) -export const ANALYTICS_ERROR_CODE_MISMATCHING_TRANS_IDS = 'web_802'; // threeDSServerTransID: ids do not match From 9b3a45317ab3ed43896d57e99523a54ca33c8d72 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 3 Jul 2023 17:46:07 +0200 Subject: [PATCH 24/94] Use await when calling collectId --- packages/lib/src/core/Analytics/Analytics.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 69f44c08c9..7e4111b45a 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -33,20 +33,19 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy const _caEventsQueue: EQObject = CAEventsQueue({ analyticsContext, clientKey, analyticsPath: ANALYTICS_PATH }); const analyticsObj: AnalyticsModule = { - send: (initialEvent: AnalyticsInitialEvent) => { + send: async (initialEvent: AnalyticsInitialEvent) => { const { enabled, payload, telemetry } = props; // TODO what is payload, is it ever used? if (enabled === true) { if (telemetry === true && !_checkoutAttemptId) { - // fetch a new checkoutAttemptId if none is already available - _collectId({ ...initialEvent, ...(payload && { ...payload }) }) - .then(checkoutAttemptId => { - _checkoutAttemptId = checkoutAttemptId; - }) - .catch(e => { - // Caught at collectId level. We do not expect this catch block to ever fire, but... just in case... - console.debug(`Fetching checkoutAttemptId failed.${e ? ` Error=${e}` : ''}`); - }); + try { + // fetch a new checkoutAttemptId if none is already available + const checkoutAttemptId = await _collectId({ ...initialEvent, ...(payload && { ...payload }) }); + _checkoutAttemptId = checkoutAttemptId; + } catch (e) { + // Caught at collectId level. We do not expect this catch block to ever fire, but... just in case... + console.debug(`Fetching checkoutAttemptId failed.${e ? ` Error=${e}` : ''}`); + } } // Log pixel // TODO once we stop using the pixel we can stop requiring both "enabled" & "telemetry" config options _logEvent(initialEvent); From 113ca9b66b89d42389078b1293e8e0749094b208 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 3 Jul 2023 17:53:48 +0200 Subject: [PATCH 25/94] Remove analyticsContext from CoreOptions --- packages/lib/src/core/Analytics/Analytics.ts | 2 +- packages/lib/src/core/core.ts | 1 - packages/lib/src/core/types.ts | 5 ----- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 7e4111b45a..0027d8dd40 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -7,7 +7,7 @@ import { ANALYTICS_ACTION_ERROR, ANALYTICS_ACTION_LOG, ANALYTICS_PATH } from './ import { debounce } from '../../components/internal/Address/utils'; import { AnalyticsModule } from '../../components/types'; -export type AnalyticsProps = Pick; +export type AnalyticsProps = Pick & { analyticsContext: string }; let _checkoutAttemptId = null; diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index 5eebaadf09..173f3d6272 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -240,7 +240,6 @@ class Core { session: this.session, loadingContext: this.loadingContext, cdnContext: this.cdnContext, - analyticsContext: this.analyticsContext, createFromAction: this.createFromAction, _parentInstance: this }; diff --git a/packages/lib/src/core/types.ts b/packages/lib/src/core/types.ts index bebb228e5e..21781005d0 100644 --- a/packages/lib/src/core/types.ts +++ b/packages/lib/src/core/types.ts @@ -154,11 +154,6 @@ export interface CoreOptions { * @internal */ loadingContext?: string; - - /** - * @internal - */ - analyticsContext?: string; } export type PaymentMethodsConfiguration = From 9b8a4955db415398c2d997787adb91d070755c28 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 3 Jul 2023 19:01:46 +0200 Subject: [PATCH 26/94] Adding createAnalyticsAction function in Analytics to make it simpler for other components to submit analytics --- packages/lib/src/components/UIElement.tsx | 11 +---------- packages/lib/src/components/types.ts | 3 ++- .../lib/src/core/Analytics/Analytics.test.ts | 7 ++++--- packages/lib/src/core/Analytics/Analytics.ts | 17 ++++++++++++----- packages/lib/src/core/Analytics/types.ts | 18 ++++++++++++++++++ packages/lib/src/core/Analytics/utils.ts | 16 ++++++++-------- packages/lib/src/core/core.ts | 18 ++++++++---------- 7 files changed, 53 insertions(+), 37 deletions(-) diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 089159e2c5..22c8d1bc91 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -11,8 +11,6 @@ import { hasOwnProperty } from '../utils/hasOwnProperty'; import DropinElement from './Dropin'; import { CoreOptions } from '../core/types'; import Core from '../core'; -import { AnalyticsObject } from '../core/Analytics/types'; -import { createAnalyticsObject } from '../core/Analytics/utils'; import { ANALYTICS_SUBMIT_STR } from '../core/Analytics/constants'; export class UIElement

extends BaseElement

implements IUIElement { @@ -57,14 +55,7 @@ export class UIElement

extends BaseElement

im component = `${component}-${subCompID.substring(0, subCompID.indexOf('-'))}`; } - const aObj: AnalyticsObject = createAnalyticsObject({ - class: 'log', - component, - type: ANALYTICS_SUBMIT_STR, - target: 'pay_button' - }); - - this.props.modules?.analytics.addAnalyticsAction('log', aObj); + this.props.modules.analytics.createAnalyticsAction({ action: 'log', data: { component, type: ANALYTICS_SUBMIT_STR, target: 'pay_button' } }); } private onSubmit(): void { diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index 2097ae4967..8d39e5ecd9 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -8,7 +8,7 @@ import { PayButtonProps } from './internal/PayButton/PayButton'; import Session from '../core/CheckoutSession'; import { SRPanel } from '../core/Errors/SRPanel'; import { Resources } from '../core/Context/Resources'; -import { AnalyticsInitialEvent, AnalyticsObject } from '../core/Analytics/types'; +import { AnalyticsInitialEvent, AnalyticsObject, CreateAnalyticsActionObject } from '../core/Analytics/types'; import { EQObject } from '../core/Analytics/CAEventsQueue'; export interface PaymentMethodData { @@ -70,6 +70,7 @@ export interface AnalyticsModule { sendAnalyticsActions: () => Promise; getCheckoutAttemptId: () => string; getEventsQueue: () => EQObject; + createAnalyticsAction: (a: CreateAnalyticsActionObject) => void; } export interface BaseElementProps { diff --git a/packages/lib/src/core/Analytics/Analytics.test.ts b/packages/lib/src/core/Analytics/Analytics.test.ts index 691a6a05cc..2d661e99c7 100644 --- a/packages/lib/src/core/Analytics/Analytics.test.ts +++ b/packages/lib/src/core/Analytics/Analytics.test.ts @@ -5,6 +5,7 @@ import { PaymentAmount } from '../../types'; import { createAnalyticsObject } from './utils'; import wait from '../../utils/wait'; import { DEFAULT_DEBOUNCE_TIME_MS } from '../../components/internal/Address/utils'; +import { ANALYTICS_ACTION } from './types'; jest.mock('../Services/analytics/collect-id'); jest.mock('../Services/analytics/log-event'); @@ -23,7 +24,7 @@ const event = { }; const analyticsEventObj = { - class: 'event', + action: 'event' as ANALYTICS_ACTION, component: 'cardComponent', type: 'Focus', target: 'PAN input' @@ -118,7 +119,7 @@ describe('Analytics initialisation and event queue', () => { expect(analytics.getEventsQueue().getQueue().events.length).toBe(1); const aObj = createAnalyticsObject({ - class: 'error', + action: 'error', component: 'threeDS2Fingerprint', code: 'web_704', errorType: 'APIError', @@ -141,7 +142,7 @@ describe('Analytics initialisation and event queue', () => { const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); const aObj = createAnalyticsObject({ - class: 'log', + action: 'log', component: 'scheme', type: 'Submit' }); diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 0027d8dd40..0b21c074fc 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -1,13 +1,11 @@ import logEvent from '../Services/analytics/log-event'; import collectId from '../Services/analytics/collect-id'; -import { CoreOptions } from '../types'; import CAEventsQueue, { EQObject } from './CAEventsQueue'; -import { ANALYTICS_ACTION, AnalyticsInitialEvent, AnalyticsObject } from './types'; +import { ANALYTICS_ACTION, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps, CreateAnalyticsActionObject } from './types'; import { ANALYTICS_ACTION_ERROR, ANALYTICS_ACTION_LOG, ANALYTICS_PATH } from './constants'; import { debounce } from '../../components/internal/Address/utils'; import { AnalyticsModule } from '../../components/types'; - -export type AnalyticsProps = Pick & { analyticsContext: string }; +import { createAnalyticsObject } from './utils'; let _checkoutAttemptId = null; @@ -72,7 +70,16 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy }, // Expose getter for testing purposes - getEventsQueue: () => _caEventsQueue + getEventsQueue: () => _caEventsQueue, + + createAnalyticsAction: ({ action, data }: CreateAnalyticsActionObject) => { + const aObj: AnalyticsObject = createAnalyticsObject({ + action, + ...data + }); + + analyticsObj.addAnalyticsAction(action, aObj); + } }; return analyticsObj; diff --git a/packages/lib/src/core/Analytics/types.ts b/packages/lib/src/core/Analytics/types.ts index 4b3c1d8e0d..cb11413120 100644 --- a/packages/lib/src/core/Analytics/types.ts +++ b/packages/lib/src/core/Analytics/types.ts @@ -1,4 +1,5 @@ import { PaymentAmount } from '../../types'; +import { CoreOptions } from '../types'; export interface Experiment { controlGroup: boolean; @@ -33,6 +34,8 @@ export interface AnalyticsOptions { experiments?: Experiment[]; } +export type AnalyticsProps = Pick & { analyticsContext?: string }; + export interface AnalyticsObject { timestamp: string; component: string; @@ -46,6 +49,8 @@ export interface AnalyticsObject { export type ANALYTICS_ACTION = 'log' | 'error' | 'event'; +export type CreateAnalyticsObject = Omit & { action: ANALYTICS_ACTION }; + export type AnalyticsInitialEvent = { containerWidth: number; component: string; @@ -61,4 +66,17 @@ export type AnalyticsConfig = { loadingContext?: string; }; +export type CreateAnalyticsActionData = { + component: string; + type: string; + target?: string; + subtype?: string; + message?: string; +}; + +export type CreateAnalyticsActionObject = { + action: ANALYTICS_ACTION; + data: CreateAnalyticsActionData; +}; + export type EventQueueProps = Pick & { analyticsPath: string }; diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index 3696193f7a..bf9101f862 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -1,4 +1,4 @@ -import { AnalyticsObject } from './types'; +import { AnalyticsObject, CreateAnalyticsObject } from './types'; import { ANALYTICS_ACTION_STR, ANALYTICS_SUBMIT_STR } from './constants'; export const getUTCTimestamp = () => Date.now(); @@ -18,13 +18,13 @@ export const getUTCTimestamp = () => Date.now(); * Event objects have, in addition to the base props: * "type" & "target" */ -export const createAnalyticsObject = (aObj): AnalyticsObject => ({ +export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObject => ({ timestamp: String(getUTCTimestamp()), component: aObj.component, - ...(aObj.class === 'error' && { code: aObj.code, errorType: aObj.errorType }), // only added if we have an error object - ...((aObj.class === 'error' || (aObj.class === 'log' && aObj.type !== ANALYTICS_SUBMIT_STR)) && { message: aObj.message }), // only added if we have an error, or log object (that's not logging a submit/pay button press) - ...(aObj.class === 'log' && { type: aObj.type }), // only added if we have a log object - ...(aObj.class === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type - ...(aObj.class === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type - ...(aObj.class === 'event' && { type: aObj.type, target: aObj.target }) // only added if we have an event object + ...(aObj.action === 'error' && { code: aObj.code, errorType: aObj.errorType }), // only added if we have an error object + ...((aObj.action === 'error' || (aObj.action === 'log' && aObj.type !== ANALYTICS_SUBMIT_STR)) && { message: aObj.message }), // only added if we have an error, or log object (that's not logging a submit/pay button press) + ...(aObj.action === 'log' && { type: aObj.type }), // only added if we have a log object + ...(aObj.action === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type + ...(aObj.action === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type + ...(aObj.action === 'event' && { type: aObj.type, target: aObj.target }) // only added if we have an event object }); diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index 173f3d6272..fc82141eb9 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -14,9 +14,7 @@ import Session from './CheckoutSession'; import { hasOwnProperty } from '../utils/hasOwnProperty'; import { Resources } from './Context/Resources'; import { SRPanel } from './Errors/SRPanel'; -import { createAnalyticsObject } from './Analytics/utils'; import { ANALYTICS_ACTION_STR } from './Analytics/constants'; -import { AnalyticsObject } from './Analytics/types'; import { capitalizeFirstLetter } from '../utils/Formatters/formatters'; class Core { @@ -150,16 +148,16 @@ class Core { if (action.type) { // Call analytics endpoint - const aObj: AnalyticsObject = createAnalyticsObject({ - class: 'log', - component: `${action.type}${action.subtype}`, - type: ANALYTICS_ACTION_STR, - subtype: capitalizeFirstLetter(action.type), - message: `${action.type}${action.subtype} is initiating` + this.modules.analytics.createAnalyticsAction({ + action: 'log', + data: { + component: `${action.type}${action.subtype}`, + type: ANALYTICS_ACTION_STR, + subtype: capitalizeFirstLetter(action.type), + message: `${action.type}${action.subtype} is initiating` + } }); - this.modules.analytics.addAnalyticsAction('log', aObj); - // Create a component based on the action const actionTypeConfiguration = getComponentConfiguration(action.type, this.options.paymentMethodsConfiguration); From 9b35a88fa51aa729aea110e887eb80408dbef9ff Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 3 Jul 2023 19:44:27 +0200 Subject: [PATCH 27/94] Renaming CAEventsQueue to EventsQueue --- packages/lib/src/components/UIElement.tsx | 2 +- packages/lib/src/components/types.ts | 4 +-- packages/lib/src/core/Analytics/Analytics.ts | 36 +++++++++---------- ...ventsQueue.test.ts => EventsQueue.test.ts} | 10 +++--- .../{CAEventsQueue.ts => EventsQueue.ts} | 20 +++++------ 5 files changed, 36 insertions(+), 36 deletions(-) rename packages/lib/src/core/Analytics/{CAEventsQueue.test.ts => EventsQueue.test.ts} (71%) rename packages/lib/src/core/Analytics/{CAEventsQueue.ts => EventsQueue.ts} (74%) diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 22c8d1bc91..989d3ec7c5 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -55,7 +55,7 @@ export class UIElement

extends BaseElement

im component = `${component}-${subCompID.substring(0, subCompID.indexOf('-'))}`; } - this.props.modules.analytics.createAnalyticsAction({ action: 'log', data: { component, type: ANALYTICS_SUBMIT_STR, target: 'pay_button' } }); + this.props.modules?.analytics.createAnalyticsAction({ action: 'log', data: { component, type: ANALYTICS_SUBMIT_STR, target: 'pay_button' } }); } private onSubmit(): void { diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index 8d39e5ecd9..02b10330dc 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -9,7 +9,7 @@ import Session from '../core/CheckoutSession'; import { SRPanel } from '../core/Errors/SRPanel'; import { Resources } from '../core/Context/Resources'; import { AnalyticsInitialEvent, AnalyticsObject, CreateAnalyticsActionObject } from '../core/Analytics/types'; -import { EQObject } from '../core/Analytics/CAEventsQueue'; +import { EventsQueueObject } from '../core/Analytics/EventsQueue'; export interface PaymentMethodData { paymentMethod: { @@ -69,7 +69,7 @@ export interface AnalyticsModule { addAnalyticsAction: (s: string, o: AnalyticsObject) => void; sendAnalyticsActions: () => Promise; getCheckoutAttemptId: () => string; - getEventsQueue: () => EQObject; + getEventsQueue: () => EventsQueueObject; createAnalyticsAction: (a: CreateAnalyticsActionObject) => void; } diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 0b21c074fc..5597ee9b4c 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -1,15 +1,15 @@ -import logEvent from '../Services/analytics/log-event'; -import collectId from '../Services/analytics/collect-id'; -import CAEventsQueue, { EQObject } from './CAEventsQueue'; +import LogEvent from '../Services/analytics/log-event'; +import CollectId from '../Services/analytics/collect-id'; +import EventsQueue, { EventsQueueObject } from './EventsQueue'; import { ANALYTICS_ACTION, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps, CreateAnalyticsActionObject } from './types'; import { ANALYTICS_ACTION_ERROR, ANALYTICS_ACTION_LOG, ANALYTICS_PATH } from './constants'; import { debounce } from '../../components/internal/Address/utils'; import { AnalyticsModule } from '../../components/types'; import { createAnalyticsObject } from './utils'; -let _checkoutAttemptId = null; +let capturedCheckoutAttemptId = null; -const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analyticsContext }: AnalyticsProps) => { +const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analyticsContext }: AnalyticsProps): AnalyticsModule => { const defaultProps = { enabled: true, telemetry: true, @@ -22,38 +22,38 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy if (telemetry === true && enabled === true) { if (props.checkoutAttemptId) { // handle prefilled checkoutAttemptId // TODO is this still something that ever happens? - _checkoutAttemptId = props.checkoutAttemptId; + capturedCheckoutAttemptId = props.checkoutAttemptId; } } - const _logEvent = logEvent({ loadingContext, locale }); - const _collectId = collectId({ analyticsContext, clientKey, locale, amount, analyticsPath: ANALYTICS_PATH }); - const _caEventsQueue: EQObject = CAEventsQueue({ analyticsContext, clientKey, analyticsPath: ANALYTICS_PATH }); + const logEvent = LogEvent({ loadingContext, locale }); + const collectId = CollectId({ analyticsContext, clientKey, locale, amount, analyticsPath: ANALYTICS_PATH }); + const eventsQueue: EventsQueueObject = EventsQueue({ analyticsContext, clientKey, analyticsPath: ANALYTICS_PATH }); const analyticsObj: AnalyticsModule = { send: async (initialEvent: AnalyticsInitialEvent) => { const { enabled, payload, telemetry } = props; // TODO what is payload, is it ever used? if (enabled === true) { - if (telemetry === true && !_checkoutAttemptId) { + if (telemetry === true && !capturedCheckoutAttemptId) { try { // fetch a new checkoutAttemptId if none is already available - const checkoutAttemptId = await _collectId({ ...initialEvent, ...(payload && { ...payload }) }); - _checkoutAttemptId = checkoutAttemptId; + const checkoutAttemptId = await collectId({ ...initialEvent, ...(payload && { ...payload }) }); + capturedCheckoutAttemptId = checkoutAttemptId; } catch (e) { // Caught at collectId level. We do not expect this catch block to ever fire, but... just in case... console.debug(`Fetching checkoutAttemptId failed.${e ? ` Error=${e}` : ''}`); } } // Log pixel // TODO once we stop using the pixel we can stop requiring both "enabled" & "telemetry" config options - _logEvent(initialEvent); + logEvent(initialEvent); } }, - getCheckoutAttemptId: (): string => _checkoutAttemptId, + getCheckoutAttemptId: (): string => capturedCheckoutAttemptId, addAnalyticsAction: (type: ANALYTICS_ACTION, obj: AnalyticsObject) => { - _caEventsQueue.add(`${type}s`, obj); + eventsQueue.add(`${type}s`, obj); // errors get sent straight away, logs almost do (with a debounce), events are stored until an error or log comes along if (type === ANALYTICS_ACTION_LOG || type === ANALYTICS_ACTION_ERROR) { @@ -63,14 +63,14 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy }, sendAnalyticsActions: () => { - if (_checkoutAttemptId) { - return _caEventsQueue.run(_checkoutAttemptId); + if (capturedCheckoutAttemptId) { + return eventsQueue.run(capturedCheckoutAttemptId); } return Promise.resolve(null); }, // Expose getter for testing purposes - getEventsQueue: () => _caEventsQueue, + getEventsQueue: () => eventsQueue, createAnalyticsAction: ({ action, data }: CreateAnalyticsActionObject) => { const aObj: AnalyticsObject = createAnalyticsObject({ diff --git a/packages/lib/src/core/Analytics/CAEventsQueue.test.ts b/packages/lib/src/core/Analytics/EventsQueue.test.ts similarity index 71% rename from packages/lib/src/core/Analytics/CAEventsQueue.test.ts rename to packages/lib/src/core/Analytics/EventsQueue.test.ts index 214715e246..faef111893 100644 --- a/packages/lib/src/core/Analytics/CAEventsQueue.test.ts +++ b/packages/lib/src/core/Analytics/EventsQueue.test.ts @@ -1,22 +1,22 @@ -import CAEventsQueue from './CAEventsQueue'; +import EventsQueue from './EventsQueue'; +import { ANALYTICS_PATH } from './constants'; + +const task1 = { foo: 'bar', timestamp: '1234', component: 'scheme' }; describe('CAEventsQueue', () => { - const queue = CAEventsQueue({ analyticsContext: 'https://mydomain.com', clientKey: 'fsdjkh' }); + const queue = EventsQueue({ analyticsContext: 'https://mydomain.com', clientKey: 'fsdjkh', analyticsPath: ANALYTICS_PATH }); test('adds log to the queue', () => { - const task1 = { foo: 'bar' }; queue.add('logs', task1); expect(queue.getQueue().logs.length).toBe(1); }); test('adds event to the queue', () => { - const task1 = { foo: 'bar' }; queue.add('events', task1); expect(queue.getQueue().events.length).toBe(1); }); test('adds error to the queue', () => { - const task1 = { foo: 'bar' }; queue.add('errors', task1); expect(queue.getQueue().errors.length).toBe(1); }); diff --git a/packages/lib/src/core/Analytics/CAEventsQueue.ts b/packages/lib/src/core/Analytics/EventsQueue.ts similarity index 74% rename from packages/lib/src/core/Analytics/CAEventsQueue.ts rename to packages/lib/src/core/Analytics/EventsQueue.ts index e96b15c82a..9c31b2ba1c 100644 --- a/packages/lib/src/core/Analytics/CAEventsQueue.ts +++ b/packages/lib/src/core/Analytics/EventsQueue.ts @@ -8,14 +8,14 @@ interface CAActions { logs: AnalyticsObject[]; } -export interface EQObject { - add: (t, a) => void; - run: (id) => Promise; +export interface EventsQueueObject { + add: (t: string, o: AnalyticsObject) => void; + run: (id: string) => Promise; getQueue: () => CAActions; - _runQueue: (id) => Promise; + runQueue: (id: string) => Promise; } -const CAEventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueueProps) => { +const EventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueueProps): EventsQueueObject => { const caActions: CAActions = { channel: 'Web', events: [], @@ -23,13 +23,13 @@ const CAEventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueu logs: [] }; - const eqObject: EQObject = { + const eqObject: EventsQueueObject = { add: (type, actionObj) => { caActions[type].push(actionObj); }, run: (checkoutAttemptId: string) => { - const promise = eqObject._runQueue(checkoutAttemptId); + const promise = eqObject.runQueue(checkoutAttemptId); caActions.events = []; caActions.errors = []; @@ -41,7 +41,7 @@ const CAEventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueu // Expose getter for testing purposes getQueue: () => caActions, - _runQueue: (checkoutAttemptId: string): Promise => { + runQueue: (checkoutAttemptId: string): Promise => { if (!caActions.events.length && !caActions.logs.length && !caActions.errors.length) { return Promise.resolve(null); } @@ -59,7 +59,7 @@ const CAEventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueu }) .catch(() => { // Caught, silently, at http level. We do not expect this catch block to ever fire, but... just in case... - console.debug('### CAEventsQueue:::: send has failed'); + console.debug('### EventsQueue:::: send has failed'); }); return promise; @@ -69,4 +69,4 @@ const CAEventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueu return eqObject; }; -export default CAEventsQueue; +export default EventsQueue; From 6b7f5e2c725c4edebbf81143cafffa56f1944e0b Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 3 Jul 2023 20:00:48 +0200 Subject: [PATCH 28/94] Clarifying the "modular" nature of Analytics and EventsQueue --- packages/lib/src/components/types.ts | 4 +- packages/lib/src/core/Analytics/Analytics.ts | 12 ++-- .../lib/src/core/Analytics/EventsQueue.ts | 61 +++++++++---------- 3 files changed, 38 insertions(+), 39 deletions(-) diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index 02b10330dc..a2b82b7fee 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -9,7 +9,7 @@ import Session from '../core/CheckoutSession'; import { SRPanel } from '../core/Errors/SRPanel'; import { Resources } from '../core/Context/Resources'; import { AnalyticsInitialEvent, AnalyticsObject, CreateAnalyticsActionObject } from '../core/Analytics/types'; -import { EventsQueueObject } from '../core/Analytics/EventsQueue'; +import { EventsQueueModule } from '../core/Analytics/EventsQueue'; export interface PaymentMethodData { paymentMethod: { @@ -69,7 +69,7 @@ export interface AnalyticsModule { addAnalyticsAction: (s: string, o: AnalyticsObject) => void; sendAnalyticsActions: () => Promise; getCheckoutAttemptId: () => string; - getEventsQueue: () => EventsQueueObject; + getEventsQueue: () => EventsQueueModule; createAnalyticsAction: (a: CreateAnalyticsActionObject) => void; } diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 5597ee9b4c..cfbf65e511 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -1,6 +1,6 @@ import LogEvent from '../Services/analytics/log-event'; import CollectId from '../Services/analytics/collect-id'; -import EventsQueue, { EventsQueueObject } from './EventsQueue'; +import EventsQueue, { EventsQueueModule } from './EventsQueue'; import { ANALYTICS_ACTION, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps, CreateAnalyticsActionObject } from './types'; import { ANALYTICS_ACTION_ERROR, ANALYTICS_ACTION_LOG, ANALYTICS_PATH } from './constants'; import { debounce } from '../../components/internal/Address/utils'; @@ -28,9 +28,9 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy const logEvent = LogEvent({ loadingContext, locale }); const collectId = CollectId({ analyticsContext, clientKey, locale, amount, analyticsPath: ANALYTICS_PATH }); - const eventsQueue: EventsQueueObject = EventsQueue({ analyticsContext, clientKey, analyticsPath: ANALYTICS_PATH }); + const eventsQueue: EventsQueueModule = EventsQueue({ analyticsContext, clientKey, analyticsPath: ANALYTICS_PATH }); - const analyticsObj: AnalyticsModule = { + const anlModule: AnalyticsModule = { send: async (initialEvent: AnalyticsInitialEvent) => { const { enabled, payload, telemetry } = props; // TODO what is payload, is it ever used? @@ -58,7 +58,7 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy // errors get sent straight away, logs almost do (with a debounce), events are stored until an error or log comes along if (type === ANALYTICS_ACTION_LOG || type === ANALYTICS_ACTION_ERROR) { const debounceFn = type === ANALYTICS_ACTION_ERROR ? fn => fn : debounce; - debounceFn(analyticsObj.sendAnalyticsActions)(); + debounceFn(anlModule.sendAnalyticsActions)(); } }, @@ -78,11 +78,11 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy ...data }); - analyticsObj.addAnalyticsAction(action, aObj); + anlModule.addAnalyticsAction(action, aObj); } }; - return analyticsObj; + return anlModule; }; export default Analytics; diff --git a/packages/lib/src/core/Analytics/EventsQueue.ts b/packages/lib/src/core/Analytics/EventsQueue.ts index 9c31b2ba1c..341d59dfba 100644 --- a/packages/lib/src/core/Analytics/EventsQueue.ts +++ b/packages/lib/src/core/Analytics/EventsQueue.ts @@ -8,14 +8,13 @@ interface CAActions { logs: AnalyticsObject[]; } -export interface EventsQueueObject { +export interface EventsQueueModule { add: (t: string, o: AnalyticsObject) => void; run: (id: string) => Promise; getQueue: () => CAActions; - runQueue: (id: string) => Promise; } -const EventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueueProps): EventsQueueObject => { +const EventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueueProps): EventsQueueModule => { const caActions: CAActions = { channel: 'Web', events: [], @@ -23,13 +22,37 @@ const EventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueueP logs: [] }; - const eqObject: EventsQueueObject = { + const runQueue = (checkoutAttemptId: string): Promise => { + if (!caActions.events.length && !caActions.logs.length && !caActions.errors.length) { + return Promise.resolve(null); + } + + const options: HttpOptions = { + errorLevel: 'silent' as const, + loadingContext: analyticsContext, + path: `${analyticsPath}/${checkoutAttemptId}?clientKey=${clientKey}` + }; + + const promise = httpPost(options, caActions) + .then(() => { + // Succeed, silently + return undefined; + }) + .catch(() => { + // Caught, silently, at http level. We do not expect this catch block to ever fire, but... just in case... + console.debug('### EventsQueue:::: send has failed'); + }); + + return promise; + }; + + const eqModule: EventsQueueModule = { add: (type, actionObj) => { caActions[type].push(actionObj); }, run: (checkoutAttemptId: string) => { - const promise = eqObject.runQueue(checkoutAttemptId); + const promise = runQueue(checkoutAttemptId); caActions.events = []; caActions.errors = []; @@ -39,34 +62,10 @@ const EventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueueP }, // Expose getter for testing purposes - getQueue: () => caActions, - - runQueue: (checkoutAttemptId: string): Promise => { - if (!caActions.events.length && !caActions.logs.length && !caActions.errors.length) { - return Promise.resolve(null); - } - - const options: HttpOptions = { - errorLevel: 'silent' as const, - loadingContext: analyticsContext, - path: `${analyticsPath}/${checkoutAttemptId}?clientKey=${clientKey}` - }; - - const promise = httpPost(options, caActions) - .then(() => { - // Succeed, silently - return undefined; - }) - .catch(() => { - // Caught, silently, at http level. We do not expect this catch block to ever fire, but... just in case... - console.debug('### EventsQueue:::: send has failed'); - }); - - return promise; - } + getQueue: () => caActions }; - return eqObject; + return eqModule; }; export default EventsQueue; From 9ac36bc8621f1837907c7abad4fc977148bf954c Mon Sep 17 00:00:00 2001 From: nicholas Date: Tue, 4 Jul 2023 12:46:32 +0200 Subject: [PATCH 29/94] Fixing return type --- packages/lib/src/components/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index a2b82b7fee..09793c7665 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -65,7 +65,7 @@ export interface RawPaymentResponse extends PaymentResponse { } export interface AnalyticsModule { - send: (a: AnalyticsInitialEvent) => void; + send: (a: AnalyticsInitialEvent) => Promise; addAnalyticsAction: (s: string, o: AnalyticsObject) => void; sendAnalyticsActions: () => Promise; getCheckoutAttemptId: () => string; From 34e34c23afac262b1902fba66096a3bd37db33d7 Mon Sep 17 00:00:00 2001 From: nicholas Date: Tue, 4 Jul 2023 16:34:04 +0200 Subject: [PATCH 30/94] Pass in containerWidth with initial analytics call --- packages/lib/src/components/BaseElement.ts | 2 +- packages/lib/src/components/UIElement.tsx | 2 +- packages/lib/src/core/core.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index 052521a271..070fb53ee9 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -95,7 +95,7 @@ class BaseElement

{ // 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, + containerWidth: node && (node as HTMLElement).offsetWidth, component: this.constructor['analyticsType'] ?? this.constructor['type'], flavor: 'components' }); diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 989d3ec7c5..56b4816fd8 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -47,7 +47,7 @@ export class UIElement

extends BaseElement

im } /* eslint-disable-next-line */ - protected submitAnalytics(obj = null) { + protected submitAnalytics(obj?) { // Call analytics endpoint let component = this.elementRef._id?.substring(0, this.elementRef._id.indexOf('-')); if (component === 'dropin') { diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index fc82141eb9..8e8f0c0d0b 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -151,10 +151,10 @@ class Core { this.modules.analytics.createAnalyticsAction({ action: 'log', data: { - component: `${action.type}${action.subtype}`, + component: `${action.type}${action.subtype ?? ''}`, type: ANALYTICS_ACTION_STR, subtype: capitalizeFirstLetter(action.type), - message: `${action.type}${action.subtype} is initiating` + message: `${action.type}${action.subtype ?? ''} is initiating` } }); From 660bfa41c99a9db4eda721c5ae14bc77cc789df0 Mon Sep 17 00:00:00 2001 From: nicholas Date: Wed, 5 Jul 2023 17:14:15 +0200 Subject: [PATCH 31/94] Drop addAnalyticsAction from Analytics module. Tidy up Analytics' types --- packages/lib/src/components/types.ts | 3 +- .../lib/src/core/Analytics/Analytics.test.ts | 9 +++--- packages/lib/src/core/Analytics/Analytics.ts | 32 ++++++++++++------- packages/lib/src/core/Analytics/types.ts | 9 ++---- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index 09793c7665..430575b98c 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -8,7 +8,7 @@ import { PayButtonProps } from './internal/PayButton/PayButton'; import Session from '../core/CheckoutSession'; import { SRPanel } from '../core/Errors/SRPanel'; import { Resources } from '../core/Context/Resources'; -import { AnalyticsInitialEvent, AnalyticsObject, CreateAnalyticsActionObject } from '../core/Analytics/types'; +import { AnalyticsInitialEvent, CreateAnalyticsActionObject } from '../core/Analytics/types'; import { EventsQueueModule } from '../core/Analytics/EventsQueue'; export interface PaymentMethodData { @@ -66,7 +66,6 @@ export interface RawPaymentResponse extends PaymentResponse { export interface AnalyticsModule { send: (a: AnalyticsInitialEvent) => Promise; - addAnalyticsAction: (s: string, o: AnalyticsObject) => void; sendAnalyticsActions: () => Promise; getCheckoutAttemptId: () => string; getEventsQueue: () => EventsQueueModule; diff --git a/packages/lib/src/core/Analytics/Analytics.test.ts b/packages/lib/src/core/Analytics/Analytics.test.ts index 2d661e99c7..073328034f 100644 --- a/packages/lib/src/core/Analytics/Analytics.test.ts +++ b/packages/lib/src/core/Analytics/Analytics.test.ts @@ -48,7 +48,6 @@ describe('Analytics initialisation and event queue', () => { const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); expect(analytics.send).not.toBe(null); expect(analytics.getCheckoutAttemptId).not.toBe(null); - expect(analytics.addAnalyticsAction).not.toBe(null); expect(analytics.sendAnalyticsActions).not.toBe(null); expect(collectIdPromiseMock).toHaveLength(0); }); @@ -104,7 +103,7 @@ describe('Analytics initialisation and event queue', () => { // no message prop for events expect(aObj.message).toBe(undefined); - analytics.addAnalyticsAction('event', aObj); + analytics.createAnalyticsAction({ action: 'event', data: aObj }); // event object should not be sent immediately expect(analytics.getEventsQueue().getQueue().events.length).toBe(1); @@ -114,7 +113,7 @@ describe('Analytics initialisation and event queue', () => { const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); const evObj = createAnalyticsObject(analyticsEventObj); - analytics.addAnalyticsAction('event', evObj); + analytics.createAnalyticsAction({ action: 'event', data: evObj }); expect(analytics.getEventsQueue().getQueue().events.length).toBe(1); @@ -131,7 +130,7 @@ describe('Analytics initialisation and event queue', () => { expect(aObj.errorType).toEqual('APIError'); expect(aObj.message).not.toBe(undefined); - analytics.addAnalyticsAction('error', aObj); + analytics.createAnalyticsAction({ action: 'error', data: aObj }); // error object should be sent immediately, sending any events as well expect(analytics.getEventsQueue().getQueue().errors.length).toBe(0); @@ -154,7 +153,7 @@ describe('Analytics initialisation and event queue', () => { // no message prop for a log with type 'Submit' expect(aObj.message).toBe(undefined); - analytics.addAnalyticsAction('log', aObj); + analytics.createAnalyticsAction({ action: 'log', data: aObj }); // log object should be sent almost immediately (after a debounce interval) await wait(DEFAULT_DEBOUNCE_TIME_MS); diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index cfbf65e511..b63b0555e6 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -30,6 +30,22 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy const collectId = CollectId({ analyticsContext, clientKey, locale, amount, analyticsPath: ANALYTICS_PATH }); const eventsQueue: EventsQueueModule = EventsQueue({ analyticsContext, clientKey, analyticsPath: ANALYTICS_PATH }); + const addAnalyticsAction = (type: ANALYTICS_ACTION, obj: AnalyticsObject) => { + eventsQueue.add(`${type}s`, obj); + + /** + * The logic is: + * - events are stored until a log or error comes along + * - errors get sent straightaway + * - logs also get sent straightaway... but... tests with the 3DS2 process show that many logs can happen almost at the same time, + * so instead of making (up to 4) sequential api calls we "batch" them using debounce + */ + if (type === ANALYTICS_ACTION_LOG || type === ANALYTICS_ACTION_ERROR) { + const debounceFn = type === ANALYTICS_ACTION_ERROR ? fn => fn : debounce; + debounceFn(anlModule.sendAnalyticsActions)(); + } + }; + const anlModule: AnalyticsModule = { send: async (initialEvent: AnalyticsInitialEvent) => { const { enabled, payload, telemetry } = props; // TODO what is payload, is it ever used? @@ -45,23 +61,15 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy console.debug(`Fetching checkoutAttemptId failed.${e ? ` Error=${e}` : ''}`); } } - // Log pixel // TODO once we stop using the pixel we can stop requiring both "enabled" & "telemetry" config options + // Log pixel + // TODO once we stop using the pixel we can stop requiring both "enabled" & "telemetry" config options. + // And v6 will have a "level: 'none" | "all" | "minimal" config prop logEvent(initialEvent); } }, getCheckoutAttemptId: (): string => capturedCheckoutAttemptId, - addAnalyticsAction: (type: ANALYTICS_ACTION, obj: AnalyticsObject) => { - eventsQueue.add(`${type}s`, obj); - - // errors get sent straight away, logs almost do (with a debounce), events are stored until an error or log comes along - if (type === ANALYTICS_ACTION_LOG || type === ANALYTICS_ACTION_ERROR) { - const debounceFn = type === ANALYTICS_ACTION_ERROR ? fn => fn : debounce; - debounceFn(anlModule.sendAnalyticsActions)(); - } - }, - sendAnalyticsActions: () => { if (capturedCheckoutAttemptId) { return eventsQueue.run(capturedCheckoutAttemptId); @@ -78,7 +86,7 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy ...data }); - anlModule.addAnalyticsAction(action, aObj); + addAnalyticsAction(action, aObj); } }; diff --git a/packages/lib/src/core/Analytics/types.ts b/packages/lib/src/core/Analytics/types.ts index cb11413120..cff639a9a3 100644 --- a/packages/lib/src/core/Analytics/types.ts +++ b/packages/lib/src/core/Analytics/types.ts @@ -45,6 +45,7 @@ export interface AnalyticsObject { type?: string; subtype?: string; target?: string; + metadata?: Record; } export type ANALYTICS_ACTION = 'log' | 'error' | 'event'; @@ -66,13 +67,7 @@ export type AnalyticsConfig = { loadingContext?: string; }; -export type CreateAnalyticsActionData = { - component: string; - type: string; - target?: string; - subtype?: string; - message?: string; -}; +export type CreateAnalyticsActionData = Omit; export type CreateAnalyticsActionObject = { action: ANALYTICS_ACTION; From c04a45a70885f993382a7559cdf9bba4777b928a Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 21 Aug 2023 13:32:53 +0200 Subject: [PATCH 32/94] Switch to v3 of the endpoint --- packages/lib/src/core/Analytics/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index 5cb17db9cb..f6676fa475 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -1,4 +1,4 @@ -export const ANALYTICS_PATH = 'v2/analytics'; +export const ANALYTICS_PATH = 'v3/analytics'; export const ANALYTICS_ACTION_LOG = 'log'; export const ANALYTICS_ACTION_ERROR = 'error'; From 5ff8daf3092faf9398e101653a74160c5ce55969 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 21 Aug 2023 14:23:41 +0200 Subject: [PATCH 33/94] Fix unit test (set new analytics version) --- packages/lib/src/core/Services/analytics/collect-id.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/core/Services/analytics/collect-id.test.ts b/packages/lib/src/core/Services/analytics/collect-id.test.ts index c9c2966609..210d37ce89 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.test.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.test.ts @@ -81,7 +81,7 @@ test('Should send expected data to http service', () => { { errorLevel: 'fatal', loadingContext: 'https://checkoutanalytics-test.adyen.com/checkoutanalytics/', - path: 'v2/analytics?clientKey=xxxx-yyyy' + path: `${ANALYTICS_PATH}?clientKey=xxxx-yyyy` }, { // amount: configuration.amount,// TODO will be supported in the future From 3a0fb283919fe83f6ae9ec928bef1cda73e95844 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 24 Aug 2023 16:02:02 +0200 Subject: [PATCH 34/94] Changes reflecting discussion on final API tweaks --- .../Dropin/components/DropinComponent.tsx | 2 ++ packages/lib/src/components/UIElement.tsx | 36 +++++++++++++++---- packages/lib/src/components/types.ts | 1 + packages/lib/src/core/Analytics/constants.ts | 1 + packages/lib/src/core/Analytics/utils.ts | 7 ++-- .../Services/analytics/collect-id.test.ts | 1 + .../src/core/Services/analytics/collect-id.ts | 3 ++ 7 files changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/lib/src/components/Dropin/components/DropinComponent.tsx b/packages/lib/src/components/Dropin/components/DropinComponent.tsx index 15a7bca0a7..65158fd5ac 100644 --- a/packages/lib/src/components/Dropin/components/DropinComponent.tsx +++ b/packages/lib/src/components/Dropin/components/DropinComponent.tsx @@ -74,6 +74,8 @@ export class DropinComponent extends Component extends BaseElement

implements IUIElement { protected componentRef: any; @@ -49,13 +49,37 @@ export class UIElement

extends BaseElement

im /* eslint-disable-next-line */ protected submitAnalytics(obj?) { // Call analytics endpoint - let component = this.elementRef._id?.substring(0, this.elementRef._id.indexOf('-')); - if (component === 'dropin') { - const subCompID = this.elementRef['dropinRef'].state.activePaymentMethod._id; - component = `${component}-${subCompID.substring(0, subCompID.indexOf('-'))}`; + const isDropin = this.elementRef._id?.substring(0, this.elementRef._id.indexOf('-')) === 'dropin'; + + const component = this.props.type === 'card' ? 'scheme' : this.props.type; + + if (isDropin) { + // PM selected scenario + if (obj === 'select') { + let storedCardIndicator; + // Check if it's a storedCard + if (component === 'scheme') { + if (hasOwnProperty(this.props, 'supportedShopperInteractions')) { + storedCardIndicator = { + isStoredPaymentMethod: true, + brand: this.props.brand + }; + } + } + + this.props.modules?.analytics.createAnalyticsAction({ + action: 'event', + data: { component, type: ANALYTICS_SELECTED_STR, ...storedCardIndicator } + }); + return; + } } - this.props.modules?.analytics.createAnalyticsAction({ action: 'log', data: { component, type: ANALYTICS_SUBMIT_STR, target: 'pay_button' } }); + // PM pay button pressed scenario + this.props.modules?.analytics.createAnalyticsAction({ + action: 'log', + data: { component, type: ANALYTICS_SUBMIT_STR, target: 'payButton', message: 'Shopper clicked pay' } + }); } private onSubmit(): void { diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index c4670a5031..b5471fb083 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -142,6 +142,7 @@ export interface UIElementProps extends BaseElementProps { icon?: string; amount?: PaymentAmount; secondaryAmount?: PaymentAmountExtended; + brand?: string; /** * Show/Hide pay button diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index f6676fa475..e8298b5ef4 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -6,6 +6,7 @@ export const ANALYTICS_ACTION_EVENT = 'event'; export const ANALYTICS_ACTION_STR = 'Action'; export const ANALYTICS_SUBMIT_STR = 'Submit'; +export const ANALYTICS_SELECTED_STR = 'Selected'; export const ANALYTICS_IMPLEMENTATION_ERROR = 'ImplementationError'; export const ANALYTICS_API_ERROR = 'APIError'; diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index bf9101f862..ca1495587a 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -11,9 +11,10 @@ export const getUTCTimestamp = () => Date.now(); * "code", "errorType" & "message" * * Log objects have, in addition to the base props: + * "message" & * "type" & "target" (e.g. when onSubmit is called after a pay button click), or, - * "type" & "subtype" & "message" (e.g. when an action is handled), or, - * "type" & "message" (e.g. logging during the 3DS2 process) + * "type" & "subtype" (e.g. when an action is handled), or, + * "type" (e.g. logging during the 3DS2 process) * * Event objects have, in addition to the base props: * "type" & "target" @@ -22,7 +23,7 @@ export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObj timestamp: String(getUTCTimestamp()), component: aObj.component, ...(aObj.action === 'error' && { code: aObj.code, errorType: aObj.errorType }), // only added if we have an error object - ...((aObj.action === 'error' || (aObj.action === 'log' && aObj.type !== ANALYTICS_SUBMIT_STR)) && { message: aObj.message }), // only added if we have an error, or log object (that's not logging a submit/pay button press) + ...((aObj.action === 'error' || aObj.action === 'log') && { message: aObj.message }), // only added if we have an error, or log object ...(aObj.action === 'log' && { type: aObj.type }), // only added if we have a log object ...(aObj.action === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type ...(aObj.action === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type diff --git a/packages/lib/src/core/Services/analytics/collect-id.test.ts b/packages/lib/src/core/Services/analytics/collect-id.test.ts index 210d37ce89..beae90568f 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.test.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.test.ts @@ -86,6 +86,7 @@ test('Should send expected data to http service', () => { { // amount: configuration.amount,// TODO will be supported in the future channel: 'Web', + platform: 'Web', locale: configuration.locale, referrer: 'http://localhost/', screenWidth: 0, diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index 62a2d145d9..bbf24118f8 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -36,7 +36,10 @@ const collectId = ({ analyticsContext, clientKey, locale, analyticsPath }: Colle const telemetryEvent: TelemetryEvent = { // amount, // TODO will be supported in the future version: process.env.VERSION, + // The data team want both platform & channel properties: channel: 'Web', + platform: 'Web', + buildType: window['AdyenCheckout'] ? 'umd' : 'compiled', locale, referrer: window.location.href, screenWidth: window.screen.width, From 4a01cf6b2fdff4f5787ff392e4108f4d3e65db45 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 25 Aug 2023 17:27:43 +0200 Subject: [PATCH 35/94] Create analytics-action for when PM selected or mounted. Send in sessionId in initial call --- packages/lib/src/components/BaseElement.ts | 26 ++++++-- .../Dropin/components/DropinComponent.tsx | 7 +- packages/lib/src/components/UIElement.tsx | 66 ++++++++++++------- packages/lib/src/core/Analytics/constants.ts | 1 + packages/lib/src/core/Analytics/types.ts | 3 + packages/lib/src/core/Analytics/utils.ts | 6 +- .../src/core/Services/analytics/collect-id.ts | 2 +- 7 files changed, 74 insertions(+), 37 deletions(-) diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index 4c88811e3c..9d87b75bb1 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -3,7 +3,7 @@ import getProp from '../utils/getProp'; import EventEmitter from './EventEmitter'; import uuid from '../utils/uuid'; import Core from '../core'; -import { BaseElementProps, PaymentData } from './types'; +import { BaseElementProps, PaymentData, UIElementProps } from './types'; import { RiskData } from '../core/RiskModule/RiskModule'; import { Resources } from '../core/Context/Resources'; @@ -46,6 +46,11 @@ class BaseElement

{ return {}; } + /* eslint-disable-next-line */ + protected submitAnalytics(type: string, obj?) { + return null; + } + protected setState(newState: object): void { this.state = { ...this.state, ...newState }; } @@ -93,13 +98,20 @@ class BaseElement

{ 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 + // Set up analytics (once, since this._node is undefined) if (this.props.modules && this.props.modules.analytics && !this.props.isDropin) { - this.props.modules.analytics.send({ - containerWidth: node && (node as HTMLElement).offsetWidth, - component: this.constructor['analyticsType'] ?? this.constructor['type'], - flavor: 'components' - }); + const sessionId = (this.props as UIElementProps)?.session?.id; + + this.props.modules.analytics + .send({ + containerWidth: node && (node as HTMLElement).offsetWidth, + component: this.constructor['analyticsType'] ?? this.constructor['type'], + flavor: 'components', + ...(sessionId && { sessionId }) + }) + .then(() => { + this.submitAnalytics('mounted'); + }); } } diff --git a/packages/lib/src/components/Dropin/components/DropinComponent.tsx b/packages/lib/src/components/Dropin/components/DropinComponent.tsx index 65158fd5ac..66922eaac3 100644 --- a/packages/lib/src/components/Dropin/components/DropinComponent.tsx +++ b/packages/lib/src/components/Dropin/components/DropinComponent.tsx @@ -31,12 +31,15 @@ export class DropinComponent extends Component e.props.type), // TODO will be supported in the initial request to checkoutanalytics component: 'dropin', - flavor: 'dropin' + flavor: 'dropin', + ...(sessionId && { sessionId }) }); } } @@ -75,7 +78,7 @@ export class DropinComponent extends Component extends BaseElement

implements IUIElement { protected componentRef: any; @@ -47,35 +47,51 @@ export class UIElement

extends BaseElement

im } /* eslint-disable-next-line */ - protected submitAnalytics(obj?) { + protected submitAnalytics(type = 'action', obj?) { // Call analytics endpoint - const isDropin = this.elementRef._id?.substring(0, this.elementRef._id.indexOf('-')) === 'dropin'; - - const component = this.props.type === 'card' ? 'scheme' : this.props.type; - - if (isDropin) { - // PM selected scenario - if (obj === 'select') { - let storedCardIndicator; - // Check if it's a storedCard - if (component === 'scheme') { - if (hasOwnProperty(this.props, 'supportedShopperInteractions')) { - storedCardIndicator = { - isStoredPaymentMethod: true, - brand: this.props.brand - }; - } - } + // const isDropin = this.elementRef._id?.substring(0, this.elementRef._id.indexOf('-')) === 'dropin'; - this.props.modules?.analytics.createAnalyticsAction({ - action: 'event', - data: { component, type: ANALYTICS_SELECTED_STR, ...storedCardIndicator } - }); - return; + // let component_orig = this.props.type === 'card' ? 'scheme' : this.props.type; + + /** Work out what the component's "type" is: + * - first check for a dedicated "analyticsType" (currently only applies to custom-cards) + * - otherwise, distinguish cards from non-cards: cards will use their static type property, everything else will use props.type + */ + let component = this.constructor['analyticsType']; + if (!component) { + component = this.constructor['type'] === 'scheme' || this.constructor['type'] === 'bcmc' ? this.constructor['type'] : this.props.type; + } + + // if (isDropin) { + console.log('### UIElement::submitAnalytics:: component=', component); + // console.log('### UIElement::submitAnalytics:: component_orig=', component_orig); + + // Dropin PM selected, or, standalone comp mounted + if (type === 'selected' || type === 'mounted') { + let storedCardIndicator; + // Check if it's a storedCard + if (component === 'scheme') { + if (hasOwnProperty(this.props, 'supportedShopperInteractions')) { + storedCardIndicator = { + isStoredPaymentMethod: true, + brand: this.props.brand + }; + } } + + const data = { component, type: this.props.isDropin ? ANALYTICS_SELECTED_STR : ANALYTICS_MOUNTED_STR, ...storedCardIndicator }; + console.log('### UIElement::submitAnalytics:: SELECTED data=', data); + + // TODO - comment in once API is ready + this.props.modules?.analytics.createAnalyticsAction({ + action: 'event', + data + }); + return; } + // } - // PM pay button pressed scenario + // PM pay button pressed this.props.modules?.analytics.createAnalyticsAction({ action: 'log', data: { component, type: ANALYTICS_SUBMIT_STR, target: 'payButton', message: 'Shopper clicked pay' } diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index e8298b5ef4..905f078ca8 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -7,6 +7,7 @@ export const ANALYTICS_ACTION_EVENT = 'event'; export const ANALYTICS_ACTION_STR = 'Action'; export const ANALYTICS_SUBMIT_STR = 'Submit'; export const ANALYTICS_SELECTED_STR = 'Selected'; +export const ANALYTICS_MOUNTED_STR = 'Mounted'; export const ANALYTICS_IMPLEMENTATION_ERROR = 'ImplementationError'; export const ANALYTICS_API_ERROR = 'APIError'; diff --git a/packages/lib/src/core/Analytics/types.ts b/packages/lib/src/core/Analytics/types.ts index cff639a9a3..b12cbd6724 100644 --- a/packages/lib/src/core/Analytics/types.ts +++ b/packages/lib/src/core/Analytics/types.ts @@ -46,6 +46,8 @@ export interface AnalyticsObject { subtype?: string; target?: string; metadata?: Record; + isStoredPaymentMethod?: boolean; + brand?: string; } export type ANALYTICS_ACTION = 'log' | 'error' | 'event'; @@ -57,6 +59,7 @@ export type AnalyticsInitialEvent = { component: string; flavor: string; paymentMethods?: any[]; + sessionId?: string; }; export type AnalyticsConfig = { diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index ca1495587a..b846453c6b 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -17,7 +17,8 @@ export const getUTCTimestamp = () => Date.now(); * "type" (e.g. logging during the 3DS2 process) * * Event objects have, in addition to the base props: - * "type" & "target" + * "type" & "target" & + * "isStoredPaymentMethod" & "brand" (when a storedCard is "selected") */ export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObject => ({ timestamp: String(getUTCTimestamp()), @@ -27,5 +28,6 @@ export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObj ...(aObj.action === 'log' && { type: aObj.type }), // only added if we have a log object ...(aObj.action === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type ...(aObj.action === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type - ...(aObj.action === 'event' && { type: aObj.type, target: aObj.target }) // only added if we have an event object + ...(aObj.action === 'event' && { type: aObj.type, target: aObj.target }), // only added if we have an event object + ...(aObj.action === 'event' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }) // only added when a storedCard is selected/mounted }); diff --git a/packages/lib/src/core/Services/analytics/collect-id.ts b/packages/lib/src/core/Services/analytics/collect-id.ts index bbf24118f8..20161cbbd7 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.ts @@ -46,7 +46,7 @@ const collectId = ({ analyticsContext, clientKey, locale, analyticsPath }: Colle ...event }; - if (promise) return promise; + if (promise) return promise; // Prevents multiple standalone components on the same page from making multiple calls to collect a checkoutAttemptId if (!clientKey) return Promise.reject('no-client-key'); const storage = new Storage('checkout-attempt-id', 'sessionStorage'); From 0bb61bfe60ebfcac8af6b6e0da5a8838b4998481 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 25 Aug 2023 17:30:37 +0200 Subject: [PATCH 36/94] Commented out logs and removed unused code --- packages/lib/src/components/UIElement.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index f596a596c5..c96acc872e 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -48,11 +48,6 @@ export class UIElement

extends BaseElement

im /* eslint-disable-next-line */ protected submitAnalytics(type = 'action', obj?) { - // Call analytics endpoint - // const isDropin = this.elementRef._id?.substring(0, this.elementRef._id.indexOf('-')) === 'dropin'; - - // let component_orig = this.props.type === 'card' ? 'scheme' : this.props.type; - /** Work out what the component's "type" is: * - first check for a dedicated "analyticsType" (currently only applies to custom-cards) * - otherwise, distinguish cards from non-cards: cards will use their static type property, everything else will use props.type @@ -62,9 +57,7 @@ export class UIElement

extends BaseElement

im component = this.constructor['type'] === 'scheme' || this.constructor['type'] === 'bcmc' ? this.constructor['type'] : this.props.type; } - // if (isDropin) { - console.log('### UIElement::submitAnalytics:: component=', component); - // console.log('### UIElement::submitAnalytics:: component_orig=', component_orig); + // console.log('### UIElement::submitAnalytics:: component=', component); // Dropin PM selected, or, standalone comp mounted if (type === 'selected' || type === 'mounted') { @@ -80,16 +73,14 @@ export class UIElement

extends BaseElement

im } const data = { component, type: this.props.isDropin ? ANALYTICS_SELECTED_STR : ANALYTICS_MOUNTED_STR, ...storedCardIndicator }; - console.log('### UIElement::submitAnalytics:: SELECTED data=', data); + // console.log('### UIElement::submitAnalytics:: SELECTED data=', data); - // TODO - comment in once API is ready this.props.modules?.analytics.createAnalyticsAction({ action: 'event', data }); return; } - // } // PM pay button pressed this.props.modules?.analytics.createAnalyticsAction({ From 4c7364c48017bc3184c1358e57dc44f33b3cfe39 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 25 Aug 2023 17:51:04 +0200 Subject: [PATCH 37/94] Moved initial analytics setup to UIElement now that it (potentially) needs to pass on the session.id --- packages/lib/src/components/BaseElement.ts | 21 +++++++-------------- packages/lib/src/components/UIElement.tsx | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index 9d87b75bb1..58752915c6 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -3,7 +3,7 @@ import getProp from '../utils/getProp'; import EventEmitter from './EventEmitter'; import uuid from '../utils/uuid'; import Core from '../core'; -import { BaseElementProps, PaymentData, UIElementProps } from './types'; +import { BaseElementProps, PaymentData } from './types'; import { RiskData } from '../core/RiskModule/RiskModule'; import { Resources } from '../core/Context/Resources'; @@ -47,7 +47,7 @@ class BaseElement

{ } /* eslint-disable-next-line */ - protected submitAnalytics(type: string, obj?) { + protected setUpAnalytics(setUpAnalyticsObj) { return null; } @@ -100,18 +100,11 @@ class BaseElement

{ } else { // Set up analytics (once, since this._node is undefined) if (this.props.modules && this.props.modules.analytics && !this.props.isDropin) { - const sessionId = (this.props as UIElementProps)?.session?.id; - - this.props.modules.analytics - .send({ - containerWidth: node && (node as HTMLElement).offsetWidth, - component: this.constructor['analyticsType'] ?? this.constructor['type'], - flavor: 'components', - ...(sessionId && { sessionId }) - }) - .then(() => { - this.submitAnalytics('mounted'); - }); + this.setUpAnalytics({ + containerWidth: node && (node as HTMLElement).offsetWidth, + component: this.constructor['analyticsType'] ?? this.constructor['type'], + flavor: 'components' + }); } } diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index c96acc872e..af89efa79b 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -12,6 +12,7 @@ import DropinElement from './Dropin'; import { CoreOptions } from '../core/types'; import Core from '../core'; import { ANALYTICS_MOUNTED_STR, ANALYTICS_SELECTED_STR, ANALYTICS_SUBMIT_STR } from '../core/Analytics/constants'; +import { AnalyticsInitialEvent } from '../core/Analytics/types'; export class UIElement

extends BaseElement

implements IUIElement { protected componentRef: any; @@ -46,6 +47,22 @@ export class UIElement

extends BaseElement

im return state; } + // Only called once, for non Dropin based UIElements, as they are being mounted + protected setUpAnalytics(setUpAnalyticsObj: AnalyticsInitialEvent) { + const sessionId = this.props.session?.id; + + this.props.modules.analytics + .send({ + ...setUpAnalyticsObj, + ...(sessionId && { sessionId }) + }) + .then(() => { + // Once the initial analytics set up call has been made... + // ...create an analytics-action "event" declaring that the component has been mounted + this.submitAnalytics('mounted'); + }); + } + /* eslint-disable-next-line */ protected submitAnalytics(type = 'action', obj?) { /** Work out what the component's "type" is: From 1d203c1bc70e2683d19dc9a10bbd044b00730743 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 25 Aug 2023 17:54:34 +0200 Subject: [PATCH 38/94] Added type --- packages/lib/src/components/BaseElement.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index 58752915c6..f9b88c8ee3 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -6,6 +6,7 @@ import Core from '../core'; import { BaseElementProps, PaymentData } from './types'; import { RiskData } from '../core/RiskModule/RiskModule'; import { Resources } from '../core/Context/Resources'; +import { AnalyticsInitialEvent } from '../core/Analytics/types'; class BaseElement

{ public readonly _id = `${this.constructor['type']}-${uuid()}`; @@ -47,7 +48,7 @@ class BaseElement

{ } /* eslint-disable-next-line */ - protected setUpAnalytics(setUpAnalyticsObj) { + protected setUpAnalytics(setUpAnalyticsObj: AnalyticsInitialEvent) { return null; } From 3bd2dbed91cc96024ab0fd9194d0db8f56df822d Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 25 Aug 2023 19:02:10 +0200 Subject: [PATCH 39/94] Fixed unit test --- packages/lib/src/core/Services/analytics/collect-id.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/core/Services/analytics/collect-id.test.ts b/packages/lib/src/core/Services/analytics/collect-id.test.ts index beae90568f..4b2db7a8c4 100644 --- a/packages/lib/src/core/Services/analytics/collect-id.test.ts +++ b/packages/lib/src/core/Services/analytics/collect-id.test.ts @@ -93,7 +93,8 @@ test('Should send expected data to http service', () => { version: 'x.x.x', flavor: customEvent.flavor, containerWidth: customEvent.containerWidth, - component: customEvent.component + component: customEvent.component, + buildType: 'compiled' } ); From 30f3f08f00ef7a2ebb6edce36c94646d30290429 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 1 Sep 2023 12:53:20 +0200 Subject: [PATCH 40/94] Temporarily don't pass isStoredPaymentMethod (waiting for API fix) --- packages/lib/src/core/Analytics/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index b846453c6b..95cfcd4f1a 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -29,5 +29,6 @@ export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObj ...(aObj.action === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type ...(aObj.action === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type ...(aObj.action === 'event' && { type: aObj.type, target: aObj.target }), // only added if we have an event object - ...(aObj.action === 'event' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }) // only added when a storedCard is selected/mounted + ...(aObj.action === 'event' && aObj.brand && { brand: aObj.brand }) // only added when a storedCard is selected/mounted // TODO - add: b/e currently can't handle the "isStoredPaymentMethod" prop + // ...(aObj.action === 'event' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }) // TODO - replace above line with this one once b/e is ready }); From 8f9dd4cd1c1e06331c137714c31e6dd33c19a1bf Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 9 Oct 2023 12:16:36 +0200 Subject: [PATCH 41/94] Adding isStoredPaymentMethod to analytics event action now the b/e supports it --- packages/lib/src/core/Analytics/utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index 95cfcd4f1a..36d5398d84 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -29,6 +29,5 @@ export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObj ...(aObj.action === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type ...(aObj.action === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type ...(aObj.action === 'event' && { type: aObj.type, target: aObj.target }), // only added if we have an event object - ...(aObj.action === 'event' && aObj.brand && { brand: aObj.brand }) // only added when a storedCard is selected/mounted // TODO - add: b/e currently can't handle the "isStoredPaymentMethod" prop - // ...(aObj.action === 'event' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }) // TODO - replace above line with this one once b/e is ready + ...(aObj.action === 'event' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }) }); From 02e30bfe45ebffcf88647fbce15c334b10e20ca6 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 9 Oct 2023 12:18:22 +0200 Subject: [PATCH 42/94] Adding getter for Analytics' enabled prop so 'do-not-track' can be added as checkoutAttemptId value, if required --- packages/lib/src/components/BaseElement.ts | 4 ++-- packages/lib/src/components/types.ts | 1 + packages/lib/src/core/Analytics/Analytics.test.ts | 2 ++ packages/lib/src/core/Analytics/Analytics.ts | 4 +++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index f9b88c8ee3..166f87295f 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -62,8 +62,8 @@ class BaseElement

{ */ 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(); diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index b5471fb083..8976e1200a 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -83,6 +83,7 @@ export interface AnalyticsModule { getCheckoutAttemptId: () => string; getEventsQueue: () => EventsQueueModule; createAnalyticsAction: (a: CreateAnalyticsActionObject) => void; + getEnabled: () => boolean; } export interface BaseElementProps { diff --git a/packages/lib/src/core/Analytics/Analytics.test.ts b/packages/lib/src/core/Analytics/Analytics.test.ts index 073328034f..054009487c 100644 --- a/packages/lib/src/core/Analytics/Analytics.test.ts +++ b/packages/lib/src/core/Analytics/Analytics.test.ts @@ -49,6 +49,8 @@ describe('Analytics initialisation and event queue', () => { expect(analytics.send).not.toBe(null); expect(analytics.getCheckoutAttemptId).not.toBe(null); expect(analytics.sendAnalyticsActions).not.toBe(null); + expect(analytics.getEnabled).not.toBe(null); + expect(analytics.getEnabled()).toBe(true); expect(collectIdPromiseMock).toHaveLength(0); }); diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index b63b0555e6..4f5ac05165 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -87,7 +87,9 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy }); addAnalyticsAction(action, aObj); - } + }, + + getEnabled: () => props.enabled }; return anlModule; From da70aeb7c3c47da6ed80794e38408365d2483656 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 9 Oct 2023 12:18:56 +0200 Subject: [PATCH 43/94] fixing e2e tests --- packages/e2e/tests/cards/utils/constants.js | 2 +- packages/e2e/tests/issuerLists/ideal/ideal.test.js | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/e2e/tests/cards/utils/constants.js b/packages/e2e/tests/cards/utils/constants.js index 067d864dac..638668a26d 100644 --- a/packages/e2e/tests/cards/utils/constants.js +++ b/packages/e2e/tests/cards/utils/constants.js @@ -7,7 +7,7 @@ export const MAESTRO_CARD = '5000550000000029'; export const BCMC_CARD = '6703444444444449'; // actually dual branded bcmc & maestro export const BCMC_DUAL_BRANDED_VISA = '4871049999999910'; // dual branded visa & bcmc export const UNKNOWN_BIN_CARD = '135410014004955'; // card that is not in the test DBs (uatp) -export const UNKNOWN_VISA_CARD = '4111111111111111'; // card that is not in the test DBs (visa) +export const UNKNOWN_VISA_CARD = '41111111'; // card is now in the test DBs (visa) - so keep it short to stop it firing binLookup export const AMEX_CARD = '370000000000002'; export const DUAL_BRANDED_CARD_EXCLUDED = '4001230000000004'; // dual branded visa/star diff --git a/packages/e2e/tests/issuerLists/ideal/ideal.test.js b/packages/e2e/tests/issuerLists/ideal/ideal.test.js index 1e6b7e08ab..cb88fc1f79 100644 --- a/packages/e2e/tests/issuerLists/ideal/ideal.test.js +++ b/packages/e2e/tests/issuerLists/ideal/ideal.test.js @@ -1,7 +1,7 @@ import { Selector, ClientFunction } from 'testcafe'; import { ISSUERLISTS_URL } from '../../pages'; -fixture`Testing iDeal (IssuerLists)`.page(`${ISSUERLISTS_URL}?countryCode=NL`); +fixture.only`Testing iDeal (IssuerLists)`.page(`${ISSUERLISTS_URL}?countryCode=NL`); const getComponentData = ClientFunction(() => { return window.ideal.data; @@ -14,13 +14,14 @@ test('should make an iDeal payment', async t => { .expect(Selector('.adyen-checkout__dropdown__list').hasClass('adyen-checkout__dropdown__list--active')) .ok(); - await t.click(Selector('.adyen-checkout__dropdown__list').child(0)); + await t.click(Selector('.adyen-checkout__dropdown__list').child(1)); const stateData = await getComponentData(); await t.expect(stateData.paymentMethod).eql({ type: 'ideal', - issuer: '1121' + issuer: '1121', + checkoutAttemptId: 'do-not-track' }); await t.expect(stateData.clientStateDataIndicator).eql(true); @@ -35,7 +36,8 @@ test('should make an iDeal payment using a highlighted issuer', async t => { await t.expect(stateData.paymentMethod).eql({ type: 'ideal', - issuer: '1121' + issuer: '1121', + checkoutAttemptId: 'do-not-track' }); await t.expect(stateData.clientStateDataIndicator).eql(true); From 198d266223d3e45711fa4f55a4fd1f5a25ebd6c5 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 16 Oct 2023 10:29:41 +0200 Subject: [PATCH 44/94] remove .only from e2e test --- packages/e2e/tests/issuerLists/ideal/ideal.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e/tests/issuerLists/ideal/ideal.test.js b/packages/e2e/tests/issuerLists/ideal/ideal.test.js index cb88fc1f79..9a2a75a3b9 100644 --- a/packages/e2e/tests/issuerLists/ideal/ideal.test.js +++ b/packages/e2e/tests/issuerLists/ideal/ideal.test.js @@ -1,7 +1,7 @@ import { Selector, ClientFunction } from 'testcafe'; import { ISSUERLISTS_URL } from '../../pages'; -fixture.only`Testing iDeal (IssuerLists)`.page(`${ISSUERLISTS_URL}?countryCode=NL`); +fixture`Testing iDeal (IssuerLists)`.page(`${ISSUERLISTS_URL}?countryCode=NL`); const getComponentData = ClientFunction(() => { return window.ideal.data; From aaf3ca8b231be0f927a3cc576e6c07f4d5874f9e Mon Sep 17 00:00:00 2001 From: nicholas Date: Wed, 6 Dec 2023 17:16:46 +0100 Subject: [PATCH 45/94] Only load analytics pixel once --- packages/lib/src/core/Analytics/Analytics.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 4f5ac05165..24d6850db5 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -8,6 +8,7 @@ import { AnalyticsModule } from '../../components/types'; import { createAnalyticsObject } from './utils'; let capturedCheckoutAttemptId = null; +let hasLoggedPixel = false; const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analyticsContext }: AnalyticsProps): AnalyticsModule => { const defaultProps = { @@ -61,10 +62,14 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy console.debug(`Fetching checkoutAttemptId failed.${e ? ` Error=${e}` : ''}`); } } - // Log pixel - // TODO once we stop using the pixel we can stop requiring both "enabled" & "telemetry" config options. - // And v6 will have a "level: 'none" | "all" | "minimal" config prop - logEvent(initialEvent); + + if (!hasLoggedPixel) { + // Log pixel + // TODO once we stop using the pixel we can stop requiring both "enabled" & "telemetry" config options. + // And v6 will have a "level: 'none" | "all" | "minimal" config prop + logEvent(initialEvent); + hasLoggedPixel = true; + } } }, From 936a2fc7952a3be53ecdc5163dbec492ae4b7842 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 7 Dec 2023 15:20:52 +0100 Subject: [PATCH 46/94] comments added --- packages/lib/src/components/UIElement.tsx | 3 ++- packages/lib/src/core/core.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index fb47f5cc81..44b2475526 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -92,6 +92,7 @@ export class UIElement

extends BaseElement

im const data = { component, type: this.props.isDropin ? ANALYTICS_SELECTED_STR : ANALYTICS_MOUNTED_STR, ...storedCardIndicator }; // console.log('### UIElement::submitAnalytics:: SELECTED data=', data); + // AnalyticsAction: action: 'event' type:'mounted'|'selected' this.props.modules?.analytics.createAnalyticsAction({ action: 'event', data @@ -99,7 +100,7 @@ export class UIElement

extends BaseElement

im return; } - // PM pay button pressed + // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' this.props.modules?.analytics.createAnalyticsAction({ action: 'log', data: { component, type: ANALYTICS_SUBMIT_STR, target: 'payButton', message: 'Shopper clicked pay' } diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index 5390bd8028..21e09b3a25 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -148,7 +148,7 @@ class Core { } if (action.type) { - // Call analytics endpoint + // AnalyticsAction: action: 'log' type:'action' this.modules.analytics.createAnalyticsAction({ action: 'log', data: { From 921c54c826b834041501c9f5d82573e0a3c1f925 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 4 Jan 2024 13:38:42 +0100 Subject: [PATCH 47/94] Aligning Dropin & comps so that a 'mounted' event is always sent, from the same place, for both implementations --- packages/lib/src/components/BaseElement.ts | 6 +++--- .../Dropin/components/DropinComponent.tsx | 15 ++------------- packages/lib/src/components/UIElement.tsx | 10 +++++----- packages/lib/src/core/Analytics/constants.ts | 4 ++-- 4 files changed, 12 insertions(+), 23 deletions(-) diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index 166f87295f..d386dba29d 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -100,11 +100,11 @@ class BaseElement

{ 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, since this._node is undefined) - if (this.props.modules && this.props.modules.analytics && !this.props.isDropin) { + if (this.props.modules && this.props.modules.analytics) { this.setUpAnalytics({ containerWidth: node && (node as HTMLElement).offsetWidth, - component: this.constructor['analyticsType'] ?? this.constructor['type'], - flavor: 'components' + component: !this.props.isDropin ? this.constructor['analyticsType'] ?? this.constructor['type'] : 'dropin', + flavor: !this.props.isDropin ? 'components' : 'dropin' }); } } diff --git a/packages/lib/src/components/Dropin/components/DropinComponent.tsx b/packages/lib/src/components/Dropin/components/DropinComponent.tsx index 66922eaac3..5890649e70 100644 --- a/packages/lib/src/components/Dropin/components/DropinComponent.tsx +++ b/packages/lib/src/components/Dropin/components/DropinComponent.tsx @@ -5,6 +5,7 @@ import getOrderStatus from '../../../core/Services/order-status'; import { DropinComponentProps, DropinComponentState, DropinStatusProps, onOrderCancelData } from '../types'; import './DropinComponent.scss'; import { UIElementStatus } from '../../types'; +import { ANALYTICS_SELECTED_STR } from '../../../core/Analytics/constants'; export class DropinComponent extends Component { public state: DropinComponentState = { @@ -30,18 +31,6 @@ export class DropinComponent extends Component { this.setState({ instantPaymentElements, elements: [...storedElements, ...elements], orderStatus }); this.setStatus('ready'); - - const sessionId = this.props?.session?.id; - - if (this.props.modules.analytics) { - this.props.modules.analytics.send({ - containerWidth: this.base && (this.base as HTMLElement).offsetWidth, - // paymentMethods: elements.map(e => e.props.type), // TODO will be supported in the initial request to checkoutanalytics - component: 'dropin', - flavor: 'dropin', - ...(sessionId && { sessionId }) - }); - } } ); @@ -78,7 +67,7 @@ export class DropinComponent extends Component extends BaseElement

im return state; } - // Only called once, for non Dropin based UIElements, as they are being mounted + // Only called once, for UIElements (including Dropin), as they are being mounted protected setUpAnalytics(setUpAnalyticsObj: AnalyticsInitialEvent) { const sessionId = this.props.session?.id; @@ -59,7 +59,7 @@ export class UIElement

extends BaseElement

im .then(() => { // Once the initial analytics set up call has been made... // ...create an analytics-action "event" declaring that the component has been mounted - this.submitAnalytics('mounted'); + this.submitAnalytics(ANALYTICS_MOUNTED_STR); }); } @@ -76,8 +76,8 @@ export class UIElement

extends BaseElement

im // console.log('### UIElement::submitAnalytics:: component=', component); - // Dropin PM selected, or, standalone comp mounted - if (type === 'selected' || type === 'mounted') { + // Dropin PM selected, or, UIElement mounted (called once only) + if (type === ANALYTICS_SELECTED_STR || type === ANALYTICS_MOUNTED_STR) { let storedCardIndicator; // Check if it's a storedCard if (component === 'scheme') { @@ -89,7 +89,7 @@ export class UIElement

extends BaseElement

im } } - const data = { component, type: this.props.isDropin ? ANALYTICS_SELECTED_STR : ANALYTICS_MOUNTED_STR, ...storedCardIndicator }; + const data = { component, type, ...storedCardIndicator }; // console.log('### UIElement::submitAnalytics:: SELECTED data=', data); // AnalyticsAction: action: 'event' type:'mounted'|'selected' diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index 905f078ca8..0e35290b27 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -6,8 +6,8 @@ export const ANALYTICS_ACTION_EVENT = 'event'; export const ANALYTICS_ACTION_STR = 'Action'; export const ANALYTICS_SUBMIT_STR = 'Submit'; -export const ANALYTICS_SELECTED_STR = 'Selected'; -export const ANALYTICS_MOUNTED_STR = 'Mounted'; +export const ANALYTICS_SELECTED_STR = 'selected'; +export const ANALYTICS_MOUNTED_STR = 'mounted'; export const ANALYTICS_IMPLEMENTATION_ERROR = 'ImplementationError'; export const ANALYTICS_API_ERROR = 'APIError'; From 8443c142f3cde2b2841c07d5762428f61530d71a Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 4 Jan 2024 13:42:31 +0100 Subject: [PATCH 48/94] Renamed initial Analytics call from "send" to "setUp" --- packages/lib/src/components/UIElement.tsx | 2 +- packages/lib/src/components/types.ts | 2 +- packages/lib/src/core/Analytics/Analytics.ts | 2 +- packages/playground/src/pages/Dropin/session.js | 11 ++++++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index ae97e46d95..4e2fe26eaf 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -52,7 +52,7 @@ export class UIElement

extends BaseElement

im const sessionId = this.props.session?.id; this.props.modules.analytics - .send({ + .setUp({ ...setUpAnalyticsObj, ...(sessionId && { sessionId }) }) diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index 99547e9be7..9e6fcac301 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -78,7 +78,7 @@ export interface RawPaymentResponse extends PaymentResponse { } export interface AnalyticsModule { - send: (a: AnalyticsInitialEvent) => Promise; + setUp: (a: AnalyticsInitialEvent) => Promise; sendAnalyticsActions: () => Promise; getCheckoutAttemptId: () => string; getEventsQueue: () => EventsQueueModule; diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 24d6850db5..9d137f615f 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -48,7 +48,7 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy }; const anlModule: AnalyticsModule = { - send: async (initialEvent: AnalyticsInitialEvent) => { + setUp: async (initialEvent: AnalyticsInitialEvent) => { const { enabled, payload, telemetry } = props; // TODO what is payload, is it ever used? if (enabled === true) { diff --git a/packages/playground/src/pages/Dropin/session.js b/packages/playground/src/pages/Dropin/session.js index cda08851eb..fbb6349ce6 100644 --- a/packages/playground/src/pages/Dropin/session.js +++ b/packages/playground/src/pages/Dropin/session.js @@ -30,9 +30,9 @@ export async function initSession() { onError: (error, component) => { console.info(JSON.stringify(error), component); }, - onChange: (state, component) => { - console.log('onChange', state); - }, + // onChange: (state, component) => { + // console.log('onChange', state); + // }, paymentMethodsConfiguration: { paywithgoogle: { buttonType: 'plain' @@ -45,14 +45,15 @@ export async function initSession() { // billingAddress config: billingAddressRequired: true, - billingAddressMode: 'partial' + billingAddressMode: 'partial', + _disableClickToPay: true } } }); const dropin = checkout .create('dropin', { - instantPaymentTypes: ['googlepay'] + // instantPaymentTypes: ['googlepay'] }) .mount('#dropin-container'); return [checkout, dropin]; From de111c121e366d47d7f1d9c2683ca63e355d2b37 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 4 Jan 2024 15:52:03 +0100 Subject: [PATCH 49/94] Create timer to send events after a set period of time --- packages/lib/src/components/types.ts | 1 - packages/lib/src/core/Analytics/Analytics.ts | 35 ++++++++++++++------ packages/lib/src/core/Analytics/constants.ts | 2 ++ 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index 9e6fcac301..d3dc82ee5c 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -79,7 +79,6 @@ export interface RawPaymentResponse extends PaymentResponse { export interface AnalyticsModule { setUp: (a: AnalyticsInitialEvent) => Promise; - sendAnalyticsActions: () => Promise; getCheckoutAttemptId: () => string; getEventsQueue: () => EventsQueueModule; createAnalyticsAction: (a: CreateAnalyticsActionObject) => void; diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 9d137f615f..75ef846020 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -2,13 +2,14 @@ import LogEvent from '../Services/analytics/log-event'; import CollectId from '../Services/analytics/collect-id'; import EventsQueue, { EventsQueueModule } from './EventsQueue'; import { ANALYTICS_ACTION, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps, CreateAnalyticsActionObject } from './types'; -import { ANALYTICS_ACTION_ERROR, ANALYTICS_ACTION_LOG, ANALYTICS_PATH } from './constants'; +import { ANALYTICS_ACTION_ERROR, ANALYTICS_ACTION_EVENT, ANALYTICS_ACTION_LOG, ANALYTICS_EVENT_TIMER_INTERVAL, ANALYTICS_PATH } from './constants'; import { debounce } from '../../components/internal/Address/utils'; import { AnalyticsModule } from '../../components/types'; import { createAnalyticsObject } from './utils'; let capturedCheckoutAttemptId = null; let hasLoggedPixel = false; +let sendEventsTimerId = null; const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analyticsContext }: AnalyticsProps): AnalyticsModule => { const defaultProps = { @@ -31,19 +32,37 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy const collectId = CollectId({ analyticsContext, clientKey, locale, amount, analyticsPath: ANALYTICS_PATH }); const eventsQueue: EventsQueueModule = EventsQueue({ analyticsContext, clientKey, analyticsPath: ANALYTICS_PATH }); + const sendAnalyticsActions = () => { + if (capturedCheckoutAttemptId) { + return eventsQueue.run(capturedCheckoutAttemptId); + } + return Promise.resolve(null); + }; + const addAnalyticsAction = (type: ANALYTICS_ACTION, obj: AnalyticsObject) => { eventsQueue.add(`${type}s`, obj); /** * The logic is: - * - events are stored until a log or error comes along + * - events are stored until a log or error comes along, + * but, if after a set time, no other analytics-action has come along then we send the events anyway + */ + if (type === ANALYTICS_ACTION_EVENT) { + clearTimeout(sendEventsTimerId); + sendEventsTimerId = setTimeout(sendAnalyticsActions, ANALYTICS_EVENT_TIMER_INTERVAL); + } + + /** + * The logic is: * - errors get sent straightaway * - logs also get sent straightaway... but... tests with the 3DS2 process show that many logs can happen almost at the same time, * so instead of making (up to 4) sequential api calls we "batch" them using debounce */ if (type === ANALYTICS_ACTION_LOG || type === ANALYTICS_ACTION_ERROR) { + clearTimeout(sendEventsTimerId); // clear any time that might be about to dispatch the events array + const debounceFn = type === ANALYTICS_ACTION_ERROR ? fn => fn : debounce; - debounceFn(anlModule.sendAnalyticsActions)(); + debounceFn(sendAnalyticsActions)(); } }; @@ -51,6 +70,8 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy setUp: async (initialEvent: AnalyticsInitialEvent) => { const { enabled, payload, telemetry } = props; // TODO what is payload, is it ever used? + // console.log('### Analytics::setUp:: initialEvent', initialEvent); + if (enabled === true) { if (telemetry === true && !capturedCheckoutAttemptId) { try { @@ -75,13 +96,6 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy getCheckoutAttemptId: (): string => capturedCheckoutAttemptId, - sendAnalyticsActions: () => { - if (capturedCheckoutAttemptId) { - return eventsQueue.run(capturedCheckoutAttemptId); - } - return Promise.resolve(null); - }, - // Expose getter for testing purposes getEventsQueue: () => eventsQueue, @@ -90,6 +104,7 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy action, ...data }); + // console.log('### Analytics::createAnalyticsAction:: action=', action, ' aObj=', aObj); addAnalyticsAction(action, aObj); }, diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index 0e35290b27..6a553d51af 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -1,5 +1,7 @@ export const ANALYTICS_PATH = 'v3/analytics'; +export const ANALYTICS_EVENT_TIMER_INTERVAL = 10000; + export const ANALYTICS_ACTION_LOG = 'log'; export const ANALYTICS_ACTION_ERROR = 'error'; export const ANALYTICS_ACTION_EVENT = 'event'; From 32f7e262c76c20e98c14e85c88e9d2c934e571ee Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 8 Jan 2024 16:23:31 +0100 Subject: [PATCH 50/94] Setup analytics after render has been called. Make call to submit the "mounted" event from BaseElement c.f. UIElement --- packages/lib/src/components/BaseElement.ts | 23 +++++++++++++++++----- packages/lib/src/components/UIElement.tsx | 14 ++++--------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index d386dba29d..a72de1c428 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -7,6 +7,7 @@ import { BaseElementProps, PaymentData } from './types'; import { RiskData } from '../core/RiskModule/RiskModule'; import { Resources } from '../core/Context/Resources'; import { AnalyticsInitialEvent } from '../core/Analytics/types'; +import { ANALYTICS_MOUNTED_STR } from '../core/Analytics/constants'; class BaseElement

{ public readonly _id = `${this.constructor['type']}-${uuid()}`; @@ -52,6 +53,11 @@ class BaseElement

{ return null; } + /* eslint-disable-next-line */ + protected submitAnalytics(type = 'action', obj?) { + return null; + } + protected setState(newState: object): void { this.state = { ...this.state, ...newState }; } @@ -98,21 +104,28 @@ class BaseElement

{ 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, since this._node is undefined) + } + + 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 (!this._node) { 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-action "event" declaring that the component has been mounted + this.submitAnalytics(ANALYTICS_MOUNTED_STR); }); } } this._node = node; - this._component = this.render(); - - render(this._component, node); return this; } diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 4e2fe26eaf..7b3ee5922f 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -51,16 +51,10 @@ export class UIElement

extends BaseElement

im protected setUpAnalytics(setUpAnalyticsObj: AnalyticsInitialEvent) { const sessionId = this.props.session?.id; - this.props.modules.analytics - .setUp({ - ...setUpAnalyticsObj, - ...(sessionId && { sessionId }) - }) - .then(() => { - // Once the initial analytics set up call has been made... - // ...create an analytics-action "event" declaring that the component has been mounted - this.submitAnalytics(ANALYTICS_MOUNTED_STR); - }); + return this.props.modules.analytics.setUp({ + ...setUpAnalyticsObj, + ...(sessionId && { sessionId }) + }); } /* eslint-disable-next-line */ From 5291ff3554bef2595bec30085eff98685e090c83 Mon Sep 17 00:00:00 2001 From: nicholas Date: Tue, 9 Jan 2024 10:26:48 +0100 Subject: [PATCH 51/94] DropinComponent sends 'rendered' analytics-action --- .../components/Dropin/components/DropinComponent.tsx | 9 +++++++++ packages/lib/src/components/UIElement.tsx | 10 ++++++++-- packages/lib/src/core/Analytics/utils.ts | 5 ++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/components/Dropin/components/DropinComponent.tsx b/packages/lib/src/components/Dropin/components/DropinComponent.tsx index 5890649e70..b11a6185d6 100644 --- a/packages/lib/src/components/Dropin/components/DropinComponent.tsx +++ b/packages/lib/src/components/Dropin/components/DropinComponent.tsx @@ -31,6 +31,15 @@ export class DropinComponent extends Component { this.setState({ instantPaymentElements, elements: [...storedElements, ...elements], orderStatus }); this.setStatus('ready'); + + // TODO b/e can't yet handle type:rendered - so we are using metadata as a workaround + // const data = { component: 'dropin', type: 'rendered' }; + const data = { component: 'dropin', type: 'mounted', metadata: { subtype: 'rendered' } }; + // AnalyticsAction: action: 'event' type:'rendered' + this.props.modules?.analytics.createAnalyticsAction({ + action: 'event', + data + }); } ); diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 7b3ee5922f..0a6bdf542e 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -57,6 +57,14 @@ export class UIElement

extends BaseElement

im }); } + /** + * A function for all UIElements, or BaseElement, to use to create an analytics action for when it's been: + * - mounted, + * - a PM has been selected + * - onSubmit has been called (as a result of the pay button being pressed) + * + * In some other cases e.g. 3DS2 components, this function is overridden to allow more specific analytics actions to be created + */ /* eslint-disable-next-line */ protected submitAnalytics(type = 'action', obj?) { /** Work out what the component's "type" is: @@ -68,8 +76,6 @@ export class UIElement

extends BaseElement

im component = this.constructor['type'] === 'scheme' || this.constructor['type'] === 'bcmc' ? this.constructor['type'] : this.props.type; } - // console.log('### UIElement::submitAnalytics:: component=', component); - // Dropin PM selected, or, UIElement mounted (called once only) if (type === ANALYTICS_SELECTED_STR || type === ANALYTICS_MOUNTED_STR) { let storedCardIndicator; diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index 36d5398d84..7272ba0679 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -19,6 +19,8 @@ export const getUTCTimestamp = () => Date.now(); * Event objects have, in addition to the base props: * "type" & "target" & * "isStoredPaymentMethod" & "brand" (when a storedCard is "selected") + * + * All objects can also have a "metadata" prop */ export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObject => ({ timestamp: String(getUTCTimestamp()), @@ -29,5 +31,6 @@ export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObj ...(aObj.action === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type ...(aObj.action === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type ...(aObj.action === 'event' && { type: aObj.type, target: aObj.target }), // only added if we have an event object - ...(aObj.action === 'event' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }) + ...(aObj.action === 'event' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }), + ...(aObj.metadata && { metadata: aObj.metadata }) }); From 5006b798e31eb08cccdd6203f065d0e75c6a5ba2 Mon Sep 17 00:00:00 2001 From: nicholas Date: Tue, 9 Jan 2024 10:38:37 +0100 Subject: [PATCH 52/94] For clarity UIElement uses switch in submitAnalytics function --- packages/lib/src/components/UIElement.tsx | 57 +++++++++++++---------- packages/lib/src/core/Analytics/utils.ts | 4 +- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 0a6bdf542e..5f3496e804 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -76,35 +76,42 @@ export class UIElement

extends BaseElement

im component = this.constructor['type'] === 'scheme' || this.constructor['type'] === 'bcmc' ? this.constructor['type'] : this.props.type; } - // Dropin PM selected, or, UIElement mounted (called once only) - if (type === ANALYTICS_SELECTED_STR || type === ANALYTICS_MOUNTED_STR) { - let storedCardIndicator; - // Check if it's a storedCard - if (component === 'scheme') { - if (hasOwnProperty(this.props, 'supportedShopperInteractions')) { - storedCardIndicator = { - isStoredPaymentMethod: true, - brand: this.props.brand - }; + switch (type) { + // BaseElement mounted (called once only) + // Dropin PM selected + case ANALYTICS_MOUNTED_STR: + case ANALYTICS_SELECTED_STR: { + let storedCardIndicator; + // Check if it's a storedCard + if (component === 'scheme') { + if (hasOwnProperty(this.props, 'supportedShopperInteractions')) { + storedCardIndicator = { + isStoredPaymentMethod: true, + brand: this.props.brand + }; + } } - } - const data = { component, type, ...storedCardIndicator }; - // console.log('### UIElement::submitAnalytics:: SELECTED data=', data); + const data = { component, type, ...storedCardIndicator }; + // console.log('### UIElement::submitAnalytics:: SELECTED data=', data); - // AnalyticsAction: action: 'event' type:'mounted'|'selected' - this.props.modules?.analytics.createAnalyticsAction({ - action: 'event', - data - }); - return; - } + // AnalyticsAction: action: 'event' type:'mounted'|'selected' + this.props.modules?.analytics.createAnalyticsAction({ + action: 'event', + data + }); + break; + } - // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' - this.props.modules?.analytics.createAnalyticsAction({ - action: 'log', - data: { component, type: ANALYTICS_SUBMIT_STR, target: 'payButton', message: 'Shopper clicked pay' } - }); + // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' + default: { + // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' + this.props.modules?.analytics.createAnalyticsAction({ + action: 'log', + data: { component, type: ANALYTICS_SUBMIT_STR, target: 'payButton', message: 'Shopper clicked pay' } + }); + } + } } private onSubmit(): void { diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index 7272ba0679..ea59e377e2 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -5,7 +5,7 @@ export const getUTCTimestamp = () => Date.now(); /** * All objects for the /checkoutanalytics endpoint have base props: - * "timestamp" & "component" (and an optional "metadata" object of key-value pairs) + * "timestamp" & "component" * * Error objects have, in addition to the base props: * "code", "errorType" & "message" @@ -20,7 +20,7 @@ export const getUTCTimestamp = () => Date.now(); * "type" & "target" & * "isStoredPaymentMethod" & "brand" (when a storedCard is "selected") * - * All objects can also have a "metadata" prop + * All objects can also have a "metadata" object of key-value pairs */ export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObject => ({ timestamp: String(getUTCTimestamp()), From eadc2aea4e3c050b9c785973e38635ad5fd57d66 Mon Sep 17 00:00:00 2001 From: nicholas Date: Tue, 9 Jan 2024 10:55:13 +0100 Subject: [PATCH 53/94] Added comment about Dropin also being able to pass a list of paymentMethods to analytics --- .../components/Dropin/components/DropinComponent.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/components/Dropin/components/DropinComponent.tsx b/packages/lib/src/components/Dropin/components/DropinComponent.tsx index b11a6185d6..4c0c44e91d 100644 --- a/packages/lib/src/components/Dropin/components/DropinComponent.tsx +++ b/packages/lib/src/components/Dropin/components/DropinComponent.tsx @@ -32,9 +32,13 @@ export class DropinComponent extends Component e.props.type), // TODO might be added (used to be in original analytics, in the setup call) + }; // AnalyticsAction: action: 'event' type:'rendered' this.props.modules?.analytics.createAnalyticsAction({ action: 'event', From b8f83e88d8350834789821d386e3f7a039b34f66 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 12 Jan 2024 11:18:55 +0100 Subject: [PATCH 54/94] Use single "rendered" event to describe component mounting, dropin pm list rendering & dropin pm selection --- packages/lib/src/components/BaseElement.ts | 11 ++++++---- .../Dropin/components/DropinComponent.tsx | 9 ++++---- packages/lib/src/components/UIElement.tsx | 22 +++++++++---------- packages/lib/src/core/Analytics/constants.ts | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index a72de1c428..a9b46e7c83 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -7,7 +7,7 @@ import { BaseElementProps, PaymentData } from './types'; import { RiskData } from '../core/RiskModule/RiskModule'; import { Resources } from '../core/Context/Resources'; import { AnalyticsInitialEvent } from '../core/Analytics/types'; -import { ANALYTICS_MOUNTED_STR } from '../core/Analytics/constants'; +import { ANALYTICS_RENDERED_STR } from '../core/Analytics/constants'; class BaseElement

{ public readonly _id = `${this.constructor['type']}-${uuid()}`; @@ -54,7 +54,7 @@ class BaseElement

{ } /* eslint-disable-next-line */ - protected submitAnalytics(type = 'action', obj?) { + protected submitAnalytics(analyticsObj?: any) { return null; } @@ -119,8 +119,11 @@ class BaseElement

{ flavor: !this.props.isDropin ? 'components' : 'dropin' }).then(() => { // Once the initial analytics set up call has been made... - // ...create an analytics-action "event" declaring that the component has been mounted - this.submitAnalytics(ANALYTICS_MOUNTED_STR); + // ...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 }); + } }); } } diff --git a/packages/lib/src/components/Dropin/components/DropinComponent.tsx b/packages/lib/src/components/Dropin/components/DropinComponent.tsx index 4c0c44e91d..61c4da3a0f 100644 --- a/packages/lib/src/components/Dropin/components/DropinComponent.tsx +++ b/packages/lib/src/components/Dropin/components/DropinComponent.tsx @@ -5,7 +5,7 @@ import getOrderStatus from '../../../core/Services/order-status'; import { DropinComponentProps, DropinComponentState, DropinStatusProps, onOrderCancelData } from '../types'; import './DropinComponent.scss'; import { UIElementStatus } from '../../types'; -import { ANALYTICS_SELECTED_STR } from '../../../core/Analytics/constants'; +import { ANALYTICS_RENDERED_STR } from '../../../core/Analytics/constants'; export class DropinComponent extends Component { public state: DropinComponentState = { @@ -32,13 +32,12 @@ export class DropinComponent extends Component e.props.type), // TODO might be added (used to be in original analytics, in the setup call) }; + // AnalyticsAction: action: 'event' type:'rendered' this.props.modules?.analytics.createAnalyticsAction({ action: 'event', @@ -80,7 +79,7 @@ export class DropinComponent extends Component extends BaseElement

implements IUIElement { @@ -66,7 +66,7 @@ export class UIElement

extends BaseElement

im * In some other cases e.g. 3DS2 components, this function is overridden to allow more specific analytics actions to be created */ /* eslint-disable-next-line */ - protected submitAnalytics(type = 'action', obj?) { + protected submitAnalytics(analyticsObj: any) { /** Work out what the component's "type" is: * - first check for a dedicated "analyticsType" (currently only applies to custom-cards) * - otherwise, distinguish cards from non-cards: cards will use their static type property, everything else will use props.type @@ -76,11 +76,10 @@ export class UIElement

extends BaseElement

im component = this.constructor['type'] === 'scheme' || this.constructor['type'] === 'bcmc' ? this.constructor['type'] : this.props.type; } - switch (type) { - // BaseElement mounted (called once only) - // Dropin PM selected - case ANALYTICS_MOUNTED_STR: - case ANALYTICS_SELECTED_STR: { + switch (analyticsObj.type) { + // Called from BaseElement (when component mounted) or, from DropinComponent (after mounting, when it has finished resolving all the PM promises) + // &/or, from DropinComponent when a PM is selected + case ANALYTICS_RENDERED_STR: { let storedCardIndicator; // Check if it's a storedCard if (component === 'scheme') { @@ -92,10 +91,9 @@ export class UIElement

extends BaseElement

im } } - const data = { component, type, ...storedCardIndicator }; - // console.log('### UIElement::submitAnalytics:: SELECTED data=', data); + const data = { component, type: analyticsObj.type, ...storedCardIndicator }; - // AnalyticsAction: action: 'event' type:'mounted'|'selected' + // AnalyticsAction: action: 'event' type:'rendered'|'selected' this.props.modules?.analytics.createAnalyticsAction({ action: 'event', data @@ -128,7 +126,7 @@ export class UIElement

extends BaseElement

im if (this.props.onSubmit) { /** Classic flow */ // Call analytics endpoint - this.submitAnalytics(); + this.submitAnalytics({ type: 'submit' }); // Call onSubmit handler this.props.onSubmit({ data: this.data, isValid: this.isValid }, this.elementRef); @@ -147,7 +145,7 @@ export class UIElement

extends BaseElement

im beforeSubmitEvent .then(data => { // Call analytics endpoint - this.submitAnalytics(); + this.submitAnalytics({ type: 'submit' }); // Submit payment return this.submitPayment(data); }) diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index 6a553d51af..f18bf84faa 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -9,7 +9,7 @@ export const ANALYTICS_ACTION_EVENT = 'event'; export const ANALYTICS_ACTION_STR = 'Action'; export const ANALYTICS_SUBMIT_STR = 'Submit'; export const ANALYTICS_SELECTED_STR = 'selected'; -export const ANALYTICS_MOUNTED_STR = 'mounted'; +export const ANALYTICS_RENDERED_STR = 'rendered'; export const ANALYTICS_IMPLEMENTATION_ERROR = 'ImplementationError'; export const ANALYTICS_API_ERROR = 'APIError'; From ecec6c52e263bc4e707420a84a64763ad67ae7ed Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 12 Jan 2024 13:12:53 +0100 Subject: [PATCH 55/94] Changing analytics terminology - generic term is events with specific types being: info, log, error --- .../Dropin/components/DropinComponent.tsx | 4 +-- packages/lib/src/components/UIElement.tsx | 8 ++--- packages/lib/src/components/types.ts | 4 +-- packages/lib/src/core/Analytics/Analytics.ts | 32 +++++++++---------- .../lib/src/core/Analytics/EventsQueue.ts | 8 ++--- packages/lib/src/core/Analytics/constants.ts | 8 ++--- packages/lib/src/core/Analytics/types.ts | 12 +++---- packages/lib/src/core/Analytics/utils.ts | 16 +++++----- packages/lib/src/core/core.ts | 4 +-- 9 files changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/lib/src/components/Dropin/components/DropinComponent.tsx b/packages/lib/src/components/Dropin/components/DropinComponent.tsx index 61c4da3a0f..8744ce1fea 100644 --- a/packages/lib/src/components/Dropin/components/DropinComponent.tsx +++ b/packages/lib/src/components/Dropin/components/DropinComponent.tsx @@ -39,8 +39,8 @@ export class DropinComponent extends Component extends BaseElement

im const data = { component, type: analyticsObj.type, ...storedCardIndicator }; // AnalyticsAction: action: 'event' type:'rendered'|'selected' - this.props.modules?.analytics.createAnalyticsAction({ - action: 'event', + this.props.modules?.analytics.createAnalyticsEvent({ + event: 'info', data }); break; @@ -104,8 +104,8 @@ export class UIElement

extends BaseElement

im // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' default: { // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' - this.props.modules?.analytics.createAnalyticsAction({ - action: 'log', + this.props.modules?.analytics.createAnalyticsEvent({ + event: 'log', data: { component, type: ANALYTICS_SUBMIT_STR, target: 'payButton', message: 'Shopper clicked pay' } }); } diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index d3dc82ee5c..6d6a3ecb80 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -8,7 +8,7 @@ import { PayButtonProps } from './internal/PayButton/PayButton'; import Session from '../core/CheckoutSession'; import { SRPanel } from '../core/Errors/SRPanel'; import { Resources } from '../core/Context/Resources'; -import { AnalyticsInitialEvent, CreateAnalyticsActionObject } from '../core/Analytics/types'; +import { AnalyticsInitialEvent, CreateAnalyticsEventObject } from '../core/Analytics/types'; import { EventsQueueModule } from '../core/Analytics/EventsQueue'; export interface PaymentMethodData { @@ -81,7 +81,7 @@ export interface AnalyticsModule { setUp: (a: AnalyticsInitialEvent) => Promise; getCheckoutAttemptId: () => string; getEventsQueue: () => EventsQueueModule; - createAnalyticsAction: (a: CreateAnalyticsActionObject) => void; + createAnalyticsEvent: (a: CreateAnalyticsEventObject) => void; getEnabled: () => boolean; } diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 75ef846020..a973f02c6f 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -1,8 +1,8 @@ import LogEvent from '../Services/analytics/log-event'; import CollectId from '../Services/analytics/collect-id'; import EventsQueue, { EventsQueueModule } from './EventsQueue'; -import { ANALYTICS_ACTION, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps, CreateAnalyticsActionObject } from './types'; -import { ANALYTICS_ACTION_ERROR, ANALYTICS_ACTION_EVENT, ANALYTICS_ACTION_LOG, ANALYTICS_EVENT_TIMER_INTERVAL, ANALYTICS_PATH } from './constants'; +import { ANALYTICS_EVENT, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps, CreateAnalyticsEventObject } from './types'; +import { ANALYTICS_EVENT_ERROR, ANALYTICS_EVENT_INFO, ANALYTICS_EVENT_LOG, ANALYTICS_INFO_TIMER_INTERVAL, ANALYTICS_PATH } from './constants'; import { debounce } from '../../components/internal/Address/utils'; import { AnalyticsModule } from '../../components/types'; import { createAnalyticsObject } from './utils'; @@ -32,24 +32,24 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy const collectId = CollectId({ analyticsContext, clientKey, locale, amount, analyticsPath: ANALYTICS_PATH }); const eventsQueue: EventsQueueModule = EventsQueue({ analyticsContext, clientKey, analyticsPath: ANALYTICS_PATH }); - const sendAnalyticsActions = () => { + const sendAnalyticsEvents = () => { if (capturedCheckoutAttemptId) { return eventsQueue.run(capturedCheckoutAttemptId); } return Promise.resolve(null); }; - const addAnalyticsAction = (type: ANALYTICS_ACTION, obj: AnalyticsObject) => { + const addAnalyticsEvent = (type: ANALYTICS_EVENT, obj: AnalyticsObject) => { eventsQueue.add(`${type}s`, obj); /** * The logic is: - * - events are stored until a log or error comes along, - * but, if after a set time, no other analytics-action has come along then we send the events anyway + * - info events are stored until a log or error comes along, + * but, if after a set time, no other analytics event (log or error) has come along then we send the info events anyway */ - if (type === ANALYTICS_ACTION_EVENT) { + if (type === ANALYTICS_EVENT_INFO) { clearTimeout(sendEventsTimerId); - sendEventsTimerId = setTimeout(sendAnalyticsActions, ANALYTICS_EVENT_TIMER_INTERVAL); + sendEventsTimerId = setTimeout(sendAnalyticsEvents, ANALYTICS_INFO_TIMER_INTERVAL); } /** @@ -58,11 +58,11 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy * - logs also get sent straightaway... but... tests with the 3DS2 process show that many logs can happen almost at the same time, * so instead of making (up to 4) sequential api calls we "batch" them using debounce */ - if (type === ANALYTICS_ACTION_LOG || type === ANALYTICS_ACTION_ERROR) { - clearTimeout(sendEventsTimerId); // clear any time that might be about to dispatch the events array + if (type === ANALYTICS_EVENT_LOG || type === ANALYTICS_EVENT_ERROR) { + clearTimeout(sendEventsTimerId); // clear any timer that might be about to dispatch the info events array - const debounceFn = type === ANALYTICS_ACTION_ERROR ? fn => fn : debounce; - debounceFn(sendAnalyticsActions)(); + const debounceFn = type === ANALYTICS_EVENT_ERROR ? fn => fn : debounce; + debounceFn(sendAnalyticsEvents)(); } }; @@ -99,14 +99,14 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy // Expose getter for testing purposes getEventsQueue: () => eventsQueue, - createAnalyticsAction: ({ action, data }: CreateAnalyticsActionObject) => { + createAnalyticsEvent: ({ event, data }: CreateAnalyticsEventObject) => { const aObj: AnalyticsObject = createAnalyticsObject({ - action, + event, ...data }); - // console.log('### Analytics::createAnalyticsAction:: action=', action, ' aObj=', aObj); + // console.log('### Analytics::createAnalyticsEvent:: event=', event, ' aObj=', aObj); - addAnalyticsAction(action, aObj); + addAnalyticsEvent(event, aObj); }, getEnabled: () => props.enabled diff --git a/packages/lib/src/core/Analytics/EventsQueue.ts b/packages/lib/src/core/Analytics/EventsQueue.ts index 341d59dfba..ea528c9786 100644 --- a/packages/lib/src/core/Analytics/EventsQueue.ts +++ b/packages/lib/src/core/Analytics/EventsQueue.ts @@ -3,7 +3,7 @@ import { AnalyticsObject, EventQueueProps } from './types'; interface CAActions { channel: 'Web'; - events: AnalyticsObject[]; + infos: AnalyticsObject[]; // Not a nice pluralisation, but it can't be helped errors: AnalyticsObject[]; logs: AnalyticsObject[]; } @@ -17,13 +17,13 @@ export interface EventsQueueModule { const EventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueueProps): EventsQueueModule => { const caActions: CAActions = { channel: 'Web', - events: [], + infos: [], errors: [], logs: [] }; const runQueue = (checkoutAttemptId: string): Promise => { - if (!caActions.events.length && !caActions.logs.length && !caActions.errors.length) { + if (!caActions.infos.length && !caActions.logs.length && !caActions.errors.length) { return Promise.resolve(null); } @@ -54,7 +54,7 @@ const EventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueueP run: (checkoutAttemptId: string) => { const promise = runQueue(checkoutAttemptId); - caActions.events = []; + caActions.infos = []; caActions.errors = []; caActions.logs = []; diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index f18bf84faa..b3287bea70 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -1,10 +1,10 @@ export const ANALYTICS_PATH = 'v3/analytics'; -export const ANALYTICS_EVENT_TIMER_INTERVAL = 10000; +export const ANALYTICS_INFO_TIMER_INTERVAL = 10000; // ANALYTICS_EVENT_TIMER_INTERVAL -export const ANALYTICS_ACTION_LOG = 'log'; -export const ANALYTICS_ACTION_ERROR = 'error'; -export const ANALYTICS_ACTION_EVENT = 'event'; +export const ANALYTICS_EVENT_LOG = 'log'; // ANALYTICS_ACTION_LOG +export const ANALYTICS_EVENT_ERROR = 'error'; // ANALYTICS_ACTION_ERROR +export const ANALYTICS_EVENT_INFO = 'info'; // ANALYTICS_ACTION_EVENT export const ANALYTICS_ACTION_STR = 'Action'; export const ANALYTICS_SUBMIT_STR = 'Submit'; diff --git a/packages/lib/src/core/Analytics/types.ts b/packages/lib/src/core/Analytics/types.ts index b12cbd6724..7417ccd0da 100644 --- a/packages/lib/src/core/Analytics/types.ts +++ b/packages/lib/src/core/Analytics/types.ts @@ -50,9 +50,9 @@ export interface AnalyticsObject { brand?: string; } -export type ANALYTICS_ACTION = 'log' | 'error' | 'event'; +export type ANALYTICS_EVENT = 'log' | 'error' | 'info'; -export type CreateAnalyticsObject = Omit & { action: ANALYTICS_ACTION }; +export type CreateAnalyticsObject = Omit & { event: ANALYTICS_EVENT }; export type AnalyticsInitialEvent = { containerWidth: number; @@ -70,11 +70,11 @@ export type AnalyticsConfig = { loadingContext?: string; }; -export type CreateAnalyticsActionData = Omit; +export type CreateAnalyticsEventData = Omit; -export type CreateAnalyticsActionObject = { - action: ANALYTICS_ACTION; - data: CreateAnalyticsActionData; +export type CreateAnalyticsEventObject = { + event: ANALYTICS_EVENT; + data: CreateAnalyticsEventData; }; export type EventQueueProps = Pick & { analyticsPath: string }; diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index ea59e377e2..fe391d0e60 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -16,7 +16,7 @@ export const getUTCTimestamp = () => Date.now(); * "type" & "subtype" (e.g. when an action is handled), or, * "type" (e.g. logging during the 3DS2 process) * - * Event objects have, in addition to the base props: + * Info objects have, in addition to the base props: * "type" & "target" & * "isStoredPaymentMethod" & "brand" (when a storedCard is "selected") * @@ -25,12 +25,12 @@ export const getUTCTimestamp = () => Date.now(); export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObject => ({ timestamp: String(getUTCTimestamp()), component: aObj.component, - ...(aObj.action === 'error' && { code: aObj.code, errorType: aObj.errorType }), // only added if we have an error object - ...((aObj.action === 'error' || aObj.action === 'log') && { message: aObj.message }), // only added if we have an error, or log object - ...(aObj.action === 'log' && { type: aObj.type }), // only added if we have a log object - ...(aObj.action === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type - ...(aObj.action === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type - ...(aObj.action === 'event' && { type: aObj.type, target: aObj.target }), // only added if we have an event object - ...(aObj.action === 'event' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }), + ...(aObj.event === 'error' && { code: aObj.code, errorType: aObj.errorType }), // only added if we have an error object + ...((aObj.event === 'error' || aObj.event === 'log') && { message: aObj.message }), // only added if we have an error, or log object + ...(aObj.event === 'log' && { type: aObj.type }), // only added if we have a log object + ...(aObj.event === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type + ...(aObj.event === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type + ...(aObj.event === 'info' && { type: aObj.type, target: aObj.target }), // only added if we have an event object + ...(aObj.event === 'info' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }), ...(aObj.metadata && { metadata: aObj.metadata }) }); diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index 21e09b3a25..70680d2e2b 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -149,8 +149,8 @@ class Core { if (action.type) { // AnalyticsAction: action: 'log' type:'action' - this.modules.analytics.createAnalyticsAction({ - action: 'log', + this.modules.analytics.createAnalyticsEvent({ + event: 'log', data: { component: `${action.type}${action.subtype ?? ''}`, type: ANALYTICS_ACTION_STR, From c213f5ef13dd1a5a035aa8a6611eb6002f4ae211 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 12 Jan 2024 13:56:32 +0100 Subject: [PATCH 56/94] Changing analytics terminology - info events are collected into an an array named "info" (as opposed to the "logs" & "errors" arrays) --- packages/lib/src/core/Analytics/Analytics.ts | 3 ++- packages/lib/src/core/Analytics/EventsQueue.ts | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index a973f02c6f..57d0655a84 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -40,7 +40,8 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy }; const addAnalyticsEvent = (type: ANALYTICS_EVENT, obj: AnalyticsObject) => { - eventsQueue.add(`${type}s`, obj); + const arrayName = type === ANALYTICS_EVENT_INFO ? type : `${type}s`; + eventsQueue.add(`${arrayName}`, obj); /** * The logic is: diff --git a/packages/lib/src/core/Analytics/EventsQueue.ts b/packages/lib/src/core/Analytics/EventsQueue.ts index ea528c9786..16058dad6c 100644 --- a/packages/lib/src/core/Analytics/EventsQueue.ts +++ b/packages/lib/src/core/Analytics/EventsQueue.ts @@ -3,7 +3,7 @@ import { AnalyticsObject, EventQueueProps } from './types'; interface CAActions { channel: 'Web'; - infos: AnalyticsObject[]; // Not a nice pluralisation, but it can't be helped + info: AnalyticsObject[]; errors: AnalyticsObject[]; logs: AnalyticsObject[]; } @@ -17,13 +17,13 @@ export interface EventsQueueModule { const EventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueueProps): EventsQueueModule => { const caActions: CAActions = { channel: 'Web', - infos: [], + info: [], errors: [], logs: [] }; const runQueue = (checkoutAttemptId: string): Promise => { - if (!caActions.infos.length && !caActions.logs.length && !caActions.errors.length) { + if (!caActions.info.length && !caActions.logs.length && !caActions.errors.length) { return Promise.resolve(null); } @@ -54,7 +54,7 @@ const EventsQueue = ({ analyticsContext, clientKey, analyticsPath }: EventQueueP run: (checkoutAttemptId: string) => { const promise = runQueue(checkoutAttemptId); - caActions.infos = []; + caActions.info = []; caActions.errors = []; caActions.logs = []; From 0bcf663e14943f8954f800531a1833c445681953 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 12 Jan 2024 17:24:44 +0100 Subject: [PATCH 57/94] Fixing unit tests --- .../lib/src/core/Analytics/Analytics.test.ts | 33 +++++++++---------- .../src/core/Analytics/EventsQueue.test.ts | 6 ++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/lib/src/core/Analytics/Analytics.test.ts b/packages/lib/src/core/Analytics/Analytics.test.ts index 054009487c..9f3049b2be 100644 --- a/packages/lib/src/core/Analytics/Analytics.test.ts +++ b/packages/lib/src/core/Analytics/Analytics.test.ts @@ -5,7 +5,7 @@ import { PaymentAmount } from '../../types'; import { createAnalyticsObject } from './utils'; import wait from '../../utils/wait'; import { DEFAULT_DEBOUNCE_TIME_MS } from '../../components/internal/Address/utils'; -import { ANALYTICS_ACTION } from './types'; +import { ANALYTICS_EVENT } from './types'; jest.mock('../Services/analytics/collect-id'); jest.mock('../Services/analytics/log-event'); @@ -24,7 +24,7 @@ const event = { }; const analyticsEventObj = { - action: 'event' as ANALYTICS_ACTION, + event: 'info' as ANALYTICS_EVENT, component: 'cardComponent', type: 'Focus', target: 'PAN input' @@ -46,9 +46,8 @@ describe('Analytics initialisation and event queue', () => { test('Creates an Analytics module with defaultProps', () => { const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); - expect(analytics.send).not.toBe(null); + expect(analytics.setUp).not.toBe(null); expect(analytics.getCheckoutAttemptId).not.toBe(null); - expect(analytics.sendAnalyticsActions).not.toBe(null); expect(analytics.getEnabled).not.toBe(null); expect(analytics.getEnabled()).toBe(true); expect(collectIdPromiseMock).toHaveLength(0); @@ -57,7 +56,7 @@ describe('Analytics initialisation and event queue', () => { test('Should not fire any calls if analytics is disabled', () => { const analytics = Analytics({ analytics: { enabled: false }, loadingContext: '', locale: '', clientKey: '', amount }); - analytics.send(event); + analytics.setUp(event); expect(collectIdPromiseMock).not.toHaveBeenCalled(); expect(logEventPromiseMock).not.toHaveBeenCalled(); }); @@ -65,7 +64,7 @@ describe('Analytics initialisation and event queue', () => { test('Will not call the collectId endpoint if telemetry is disabled, but will call the logEvent (analytics pixel)', () => { const analytics = Analytics({ analytics: { telemetry: false }, loadingContext: '', locale: '', clientKey: '', amount }); expect(collectIdPromiseMock).not.toHaveBeenCalled(); - analytics.send(event); + analytics.setUp(event); expect(collectIdPromiseMock).not.toHaveBeenCalled(); expect(logEventPromiseMock).toHaveBeenCalledWith({ ...event }); @@ -73,7 +72,7 @@ describe('Analytics initialisation and event queue', () => { test('Calls the collectId endpoint by default, adding expected fields', async () => { const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); - analytics.send(event); + analytics.setUp(event); expect(collectIdPromiseMock).toHaveBeenCalled(); await Promise.resolve(); // wait for the next tick @@ -88,7 +87,7 @@ describe('Analytics initialisation and event queue', () => { }; const analytics = Analytics({ analytics: { payload }, loadingContext: '', locale: '', clientKey: '', amount }); - analytics.send(event); + analytics.setUp(event); expect(collectIdPromiseMock).toHaveLength(0); }); @@ -105,22 +104,22 @@ describe('Analytics initialisation and event queue', () => { // no message prop for events expect(aObj.message).toBe(undefined); - analytics.createAnalyticsAction({ action: 'event', data: aObj }); + analytics.createAnalyticsEvent({ event: 'info', data: aObj }); // event object should not be sent immediately - expect(analytics.getEventsQueue().getQueue().events.length).toBe(1); + expect(analytics.getEventsQueue().getQueue().info.length).toBe(1); }); test('Analytics events queue sends error object', async () => { const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); const evObj = createAnalyticsObject(analyticsEventObj); - analytics.createAnalyticsAction({ action: 'event', data: evObj }); + analytics.createAnalyticsEvent({ event: 'info', data: evObj }); - expect(analytics.getEventsQueue().getQueue().events.length).toBe(1); + expect(analytics.getEventsQueue().getQueue().info.length).toBe(1); const aObj = createAnalyticsObject({ - action: 'error', + event: 'error', component: 'threeDS2Fingerprint', code: 'web_704', errorType: 'APIError', @@ -132,18 +131,18 @@ describe('Analytics initialisation and event queue', () => { expect(aObj.errorType).toEqual('APIError'); expect(aObj.message).not.toBe(undefined); - analytics.createAnalyticsAction({ action: 'error', data: aObj }); + analytics.createAnalyticsEvent({ event: 'error', data: aObj }); // error object should be sent immediately, sending any events as well expect(analytics.getEventsQueue().getQueue().errors.length).toBe(0); - expect(analytics.getEventsQueue().getQueue().events.length).toBe(0); + expect(analytics.getEventsQueue().getQueue().info.length).toBe(0); }); test('Analytics events queue sends log object', async () => { const analytics = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '', amount }); const aObj = createAnalyticsObject({ - action: 'log', + event: 'log', component: 'scheme', type: 'Submit' }); @@ -155,7 +154,7 @@ describe('Analytics initialisation and event queue', () => { // no message prop for a log with type 'Submit' expect(aObj.message).toBe(undefined); - analytics.createAnalyticsAction({ action: 'log', data: aObj }); + analytics.createAnalyticsEvent({ event: 'log', data: aObj }); // log object should be sent almost immediately (after a debounce interval) await wait(DEFAULT_DEBOUNCE_TIME_MS); diff --git a/packages/lib/src/core/Analytics/EventsQueue.test.ts b/packages/lib/src/core/Analytics/EventsQueue.test.ts index faef111893..af28f16246 100644 --- a/packages/lib/src/core/Analytics/EventsQueue.test.ts +++ b/packages/lib/src/core/Analytics/EventsQueue.test.ts @@ -12,8 +12,8 @@ describe('CAEventsQueue', () => { }); test('adds event to the queue', () => { - queue.add('events', task1); - expect(queue.getQueue().events.length).toBe(1); + queue.add('info', task1); + expect(queue.getQueue().info.length).toBe(1); }); test('adds error to the queue', () => { @@ -25,7 +25,7 @@ describe('CAEventsQueue', () => { queue.run('checkoutAttemptId'); expect(queue.getQueue().logs.length).toBe(0); - expect(queue.getQueue().events.length).toBe(0); + expect(queue.getQueue().info.length).toBe(0); expect(queue.getQueue().errors.length).toBe(0); }); }); From 658b79d0992fd6f3d0ab20dbced8de27868f85ce Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 15 Jan 2024 17:04:36 +0100 Subject: [PATCH 58/94] Also debounce errors --- packages/lib/src/core/Analytics/Analytics.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 57d0655a84..3b4b70e296 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -55,15 +55,14 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy /** * The logic is: - * - errors get sent straightaway - * - logs also get sent straightaway... but... tests with the 3DS2 process show that many logs can happen almost at the same time, - * so instead of making (up to 4) sequential api calls we "batch" them using debounce + * - errors and logs get sent straightaway + * ...but... tests with the 3DS2 process show that many logs can happen almost at the same time (or you can have an error followed immediately by a log), + * so instead of making several sequential api calls we see if we can "batch" them using debounce */ if (type === ANALYTICS_EVENT_LOG || type === ANALYTICS_EVENT_ERROR) { clearTimeout(sendEventsTimerId); // clear any timer that might be about to dispatch the info events array - const debounceFn = type === ANALYTICS_EVENT_ERROR ? fn => fn : debounce; - debounceFn(sendAnalyticsEvents)(); + debounce(sendAnalyticsEvents)(); } }; From 5cb067e7077d0143852fc4257161bf006ec17acf Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 15 Jan 2024 17:19:17 +0100 Subject: [PATCH 59/94] Fixed unit test --- packages/lib/src/core/Analytics/Analytics.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/core/Analytics/Analytics.test.ts b/packages/lib/src/core/Analytics/Analytics.test.ts index 9f3049b2be..15134637ee 100644 --- a/packages/lib/src/core/Analytics/Analytics.test.ts +++ b/packages/lib/src/core/Analytics/Analytics.test.ts @@ -133,7 +133,8 @@ describe('Analytics initialisation and event queue', () => { analytics.createAnalyticsEvent({ event: 'error', data: aObj }); - // error object should be sent immediately, sending any events as well + // error object should be sent almost immediately (after a debounce interval), sending any events as well + await wait(DEFAULT_DEBOUNCE_TIME_MS); expect(analytics.getEventsQueue().getQueue().errors.length).toBe(0); expect(analytics.getEventsQueue().getQueue().info.length).toBe(0); }); From e348e6fd4a1067fc6be00d93f10b5766d9fa7917 Mon Sep 17 00:00:00 2001 From: nicholas Date: Wed, 17 Jan 2024 14:23:51 +0100 Subject: [PATCH 60/94] Extends feature/using_checkoutanalytics. First draft: adding focus/blur events for Credit card fields --- packages/lib/src/components/Card/Card.tsx | 39 +++++++++++++++++++ .../Card/components/CardInput/CardInput.tsx | 12 ++++++ .../components/CardFieldsWrapper.tsx | 11 +++++- .../CardInput/components/CardHolderName.tsx | 15 ++++++- .../components/KCPAuthentication.tsx | 2 + .../components/CardInput/components/types.ts | 4 ++ packages/lib/src/components/UIElement.tsx | 14 ++++--- .../internal/FormFields/Field/Field.tsx | 4 +- .../lib/configuration/constants.ts | 2 + .../SocialSecurityNumberBrazil.tsx | 14 ++++++- packages/lib/src/core/Analytics/constants.ts | 15 ++++--- packages/lib/src/core/Analytics/utils.ts | 5 ++- packages/lib/src/utils/textUtils.ts | 3 ++ 13 files changed, 123 insertions(+), 17 deletions(-) create mode 100644 packages/lib/src/utils/textUtils.ts diff --git a/packages/lib/src/components/Card/Card.tsx b/packages/lib/src/components/Card/Card.tsx index 56960a23c1..44514dcc52 100644 --- a/packages/lib/src/components/Card/Card.tsx +++ b/packages/lib/src/components/Card/Card.tsx @@ -14,6 +14,9 @@ import ClickToPayWrapper from './components/ClickToPayWrapper'; import { PayButtonFunctionProps, UIElementStatus } from '../types'; import SRPanelProvider from '../../core/Errors/SRPanelProvider'; import PayButton from '../internal/PayButton'; +import { ANALYTICS_FOCUS_STR, ANALYTICS_UNFOCUS_STR } from '../../core/Analytics/constants'; +import { ALL_SECURED_FIELDS, ENCRYPTED } from '../internal/SecuredFields/lib/configuration/constants'; +import { camelCaseToSnakeCase } from '../../utils/textUtils'; export class CardElement extends UIElement { public static type = 'scheme'; @@ -162,6 +165,40 @@ export class CardElement extends UIElement { } } + private onFocus = obj => { + let target = camelCaseToSnakeCase(obj.fieldType); + + // SFs need their fieldType mapped to what the endpoint expects + if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { + target = target.substring(ENCRYPTED.length + 1); // strip 'encrypted_' off the string + } + + this.submitAnalytics({ + event: 'info', + data: { component: CardElement.type, type: ANALYTICS_FOCUS_STR, target } + }); + + // Call merchant defined callback + this.props.onFocus?.(obj); + }; + + private onBlur = obj => { + let target = camelCaseToSnakeCase(obj.fieldType); + + // SFs need their fieldType mapped to what the endpoint expects + if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { + target = target.substring(ENCRYPTED.length + 1); // strip 'encrypted_' off the string + } + + this.submitAnalytics({ + event: 'info', + data: { component: CardElement.type, type: ANALYTICS_UNFOCUS_STR, target } + }); + + // Call merchant defined callback + this.props.onBlur?.(obj); + }; + public onBinValue = triggerBinLookUp(this); get isValid() { @@ -239,6 +276,8 @@ export class CardElement extends UIElement { brandsIcons={this.brands} isPayButtonPrimaryVariant={isCardPrimaryInput} resources={this.resources} + onFocus={this.onFocus} + onBlur={this.onBlur} /> ); } diff --git a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx index d65a321d88..54e7f2dd56 100644 --- a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx +++ b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx @@ -142,6 +142,14 @@ const CardInput: FunctionalComponent = props => { // SecuredField-only handler const handleFocus = getFocusHandler(setFocusedElement, props.onFocus, props.onBlur); + // Handlers for focus & blur on non-securedFields. Can be renamed to onFieldFocus once the onFocusField is renamed in Field.tsx + const onFieldFocusAnalytics = (who, e) => { + props.onFocus({ fieldType: who, event: e }); + }; + const onFieldBlurAnalytics = (who, e) => { + props.onBlur({ fieldType: who, event: e }); + }; + const retrieveLayout = (): string[] => { return getLayout({ props, @@ -190,6 +198,7 @@ const CardInput: FunctionalComponent = props => { return; } + // console.log('### CardInput::handleSecuredFieldsChange:: .sfState.errors', sfState.errors); /** * If PAN has just become valid: decide if we can shift focus to the next field. * @@ -497,6 +506,9 @@ const CardInput: FunctionalComponent = props => { addressSearchDebounceMs={props.addressSearchDebounceMs} // iOSFocusedField={iOSFocusedField} + // + onFieldFocusAnalytics={onFieldFocusAnalytics} + onFieldBlurAnalytics={onFieldBlurAnalytics} /> )} diff --git a/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx b/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx index d62d4be4df..4f5768efba 100644 --- a/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx +++ b/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx @@ -66,7 +66,10 @@ export const CardFieldsWrapper = ({ showBrandsUnderCardNumber, // iOSFocusedField, - disclaimerMessage + disclaimerMessage, + // + onFieldFocusAnalytics, + onFieldBlurAnalytics }) => { const cardHolderField = ( ); @@ -120,6 +125,8 @@ export const CardFieldsWrapper = ({ onBlur={handleChangeFor('taxNumber', 'blur')} onInput={handleChangeFor('taxNumber', 'input')} disabled={iOSFocusedField && iOSFocusedField !== 'kcpTaxNumberOrDOB'} + onFieldFocusAnalytics={onFieldFocusAnalytics} + onFieldBlurAnalytics={onFieldBlurAnalytics} /> )} @@ -133,6 +140,8 @@ export const CardFieldsWrapper = ({ data={socialSecurityNumber} required={true} disabled={iOSFocusedField && iOSFocusedField !== 'socialSecurityNumber'} + onFieldFocusAnalytics={onFieldFocusAnalytics} + onFieldBlurAnalytics={onFieldBlurAnalytics} /> )} diff --git a/packages/lib/src/components/Card/components/CardInput/components/CardHolderName.tsx b/packages/lib/src/components/Card/components/CardInput/components/CardHolderName.tsx index 114856ab96..8beb4fd5b5 100644 --- a/packages/lib/src/components/Card/components/CardInput/components/CardHolderName.tsx +++ b/packages/lib/src/components/Card/components/CardInput/components/CardHolderName.tsx @@ -5,7 +5,18 @@ import { CardHolderNameProps } from './types'; import styles from '../CardInput.module.scss'; import InputText from '../../../../internal/FormFields/InputText'; -export default function CardHolderName({ onBlur, onInput, placeholder, value, required, error = false, isValid, disabled }: CardHolderNameProps) { +export default function CardHolderName({ + onBlur, + onInput, + placeholder, + value, + required, + error = false, + isValid, + disabled, + onFieldFocusAnalytics, + onFieldBlurAnalytics +}: CardHolderNameProps) { const { i18n } = useCoreContext(); return ( @@ -16,6 +27,8 @@ export default function CardHolderName({ onBlur, onInput, placeholder, value, re isValid={!!isValid} name={'holderName'} i18n={i18n} + onFocus={e => onFieldFocusAnalytics('holderName', e)} + onBlur={e => onFieldBlurAnalytics('holderName', e)} > props.onFieldFocusAnalytics('taxNumber', e)} + onBlur={e => props.onFieldBlurAnalytics('taxNumber', e)} > void; + onFieldBlurAnalytics?: (who: string, event: Event) => void; } export interface CardNumberProps { @@ -131,6 +133,8 @@ export interface KCPProps { isValid: boolean; value: string; disabled?: boolean; + onFieldFocusAnalytics?: (who: string, event: Event) => void; + onFieldBlurAnalytics?: (who: string, event: Event) => void; } export type RtnType_ParamBooleanFn = (tn) => boolean; diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 08384d1527..92b9fdea03 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -101,13 +101,17 @@ export class UIElement

extends BaseElement

im break; } - // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' - default: { + case ANALYTICS_SUBMIT_STR: { // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' this.props.modules?.analytics.createAnalyticsEvent({ event: 'log', - data: { component, type: ANALYTICS_SUBMIT_STR, target: 'payButton', message: 'Shopper clicked pay' } + data: { component, type: analyticsObj.type, target: 'payButton', message: 'Shopper clicked pay' } }); + break; + } + + default: { + this.props.modules?.analytics.createAnalyticsEvent(analyticsObj); } } } @@ -126,7 +130,7 @@ export class UIElement

extends BaseElement

im if (this.props.onSubmit) { /** Classic flow */ // Call analytics endpoint - this.submitAnalytics({ type: 'submit' }); + this.submitAnalytics({ type: ANALYTICS_SUBMIT_STR }); // Call onSubmit handler this.props.onSubmit({ data: this.data, isValid: this.isValid }, this.elementRef); @@ -145,7 +149,7 @@ export class UIElement

extends BaseElement

im beforeSubmitEvent .then(data => { // Call analytics endpoint - this.submitAnalytics({ type: 'submit' }); + this.submitAnalytics({ type: ANALYTICS_SUBMIT_STR }); // Submit payment return this.submitPayment(data); }) diff --git a/packages/lib/src/components/internal/FormFields/Field/Field.tsx b/packages/lib/src/components/internal/FormFields/Field/Field.tsx index ae05158a74..7192560841 100644 --- a/packages/lib/src/components/internal/FormFields/Field/Field.tsx +++ b/packages/lib/src/components/internal/FormFields/Field/Field.tsx @@ -27,6 +27,8 @@ const Field: FunctionalComponent = props => { onBlur, onFieldBlur, onFocus, + // onFocusField is a securedField related function that allows a label click to set focus on a securedField (equates to CardInput setFocusOn) + // TODO should rename it to make its purpose clear => setFocusOnSecuredField onFocusField, showValidIcon, useLabelElement, @@ -65,7 +67,7 @@ const Field: FunctionalComponent = props => { (event: h.JSX.TargetedEvent) => { setFocused(false); onBlur?.(event); - // When we also need to fire a specific function when a field blurs + // When we also need to fire a specific function when a field blurs // TODO - what is the use case? onFieldBlur?.(event); }, [onBlur, onFieldBlur] diff --git a/packages/lib/src/components/internal/SecuredFields/lib/configuration/constants.ts b/packages/lib/src/components/internal/SecuredFields/lib/configuration/constants.ts index 8877c1bb9c..2bdcbd86b3 100644 --- a/packages/lib/src/components/internal/SecuredFields/lib/configuration/constants.ts +++ b/packages/lib/src/components/internal/SecuredFields/lib/configuration/constants.ts @@ -1,5 +1,7 @@ import { CVCPolicyType, DatePolicyType } from '../types'; +export const ENCRYPTED = 'encrypted'; + export const ENCRYPTED_CARD_NUMBER = 'encryptedCardNumber'; export const ENCRYPTED_EXPIRY_DATE = 'encryptedExpiryDate'; export const ENCRYPTED_EXPIRY_MONTH = 'encryptedExpiryMonth'; diff --git a/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx b/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx index 86e8dd3dff..6e4b2dd8e3 100644 --- a/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx +++ b/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx @@ -3,7 +3,17 @@ import Field from '../../internal/FormFields/Field'; import useCoreContext from '../../../core/Context/useCoreContext'; import InputText from '../FormFields/InputText'; -export default function ({ onBlur, onInput, valid = false, error = null, data = '', required = false, disabled = false }) { +export default function ({ + onBlur, + onInput, + valid = false, + error = null, + data = '', + required = false, + disabled = false, + onFieldFocusAnalytics, + onFieldBlurAnalytics +}) { const { i18n } = useCoreContext(); return ( @@ -13,6 +23,8 @@ export default function ({ onBlur, onInput, valid = false, error = null, data = errorMessage={error && error.errorMessage ? i18n.get(error.errorMessage) : !!error} isValid={Boolean(valid)} name={'socialSecurityNumber'} + onFocus={e => onFieldFocusAnalytics('socialSecurityNumber', e)} + onBlur={e => onFieldBlurAnalytics('socialSecurityNumber', e)} > Date.now(); * Info objects have, in addition to the base props: * "type" & "target" & * "isStoredPaymentMethod" & "brand" (when a storedCard is "selected") + * // TODO - NEW info events can also have validationErrorCode & validationErrorMessage props * * All objects can also have a "metadata" object of key-value pairs */ @@ -30,7 +31,7 @@ export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObj ...(aObj.event === 'log' && { type: aObj.type }), // only added if we have a log object ...(aObj.event === 'log' && aObj.type === ANALYTICS_ACTION_STR && { subType: aObj.subtype }), // only added if we have a log object of Action type ...(aObj.event === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type - ...(aObj.event === 'info' && { type: aObj.type, target: aObj.target }), // only added if we have an event object - ...(aObj.event === 'info' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }), + ...(aObj.event === 'info' && { type: aObj.type, target: aObj.target }), // only added if we have an info object + ...(aObj.event === 'info' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }), // only added if we have an info object about a storedPM ...(aObj.metadata && { metadata: aObj.metadata }) }); diff --git a/packages/lib/src/utils/textUtils.ts b/packages/lib/src/utils/textUtils.ts new file mode 100644 index 0000000000..2b5b31ac6c --- /dev/null +++ b/packages/lib/src/utils/textUtils.ts @@ -0,0 +1,3 @@ +export const camelCaseToSnakeCase = camelCaseString => { + return camelCaseString.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); +}; From e88e4e995a88758fdd40403a077350b361a77bce Mon Sep 17 00:00:00 2001 From: nicholas Date: Wed, 17 Jan 2024 17:37:54 +0100 Subject: [PATCH 61/94] Second draft: adding error analytics events for Credit card fields --- packages/lib/src/components/Card/Card.tsx | 39 ++++++++++++++--------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/lib/src/components/Card/Card.tsx b/packages/lib/src/components/Card/Card.tsx index 44514dcc52..02599e101e 100644 --- a/packages/lib/src/components/Card/Card.tsx +++ b/packages/lib/src/components/Card/Card.tsx @@ -14,7 +14,7 @@ import ClickToPayWrapper from './components/ClickToPayWrapper'; import { PayButtonFunctionProps, UIElementStatus } from '../types'; import SRPanelProvider from '../../core/Errors/SRPanelProvider'; import PayButton from '../internal/PayButton'; -import { ANALYTICS_FOCUS_STR, ANALYTICS_UNFOCUS_STR } from '../../core/Analytics/constants'; +import { ANALYTICS_FOCUS_STR, ANALYTICS_UNFOCUS_STR, ANALYTICS_VALIDATION_ERROR_STR } from '../../core/Analytics/constants'; import { ALL_SECURED_FIELDS, ENCRYPTED } from '../internal/SecuredFields/lib/configuration/constants'; import { camelCaseToSnakeCase } from '../../utils/textUtils'; @@ -165,17 +165,19 @@ export class CardElement extends UIElement { } } - private onFocus = obj => { - let target = camelCaseToSnakeCase(obj.fieldType); - + private fieldTypeToSnakeCase(fieldType) { + let str = camelCaseToSnakeCase(fieldType); // SFs need their fieldType mapped to what the endpoint expects - if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { - target = target.substring(ENCRYPTED.length + 1); // strip 'encrypted_' off the string + if (ALL_SECURED_FIELDS.includes(fieldType)) { + str = str.substring(ENCRYPTED.length + 1); // strip 'encrypted_' off the string } + return str; + } + private onFocus = obj => { this.submitAnalytics({ event: 'info', - data: { component: CardElement.type, type: ANALYTICS_FOCUS_STR, target } + data: { component: this.constructor['type'], type: ANALYTICS_FOCUS_STR, target: this.fieldTypeToSnakeCase(obj.fieldType) } }); // Call merchant defined callback @@ -183,22 +185,28 @@ export class CardElement extends UIElement { }; private onBlur = obj => { - let target = camelCaseToSnakeCase(obj.fieldType); - - // SFs need their fieldType mapped to what the endpoint expects - if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { - target = target.substring(ENCRYPTED.length + 1); // strip 'encrypted_' off the string - } - this.submitAnalytics({ event: 'info', - data: { component: CardElement.type, type: ANALYTICS_UNFOCUS_STR, target } + data: { component: this.constructor['type'], type: ANALYTICS_UNFOCUS_STR, target: this.fieldTypeToSnakeCase(obj.fieldType) } }); // Call merchant defined callback this.props.onBlur?.(obj); }; + private onErrorAnalytics = obj => { + this.submitAnalytics({ + event: 'info', + data: { + component: this.constructor['type'], + type: ANALYTICS_VALIDATION_ERROR_STR, + target: this.fieldTypeToSnakeCase(obj.fieldType), + validationErrorCode: obj.errorCode, + validationErrorMessage: obj.errorMessage + } + }); + }; + public onBinValue = triggerBinLookUp(this); get isValid() { @@ -278,6 +286,7 @@ export class CardElement extends UIElement { resources={this.resources} onFocus={this.onFocus} onBlur={this.onBlur} + onErrorAnalytics={this.onErrorAnalytics} /> ); } From 7daa146a5c518071f40ef63bf9ff902393c020a3 Mon Sep 17 00:00:00 2001 From: nicholas Date: Wed, 17 Jan 2024 17:38:31 +0100 Subject: [PATCH 62/94] Second draft: adding error analytics events for Credit card fields --- .../Card/components/CardInput/CardInput.tsx | 21 ++++++++++++++++--- .../Card/components/CardInput/types.ts | 1 + packages/lib/src/core/Analytics/constants.ts | 1 + packages/lib/src/core/Analytics/types.ts | 2 ++ packages/lib/src/core/Analytics/utils.ts | 7 ++++++- 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx index 54e7f2dd56..d7e45df057 100644 --- a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx +++ b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx @@ -198,7 +198,6 @@ const CardInput: FunctionalComponent = props => { return; } - // console.log('### CardInput::handleSecuredFieldsChange:: .sfState.errors', sfState.errors); /** * If PAN has just become valid: decide if we can shift focus to the next field. * @@ -404,8 +403,10 @@ const CardInput: FunctionalComponent = props => { // Use the error code to look up whether error is actually a blur based one (most are but some SF ones aren't) const isBlurBasedError = lookupBlurBasedErrors(latestErrorMsg.errorCode); - // Only add blur based errors to the error panel - doing this step prevents the non-blur based errors from being read out twice - // (once from the aria-live, error panel & once from the aria-describedby element) + /** + * ONLY ADD BLUR BASED ERRORS TO THE ERROR PANEL - doing this step prevents the non-blur based errors from being read out twice + * (once from the aria-live, error panel & once from the aria-describedby element) + */ const latestSRError = isBlurBasedError ? latestErrorMsg.errorMessage : null; // console.log('### CardInput2::componentDidUpdate:: #2 (not validating) single error:: latestSRError', latestSRError); setSRMessagesFromStrings(latestSRError); @@ -420,6 +421,20 @@ const CardInput: FunctionalComponent = props => { break; } + // Analytics + const newErrors = getArrayDifferences(currentErrorsSortedByLayout, previousSortedErrors, 'field'); + // console.log('### CardInput:::: new errors', newErrors); + newErrors?.forEach(errorItem => { + const aObj = { + fieldType: errorItem.field, + errorCode: errorItem.errorCode, + errorMessage: errorItem.errorMessage + }; + + // console.log('### CardInput:::: analytics error obj=', aObj); + props.onErrorAnalytics(aObj); + }); + props.onChange({ data, valid, diff --git a/packages/lib/src/components/Card/components/CardInput/types.ts b/packages/lib/src/components/Card/components/CardInput/types.ts index 65aef8712a..8424c02de4 100644 --- a/packages/lib/src/components/Card/components/CardInput/types.ts +++ b/packages/lib/src/components/Card/components/CardInput/types.ts @@ -127,6 +127,7 @@ export interface CardInputProps { type?: string; maskSecurityCode?: boolean; disclaimerMessage?: DisclaimerMsgObject; + onErrorAnalytics?: (obj) => {}; } export interface CardInputState { diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index cf9f5ab83b..7e7c4b54cb 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -10,6 +10,7 @@ export const ANALYTICS_ACTION_STR = 'action'; export const ANALYTICS_SUBMIT_STR = 'submit'; export const ANALYTICS_SELECTED_STR = 'selected'; export const ANALYTICS_RENDERED_STR = 'rendered'; +export const ANALYTICS_VALIDATION_ERROR_STR = 'validationError'; export const ANALYTICS_FOCUS_STR = 'focus'; export const ANALYTICS_UNFOCUS_STR = 'blur'; diff --git a/packages/lib/src/core/Analytics/types.ts b/packages/lib/src/core/Analytics/types.ts index 7417ccd0da..884b5079ed 100644 --- a/packages/lib/src/core/Analytics/types.ts +++ b/packages/lib/src/core/Analytics/types.ts @@ -48,6 +48,8 @@ export interface AnalyticsObject { metadata?: Record; isStoredPaymentMethod?: boolean; brand?: string; + validationErrorCode?: string; + validationErrorMessage?: string; } export type ANALYTICS_EVENT = 'log' | 'error' | 'info'; diff --git a/packages/lib/src/core/Analytics/utils.ts b/packages/lib/src/core/Analytics/utils.ts index de6fbbd3af..7881156348 100644 --- a/packages/lib/src/core/Analytics/utils.ts +++ b/packages/lib/src/core/Analytics/utils.ts @@ -1,5 +1,5 @@ import { AnalyticsObject, CreateAnalyticsObject } from './types'; -import { ANALYTICS_ACTION_STR, ANALYTICS_SUBMIT_STR } from './constants'; +import { ANALYTICS_ACTION_STR, ANALYTICS_SUBMIT_STR, ANALYTICS_VALIDATION_ERROR_STR } from './constants'; export const getUTCTimestamp = () => Date.now(); @@ -33,5 +33,10 @@ export const createAnalyticsObject = (aObj: CreateAnalyticsObject): AnalyticsObj ...(aObj.event === 'log' && aObj.type === ANALYTICS_SUBMIT_STR && { target: aObj.target }), // only added if we have a log object of Submit type ...(aObj.event === 'info' && { type: aObj.type, target: aObj.target }), // only added if we have an info object ...(aObj.event === 'info' && aObj.isStoredPaymentMethod && { isStoredPaymentMethod: aObj.isStoredPaymentMethod, brand: aObj.brand }), // only added if we have an info object about a storedPM + ...(aObj.event === 'info' && + aObj.type === ANALYTICS_VALIDATION_ERROR_STR && { + validationErrorCode: aObj.validationErrorCode, + validationErrorMessage: aObj.validationErrorMessage + }), ...(aObj.metadata && { metadata: aObj.metadata }) }); From 6922725a2a70b39df3163a6668fdf9bc5cd60ee1 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 18 Jan 2024 10:49:22 +0100 Subject: [PATCH 63/94] Second draft: adding error analytics events for Credit card fields --- packages/lib/src/components/Card/Card.tsx | 21 +++++------- .../Card/components/CardInput/CardInput.tsx | 2 +- .../Card/components/CardInput/handlers.ts | 10 +++++- .../Card/components/CardInput/types.ts | 1 + .../Card/components/CardInput/utils.ts | 12 ++++++- packages/lib/src/components/UIElement.tsx | 34 ++++++++++++++++--- 6 files changed, 60 insertions(+), 20 deletions(-) diff --git a/packages/lib/src/components/Card/Card.tsx b/packages/lib/src/components/Card/Card.tsx index 02599e101e..d513030cb9 100644 --- a/packages/lib/src/components/Card/Card.tsx +++ b/packages/lib/src/components/Card/Card.tsx @@ -176,8 +176,8 @@ export class CardElement extends UIElement { private onFocus = obj => { this.submitAnalytics({ - event: 'info', - data: { component: this.constructor['type'], type: ANALYTICS_FOCUS_STR, target: this.fieldTypeToSnakeCase(obj.fieldType) } + type: ANALYTICS_FOCUS_STR, + target: this.fieldTypeToSnakeCase(obj.fieldType) }); // Call merchant defined callback @@ -186,8 +186,8 @@ export class CardElement extends UIElement { private onBlur = obj => { this.submitAnalytics({ - event: 'info', - data: { component: this.constructor['type'], type: ANALYTICS_UNFOCUS_STR, target: this.fieldTypeToSnakeCase(obj.fieldType) } + type: ANALYTICS_UNFOCUS_STR, + target: this.fieldTypeToSnakeCase(obj.fieldType) }); // Call merchant defined callback @@ -196,14 +196,10 @@ export class CardElement extends UIElement { private onErrorAnalytics = obj => { this.submitAnalytics({ - event: 'info', - data: { - component: this.constructor['type'], - type: ANALYTICS_VALIDATION_ERROR_STR, - target: this.fieldTypeToSnakeCase(obj.fieldType), - validationErrorCode: obj.errorCode, - validationErrorMessage: obj.errorMessage - } + type: ANALYTICS_VALIDATION_ERROR_STR, + target: this.fieldTypeToSnakeCase(obj.fieldType), + validationErrorCode: obj.errorCode, + validationErrorMessage: obj.errorMessage }); }; @@ -287,6 +283,7 @@ export class CardElement extends UIElement { onFocus={this.onFocus} onBlur={this.onBlur} onErrorAnalytics={this.onErrorAnalytics} + // onSubmitAnalytics={this.submitAnalytics} /> ); } diff --git a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx index d7e45df057..103c8a8478 100644 --- a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx +++ b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx @@ -140,7 +140,7 @@ const CardInput: FunctionalComponent = props => { * HANDLERS */ // SecuredField-only handler - const handleFocus = getFocusHandler(setFocusedElement, props.onFocus, props.onBlur); + const handleFocus = getFocusHandler(setFocusedElement, props.onFocus, props.onBlur); //, props.onSubmitAnalytics); // Handlers for focus & blur on non-securedFields. Can be renamed to onFieldFocus once the onFocusField is renamed in Field.tsx const onFieldFocusAnalytics = (who, e) => { diff --git a/packages/lib/src/components/Card/components/CardInput/handlers.ts b/packages/lib/src/components/Card/components/CardInput/handlers.ts index 57408cd420..c4a39b3e41 100644 --- a/packages/lib/src/components/Card/components/CardInput/handlers.ts +++ b/packages/lib/src/components/Card/components/CardInput/handlers.ts @@ -1,6 +1,8 @@ import { ENCRYPTED_CARD_NUMBER, CREDIT_CARD_SF_FIELDS } from '../../../internal/SecuredFields/lib/configuration/constants'; import { selectOne } from '../../../internal/SecuredFields/lib/utilities/dom'; import { CbObjOnFocus } from '../../../internal/SecuredFields/lib/types'; +import { ANALYTICS_FOCUS_STR, ANALYTICS_UNFOCUS_STR } from '../../../../core/Analytics/constants'; +import { fieldTypeToSnakeCase } from './utils'; /** * Helper for CardInput - gets a field name and sets focus on it @@ -26,11 +28,17 @@ export const getAddressHandler = (setFormData, setFormValid, setFormErrors) => { }; }; -export const getFocusHandler = (setFocusedElement, onFocus, onBlur) => { +export const getFocusHandler = (setFocusedElement, onFocus, onBlur, onSubmitAnalytics?) => { // Return Handler fn: return (e: CbObjOnFocus) => { setFocusedElement(e.currentFocusObject); e.focus === true ? onFocus(e) : onBlur(e); + // console.log('### handlers:::: ONFOCUS e', e); + + // onSubmitAnalytics({ + // type: e.focus === true ? ANALYTICS_FOCUS_STR : ANALYTICS_UNFOCUS_STR, + // target: fieldTypeToSnakeCase(e.fieldType) + // }); }; }; diff --git a/packages/lib/src/components/Card/components/CardInput/types.ts b/packages/lib/src/components/Card/components/CardInput/types.ts index 8424c02de4..888ffd3190 100644 --- a/packages/lib/src/components/Card/components/CardInput/types.ts +++ b/packages/lib/src/components/Card/components/CardInput/types.ts @@ -128,6 +128,7 @@ export interface CardInputProps { maskSecurityCode?: boolean; disclaimerMessage?: DisclaimerMsgObject; onErrorAnalytics?: (obj) => {}; + onSubmitAnalytics?: (obj) => {}; } export interface CardInputState { diff --git a/packages/lib/src/components/Card/components/CardInput/utils.ts b/packages/lib/src/components/Card/components/CardInput/utils.ts index e31754f41d..e6550a2fdc 100644 --- a/packages/lib/src/components/Card/components/CardInput/utils.ts +++ b/packages/lib/src/components/Card/components/CardInput/utils.ts @@ -15,8 +15,9 @@ import { AddressSpecifications, StringObject } from '../../../internal/Address/t import { PARTIAL_ADDRESS_SCHEMA } from '../../../internal/Address/constants'; import { InstallmentsObj } from './components/Installments/Installments'; import { SFPProps } from '../../../internal/SecuredFields/SFP/types'; -import { BRAND_READABLE_NAME_MAP } from '../../../internal/SecuredFields/lib/configuration/constants'; +import { ALL_SECURED_FIELDS, BRAND_READABLE_NAME_MAP, ENCRYPTED } from '../../../internal/SecuredFields/lib/configuration/constants'; import { UseImageHookType } from '../../../../core/Context/useImage'; +import { camelCaseToSnakeCase } from '../../../../utils/textUtils'; export const getCardImageUrl = (brand: string, getImage: UseImageHookType): string => { const imageOptions = { @@ -177,3 +178,12 @@ export function lookupBlurBasedErrors(errorCode) { export function getFullBrandName(brand) { return BRAND_READABLE_NAME_MAP[brand] ?? brand; } + +export function fieldTypeToSnakeCase(fieldType) { + let str = camelCaseToSnakeCase(fieldType); + // SFs need their fieldType mapped to what the endpoint expects + if (ALL_SECURED_FIELDS.includes(fieldType)) { + str = str.substring(ENCRYPTED.length + 1); // strip 'encrypted_' off the string + } + return str; +} diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 92b9fdea03..734939c154 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -11,7 +11,13 @@ import { hasOwnProperty } from '../utils/hasOwnProperty'; import DropinElement from './Dropin'; import { CoreOptions } from '../core/types'; import Core from '../core'; -import { ANALYTICS_RENDERED_STR, ANALYTICS_SUBMIT_STR } from '../core/Analytics/constants'; +import { + ANALYTICS_FOCUS_STR, + ANALYTICS_RENDERED_STR, + ANALYTICS_SUBMIT_STR, + ANALYTICS_UNFOCUS_STR, + ANALYTICS_VALIDATION_ERROR_STR +} from '../core/Analytics/constants'; import { AnalyticsInitialEvent } from '../core/Analytics/types'; export class UIElement

extends BaseElement

implements IUIElement { @@ -76,7 +82,9 @@ export class UIElement

extends BaseElement

im component = this.constructor['type'] === 'scheme' || this.constructor['type'] === 'bcmc' ? this.constructor['type'] : this.props.type; } - switch (analyticsObj.type) { + const { type, target } = analyticsObj; + + switch (type) { // Called from BaseElement (when component mounted) or, from DropinComponent (after mounting, when it has finished resolving all the PM promises) // &/or, from DropinComponent when a PM is selected case ANALYTICS_RENDERED_STR: { @@ -91,7 +99,7 @@ export class UIElement

extends BaseElement

im } } - const data = { component, type: analyticsObj.type, ...storedCardIndicator }; + const data = { component, type, ...storedCardIndicator }; // AnalyticsAction: action: 'event' type:'rendered'|'selected' this.props.modules?.analytics.createAnalyticsEvent({ @@ -101,11 +109,27 @@ export class UIElement

extends BaseElement

im break; } - case ANALYTICS_SUBMIT_STR: { + case ANALYTICS_SUBMIT_STR: // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' this.props.modules?.analytics.createAnalyticsEvent({ event: 'log', - data: { component, type: analyticsObj.type, target: 'payButton', message: 'Shopper clicked pay' } + data: { component, type, target: 'payButton', message: 'Shopper clicked pay' } + }); + break; + + case ANALYTICS_FOCUS_STR: + case ANALYTICS_UNFOCUS_STR: + this.props.modules?.analytics.createAnalyticsEvent({ + event: 'info', + data: { component, type, target } + }); + break; + + case ANALYTICS_VALIDATION_ERROR_STR: { + const { validationErrorCode, validationErrorMessage } = analyticsObj; + this.props.modules?.analytics.createAnalyticsEvent({ + event: 'info', + data: { component, type, target, validationErrorCode, validationErrorMessage } }); break; } From d779ea9e506b829c015769da305492aaea7f7661 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 18 Jan 2024 12:29:45 +0100 Subject: [PATCH 64/94] Using UIElement.submitAnalytics as a gateway for all analytics events (to set event type and create final analytics worthy objects) --- packages/lib/src/components/Card/Card.tsx | 21 ++++++++++++++++--- .../Card/components/CardInput/CardInput.tsx | 9 ++++---- .../Card/components/CardInput/handlers.ts | 13 +++--------- .../Card/components/CardInput/types.ts | 1 - .../Card/components/CardInput/utils.ts | 12 +---------- 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/packages/lib/src/components/Card/Card.tsx b/packages/lib/src/components/Card/Card.tsx index d513030cb9..db9cb8f4a6 100644 --- a/packages/lib/src/components/Card/Card.tsx +++ b/packages/lib/src/components/Card/Card.tsx @@ -181,7 +181,15 @@ export class CardElement extends UIElement { }); // Call merchant defined callback - this.props.onFocus?.(obj); + + // TODO - decide if this is a breaking change now we call onFocus/onBlur callbacks for non-SF fields + // if so: then for v5 don't call this callback for for non-SF fields (and only send obj.event) + if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { + this.props.onFocus?.(obj.event); + } + + // v6 version + // this.props.onFocus?.(obj); }; private onBlur = obj => { @@ -191,7 +199,15 @@ export class CardElement extends UIElement { }); // Call merchant defined callback - this.props.onBlur?.(obj); + + // TODO - decide if this is a breaking change now we call onFocus/onBlur callbacks for non-SF fields + // if so: then for v5 don't call this callback for for non-SF fields (and only send obj.event) + if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { + this.props.onBlur?.(obj.event); + } + + // v6 version + // this.props.onBlur?.(obj); }; private onErrorAnalytics = obj => { @@ -283,7 +299,6 @@ export class CardElement extends UIElement { onFocus={this.onFocus} onBlur={this.onBlur} onErrorAnalytics={this.onErrorAnalytics} - // onSubmitAnalytics={this.submitAnalytics} /> ); } diff --git a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx index 103c8a8478..85293b3a13 100644 --- a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx +++ b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx @@ -139,10 +139,7 @@ const CardInput: FunctionalComponent = props => { /** * HANDLERS */ - // SecuredField-only handler - const handleFocus = getFocusHandler(setFocusedElement, props.onFocus, props.onBlur); //, props.onSubmitAnalytics); - - // Handlers for focus & blur on non-securedFields. Can be renamed to onFieldFocus once the onFocusField is renamed in Field.tsx + // Handlers for focus & blur on all fields. Can be renamed to onFieldFocus once the onFocusField is renamed in Field.tsx const onFieldFocusAnalytics = (who, e) => { props.onFocus({ fieldType: who, event: e }); }; @@ -150,6 +147,10 @@ const CardInput: FunctionalComponent = props => { props.onBlur({ fieldType: who, event: e }); }; + // Make SecuredFields aware of the focus & blur handlers + // const handleFocus = getFocusHandler(setFocusedElement, props.onFocus, props.onBlur); + const handleFocus = getFocusHandler(setFocusedElement, onFieldFocusAnalytics, onFieldBlurAnalytics); + const retrieveLayout = (): string[] => { return getLayout({ props, diff --git a/packages/lib/src/components/Card/components/CardInput/handlers.ts b/packages/lib/src/components/Card/components/CardInput/handlers.ts index c4a39b3e41..e4ebd39878 100644 --- a/packages/lib/src/components/Card/components/CardInput/handlers.ts +++ b/packages/lib/src/components/Card/components/CardInput/handlers.ts @@ -1,8 +1,6 @@ import { ENCRYPTED_CARD_NUMBER, CREDIT_CARD_SF_FIELDS } from '../../../internal/SecuredFields/lib/configuration/constants'; import { selectOne } from '../../../internal/SecuredFields/lib/utilities/dom'; import { CbObjOnFocus } from '../../../internal/SecuredFields/lib/types'; -import { ANALYTICS_FOCUS_STR, ANALYTICS_UNFOCUS_STR } from '../../../../core/Analytics/constants'; -import { fieldTypeToSnakeCase } from './utils'; /** * Helper for CardInput - gets a field name and sets focus on it @@ -28,17 +26,12 @@ export const getAddressHandler = (setFormData, setFormValid, setFormErrors) => { }; }; -export const getFocusHandler = (setFocusedElement, onFocus, onBlur, onSubmitAnalytics?) => { +export const getFocusHandler = (setFocusedElement, onFocus, onBlur) => { // Return Handler fn: return (e: CbObjOnFocus) => { setFocusedElement(e.currentFocusObject); - e.focus === true ? onFocus(e) : onBlur(e); - // console.log('### handlers:::: ONFOCUS e', e); - - // onSubmitAnalytics({ - // type: e.focus === true ? ANALYTICS_FOCUS_STR : ANALYTICS_UNFOCUS_STR, - // target: fieldTypeToSnakeCase(e.fieldType) - // }); + // e.focus === true ? onFocus(e) : onBlur(e); + e.focus === true ? onFocus(e.fieldType, e) : onBlur(e.fieldType, e); }; }; diff --git a/packages/lib/src/components/Card/components/CardInput/types.ts b/packages/lib/src/components/Card/components/CardInput/types.ts index 888ffd3190..8424c02de4 100644 --- a/packages/lib/src/components/Card/components/CardInput/types.ts +++ b/packages/lib/src/components/Card/components/CardInput/types.ts @@ -128,7 +128,6 @@ export interface CardInputProps { maskSecurityCode?: boolean; disclaimerMessage?: DisclaimerMsgObject; onErrorAnalytics?: (obj) => {}; - onSubmitAnalytics?: (obj) => {}; } export interface CardInputState { diff --git a/packages/lib/src/components/Card/components/CardInput/utils.ts b/packages/lib/src/components/Card/components/CardInput/utils.ts index e6550a2fdc..e31754f41d 100644 --- a/packages/lib/src/components/Card/components/CardInput/utils.ts +++ b/packages/lib/src/components/Card/components/CardInput/utils.ts @@ -15,9 +15,8 @@ import { AddressSpecifications, StringObject } from '../../../internal/Address/t import { PARTIAL_ADDRESS_SCHEMA } from '../../../internal/Address/constants'; import { InstallmentsObj } from './components/Installments/Installments'; import { SFPProps } from '../../../internal/SecuredFields/SFP/types'; -import { ALL_SECURED_FIELDS, BRAND_READABLE_NAME_MAP, ENCRYPTED } from '../../../internal/SecuredFields/lib/configuration/constants'; +import { BRAND_READABLE_NAME_MAP } from '../../../internal/SecuredFields/lib/configuration/constants'; import { UseImageHookType } from '../../../../core/Context/useImage'; -import { camelCaseToSnakeCase } from '../../../../utils/textUtils'; export const getCardImageUrl = (brand: string, getImage: UseImageHookType): string => { const imageOptions = { @@ -178,12 +177,3 @@ export function lookupBlurBasedErrors(errorCode) { export function getFullBrandName(brand) { return BRAND_READABLE_NAME_MAP[brand] ?? brand; } - -export function fieldTypeToSnakeCase(fieldType) { - let str = camelCaseToSnakeCase(fieldType); - // SFs need their fieldType mapped to what the endpoint expects - if (ALL_SECURED_FIELDS.includes(fieldType)) { - str = str.substring(ENCRYPTED.length + 1); // strip 'encrypted_' off the string - } - return str; -} From 80c1259e64b7948b9405eb999430725781151d83 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 18 Jan 2024 15:06:44 +0100 Subject: [PATCH 65/94] Added comment about onFocus/onBlur callbacks now working for non-SFs --- packages/lib/src/components/Card/Card.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/lib/src/components/Card/Card.tsx b/packages/lib/src/components/Card/Card.tsx index db9cb8f4a6..24bb54c52a 100644 --- a/packages/lib/src/components/Card/Card.tsx +++ b/packages/lib/src/components/Card/Card.tsx @@ -175,6 +175,7 @@ export class CardElement extends UIElement { } private onFocus = obj => { + // console.log('### Card::onFocus:: fieldType', obj.fieldType, 'target', this.fieldTypeToSnakeCase(obj.fieldType)); this.submitAnalytics({ type: ANALYTICS_FOCUS_STR, target: this.fieldTypeToSnakeCase(obj.fieldType) @@ -184,12 +185,14 @@ export class CardElement extends UIElement { // TODO - decide if this is a breaking change now we call onFocus/onBlur callbacks for non-SF fields // if so: then for v5 don't call this callback for for non-SF fields (and only send obj.event) - if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { - this.props.onFocus?.(obj.event); - } + // if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { + // this.props.onFocus?.(obj.event); + // } else { + // this.props.onFocus?.(obj); + // } // v6 version - // this.props.onFocus?.(obj); + this.props.onFocus?.(obj); }; private onBlur = obj => { @@ -202,12 +205,12 @@ export class CardElement extends UIElement { // TODO - decide if this is a breaking change now we call onFocus/onBlur callbacks for non-SF fields // if so: then for v5 don't call this callback for for non-SF fields (and only send obj.event) - if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { - this.props.onBlur?.(obj.event); - } + // if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { + // this.props.onBlur?.(obj.event); + // } // v6 version - // this.props.onBlur?.(obj); + this.props.onBlur?.(obj); }; private onErrorAnalytics = obj => { From 691ead3416897597133f0089768c306b5ad90733 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 18 Jan 2024 15:18:36 +0100 Subject: [PATCH 66/94] Changed constant ANALYTICS_UNFOCUS_STR to have value "unfocus" --- packages/lib/src/core/Analytics/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index 7e7c4b54cb..620ef85cd5 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -13,7 +13,7 @@ export const ANALYTICS_RENDERED_STR = 'rendered'; export const ANALYTICS_VALIDATION_ERROR_STR = 'validationError'; export const ANALYTICS_FOCUS_STR = 'focus'; -export const ANALYTICS_UNFOCUS_STR = 'blur'; +export const ANALYTICS_UNFOCUS_STR = 'unfocus'; export const ANALYTICS_IMPLEMENTATION_ERROR = 'ImplementationError'; export const ANALYTICS_API_ERROR = 'APIError'; From 1aab30dfd4e975be68bc365be08124d03e817050 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 18 Jan 2024 15:19:42 +0100 Subject: [PATCH 67/94] Non dropdown fields for Address also have focus/blur analytics --- .../components/CardInput/components/CardFieldsWrapper.tsx | 2 ++ packages/lib/src/components/internal/Address/Address.tsx | 2 ++ .../components/internal/Address/components/FieldContainer.tsx | 2 ++ packages/lib/src/components/internal/Address/types.ts | 4 ++++ 4 files changed, 10 insertions(+) diff --git a/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx b/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx index 4f5768efba..1201098fcb 100644 --- a/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx +++ b/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx @@ -172,6 +172,8 @@ export const CardFieldsWrapper = ({ onAddressLookup={onAddressLookup} onAddressSelected={onAddressSelected} addressSearchDebounceMs={addressSearchDebounceMs} + onFieldFocusAnalytics={onFieldFocusAnalytics} + onFieldBlurAnalytics={onFieldBlurAnalytics} /> )} diff --git a/packages/lib/src/components/internal/Address/Address.tsx b/packages/lib/src/components/internal/Address/Address.tsx index ea23d1861d..4bd3870928 100644 --- a/packages/lib/src/components/internal/Address/Address.tsx +++ b/packages/lib/src/components/internal/Address/Address.tsx @@ -157,6 +157,8 @@ export default function Address(props: AddressProps) { maxLength={getMaxLengthByFieldAndCountry(countrySpecificFormatters, fieldName, data.country, true)} trimOnBlur={true} disabled={!enabledFields.includes(fieldName)} + onFieldFocusAnalytics={props.onFieldFocusAnalytics} + onFieldBlurAnalytics={props.onFieldBlurAnalytics} /> ); }; diff --git a/packages/lib/src/components/internal/Address/components/FieldContainer.tsx b/packages/lib/src/components/internal/Address/components/FieldContainer.tsx index 2f8d0a8363..b555e5eb18 100644 --- a/packages/lib/src/components/internal/Address/components/FieldContainer.tsx +++ b/packages/lib/src/components/internal/Address/components/FieldContainer.tsx @@ -66,6 +66,8 @@ function FieldContainer(props: FieldContainerProps) { isValid={valid[fieldName]} name={fieldName} i18n={i18n} + onFocus={e => props.onFieldFocusAnalytics(fieldName, e)} + onBlur={e => props.onFieldBlurAnalytics(fieldName, e)} > {}; showPayButton?: boolean; setComponentRef?: (ref) => void; + onFieldFocusAnalytics?: (who: string, event: Event) => void; + onFieldBlurAnalytics?: (who: string, event: Event) => void; } export interface AddressLookupItem extends AddressData { @@ -60,6 +62,8 @@ export interface FieldContainerProps { maxLength?: number; trimOnBlur?: boolean; disabled?: boolean; + onFieldFocusAnalytics?: (who: string, event: Event) => void; + onFieldBlurAnalytics?: (who: string, event: Event) => void; } export interface ReadOnlyAddressProps { From 630ae5e92bfd88a75fda7c1c7c55a29b06f8e1aa Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 18 Jan 2024 15:31:39 +0100 Subject: [PATCH 68/94] Reduce the analytics info event timer to 5 secs when in development mode --- packages/lib/src/core/Analytics/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/core/Analytics/constants.ts b/packages/lib/src/core/Analytics/constants.ts index 620ef85cd5..df049938d5 100644 --- a/packages/lib/src/core/Analytics/constants.ts +++ b/packages/lib/src/core/Analytics/constants.ts @@ -1,6 +1,6 @@ export const ANALYTICS_PATH = 'v3/analytics'; -export const ANALYTICS_INFO_TIMER_INTERVAL = 10000; +export const ANALYTICS_INFO_TIMER_INTERVAL = process.env.NODE_ENV === 'development' ? 5000 : 10000; export const ANALYTICS_EVENT_LOG = 'log'; export const ANALYTICS_EVENT_ERROR = 'error'; From b96f01143b8631408ed01e6e25f51677c2ab0977 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 18 Jan 2024 17:21:19 +0100 Subject: [PATCH 69/94] Aligning some analytics values with what the endpoint expects --- packages/lib/src/components/UIElement.tsx | 2 +- packages/lib/src/core/core.ts | 2 +- packages/playground/src/pages/Cards/Cards.js | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 734939c154..eebd3c92ec 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -113,7 +113,7 @@ export class UIElement

extends BaseElement

im // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' this.props.modules?.analytics.createAnalyticsEvent({ event: 'log', - data: { component, type, target: 'payButton', message: 'Shopper clicked pay' } + data: { component, type, target: 'pay_button', message: 'Shopper clicked pay' } }); break; diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index 70680d2e2b..8a568724fc 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -155,7 +155,7 @@ class Core { component: `${action.type}${action.subtype ?? ''}`, type: ANALYTICS_ACTION_STR, subtype: capitalizeFirstLetter(action.type), - message: `${action.type}${action.subtype ?? ''} is initiating` + message: `${action.type}${action.subtype ?? ''} action was handle by the SDK` } }); diff --git a/packages/playground/src/pages/Cards/Cards.js b/packages/playground/src/pages/Cards/Cards.js index 5907a63b94..eaaa4a97c0 100644 --- a/packages/playground/src/pages/Cards/Cards.js +++ b/packages/playground/src/pages/Cards/Cards.js @@ -8,7 +8,7 @@ import '../../style.scss'; import { MockReactApp } from './MockReactApp'; import { searchFunctionExample } from '../../utils'; -const onlyShowCard = false; +const onlyShowCard = true; const showComps = { clickToPay: true, @@ -66,6 +66,7 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = if (onlyShowCard || showComps.card) { window.card = checkout .create('card', { + // brands: ['visa', 'amex', 'bcmc', 'maestro'], challengeWindowSize: '01', _disableClickToPay: true, // hasHolderName: true, From 9d2533fe53d02969e49e060416955ba5e04005cb Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 18 Jan 2024 17:22:49 +0100 Subject: [PATCH 70/94] Fixing typo --- packages/lib/src/core/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/src/core/core.ts b/packages/lib/src/core/core.ts index 8a568724fc..a6e99146b9 100644 --- a/packages/lib/src/core/core.ts +++ b/packages/lib/src/core/core.ts @@ -155,7 +155,7 @@ class Core { component: `${action.type}${action.subtype ?? ''}`, type: ANALYTICS_ACTION_STR, subtype: capitalizeFirstLetter(action.type), - message: `${action.type}${action.subtype ?? ''} action was handle by the SDK` + message: `${action.type}${action.subtype ?? ''} action was handled by the SDK` } }); From c9ad485f8dc13364965dd95880fbb5f55cfc14aa Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 18 Jan 2024 17:58:08 +0100 Subject: [PATCH 71/94] Clauses added so unit tests pass --- .../Card/components/CardInput/CardInput.tsx | 24 +++++++++---------- .../SocialSecurityNumberBrazil.tsx | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx index 85293b3a13..4eb35907e1 100644 --- a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx +++ b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx @@ -423,18 +423,18 @@ const CardInput: FunctionalComponent = props => { } // Analytics - const newErrors = getArrayDifferences(currentErrorsSortedByLayout, previousSortedErrors, 'field'); - // console.log('### CardInput:::: new errors', newErrors); - newErrors?.forEach(errorItem => { - const aObj = { - fieldType: errorItem.field, - errorCode: errorItem.errorCode, - errorMessage: errorItem.errorMessage - }; - - // console.log('### CardInput:::: analytics error obj=', aObj); - props.onErrorAnalytics(aObj); - }); + if (currentErrorsSortedByLayout) { + const newErrors = getArrayDifferences(currentErrorsSortedByLayout, previousSortedErrors, 'field'); + newErrors?.forEach(errorItem => { + const aObj = { + fieldType: errorItem.field, + errorCode: errorItem.errorCode, + errorMessage: errorItem.errorMessage + }; + + props.onErrorAnalytics(aObj); + }); + } props.onChange({ data, diff --git a/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx b/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx index 6e4b2dd8e3..a76de06b64 100644 --- a/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx +++ b/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx @@ -23,8 +23,8 @@ export default function ({ errorMessage={error && error.errorMessage ? i18n.get(error.errorMessage) : !!error} isValid={Boolean(valid)} name={'socialSecurityNumber'} - onFocus={e => onFieldFocusAnalytics('socialSecurityNumber', e)} - onBlur={e => onFieldBlurAnalytics('socialSecurityNumber', e)} + onFocus={e => onFieldFocusAnalytics?.('socialSecurityNumber', e)} + onBlur={e => onFieldBlurAnalytics?.('socialSecurityNumber', e)} > Date: Thu, 18 Jan 2024 19:00:25 +0100 Subject: [PATCH 72/94] Adding analytics for when an instant PM button is pressed --- packages/lib/src/components/ApplePay/ApplePay.tsx | 6 ++++++ packages/lib/src/components/GooglePay/GooglePay.tsx | 6 ++++++ packages/lib/src/components/UIElement.tsx | 8 ++++++++ 3 files changed, 20 insertions(+) diff --git a/packages/lib/src/components/ApplePay/ApplePay.tsx b/packages/lib/src/components/ApplePay/ApplePay.tsx index e8a6eecf7e..138faeebab 100644 --- a/packages/lib/src/components/ApplePay/ApplePay.tsx +++ b/packages/lib/src/components/ApplePay/ApplePay.tsx @@ -10,6 +10,7 @@ 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_SELECTED_STR } from '../../core/Analytics/constants'; const latestSupportedVersion = 14; @@ -53,6 +54,11 @@ class ApplePayElement extends UIElement { } submit() { + // Analytics + if (this.props.isInstantPayment) { + this.submitAnalytics({ type: ANALYTICS_SELECTED_STR, target: 'instant_payment_button' }); + } + return this.startSession(this.props.onAuthorized); } diff --git a/packages/lib/src/components/GooglePay/GooglePay.tsx b/packages/lib/src/components/GooglePay/GooglePay.tsx index 2bbce671c8..01300e309d 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.tsx +++ b/packages/lib/src/components/GooglePay/GooglePay.tsx @@ -7,6 +7,7 @@ import { GooglePayProps } from './types'; import { mapBrands, getGooglePayLocale } from './utils'; import collectBrowserInfo from '../../utils/browserInfo'; import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError'; +import { ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants'; class GooglePay extends UIElement { public static type = 'paywithgoogle'; @@ -45,6 +46,11 @@ class GooglePay extends UIElement { } public submit = () => { + // Analytics + if (this.props.isInstantPayment) { + this.submitAnalytics({ type: ANALYTICS_SELECTED_STR, target: 'instant_payment_button' }); + } + const { onAuthorized = () => {} } = this.props; return new Promise((resolve, reject) => this.props.onClick(resolve, reject)) diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index eebd3c92ec..ae0c80d269 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -14,6 +14,7 @@ import Core from '../core'; import { ANALYTICS_FOCUS_STR, ANALYTICS_RENDERED_STR, + ANALYTICS_SELECTED_STR, ANALYTICS_SUBMIT_STR, ANALYTICS_UNFOCUS_STR, ANALYTICS_VALIDATION_ERROR_STR @@ -134,6 +135,13 @@ export class UIElement

extends BaseElement

im break; } + case ANALYTICS_SELECTED_STR: + this.props.modules?.analytics.createAnalyticsEvent({ + event: 'info', + data: { component, type, target } + }); + break; + default: { this.props.modules?.analytics.createAnalyticsEvent(analyticsObj); } From 2b1d742ad245680e5cba65602f074281365f8479 Mon Sep 17 00:00:00 2001 From: nicholas Date: Thu, 18 Jan 2024 19:04:44 +0100 Subject: [PATCH 73/94] Fixed TS error --- .../SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx b/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx index a76de06b64..828f38bceb 100644 --- a/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx +++ b/packages/lib/src/components/internal/SocialSecurityNumberBrazil/SocialSecurityNumberBrazil.tsx @@ -11,8 +11,8 @@ export default function ({ data = '', required = false, disabled = false, - onFieldFocusAnalytics, - onFieldBlurAnalytics + onFieldFocusAnalytics = null, + onFieldBlurAnalytics = null }) { const { i18n } = useCoreContext(); From 950de66a1e6bf4beb6780bad9b054b7045741f88 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 19 Jan 2024 09:43:28 +0100 Subject: [PATCH 74/94] Removing unused code --- .../lib/src/components/Card/components/CardInput/CardInput.tsx | 1 - .../lib/src/components/Card/components/CardInput/handlers.ts | 1 - packages/playground/src/pages/Cards/Cards.js | 3 +-- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx index 4eb35907e1..801fbfcb55 100644 --- a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx +++ b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx @@ -148,7 +148,6 @@ const CardInput: FunctionalComponent = props => { }; // Make SecuredFields aware of the focus & blur handlers - // const handleFocus = getFocusHandler(setFocusedElement, props.onFocus, props.onBlur); const handleFocus = getFocusHandler(setFocusedElement, onFieldFocusAnalytics, onFieldBlurAnalytics); const retrieveLayout = (): string[] => { diff --git a/packages/lib/src/components/Card/components/CardInput/handlers.ts b/packages/lib/src/components/Card/components/CardInput/handlers.ts index e4ebd39878..5ed0dcd751 100644 --- a/packages/lib/src/components/Card/components/CardInput/handlers.ts +++ b/packages/lib/src/components/Card/components/CardInput/handlers.ts @@ -30,7 +30,6 @@ export const getFocusHandler = (setFocusedElement, onFocus, onBlur) => { // Return Handler fn: return (e: CbObjOnFocus) => { setFocusedElement(e.currentFocusObject); - // e.focus === true ? onFocus(e) : onBlur(e); e.focus === true ? onFocus(e.fieldType, e) : onBlur(e.fieldType, e); }; }; diff --git a/packages/playground/src/pages/Cards/Cards.js b/packages/playground/src/pages/Cards/Cards.js index eaaa4a97c0..5907a63b94 100644 --- a/packages/playground/src/pages/Cards/Cards.js +++ b/packages/playground/src/pages/Cards/Cards.js @@ -8,7 +8,7 @@ import '../../style.scss'; import { MockReactApp } from './MockReactApp'; import { searchFunctionExample } from '../../utils'; -const onlyShowCard = true; +const onlyShowCard = false; const showComps = { clickToPay: true, @@ -66,7 +66,6 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = if (onlyShowCard || showComps.card) { window.card = checkout .create('card', { - // brands: ['visa', 'amex', 'bcmc', 'maestro'], challengeWindowSize: '01', _disableClickToPay: true, // hasHolderName: true, From 63937f0b11589799e4eb010c2929031e3574e2e8 Mon Sep 17 00:00:00 2001 From: nicholas Date: Fri, 19 Jan 2024 10:11:47 +0100 Subject: [PATCH 75/94] Keep object sent to onFocus & onBlur callbacks in the form expected by v5 users --- packages/lib/src/components/Card/Card.tsx | 30 ++++++++--------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/lib/src/components/Card/Card.tsx b/packages/lib/src/components/Card/Card.tsx index 24bb54c52a..3bea74f8e5 100644 --- a/packages/lib/src/components/Card/Card.tsx +++ b/packages/lib/src/components/Card/Card.tsx @@ -182,17 +182,11 @@ export class CardElement extends UIElement { }); // Call merchant defined callback - - // TODO - decide if this is a breaking change now we call onFocus/onBlur callbacks for non-SF fields - // if so: then for v5 don't call this callback for for non-SF fields (and only send obj.event) - // if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { - // this.props.onFocus?.(obj.event); - // } else { - // this.props.onFocus?.(obj); - // } - - // v6 version - this.props.onFocus?.(obj); + if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { + this.props.onFocus?.(obj.event); + } else { + this.props.onFocus?.(obj); + } }; private onBlur = obj => { @@ -202,15 +196,11 @@ export class CardElement extends UIElement { }); // Call merchant defined callback - - // TODO - decide if this is a breaking change now we call onFocus/onBlur callbacks for non-SF fields - // if so: then for v5 don't call this callback for for non-SF fields (and only send obj.event) - // if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { - // this.props.onBlur?.(obj.event); - // } - - // v6 version - this.props.onBlur?.(obj); + if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { + this.props.onBlur?.(obj.event); + } else { + this.props.onBlur?.(obj); + } }; private onErrorAnalytics = obj => { From fff6f0abb7766e9031a062b0655e459f7f9482f5 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 22 Jan 2024 14:43:16 +0100 Subject: [PATCH 76/94] Moving logic to create different types of Analytics events into the Analytics module --- .../Dropin/components/DropinComponent.tsx | 12 +-- packages/lib/src/components/UIElement.tsx | 82 ++++--------------- packages/lib/src/components/types.ts | 3 +- packages/lib/src/core/Analytics/Analytics.ts | 75 ++++++++++++++++- packages/lib/src/core/Analytics/types.ts | 5 ++ 5 files changed, 94 insertions(+), 83 deletions(-) diff --git a/packages/lib/src/components/Dropin/components/DropinComponent.tsx b/packages/lib/src/components/Dropin/components/DropinComponent.tsx index 8fb1a72ac4..1ca0cefac3 100644 --- a/packages/lib/src/components/Dropin/components/DropinComponent.tsx +++ b/packages/lib/src/components/Dropin/components/DropinComponent.tsx @@ -32,17 +32,7 @@ export class DropinComponent extends Component e.props.type), // TODO might be added (used to be in original analytics, in the setup call) - }; - - // AnalyticsAction: action: 'event' type:'rendered' - this.props.modules?.analytics.createAnalyticsEvent({ - event: 'info', - data - }); + this.props.modules?.analytics.sendAnalytics('dropin', { type: ANALYTICS_RENDERED_STR }); } ); diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index ae0c80d269..2730d1d743 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -11,14 +11,7 @@ import { hasOwnProperty } from '../utils/hasOwnProperty'; import DropinElement from './Dropin'; import { CoreOptions } from '../core/types'; import Core from '../core'; -import { - ANALYTICS_FOCUS_STR, - ANALYTICS_RENDERED_STR, - ANALYTICS_SELECTED_STR, - ANALYTICS_SUBMIT_STR, - ANALYTICS_UNFOCUS_STR, - ANALYTICS_VALIDATION_ERROR_STR -} from '../core/Analytics/constants'; +import { ANALYTICS_RENDERED_STR, ANALYTICS_SUBMIT_STR } from '../core/Analytics/constants'; import { AnalyticsInitialEvent } from '../core/Analytics/types'; export class UIElement

extends BaseElement

implements IUIElement { @@ -83,69 +76,22 @@ export class UIElement

extends BaseElement

im component = this.constructor['type'] === 'scheme' || this.constructor['type'] === 'bcmc' ? this.constructor['type'] : this.props.type; } - const { type, target } = analyticsObj; - - switch (type) { - // Called from BaseElement (when component mounted) or, from DropinComponent (after mounting, when it has finished resolving all the PM promises) - // &/or, from DropinComponent when a PM is selected - case ANALYTICS_RENDERED_STR: { - let storedCardIndicator; - // Check if it's a storedCard - if (component === 'scheme') { - if (hasOwnProperty(this.props, 'supportedShopperInteractions')) { - storedCardIndicator = { - isStoredPaymentMethod: true, - brand: this.props.brand - }; - } + const { type } = analyticsObj; + + let storedCardIndicator; + if (type === ANALYTICS_RENDERED_STR) { + // Check if it's a storedCard + if (component === 'scheme') { + if (hasOwnProperty(this.props, 'supportedShopperInteractions')) { + storedCardIndicator = { + isStoredPaymentMethod: true, + brand: this.props.brand + }; } - - const data = { component, type, ...storedCardIndicator }; - - // AnalyticsAction: action: 'event' type:'rendered'|'selected' - this.props.modules?.analytics.createAnalyticsEvent({ - event: 'info', - data - }); - break; - } - - case ANALYTICS_SUBMIT_STR: - // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' - this.props.modules?.analytics.createAnalyticsEvent({ - event: 'log', - data: { component, type, target: 'pay_button', message: 'Shopper clicked pay' } - }); - break; - - case ANALYTICS_FOCUS_STR: - case ANALYTICS_UNFOCUS_STR: - this.props.modules?.analytics.createAnalyticsEvent({ - event: 'info', - data: { component, type, target } - }); - break; - - case ANALYTICS_VALIDATION_ERROR_STR: { - const { validationErrorCode, validationErrorMessage } = analyticsObj; - this.props.modules?.analytics.createAnalyticsEvent({ - event: 'info', - data: { component, type, target, validationErrorCode, validationErrorMessage } - }); - break; - } - - case ANALYTICS_SELECTED_STR: - this.props.modules?.analytics.createAnalyticsEvent({ - event: 'info', - data: { component, type, target } - }); - break; - - default: { - this.props.modules?.analytics.createAnalyticsEvent(analyticsObj); } } + + this.props.modules?.analytics.sendAnalytics(component, analyticsObj, storedCardIndicator); } private onSubmit(): void { diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index 6d6a3ecb80..ff1b7e1a39 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -8,7 +8,7 @@ import { PayButtonProps } from './internal/PayButton/PayButton'; import Session from '../core/CheckoutSession'; import { SRPanel } from '../core/Errors/SRPanel'; import { Resources } from '../core/Context/Resources'; -import { AnalyticsInitialEvent, CreateAnalyticsEventObject } from '../core/Analytics/types'; +import { AnalyticsInitialEvent, CreateAnalyticsEventObject, StoredCardIndicator } from '../core/Analytics/types'; import { EventsQueueModule } from '../core/Analytics/EventsQueue'; export interface PaymentMethodData { @@ -83,6 +83,7 @@ export interface AnalyticsModule { getEventsQueue: () => EventsQueueModule; createAnalyticsEvent: (a: CreateAnalyticsEventObject) => void; getEnabled: () => boolean; + sendAnalytics: (component: string, analyticsObj: any, storedCardIndicator?: StoredCardIndicator) => void; } export interface BaseElementProps { diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index 3b4b70e296..fa4dbcc213 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -1,8 +1,20 @@ import LogEvent from '../Services/analytics/log-event'; import CollectId from '../Services/analytics/collect-id'; import EventsQueue, { EventsQueueModule } from './EventsQueue'; -import { ANALYTICS_EVENT, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps, CreateAnalyticsEventObject } from './types'; -import { ANALYTICS_EVENT_ERROR, ANALYTICS_EVENT_INFO, ANALYTICS_EVENT_LOG, ANALYTICS_INFO_TIMER_INTERVAL, ANALYTICS_PATH } from './constants'; +import { ANALYTICS_EVENT, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps, CreateAnalyticsEventObject, StoredCardIndicator } from './types'; +import { + ANALYTICS_EVENT_ERROR, + ANALYTICS_EVENT_INFO, + ANALYTICS_EVENT_LOG, + ANALYTICS_FOCUS_STR, + ANALYTICS_INFO_TIMER_INTERVAL, + ANALYTICS_PATH, + ANALYTICS_RENDERED_STR, + ANALYTICS_SELECTED_STR, + ANALYTICS_SUBMIT_STR, + ANALYTICS_UNFOCUS_STR, + ANALYTICS_VALIDATION_ERROR_STR +} from './constants'; import { debounce } from '../../components/internal/Address/utils'; import { AnalyticsModule } from '../../components/types'; import { createAnalyticsObject } from './utils'; @@ -109,7 +121,64 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy addAnalyticsEvent(event, aObj); }, - getEnabled: () => props.enabled + getEnabled: () => props.enabled, + + sendAnalytics: (component: string, analyticsObj: any, storedCardIndicator?: StoredCardIndicator) => { + const { type, target } = analyticsObj; + + // analyticsObj type: type, target, validationErrorCode, validationErrorMessage, storedCardIndicator? + + switch (type) { + // Called from BaseElement (when component mounted) or, from DropinComponent (after mounting, when it has finished resolving all the PM promises) + // &/or, from DropinComponent when a PM is selected + case ANALYTICS_RENDERED_STR: { + const data = { component, type, ...storedCardIndicator }; + + // AnalyticsAction: action: 'event' type:'rendered'|'selected' + anlModule.createAnalyticsEvent({ + event: 'info', + data + }); + break; + } + + case ANALYTICS_SUBMIT_STR: + // PM pay button pressed - AnalyticsAction: action: 'log' type:'submit' + anlModule.createAnalyticsEvent({ + event: 'log', + data: { component, type, target: 'pay_button', message: 'Shopper clicked pay' } + }); + break; + + case ANALYTICS_FOCUS_STR: + case ANALYTICS_UNFOCUS_STR: + anlModule.createAnalyticsEvent({ + event: 'info', + data: { component, type, target } + }); + break; + + case ANALYTICS_VALIDATION_ERROR_STR: { + const { validationErrorCode, validationErrorMessage } = analyticsObj; + anlModule.createAnalyticsEvent({ + event: 'info', + data: { component, type, target, validationErrorCode, validationErrorMessage } + }); + break; + } + + case ANALYTICS_SELECTED_STR: + anlModule.createAnalyticsEvent({ + event: 'info', + data: { component, type, target } + }); + break; + + default: { + anlModule.createAnalyticsEvent(analyticsObj); + } + } + } }; return anlModule; diff --git a/packages/lib/src/core/Analytics/types.ts b/packages/lib/src/core/Analytics/types.ts index 884b5079ed..019770cdf3 100644 --- a/packages/lib/src/core/Analytics/types.ts +++ b/packages/lib/src/core/Analytics/types.ts @@ -80,3 +80,8 @@ export type CreateAnalyticsEventObject = { }; export type EventQueueProps = Pick & { analyticsPath: string }; + +export type StoredCardIndicator = { + isStoredPaymentMethod: boolean; + brand: string; +}; From 1edfaa294bc0eb8839361b5a583ca0bacb75a1c6 Mon Sep 17 00:00:00 2001 From: nicholas Date: Mon, 22 Jan 2024 15:22:16 +0100 Subject: [PATCH 77/94] Improving types --- packages/lib/src/components/Card/Card.tsx | 15 ++++++++------- .../Card/components/CardInput/CardInput.tsx | 8 +++++--- .../components/Card/components/CardInput/types.ts | 3 ++- packages/lib/src/components/Card/types.ts | 11 ++++++++--- packages/lib/src/components/UIElement.tsx | 14 ++++++-------- packages/lib/src/components/types.ts | 10 ++++++++-- packages/lib/src/core/Analytics/Analytics.ts | 11 +++++------ packages/lib/src/core/Analytics/types.ts | 12 +++++++++--- 8 files changed, 51 insertions(+), 33 deletions(-) diff --git a/packages/lib/src/components/Card/Card.tsx b/packages/lib/src/components/Card/Card.tsx index 3bea74f8e5..8f3519deff 100644 --- a/packages/lib/src/components/Card/Card.tsx +++ b/packages/lib/src/components/Card/Card.tsx @@ -5,18 +5,19 @@ import CoreProvider from '../../core/Context/CoreProvider'; import collectBrowserInfo from '../../utils/browserInfo'; import { BinLookupResponse, CardElementData, CardElementProps } from './types'; import triggerBinLookUp from '../internal/SecuredFields/binLookup/triggerBinLookUp'; -import { CbObjOnBinLookup } from '../internal/SecuredFields/lib/types'; +import { CbObjOnBinLookup, CbObjOnFocus } from '../internal/SecuredFields/lib/types'; import { reject } from '../internal/SecuredFields/utils'; import { hasValidInstallmentsObject } from './components/CardInput/utils'; import createClickToPayService from '../internal/ClickToPay/services/create-clicktopay-service'; import { ClickToPayCheckoutPayload, IClickToPayService } from '../internal/ClickToPay/services/types'; import ClickToPayWrapper from './components/ClickToPayWrapper'; -import { PayButtonFunctionProps, UIElementStatus } from '../types'; +import { ComponentFocusObject, PayButtonFunctionProps, UIElementStatus } from '../types'; import SRPanelProvider from '../../core/Errors/SRPanelProvider'; import PayButton from '../internal/PayButton'; import { ANALYTICS_FOCUS_STR, ANALYTICS_UNFOCUS_STR, ANALYTICS_VALIDATION_ERROR_STR } from '../../core/Analytics/constants'; import { ALL_SECURED_FIELDS, ENCRYPTED } from '../internal/SecuredFields/lib/configuration/constants'; import { camelCaseToSnakeCase } from '../../utils/textUtils'; +import { FieldErrorAnalyticsObject } from '../../core/Analytics/types'; export class CardElement extends UIElement { public static type = 'scheme'; @@ -174,7 +175,7 @@ export class CardElement extends UIElement { return str; } - private onFocus = obj => { + private onFocus = (obj: ComponentFocusObject) => { // console.log('### Card::onFocus:: fieldType', obj.fieldType, 'target', this.fieldTypeToSnakeCase(obj.fieldType)); this.submitAnalytics({ type: ANALYTICS_FOCUS_STR, @@ -183,13 +184,13 @@ export class CardElement extends UIElement { // Call merchant defined callback if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { - this.props.onFocus?.(obj.event); + this.props.onFocus?.(obj.event as CbObjOnFocus); } else { this.props.onFocus?.(obj); } }; - private onBlur = obj => { + private onBlur = (obj: ComponentFocusObject) => { this.submitAnalytics({ type: ANALYTICS_UNFOCUS_STR, target: this.fieldTypeToSnakeCase(obj.fieldType) @@ -197,13 +198,13 @@ export class CardElement extends UIElement { // Call merchant defined callback if (ALL_SECURED_FIELDS.includes(obj.fieldType)) { - this.props.onBlur?.(obj.event); + this.props.onBlur?.(obj.event as CbObjOnFocus); } else { this.props.onBlur?.(obj); } }; - private onErrorAnalytics = obj => { + private onErrorAnalytics = (obj: FieldErrorAnalyticsObject) => { this.submitAnalytics({ type: ANALYTICS_VALIDATION_ERROR_STR, target: this.fieldTypeToSnakeCase(obj.fieldType), diff --git a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx index 801fbfcb55..437d0a2b25 100644 --- a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx +++ b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx @@ -30,6 +30,8 @@ import { SetSRMessagesReturnFn } from '../../../../core/Errors/SRPanelProvider'; import useImage from '../../../../core/Context/useImage'; import { getArrayDifferences } from '../../../../utils/arrayUtils'; import FormInstruction from '../../../internal/FormInstruction'; +import { CbObjOnFocus } from '../../../internal/SecuredFields/lib/types'; +import { FieldErrorAnalyticsObject } from '../../../../core/Analytics/types'; const CardInput: FunctionalComponent = props => { const sfp = useRef(null); @@ -140,10 +142,10 @@ const CardInput: FunctionalComponent = props => { * HANDLERS */ // Handlers for focus & blur on all fields. Can be renamed to onFieldFocus once the onFocusField is renamed in Field.tsx - const onFieldFocusAnalytics = (who, e) => { + const onFieldFocusAnalytics = (who: string, e: Event | CbObjOnFocus) => { props.onFocus({ fieldType: who, event: e }); }; - const onFieldBlurAnalytics = (who, e) => { + const onFieldBlurAnalytics = (who: string, e: Event | CbObjOnFocus) => { props.onBlur({ fieldType: who, event: e }); }; @@ -425,7 +427,7 @@ const CardInput: FunctionalComponent = props => { if (currentErrorsSortedByLayout) { const newErrors = getArrayDifferences(currentErrorsSortedByLayout, previousSortedErrors, 'field'); newErrors?.forEach(errorItem => { - const aObj = { + const aObj: FieldErrorAnalyticsObject = { fieldType: errorItem.field, errorCode: errorItem.errorCode, errorMessage: errorItem.errorMessage diff --git a/packages/lib/src/components/Card/components/CardInput/types.ts b/packages/lib/src/components/Card/components/CardInput/types.ts index 8424c02de4..91e17bde82 100644 --- a/packages/lib/src/components/Card/components/CardInput/types.ts +++ b/packages/lib/src/components/Card/components/CardInput/types.ts @@ -13,6 +13,7 @@ import RiskElement from '../../../../core/RiskModule'; import { AnalyticsModule, ComponentMethodsRef } from '../../../types'; import { DisclaimerMsgObject } from '../../../internal/DisclaimerMessage/DisclaimerMessage'; import { OnAddressLookupType, OnAddressSelectedType } from '../../../internal/Address/components/AddressSearch'; +import { FieldErrorAnalyticsObject } from '../../../../core/Analytics/types'; export interface CardInputValidState { holderName?: boolean; @@ -127,7 +128,7 @@ export interface CardInputProps { type?: string; maskSecurityCode?: boolean; disclaimerMessage?: DisclaimerMsgObject; - onErrorAnalytics?: (obj) => {}; + onErrorAnalytics?: (obj: FieldErrorAnalyticsObject) => {}; } export interface CardInputState { diff --git a/packages/lib/src/components/Card/types.ts b/packages/lib/src/components/Card/types.ts index c9043e6681..78588cb596 100644 --- a/packages/lib/src/components/Card/types.ts +++ b/packages/lib/src/components/Card/types.ts @@ -1,4 +1,4 @@ -import { UIElementProps } from '../types'; +import { ComponentFocusObject, UIElementProps } from '../types'; import { AddressData, BrowserInfo } from '../../types'; import { CbObjOnBinValue, @@ -119,9 +119,14 @@ export interface CardElementProps extends UIElementProps { onError?: (event: CbObjOnError) => void; /** - * Called when a field gains or loses focus. + * Called when a field gains focus. */ - onFocus?: (event: CbObjOnFocus) => void; + onFocus?: (event: CbObjOnFocus | ComponentFocusObject) => void; + + /** + * Called when a field gains loses focus. + */ + onBlur?: (event: CbObjOnFocus | ComponentFocusObject) => void; /** * Provides the BIN Number of the card (up to 6 digits), called as the user types in the PAN. diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 2730d1d743..e3264ab7d9 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -12,7 +12,7 @@ import DropinElement from './Dropin'; import { CoreOptions } from '../core/types'; import Core from '../core'; import { ANALYTICS_RENDERED_STR, ANALYTICS_SUBMIT_STR } from '../core/Analytics/constants'; -import { AnalyticsInitialEvent } from '../core/Analytics/types'; +import { AnalyticsInitialEvent, SendAnalyticsObject } from '../core/Analytics/types'; export class UIElement

extends BaseElement

implements IUIElement { protected componentRef: any; @@ -66,7 +66,7 @@ export class UIElement

extends BaseElement

im * In some other cases e.g. 3DS2 components, this function is overridden to allow more specific analytics actions to be created */ /* eslint-disable-next-line */ - protected submitAnalytics(analyticsObj: any) { + protected submitAnalytics(analyticsObj: SendAnalyticsObject) { /** Work out what the component's "type" is: * - first check for a dedicated "analyticsType" (currently only applies to custom-cards) * - otherwise, distinguish cards from non-cards: cards will use their static type property, everything else will use props.type @@ -78,20 +78,18 @@ export class UIElement

extends BaseElement

im const { type } = analyticsObj; - let storedCardIndicator; + // let storedCardIndicator; if (type === ANALYTICS_RENDERED_STR) { // Check if it's a storedCard if (component === 'scheme') { if (hasOwnProperty(this.props, 'supportedShopperInteractions')) { - storedCardIndicator = { - isStoredPaymentMethod: true, - brand: this.props.brand - }; + analyticsObj.isStoredPaymentMethod = true; + analyticsObj.brand = this.props.brand; } } } - this.props.modules?.analytics.sendAnalytics(component, analyticsObj, storedCardIndicator); + this.props.modules?.analytics.sendAnalytics(component, analyticsObj); } private onSubmit(): void { diff --git a/packages/lib/src/components/types.ts b/packages/lib/src/components/types.ts index ff1b7e1a39..8f2521adf9 100644 --- a/packages/lib/src/components/types.ts +++ b/packages/lib/src/components/types.ts @@ -8,8 +8,9 @@ import { PayButtonProps } from './internal/PayButton/PayButton'; import Session from '../core/CheckoutSession'; import { SRPanel } from '../core/Errors/SRPanel'; import { Resources } from '../core/Context/Resources'; -import { AnalyticsInitialEvent, CreateAnalyticsEventObject, StoredCardIndicator } from '../core/Analytics/types'; +import { AnalyticsInitialEvent, CreateAnalyticsEventObject, SendAnalyticsObject } from '../core/Analytics/types'; import { EventsQueueModule } from '../core/Analytics/EventsQueue'; +import { CbObjOnFocus } from './internal/SecuredFields/lib/types'; export interface PaymentMethodData { paymentMethod: { @@ -83,7 +84,7 @@ export interface AnalyticsModule { getEventsQueue: () => EventsQueueModule; createAnalyticsEvent: (a: CreateAnalyticsEventObject) => void; getEnabled: () => boolean; - sendAnalytics: (component: string, analyticsObj: any, storedCardIndicator?: StoredCardIndicator) => void; + sendAnalytics: (component: string, analyticsObj: SendAnalyticsObject) => void; } export interface BaseElementProps { @@ -188,3 +189,8 @@ export interface ComponentMethodsRef { showValidation?: () => void; setStatus?(status: UIElementStatus): void; } + +export type ComponentFocusObject = { + fieldType: string; + event: Event | CbObjOnFocus; +}; diff --git a/packages/lib/src/core/Analytics/Analytics.ts b/packages/lib/src/core/Analytics/Analytics.ts index fa4dbcc213..3dd667989b 100644 --- a/packages/lib/src/core/Analytics/Analytics.ts +++ b/packages/lib/src/core/Analytics/Analytics.ts @@ -1,7 +1,7 @@ import LogEvent from '../Services/analytics/log-event'; import CollectId from '../Services/analytics/collect-id'; import EventsQueue, { EventsQueueModule } from './EventsQueue'; -import { ANALYTICS_EVENT, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps, CreateAnalyticsEventObject, StoredCardIndicator } from './types'; +import { ANALYTICS_EVENT, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps, CreateAnalyticsEventObject, SendAnalyticsObject } from './types'; import { ANALYTICS_EVENT_ERROR, ANALYTICS_EVENT_INFO, @@ -123,16 +123,15 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy getEnabled: () => props.enabled, - sendAnalytics: (component: string, analyticsObj: any, storedCardIndicator?: StoredCardIndicator) => { + sendAnalytics: (component: string, analyticsObj: SendAnalyticsObject) => { const { type, target } = analyticsObj; - // analyticsObj type: type, target, validationErrorCode, validationErrorMessage, storedCardIndicator? - switch (type) { // Called from BaseElement (when component mounted) or, from DropinComponent (after mounting, when it has finished resolving all the PM promises) // &/or, from DropinComponent when a PM is selected case ANALYTICS_RENDERED_STR: { - const data = { component, type, ...storedCardIndicator }; + const { isStoredPaymentMethod, brand } = analyticsObj; + const data = { component, type, isStoredPaymentMethod, brand }; // AnalyticsAction: action: 'event' type:'rendered'|'selected' anlModule.createAnalyticsEvent({ @@ -175,7 +174,7 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy break; default: { - anlModule.createAnalyticsEvent(analyticsObj); + anlModule.createAnalyticsEvent(analyticsObj as CreateAnalyticsEventObject); } } } diff --git a/packages/lib/src/core/Analytics/types.ts b/packages/lib/src/core/Analytics/types.ts index 019770cdf3..f3b9b5e1de 100644 --- a/packages/lib/src/core/Analytics/types.ts +++ b/packages/lib/src/core/Analytics/types.ts @@ -81,7 +81,13 @@ export type CreateAnalyticsEventObject = { export type EventQueueProps = Pick & { analyticsPath: string }; -export type StoredCardIndicator = { - isStoredPaymentMethod: boolean; - brand: string; +export type SendAnalyticsObject = Pick< + AnalyticsObject, + 'type' | 'target' | 'validationErrorCode' | 'validationErrorMessage' | 'isStoredPaymentMethod' | 'brand' +>; + +export type FieldErrorAnalyticsObject = { + fieldType: string; + errorCode: string; + errorMessage: string; }; From 2e568714f0b254fe123bb06d22c04c65d382dfe6 Mon Sep 17 00:00:00 2001 From: nicholas Date: Wed, 24 Jan 2024 12:13:30 +0100 Subject: [PATCH 78/94] Redeclare BaseElement.this._node *before* we render (was causing an issue in CustomCard) --- packages/lib/src/components/BaseElement.ts | 8 +++++--- .../src/pages/SecuredFields/securedFields.config.js | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index a9b46e7c83..aa94213929 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -102,16 +102,20 @@ class BaseElement

{ 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 } + 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 (!this._node) { + if (setupAnalytics) { if (this.props.modules && this.props.modules.analytics) { this.setUpAnalytics({ containerWidth: node && (node as HTMLElement).offsetWidth, @@ -128,8 +132,6 @@ class BaseElement

{ } } - this._node = node; - return this; } diff --git a/packages/playground/src/pages/SecuredFields/securedFields.config.js b/packages/playground/src/pages/SecuredFields/securedFields.config.js index 921b6c4be6..bf74c908e7 100644 --- a/packages/playground/src/pages/SecuredFields/securedFields.config.js +++ b/packages/playground/src/pages/SecuredFields/securedFields.config.js @@ -280,7 +280,7 @@ export function onChange(state, component) { } } -const setErrorClasses = function(pNode, pSetErrors) { +const setErrorClasses = function (pNode, pSetErrors) { if (pSetErrors) { if (pNode.className.indexOf('pm-input-field--error') === -1) { pNode.className += ' pm-input-field--error'; @@ -295,7 +295,7 @@ const setErrorClasses = function(pNode, pSetErrors) { } }; -const setFocusClasses = function(pNode, pSetFocus) { +const setFocusClasses = function (pNode, pSetFocus) { if (pSetFocus) { if (pNode.className.indexOf('pm-input-field--focus') === -1) { pNode.className += ' pm-input-field--focus'; From c2a0306fb456ff6d8c0420a80348707252bcd4aa Mon Sep 17 00:00:00 2001 From: sponglord Date: Tue, 30 Jan 2024 11:05:58 +0100 Subject: [PATCH 79/94] Checkoutanalytics mvp with 3DS2 events (#2531) * Add 3DS2 analytics events for data sent & iframe loaded. Plus error event when paymentData missing in 3DS2 * Using UIElement.submitAnalytics for 3DS2 * Fixed clause (had been changed to test error) * Move app specific analytics logic to a separate "processing" function. This separates concerns and makes testing easier * core.ts uses sendAnalytics function * Comments added regarding what properties are added to what analytics objects * Redeclare BaseElement.this._node *before* we render (was causing an issue in CustomCard) * Card has onConfigSuccess function to send analytics info event (type="configured") when SFS have all configured * Adding analytics logs for when 3DS2 fingerprint or challenge complete * Also pass isStoredPaymentMethod and brand with "configured" analytics events for storedCards * Added unit tests for Card & GooglePay testing the shape of the analytics objects they generate * Added unit tests for 3DS2 errors - testing the shape of the analytics objects they generate * Fixing type * Jumping through hoops for sonarcloud * Jumping through hoops for sonarcloud * Removing console.log * Specifying type. Move card related, "is it a stored card" logic, out of UIElement * Fixing linting * Fixing linting, tightening up onSubmitAnalytics type * After SDKs meeting: removing target prop from "submit" log events * Added "focus" & "unfocus" info events to Custom Card comp --- packages/lib/src/components/BaseElement.ts | 4 +- .../components/Card/Card.Analytics.test.tsx | 169 ++++++++++++++++++ packages/lib/src/components/Card/Card.tsx | 53 ++++-- .../components/GooglePay/GooglePay.test.ts | 36 ++++ .../SecuredFields/SecuredFields.tsx | 16 +- .../ThreeDS2/ThreeDS2Challenge.test.tsx | 53 ++++++ .../components/ThreeDS2/ThreeDS2Challenge.tsx | 25 ++- .../ThreeDS2DeviceFingerprint.test.tsx | 54 ++++++ .../ThreeDS2/ThreeDS2DeviceFingerprint.tsx | 43 ++++- .../components/Challenge/DoChallenge3DS2.tsx | 19 +- .../Challenge/PrepareChallenge3DS2.tsx | 31 +++- .../ThreeDS2/components/Challenge/types.ts | 3 + .../DeviceFingerprint/DoFingerprint3DS2.tsx | 7 +- .../PrepareFingerprint3DS2.test.tsx | 2 +- .../PrepareFingerprint3DS2.tsx | 45 ++++- .../components/DeviceFingerprint/types.ts | 3 + .../components/Form/ThreeDS2Form.test.tsx | 2 +- .../ThreeDS2/components/Form/ThreeDS2Form.tsx | 2 + .../lib/src/components/ThreeDS2/config.ts | 12 ++ packages/lib/src/components/UIElement.tsx | 15 +- .../lib/CSF/extensions/createSecuredFields.ts | 9 +- .../internal/SecuredFields/utils.ts | 34 ++-- .../lib/src/core/Analytics/Analytics.test.ts | 2 + packages/lib/src/core/Analytics/Analytics.ts | 75 +------- .../core/Analytics/analyticsPreProcessor.ts | 96 ++++++++++ packages/lib/src/core/Analytics/constants.ts | 2 + packages/lib/src/core/Analytics/types.ts | 5 +- packages/lib/src/core/Analytics/utils.ts | 30 ++-- .../PaymentAction/actionTypes.ts | 2 +- packages/lib/src/core/core.ts | 18 +- 30 files changed, 683 insertions(+), 184 deletions(-) create mode 100644 packages/lib/src/components/Card/Card.Analytics.test.tsx create mode 100644 packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.test.tsx create mode 100644 packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.test.tsx create mode 100644 packages/lib/src/core/Analytics/analyticsPreProcessor.ts diff --git a/packages/lib/src/components/BaseElement.ts b/packages/lib/src/components/BaseElement.ts index aa94213929..2784cafef4 100644 --- a/packages/lib/src/components/BaseElement.ts +++ b/packages/lib/src/components/BaseElement.ts @@ -6,7 +6,7 @@ import Core from '../core'; import { BaseElementProps, PaymentData } from './types'; import { RiskData } from '../core/RiskModule/RiskModule'; import { Resources } from '../core/Context/Resources'; -import { AnalyticsInitialEvent } from '../core/Analytics/types'; +import { AnalyticsInitialEvent, SendAnalyticsObject } from '../core/Analytics/types'; import { ANALYTICS_RENDERED_STR } from '../core/Analytics/constants'; class BaseElement

{ @@ -54,7 +54,7 @@ class BaseElement

{ } /* eslint-disable-next-line */ - protected submitAnalytics(analyticsObj?: any) { + protected submitAnalytics(analyticsObj?: SendAnalyticsObject) { return null; } diff --git a/packages/lib/src/components/Card/Card.Analytics.test.tsx b/packages/lib/src/components/Card/Card.Analytics.test.tsx new file mode 100644 index 0000000000..0158de0217 --- /dev/null +++ b/packages/lib/src/components/Card/Card.Analytics.test.tsx @@ -0,0 +1,169 @@ +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(obj => { + console.log('### analyticsPreProcessor.test:::: obj=', obj); + }); + }); + + 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', + errorMessage: 'Enter the complete card number-sr' + }); + + 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: 'Enter the complete card number-sr' + } + }); + }); +}); + +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 + } + }); + + analyticsModule.createAnalyticsEvent = jest.fn(obj => { + console.log('### analyticsPreProcessor.test:::: obj=', obj); + }); + }); + + 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' + } + }); + }); +}); diff --git a/packages/lib/src/components/Card/Card.tsx b/packages/lib/src/components/Card/Card.tsx index 8f3519deff..dac9414e8e 100644 --- a/packages/lib/src/components/Card/Card.tsx +++ b/packages/lib/src/components/Card/Card.tsx @@ -5,8 +5,8 @@ import CoreProvider from '../../core/Context/CoreProvider'; import collectBrowserInfo from '../../utils/browserInfo'; import { BinLookupResponse, CardElementData, CardElementProps } from './types'; import triggerBinLookUp from '../internal/SecuredFields/binLookup/triggerBinLookUp'; -import { CbObjOnBinLookup, CbObjOnFocus } from '../internal/SecuredFields/lib/types'; -import { reject } from '../internal/SecuredFields/utils'; +import { CbObjOnBinLookup, CbObjOnConfigSuccess, CbObjOnFocus } from '../internal/SecuredFields/lib/types'; +import { fieldTypeToSnakeCase, reject } from '../internal/SecuredFields/utils'; import { hasValidInstallmentsObject } from './components/CardInput/utils'; import createClickToPayService from '../internal/ClickToPay/services/create-clicktopay-service'; import { ClickToPayCheckoutPayload, IClickToPayService } from '../internal/ClickToPay/services/types'; @@ -14,10 +14,16 @@ import ClickToPayWrapper from './components/ClickToPayWrapper'; import { ComponentFocusObject, PayButtonFunctionProps, UIElementStatus } from '../types'; import SRPanelProvider from '../../core/Errors/SRPanelProvider'; import PayButton from '../internal/PayButton'; -import { ANALYTICS_FOCUS_STR, ANALYTICS_UNFOCUS_STR, ANALYTICS_VALIDATION_ERROR_STR } from '../../core/Analytics/constants'; -import { ALL_SECURED_FIELDS, ENCRYPTED } from '../internal/SecuredFields/lib/configuration/constants'; -import { camelCaseToSnakeCase } from '../../utils/textUtils'; -import { FieldErrorAnalyticsObject } from '../../core/Analytics/types'; +import { + ANALYTICS_FOCUS_STR, + ANALYTICS_CONFIGURED_STR, + ANALYTICS_UNFOCUS_STR, + ANALYTICS_VALIDATION_ERROR_STR, + ANALYTICS_RENDERED_STR +} from '../../core/Analytics/constants'; +import { ALL_SECURED_FIELDS } from '../internal/SecuredFields/lib/configuration/constants'; +import { FieldErrorAnalyticsObject, SendAnalyticsObject } from '../../core/Analytics/types'; +import { hasOwnProperty } from '../../utils/hasOwnProperty'; export class CardElement extends UIElement { public static type = 'scheme'; @@ -166,20 +172,34 @@ export class CardElement extends UIElement { } } - private fieldTypeToSnakeCase(fieldType) { - let str = camelCaseToSnakeCase(fieldType); - // SFs need their fieldType mapped to what the endpoint expects - if (ALL_SECURED_FIELDS.includes(fieldType)) { - str = str.substring(ENCRYPTED.length + 1); // strip 'encrypted_' off the string + protected submitAnalytics(analyticsObj: SendAnalyticsObject) { + const { type } = analyticsObj; + + if (type === ANALYTICS_RENDERED_STR || type === ANALYTICS_CONFIGURED_STR) { + // Check if it's a storedCard + if (this.constructor['type'] === 'scheme') { + if (hasOwnProperty(this.props, 'supportedShopperInteractions')) { + analyticsObj.isStoredPaymentMethod = true; + analyticsObj.brand = this.props.brand; + } + } } - return str; + + super.submitAnalytics(analyticsObj); } + private onConfigSuccess = (obj: CbObjOnConfigSuccess) => { + this.submitAnalytics({ + type: ANALYTICS_CONFIGURED_STR + }); + + this.props.onConfigSuccess?.(obj); + }; + private onFocus = (obj: ComponentFocusObject) => { - // console.log('### Card::onFocus:: fieldType', obj.fieldType, 'target', this.fieldTypeToSnakeCase(obj.fieldType)); this.submitAnalytics({ type: ANALYTICS_FOCUS_STR, - target: this.fieldTypeToSnakeCase(obj.fieldType) + target: fieldTypeToSnakeCase(obj.fieldType) }); // Call merchant defined callback @@ -193,7 +213,7 @@ export class CardElement extends UIElement { private onBlur = (obj: ComponentFocusObject) => { this.submitAnalytics({ type: ANALYTICS_UNFOCUS_STR, - target: this.fieldTypeToSnakeCase(obj.fieldType) + target: fieldTypeToSnakeCase(obj.fieldType) }); // Call merchant defined callback @@ -207,7 +227,7 @@ export class CardElement extends UIElement { private onErrorAnalytics = (obj: FieldErrorAnalyticsObject) => { this.submitAnalytics({ type: ANALYTICS_VALIDATION_ERROR_STR, - target: this.fieldTypeToSnakeCase(obj.fieldType), + target: fieldTypeToSnakeCase(obj.fieldType), validationErrorCode: obj.errorCode, validationErrorMessage: obj.errorMessage }); @@ -293,6 +313,7 @@ export class CardElement extends UIElement { onFocus={this.onFocus} onBlur={this.onBlur} onErrorAnalytics={this.onErrorAnalytics} + onConfigSuccess={this.onConfigSuccess} /> ); } diff --git a/packages/lib/src/components/GooglePay/GooglePay.test.ts b/packages/lib/src/components/GooglePay/GooglePay.test.ts index a3676a1d1a..79939532b2 100644 --- a/packages/lib/src/components/GooglePay/GooglePay.test.ts +++ b/packages/lib/src/components/GooglePay/GooglePay.test.ts @@ -1,4 +1,8 @@ import GooglePay from './GooglePay'; +import Analytics from '../../core/Analytics'; +import { ANALYTICS_EVENT_INFO, ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants'; + +const analyticsModule = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '' }); describe('GooglePay', () => { describe('get data', () => { @@ -66,4 +70,36 @@ describe('GooglePay', () => { expect(gpay.props.configuration.authJwt).toEqual('jwt.code'); }); }); + + describe('GooglePay: calls that generate "info" analytics should produce objects with the expected shapes ', () => { + let gpay; + beforeEach(() => { + console.log = jest.fn(() => {}); + + gpay = new GooglePay({ + type: 'googlepay', + isInstantPayment: true, + modules: { + analytics: analyticsModule + } + }); + + analyticsModule.createAnalyticsEvent = jest.fn(obj => { + console.log('### analyticsPreProcessor.test:::: obj=', obj); + }); + }); + + test('Analytics should produce an "info" event, of type "selected", for GooglePay as an instant PM', () => { + gpay.submit(); + + expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({ + event: ANALYTICS_EVENT_INFO, + data: { + component: gpay.props.type, + type: ANALYTICS_SELECTED_STR, + target: 'instant_payment_button' + } + }); + }); + }); }); diff --git a/packages/lib/src/components/SecuredFields/SecuredFields.tsx b/packages/lib/src/components/SecuredFields/SecuredFields.tsx index 54605ac607..22bd9c53ef 100644 --- a/packages/lib/src/components/SecuredFields/SecuredFields.tsx +++ b/packages/lib/src/components/SecuredFields/SecuredFields.tsx @@ -4,9 +4,10 @@ import SecuredFields from './SecuredFieldsInput'; import CoreProvider from '../../core/Context/CoreProvider'; import collectBrowserInfo from '../../utils/browserInfo'; import triggerBinLookUp from '../internal/SecuredFields/binLookup/triggerBinLookUp'; -import { CbObjOnBinLookup } from '../internal/SecuredFields/lib/types'; +import { CbObjOnBinLookup, CbObjOnFocus } from '../internal/SecuredFields/lib/types'; import { BrandObject } from '../Card/types'; -import { getCardImageUrl } from '../internal/SecuredFields/utils'; +import { fieldTypeToSnakeCase, getCardImageUrl } from '../internal/SecuredFields/utils'; +import { ANALYTICS_FOCUS_STR, ANALYTICS_UNFOCUS_STR } from '../../core/Analytics/constants'; export class SecuredFieldsElement extends UIElement { public static type = 'scheme'; @@ -89,6 +90,16 @@ export class SecuredFieldsElement extends UIElement { return collectBrowserInfo(); } + private onFocus = (obj: CbObjOnFocus) => { + this.submitAnalytics({ + type: obj.focus === true ? ANALYTICS_FOCUS_STR : ANALYTICS_UNFOCUS_STR, + target: fieldTypeToSnakeCase(obj.fieldType) + }); + + // Call merchant defined callback + this.props.onFocus?.(obj); + }; + render() { return ( @@ -103,6 +114,7 @@ export class SecuredFieldsElement extends UIElement { onBinValue={this.onBinValue} implementationType={'custom'} resources={this.resources} + onFocus={this.onFocus} /> ); diff --git a/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.test.tsx b/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.test.tsx new file mode 100644 index 0000000000..229d1c2a8c --- /dev/null +++ b/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.test.tsx @@ -0,0 +1,53 @@ +import { ThreeDS2Challenge } from './index'; +import Analytics from '../../core/Analytics'; +import { + ANALYTICS_API_ERROR, + ANALYTICS_ERROR_CODE_ACTION_IS_MISSING_PAYMENT_DATA, + ANALYTICS_EVENT_ERROR, + ANALYTICS_RENDERED_STR +} from '../../core/Analytics/constants'; +import { THREEDS2_CHALLENGE_ERROR, THREEDS2_ERROR } from './config'; + +const analyticsModule = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '' }); + +describe('ThreeDS2Challenge: calls that generate analytics should produce objects with the expected shapes ', () => { + let challenge; + beforeEach(() => { + console.log = jest.fn(() => {}); + + challenge = new ThreeDS2Challenge({ + onActionHandled: () => {}, + modules: { + analytics: analyticsModule + }, + onError: () => {} + }); + + analyticsModule.createAnalyticsEvent = jest.fn(obj => { + console.log('### analyticsPreProcessor.test:::: obj=', obj); + }); + }); + + test('A call to ThreeDS2Challenge.submitAnalytics with an object with type "rendered" should not lead to an analytics event', () => { + challenge.submitAnalytics({ type: ANALYTICS_RENDERED_STR }); + + expect(analyticsModule.createAnalyticsEvent).not.toHaveBeenCalled(); + }); + + test('ThreeDS2Challenge instantiated without paymentData should generate an analytics error', () => { + const view = challenge.render(); + + expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({ + event: ANALYTICS_EVENT_ERROR, + data: { + component: challenge.constructor['type'], + type: THREEDS2_ERROR, + errorType: ANALYTICS_API_ERROR, + message: `${THREEDS2_CHALLENGE_ERROR}: Missing 'paymentData' property from threeDS2 action`, + code: ANALYTICS_ERROR_CODE_ACTION_IS_MISSING_PAYMENT_DATA + } + }); + + expect(view).toBe(null); + }); +}); diff --git a/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx b/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx index 6cef7b0093..3821a34648 100644 --- a/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx +++ b/packages/lib/src/components/ThreeDS2/ThreeDS2Challenge.tsx @@ -2,11 +2,13 @@ import { h } from 'preact'; import UIElement from '../UIElement'; import PrepareChallenge from './components/Challenge'; import { ErrorCodeObject } from './components/utils'; -import { DEFAULT_CHALLENGE_WINDOW_SIZE } from './config'; +import { DEFAULT_CHALLENGE_WINDOW_SIZE, THREEDS2_CHALLENGE, THREEDS2_CHALLENGE_ERROR, THREEDS2_ERROR } from './config'; import { existy } from '../internal/SecuredFields/lib/utilities/commonUtils'; import { hasOwnProperty } from '../../utils/hasOwnProperty'; import Language from '../../language'; -import { ActionHandledReturnObject } from '../types'; +import { ActionHandledReturnObject, AnalyticsModule } from '../types'; +import { ANALYTICS_API_ERROR, ANALYTICS_ERROR_CODE_ACTION_IS_MISSING_PAYMENT_DATA, ANALYTICS_RENDERED_STR } from '../../core/Analytics/constants'; +import { SendAnalyticsObject } from '../../core/Analytics/types'; export interface ThreeDS2ChallengeProps { token?: string; @@ -21,6 +23,7 @@ export interface ThreeDS2ChallengeProps { useOriginalFlow?: boolean; i18n?: Language; onActionHandled: (rtnObj: ActionHandledReturnObject) => void; + modules?: { analytics: AnalyticsModule }; } class ThreeDS2Challenge extends UIElement { @@ -29,7 +32,13 @@ class ThreeDS2Challenge extends UIElement { public static defaultProps = { dataKey: 'threeDSResult', size: DEFAULT_CHALLENGE_WINDOW_SIZE, - type: 'ChallengeShopper' + type: THREEDS2_CHALLENGE + }; + + protected submitAnalytics = (aObj: SendAnalyticsObject) => { + if (aObj.type === ANALYTICS_RENDERED_STR) return; // suppress the rendered event (it will have the same timestamp as the "creq sent" event) + + super.submitAnalytics(aObj); }; onComplete(state) { @@ -48,10 +57,18 @@ class ThreeDS2Challenge extends UIElement { const dataTypeForError = hasOwnProperty(this.props, 'useOriginalFlow') ? 'paymentData' : 'authorisationToken'; this.props.onError({ errorCode: 'threeds2.challenge', message: `No ${dataTypeForError} received. Challenge cannot proceed` }); + + this.submitAnalytics({ + type: THREEDS2_ERROR, + code: ANALYTICS_ERROR_CODE_ACTION_IS_MISSING_PAYMENT_DATA, + errorType: ANALYTICS_API_ERROR, + message: `${THREEDS2_CHALLENGE_ERROR}: Missing 'paymentData' property from threeDS2 action` + }); + return null; } - return ; + return ; } } diff --git a/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.test.tsx b/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.test.tsx new file mode 100644 index 0000000000..49c7e3981b --- /dev/null +++ b/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.test.tsx @@ -0,0 +1,54 @@ +import { ThreeDS2DeviceFingerprint } from './index'; +import Analytics from '../../core/Analytics'; +import { + ANALYTICS_API_ERROR, + ANALYTICS_ERROR_CODE_ACTION_IS_MISSING_PAYMENT_DATA, + ANALYTICS_EVENT_ERROR, + ANALYTICS_RENDERED_STR +} from '../../core/Analytics/constants'; +import { THREEDS2_ERROR, THREEDS2_FINGERPRINT_ERROR } from './config'; + +const analyticsModule = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '' }); + +describe('ThreeDS2DeviceFingerprint: calls that generate analytics should produce objects with the expected shapes ', () => { + let fingerprint; + beforeEach(() => { + console.log = jest.fn(() => {}); + + fingerprint = new ThreeDS2DeviceFingerprint({ + onActionHandled: () => {}, + modules: { + analytics: analyticsModule + }, + onError: () => {}, + showSpinner: null + }); + + analyticsModule.createAnalyticsEvent = jest.fn(obj => { + console.log('### analyticsPreProcessor.test:::: obj=', obj); + }); + }); + + test('A call to ThreeDS2DeviceFingerprint.submitAnalytics with an object with type "rendered" should not lead to an analytics event', () => { + fingerprint.submitAnalytics({ type: ANALYTICS_RENDERED_STR }); + + expect(analyticsModule.createAnalyticsEvent).not.toHaveBeenCalled(); + }); + + test('ThreeDS2DeviceFingerprint instantiated without paymentData should generate an analytics error', () => { + const view = fingerprint.render(); + + expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({ + event: ANALYTICS_EVENT_ERROR, + data: { + component: fingerprint.constructor['type'], + type: THREEDS2_ERROR, + errorType: ANALYTICS_API_ERROR, + message: `${THREEDS2_FINGERPRINT_ERROR}: Missing 'paymentData' property from threeDS2 action`, + code: ANALYTICS_ERROR_CODE_ACTION_IS_MISSING_PAYMENT_DATA + } + }); + + expect(view).toBe(null); + }); +}); diff --git a/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx b/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx index ce814e37d7..ca3b60beca 100644 --- a/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx +++ b/packages/lib/src/components/ThreeDS2/ThreeDS2DeviceFingerprint.tsx @@ -4,21 +4,25 @@ import PrepareFingerprint from './components/DeviceFingerprint'; import { ErrorCodeObject } from './components/utils'; import callSubmit3DS2Fingerprint from './callSubmit3DS2Fingerprint'; import { existy } from '../internal/SecuredFields/lib/utilities/commonUtils'; -import { ActionHandledReturnObject } from '../types'; +import { ActionHandledReturnObject, AnalyticsModule } from '../types'; +import { THREEDS2_ERROR, THREEDS2_FINGERPRINT, THREEDS2_FINGERPRINT_ERROR } from './config'; +import { ANALYTICS_API_ERROR, ANALYTICS_ERROR_CODE_ACTION_IS_MISSING_PAYMENT_DATA, ANALYTICS_RENDERED_STR } from '../../core/Analytics/constants'; +import { SendAnalyticsObject } from '../../core/Analytics/types'; export interface ThreeDS2DeviceFingerprintProps { - dataKey: string; - token: string; - notificationURL: string; - onError: (error?: string | ErrorCodeObject) => void; - paymentData: string; + dataKey?: string; + token?: string; + notificationURL?: string; + onError?: (error?: string | ErrorCodeObject) => void; + paymentData?: string; showSpinner: boolean; - type: string; + type?: string; useOriginalFlow?: boolean; loadingContext?: string; clientKey?: string; elementRef?: UIElement; onActionHandled: (rtnObj: ActionHandledReturnObject) => void; + modules?: { analytics: AnalyticsModule }; } class ThreeDS2DeviceFingerprint extends UIElement { @@ -26,11 +30,17 @@ class ThreeDS2DeviceFingerprint extends UIElement { + if (aObj.type === ANALYTICS_RENDERED_STR) return; // suppress the rendered event (it will have the same timestamp as the "threeDSMethodData sent" event) + + super.submitAnalytics(aObj); + }; + onComplete(state) { super.onComplete(state); this.unmount(); // re. fixing issue around back to back fingerprinting calls @@ -46,6 +56,15 @@ class ThreeDS2DeviceFingerprint extends UIElement; + return ( + + ); } } diff --git a/packages/lib/src/components/ThreeDS2/components/Challenge/DoChallenge3DS2.tsx b/packages/lib/src/components/ThreeDS2/components/Challenge/DoChallenge3DS2.tsx index e10eea2fb8..413ba81c42 100644 --- a/packages/lib/src/components/ThreeDS2/components/Challenge/DoChallenge3DS2.tsx +++ b/packages/lib/src/components/ThreeDS2/components/Challenge/DoChallenge3DS2.tsx @@ -24,12 +24,16 @@ class DoChallenge3DS2 extends Component { this.setState({ status: 'iframeLoaded' }); - this.props.onActionHandled({ componentType: '3DS2Challenge', actionDescription: 'challenge-iframe-loaded' }); + // On Test - actually calls-back 3 times: once for challenge screen, once again as challenge.html reloads after the challenge is submitted, and once for redirect to threeDSNotificationURL. + // But for the purposes of calling the merchant defined onActionHandled callback - we only want to do it once + if (this.state.status === 'init') { + this.props.onActionHandled({ componentType: '3DS2Challenge', actionDescription: 'challenge-iframe-loaded' }); + } }; private get3DS2ChallengePromise(): Promise { @@ -64,7 +68,7 @@ class DoChallenge3DS2 extends Component}