diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 6147c992..94d545cf 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -16345,4 +16345,39 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { ).to.equal(true); }); }); + + describe('additionalTrackingKeys', () => { + it('Should send tracking events to both primary and additional keys', (done) => { + const additionalKey = 'extra-test-key'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + ...requestQueueOptions, + additionalTrackingKeys: [additionalKey], + }); + + let callCount = 0; + + const checkComplete = () => { + callCount += 1; + + if (callCount === 2) { + expect(fetchSpy).to.have.been.calledTwice; + + const firstCallUrl = fetchSpy.getCall(0).args[0]; + const secondCallUrl = fetchSpy.getCall(1).args[0]; + + expect(firstCallUrl).to.contain(`key=${testApiKey}`); + expect(secondCallUrl).to.contain(`key=${additionalKey}`); + + done(); + } + }; + + tracker.on('success', checkComplete); + tracker.on('error', checkComplete); + + expect(tracker.trackSessionStartV2()).to.equal(true); + }); + }); }); diff --git a/spec/src/utils/request-queue.js b/spec/src/utils/request-queue.js index 767e5bed..60824589 100644 --- a/spec/src/utils/request-queue.js +++ b/spec/src/utils/request-queue.js @@ -318,6 +318,121 @@ describe('ConstructorIO - Utils - Request Queue', function utilsRequestQueue() { }); }); + describe('additionalTrackingKeys', () => { + let defaultAgent; + let cleanup; + + before(() => { + helpers.clearStorage(); + }); + + beforeEach(() => { + global.CLIENT_VERSION = 'cio-mocha'; + cleanup = jsdom(); + defaultAgent = window.navigator.userAgent; + }); + + afterEach(() => { + window.navigator.__defineGetter__('userAgent', () => defaultAgent); + delete global.CLIENT_VERSION; + cleanup(); + helpers.clearStorage(); + }); + + it('Should add duplicate requests for each additional tracking key', () => { + store.session.set(humanityStorageKey, true); + const requests = new RequestQueue({ + sendTrackingEvents: true, + trackingSendDelay: 1, + apiKey: 'primary-key', + additionalTrackingKeys: ['extra-key-1', 'extra-key-2'], + }); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start&key=primary-key&_dt=123', 'POST', { action: 'session_start', key: 'primary-key' }); + + const queue = RequestQueue.get(); + expect(queue).to.be.an('array').length(3); + + // Primary request + expect(queue[0].url).to.contain('key=primary-key'); + expect(queue[0].body.key).to.equal('primary-key'); + + // First additional key + expect(queue[1].url).to.contain('key=extra-key-1'); + expect(queue[1].url).to.not.contain('key=primary-key'); + expect(queue[1].body.key).to.equal('extra-key-1'); + + // Second additional key + expect(queue[2].url).to.contain('key=extra-key-2'); + expect(queue[2].url).to.not.contain('key=primary-key'); + expect(queue[2].body.key).to.equal('extra-key-2'); + }); + + it('Should not add duplicates when additionalTrackingKeys is an empty array', () => { + store.session.set(humanityStorageKey, true); + const requests = new RequestQueue({ + sendTrackingEvents: true, + trackingSendDelay: 1, + apiKey: 'primary-key', + additionalTrackingKeys: [], + }); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start&key=primary-key&_dt=123'); + + expect(RequestQueue.get()).to.be.an('array').length(1); + }); + + it('Should not add duplicates when additionalTrackingKeys is not provided', () => { + store.session.set(humanityStorageKey, true); + const requests = new RequestQueue({ + sendTrackingEvents: true, + trackingSendDelay: 1, + apiKey: 'primary-key', + }); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start&key=primary-key&_dt=123'); + + expect(RequestQueue.get()).to.be.an('array').length(1); + }); + + it('Should add duplicate requests for GET method', () => { + store.session.set(humanityStorageKey, true); + const requests = new RequestQueue({ + sendTrackingEvents: true, + trackingSendDelay: 1, + apiKey: 'primary-key', + additionalTrackingKeys: ['extra-key-1'], + }); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start&key=primary-key&_dt=123'); + + const queue = RequestQueue.get(); + expect(queue).to.be.an('array').length(2); + + expect(queue[0].url).to.contain('key=primary-key'); + expect(queue[1].url).to.contain('key=extra-key-1'); + expect(queue[1].url).to.contain('action=session_start'); + expect(queue[1].body.key).to.equal('extra-key-1'); + }); + + it('Should skip invalid entries in additionalTrackingKeys', () => { + store.session.set(humanityStorageKey, true); + const requests = new RequestQueue({ + sendTrackingEvents: true, + trackingSendDelay: 1, + apiKey: 'primary-key', + additionalTrackingKeys: ['valid-key', '', null, 123, 'another-valid-key'], + }); + + requests.queue('https://ac.cnstrc.com/behavior?action=session_start&key=primary-key&_dt=123', 'POST', { action: 'session_start', key: 'primary-key' }); + + const queue = RequestQueue.get(); + expect(queue).to.be.an('array').length(3); + expect(queue[1].url).to.contain('key=valid-key'); + expect(queue[2].url).to.contain('key=another-valid-key'); + }); + }); + describe('send', () => { let fetchSpy = null; let cleanup; diff --git a/src/constructorio.js b/src/constructorio.js index b0691dd3..ea8c2ec9 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -59,6 +59,7 @@ class ConstructorIO { * @param {object} [parameters.networkParameters] - Parameters relevant to network requests * @param {number} [parameters.networkParameters.timeout] - Request timeout (in milliseconds) - may be overridden within individual method calls * @param {string} [parameters.humanityCheckLocation='session'] - Storage location for the humanity check flag ('session' for sessionStorage, 'local' for localStorage) + * @param {string[]} [parameters.additionalTrackingKeys] - Additional API keys to duplicate tracking events to * @property {object} search - Interface to {@link module:search} * @property {object} browse - Interface to {@link module:browse} * @property {object} autocomplete - Interface to {@link module:autocomplete} @@ -93,6 +94,7 @@ class ConstructorIO { beaconMode, networkParameters, humanityCheckLocation, + additionalTrackingKeys, } = options; if (!apiKey || typeof apiKey !== 'string') { @@ -141,6 +143,7 @@ class ConstructorIO { beaconMode: (beaconMode === false) ? false : true, // Defaults to 'true', networkParameters: networkParameters || {}, humanityCheckLocation: humanityCheckLocation || 'session', + additionalTrackingKeys, }; // Expose global modules diff --git a/src/types/index.d.ts b/src/types/index.d.ts index bd093e43..15b3ba1e 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -71,6 +71,7 @@ export interface ConstructorClientOptions { beaconMode?: boolean; networkParameters?: NetworkParameters; humanityCheckLocation?: 'session' | 'local'; + additionalTrackingKeys?: string[]; } export interface RequestFeature extends Record { diff --git a/src/utils/request-queue.js b/src/utils/request-queue.js index 291d0724..d398c85b 100644 --- a/src/utils/request-queue.js +++ b/src/utils/request-queue.js @@ -53,6 +53,28 @@ class RequestQueue { body, networkParameters, }); + + // Duplicate request for each additional tracking key + const additionalKeys = this.options?.additionalTrackingKeys; + + if (Array.isArray(additionalKeys) && additionalKeys.length) { + const encodedOriginalKey = helpers.encodeURIComponentRFC3986(this.options.apiKey); + + const validKeys = additionalKeys.filter((key) => key && typeof key === 'string'); + + validKeys.forEach((additionalKey) => { + const encodedKey = helpers.encodeURIComponentRFC3986(additionalKey); + const swappedUrl = url.replace(`key=${encodedOriginalKey}`, `key=${encodedKey}`); + + queue.push({ + url: obfuscatePiiRequest(swappedUrl), + method, + body: { ...body, key: additionalKey }, + networkParameters, + }); + }); + } + RequestQueue.set(queue); } }