From 998bf8e8f77ee6667bb680cfd7e6769ec72189fe Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Thu, 20 Jun 2024 12:34:36 -0400 Subject: [PATCH 1/3] feat(tracker): add currency context, add transactionTotal to order transaction payload --- docs/INTEGRATION_TRACKING.md | 10 +++- packages/snap-tracker/README.md | 32 +++++++++-- packages/snap-tracker/src/BeaconEvent.test.ts | 9 ++- packages/snap-tracker/src/PixelEvent.test.ts | 12 +++- packages/snap-tracker/src/PixelEvent.ts | 4 +- packages/snap-tracker/src/Tracker.test.ts | 55 ++++++++++++++----- packages/snap-tracker/src/Tracker.ts | 48 +++++++++++++--- packages/snap-tracker/src/types.ts | 10 +++- 8 files changed, 147 insertions(+), 33 deletions(-) diff --git a/docs/INTEGRATION_TRACKING.md b/docs/INTEGRATION_TRACKING.md index 933028757..f8c5bcd7f 100644 --- a/docs/INTEGRATION_TRACKING.md +++ b/docs/INTEGRATION_TRACKING.md @@ -93,7 +93,9 @@ Tracks order transaction. Should be invoked from an order confirmation page. Exp `order.id` - (optional) order id -`order.otal` - (optional) sub total of all items +`order.total` - (optional) transaction total of all items after tax and shipping + +`order.transactionTotal` - (optional) transaction total of all items before tax and shipping `order.city` - (optional) city name @@ -107,7 +109,8 @@ Tracks order transaction. Should be invoked from an order confirmation page. Exp ``` @@ -36,6 +37,9 @@ When used, shopper context should always include at least an `id`; the `cart` co } ] }; + currency = { + code: 'EUR' + }; ``` @@ -53,12 +57,15 @@ Example using multiple context variables together. ```html ``` \ No newline at end of file diff --git a/packages/snap-preact/src/Snap.tsx b/packages/snap-preact/src/Snap.tsx index 6fdbc2398..279b16e4b 100644 --- a/packages/snap-preact/src/Snap.tsx +++ b/packages/snap-preact/src/Snap.tsx @@ -308,7 +308,7 @@ export class Snap { let globalContext: ContextVariables = {}; try { // get global context - globalContext = getContext(['shopper', 'config', 'merchandising', 'siteId']); + globalContext = getContext(['shopper', 'config', 'merchandising', 'siteId', 'currency']); } catch (err) { console.error('Snap failed to find global context'); } @@ -394,7 +394,16 @@ export class Snap { this.logger = services?.logger || new Logger({ prefix: 'Snap Preact ', mode: this.mode }); // create tracker - const trackerGlobals = this.config.tracker?.globals || (this.config.client!.globals as ClientGlobals); + let trackerGlobals = this.config.tracker?.globals || (this.config.client!.globals as ClientGlobals); + + if (this.context.currency?.code) { + trackerGlobals = deepmerge(trackerGlobals || {}, { + currency: { + code: this.context.currency.code, + }, + }); + } + const trackerConfig = deepmerge(this.config.tracker?.config || {}, { framework: 'preact', mode: this.mode }); this.tracker = services?.tracker || new Tracker(trackerGlobals, trackerConfig); diff --git a/packages/snap-tracker/README.md b/packages/snap-tracker/README.md index 5f40c026f..453ae0219 100644 --- a/packages/snap-tracker/README.md +++ b/packages/snap-tracker/README.md @@ -334,7 +334,7 @@ tracker.track.order.transaction({ order: { id: '123456', total: '34.29', - transactionTotal: '31.97', + transactionTotal: '31.97', city: 'Los Angeles', state: 'CA', country: 'US', @@ -359,10 +359,10 @@ tracker.track.order.transaction({ ## Tracker properties ### `globals` property -When constructing an instance of `Tracker`, a globals object is required to be constructed. This object contains a `siteId` key and value. An optional `currency` key and value can be provided. +When constructing an instance of `Tracker`, a globals object is required to be constructed. This object contains a `siteId` key and value. An optional `currency` object with a `code` property containing a string can be provided. ```typescript -const globals = { siteId: 'abc123' }; +const globals = { siteId: 'abc123', currency: { code: 'EUR' } }; const tracker = new Tracker(globals); console.log(tracker.globals === globals) // true ``` @@ -422,7 +422,7 @@ Sets the currency code on the tracker context. ```typescript const tracker = new Tracker(); -tracker.setCurrency('EUR') +tracker.setCurrency({ code: 'EUR' }) ``` diff --git a/packages/snap-tracker/src/PixelEvent.ts b/packages/snap-tracker/src/PixelEvent.ts index c2c5e1168..7304bdac8 100644 --- a/packages/snap-tracker/src/PixelEvent.ts +++ b/packages/snap-tracker/src/PixelEvent.ts @@ -13,7 +13,6 @@ export class PixelEvent { this.src = this.endpoint + `?s=${encodeURIComponent(payload?.context?.website?.trackingCode || '')}` + - `¤cyCode=${encodeURIComponent(payload?.context?.currency?.code || '')}` + `&u=${encodeURIComponent(payload?.context?.userId || '')}` + `&ce=${featureFlags.cookies ? '1' : '0'}` + `&pt=${encodeURIComponent(document.title)}` + @@ -21,6 +20,10 @@ export class PixelEvent { `&x=${Math.floor(Math.random() * 2147483647)}` + `${window.document.referrer ? `&r=${encodeURIComponent(window.document.referrer)}` : ''}`; + if (payload?.context?.currency?.code) { + this.src += `¤cyCode=${encodeURIComponent(payload?.context?.currency?.code)}`; + } + switch (payload.category) { case BeaconCategory.PAGEVIEW: this.event = payload.event as ProductViewEvent; diff --git a/packages/snap-tracker/src/Tracker.test.ts b/packages/snap-tracker/src/Tracker.test.ts index dad74165c..7f0855abe 100644 --- a/packages/snap-tracker/src/Tracker.test.ts +++ b/packages/snap-tracker/src/Tracker.test.ts @@ -80,7 +80,7 @@ describe('Script Block Tracking', () => { const trackEvent = jest.spyOn(tracker.track.cart, 'view'); await new Promise((r) => setTimeout(r)); - expect(trackEvent).toHaveBeenCalledWith({ items }, undefined, undefined); + expect(trackEvent).toHaveBeenCalledWith({ items }, undefined); trackEvent.mockRestore(); }); @@ -113,7 +113,7 @@ describe('Script Block Tracking', () => { const trackEvent = jest.spyOn(tracker.track.order, 'transaction'); await new Promise((r) => setTimeout(r)); - expect(trackEvent).toHaveBeenCalledWith({ order, items }, undefined, undefined); + expect(trackEvent).toHaveBeenCalledWith({ order, items }, undefined); trackEvent.mockRestore(); }); @@ -480,7 +480,7 @@ describe('Tracker', () => { const customGlobals = { siteId: 'custom', - currency: 'EUR', + currency: { code: 'EUR' }, }; const tracker = new Tracker(globals); @@ -518,7 +518,7 @@ describe('Tracker', () => { expect(tracker2.context.currency).toBeDefined(); // @ts-ignore - private property - expect(tracker2.context.currency.code).toStrictEqual(customGlobals.currency); + expect(tracker2.context.currency).toStrictEqual(customGlobals.currency); }); it('can persist userId in storage if cookies are disabled', async () => { @@ -1084,13 +1084,12 @@ describe('Tracker', () => { eventFn.mockRestore(); }); - it('can invoke track.cart.view with siteId and currency override', async () => { + it('can invoke track.cart.view with siteId override', async () => { const tracker = new Tracker(globals, config); const trackEvent = jest.spyOn(tracker.track, 'event'); const cartView = jest.spyOn(tracker.track.cart, 'view'); const siteId = 'xxxxxx'; - const currency = 'EUR'; const payload = { items: [ { @@ -1105,7 +1104,7 @@ describe('Tracker', () => { }, ], }; - await tracker.track.cart.view(payload, siteId, currency); + await tracker.track.cart.view(payload, siteId); expect(trackEvent).toHaveBeenCalledWith({ type: BeaconType.CART, @@ -1116,14 +1115,11 @@ describe('Tracker', () => { website: { trackingCode: siteId, }, - currency: { - code: currency, - }, }, }), event: { ...payload }, }); - expect(cartView).toHaveBeenCalledWith(payload, siteId, currency); + expect(cartView).toHaveBeenCalledWith(payload, siteId); cartView.mockRestore(); trackEvent.mockRestore(); @@ -1283,7 +1279,7 @@ describe('Tracker', () => { eventFn.mockRestore(); }); - it('can invoke track.order.transaction with siteId and currency override', async () => { + it('can invoke track.order.transaction with siteId override', async () => { const tracker = new Tracker(globals, config); const trackEvent = jest.spyOn(tracker.track, 'event'); const orderTransaction = jest.spyOn(tracker.track.order, 'transaction'); @@ -1308,7 +1304,7 @@ describe('Tracker', () => { }, ], }; - await tracker.track.order.transaction(payload, siteId, currency); + await tracker.track.order.transaction(payload, siteId); expect(trackEvent).toHaveBeenCalledWith({ type: BeaconType.ORDER, @@ -1319,9 +1315,6 @@ describe('Tracker', () => { website: { trackingCode: siteId, }, - currency: { - code: currency, - }, }, }), event: { @@ -1334,7 +1327,7 @@ describe('Tracker', () => { items: payload.items, }, }); - expect(orderTransaction).toHaveBeenCalledWith(payload, siteId, currency); + expect(orderTransaction).toHaveBeenCalledWith(payload, siteId); orderTransaction.mockRestore(); trackEvent.mockRestore(); diff --git a/packages/snap-tracker/src/Tracker.ts b/packages/snap-tracker/src/Tracker.ts index f7ea5dd5e..e65300e45 100644 --- a/packages/snap-tracker/src/Tracker.ts +++ b/packages/snap-tracker/src/Tracker.ts @@ -25,6 +25,7 @@ import { TrackerConfig, DoNotTrackEntry, PreflightRequestModel, + CurrencyContext, } from './types'; export const BATCH_TIMEOUT = 200; @@ -89,9 +90,12 @@ export class Tracker { }, }; - if (this.globals.currency) { - this.context.currency = { - code: this.globals.currency, + if (this.globals.currency?.code) { + this.context = { + ...this.context, + currency: { + code: this.globals.currency.code, + }, }; } @@ -105,8 +109,8 @@ export class Tracker { setTimeout(() => { this.targeters.push( new DomTargeter([{ selector: 'script[type^="searchspring/track/"]', emptyTarget: false }], (target: any, elem: Element) => { - const { item, items, siteId, currency, shopper, order, type } = getContext( - ['item', 'items', 'siteId', 'currency', 'shopper', 'order', 'type'], + const { item, items, siteId, shopper, order, type } = getContext( + ['item', 'items', 'siteId', 'shopper', 'order', 'type'], elem as HTMLScriptElement ); @@ -118,10 +122,10 @@ export class Tracker { this.track.product.view(item, siteId); break; case 'searchspring/track/cart/view': - this.track.cart.view({ items }, siteId, currency); + this.track.cart.view({ items }, siteId); break; case 'searchspring/track/order/transaction': - this.track.order.transaction({ order, items }, siteId, currency); + this.track.order.transaction({ order, items }, siteId); break; default: console.error(`event '${type}' is not supported`); @@ -424,7 +428,7 @@ export class Tracker { }, }, cart: { - view: (data: CartViewEvent, siteId?: string, currency?: string): BeaconEvent | undefined => { + view: (data: CartViewEvent, siteId?: string): BeaconEvent | undefined => { if (!Array.isArray(data?.items) || !data?.items.length) { console.error( 'track.view.cart event: parameter must be an array of cart items. \nExample: track.view.cart({ items: [{ sku: "product123", childSku: "product123_a", qty: "1", price: "9.99" }] })' @@ -441,15 +445,6 @@ export class Tracker { }, }); } - if (currency) { - context = deepmerge(context, { - context: { - currency: { - code: currency, - }, - }, - }); - } const items = data.items.map((item, index) => { if (!item?.qty || !item?.price || (!item?.sku && !item?.childSku)) { console.error( @@ -490,7 +485,7 @@ export class Tracker { }, }, order: { - transaction: (data: OrderTransactionData, siteId?: string, currency?: string): BeaconEvent | undefined => { + transaction: (data: OrderTransactionData, siteId?: string): BeaconEvent | undefined => { if (!data?.items || !Array.isArray(data.items) || !data.items.length) { console.error( 'track.order.transaction event: object parameter must contain `items` array of cart items. \nExample: order.transaction({ order: { id: "1001", total: "10.71", transactionTotal: "9.99", city: "Los Angeles", state: "CA", country: "US" }, items: [{ sku: "product123", childSku: "product123_a", qty: "1", price: "9.99" }] })' @@ -507,15 +502,6 @@ export class Tracker { }, }); } - if (currency) { - context = deepmerge(context, { - context: { - currency: { - code: currency, - }, - }, - }); - } const items = data.items.map((item, index) => { if (!item?.qty || !item?.price || (!item?.sku && !item?.childSku)) { console.error( @@ -571,13 +557,11 @@ export class Tracker { } }; - setCurrency = (currency: string): void => { - if (!currency) { + setCurrency = (currency: CurrencyContext): void => { + if (!currency?.code) { return; } - this.context.currency = { - code: currency, - }; + this.context.currency = currency; }; getUserId = (): string | undefined | null => { diff --git a/packages/snap-tracker/src/types.ts b/packages/snap-tracker/src/types.ts index 18d6eb8de..9335a9e60 100644 --- a/packages/snap-tracker/src/types.ts +++ b/packages/snap-tracker/src/types.ts @@ -1,9 +1,13 @@ import { AppMode } from '@searchspring/snap-toolbox'; import { BeaconEvent } from './BeaconEvent'; +export type CurrencyContext = { + code: string; +}; + export type TrackerGlobals = { siteId: string; - currency?: string; + currency?: CurrencyContext; }; export type DoNotTrackEntry = { @@ -98,9 +102,7 @@ export interface BeaconContext { type?: string; id?: string; }; - currency?: { - code: string; - }; + currency?: CurrencyContext; } export interface BeaconMeta { @@ -220,10 +222,10 @@ export interface TrackMethods { click: (data: ProductClickEvent, siteId?: string) => BeaconEvent | undefined; }; cart: { - view: (data: CartViewEvent, siteId?: string, currency?: string) => BeaconEvent | undefined; + view: (data: CartViewEvent, siteId?: string) => BeaconEvent | undefined; }; order: { - transaction: (data: OrderTransactionData, siteId?: string, currency?: string) => BeaconEvent | undefined; + transaction: (data: OrderTransactionData, siteId?: string) => BeaconEvent | undefined; }; } From 2b982d134d90db1bfdabe376eb621254b9b4f6c0 Mon Sep 17 00:00:00 2001 From: Dennis Konieczek Date: Wed, 26 Jun 2024 11:55:42 -0400 Subject: [PATCH 3/3] refactor: pr feedback --- packages/snap-preact/src/Snap.tsx | 4 +--- packages/snap-tracker/src/Tracker.ts | 12 ++++-------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/snap-preact/src/Snap.tsx b/packages/snap-preact/src/Snap.tsx index 279b16e4b..6d878454f 100644 --- a/packages/snap-preact/src/Snap.tsx +++ b/packages/snap-preact/src/Snap.tsx @@ -398,9 +398,7 @@ export class Snap { if (this.context.currency?.code) { trackerGlobals = deepmerge(trackerGlobals || {}, { - currency: { - code: this.context.currency.code, - }, + currency: this.context.currency, }); } diff --git a/packages/snap-tracker/src/Tracker.ts b/packages/snap-tracker/src/Tracker.ts index e65300e45..c198e4405 100644 --- a/packages/snap-tracker/src/Tracker.ts +++ b/packages/snap-tracker/src/Tracker.ts @@ -91,12 +91,7 @@ export class Tracker { }; if (this.globals.currency?.code) { - this.context = { - ...this.context, - currency: { - code: this.globals.currency.code, - }, - }; + this.context.currency = this.globals.currency; } if (!window.searchspring?.tracker) { @@ -109,11 +104,12 @@ export class Tracker { setTimeout(() => { this.targeters.push( new DomTargeter([{ selector: 'script[type^="searchspring/track/"]', emptyTarget: false }], (target: any, elem: Element) => { - const { item, items, siteId, shopper, order, type } = getContext( - ['item', 'items', 'siteId', 'shopper', 'order', 'type'], + const { item, items, siteId, shopper, order, type, currency } = getContext( + ['item', 'items', 'siteId', 'shopper', 'order', 'type', 'currency'], elem as HTMLScriptElement ); + this.setCurrency(currency); switch (type) { case 'searchspring/track/shopper/login': this.track.shopper.login(shopper, siteId);