From 8ee9def79fdb7797f3a401de25dcfed1bd6ffe13 Mon Sep 17 00:00:00 2001 From: Viktor Zavala Date: Wed, 28 Jan 2026 10:27:06 +0100 Subject: [PATCH 1/6] [REM-2574] Add trackMediaImpressionClick function with tests and types --- spec/src/modules/tracker.js | 223 ++++++++++++++++++++++++++++++++++++ src/modules/tracker.js | 70 +++++++++++ src/types/tracker.d.ts | 7 ++ 3 files changed, 300 insertions(+) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 6b01874b..19d6e746 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -15772,4 +15772,227 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { ).to.equal(true); }); }); + + describe('trackMediaImpressionClick', () => { + const testApiKeyWithAdPlacements = 'u7PNVQx-prod-en-us'; + const requiredParameters = { + bannerAdId: 'AszgOLr3pCZheI0Bx7rNtSraeaN6o3IgNNWvUgan/LqMsf0pTVFaHqjDjWNj1Gz5+IfGOQOs6XOYcWVykjsYSphHF3j04TsXVAlkd2VorLK3dg39SsLiv8mOEVA6TcuBSXAmGLXZCyTmCRjD8JG6QXNEr5qWC073V6CwJRT/XnUYJ/8fiosWNIpNiv5z9VtocQLqILszRllLEMpuGFdXu2HS', + placementId: 'home-page-top-banner', + }; + + const optionalParameters = { + analyticsTags: testAnalyticsTag, + }; + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKeyWithAdPlacements, + fetch: fetchSpy, + mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('key'); + expect(requestParams).to.have.property('i'); + expect(requestParams).to.have.property('s'); + expect(requestParams).to.have.property('c').to.equal(clientVersion); + expect(requestParams).to.have.property('_dt'); + expect(requestParams).to.have.property('canonical_url').to.equal(canonicalUrl); + expect(requestParams).to.have.property('document_referrer').to.equal(referrer); + expect(requestParams) + .to.have.property('banner_ad_id') + .to.equal(requiredParameters.bannerAdId); + expect(requestParams) + .to.have.property('placement_id') + .to.equal(requiredParameters.placementId); + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect(tracker.trackMediaImpressionClick(requiredParameters)).to.equal( + true, + ); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKeyWithAdPlacements, + fetch: fetchSpy, + mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams) + .to.have.property('analytics_tags') + .to.deep.equal(testAnalyticsTag); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message'); + + done(); + }); + + expect( + tracker.trackMediaImpressionClick( + Object.assign(requiredParameters, optionalParameters), + ), + ).to.equal(true); + }); + + it('Should throw an error when invalid parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackMediaImpressionClick([])).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + expect(tracker.trackMediaImpressionClick()).to.be.an('error'); + }); + + it('Should send along origin_referrer query param if sendReferrerWithTrackingEvents is true', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKeyWithAdPlacements, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: true, + mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + validateOriginReferrer(requestParams); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackMediaImpressionClick(requiredParameters)).to.equal( + true, + ); + }); + + it('Should not send along origin_referrer query param if sendReferrerWithTrackingEvents is false', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKeyWithAdPlacements, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: false, + mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractUrlParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.not.have.property('origin_referrer'); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackMediaImpressionClick(requiredParameters)).to.equal( + true, + ); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKeyWithAdPlacements, + mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect( + tracker.trackMediaImpressionClick(requiredParameters, { timeout: 10 }), + ).to.equal(true); + }); + + it('Should be rejected when global network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKeyWithAdPlacements, + mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackMediaImpressionClick(requiredParameters)).to.equal( + true, + ); + }); + } + + it('Should properly transform non-breaking spaces in parameters', (done) => { + const breakingSpaces = '   '; + const userId = `user-id ${breakingSpaces} user-id`; + const userIdExpected = 'user-id user-id'; + const { tracker } = new ConstructorIO({ + apiKey: testApiKeyWithAdPlacements, + userId, + mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userIdExpected); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect( + tracker.trackMediaImpressionClick({ + ...requiredParameters, + userId, + }), + ).to.equal(true); + }); + }); }); diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 60da242a..24124048 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -1416,6 +1416,76 @@ class Tracker { return new Error('parameters are required of type object'); } + /** + * Send media impression click event to API + * + * @function trackMediaImpressionClick + * @param {object} parameters - Additional parameters to be sent with request + * @param {string} parameters.bannerAdId - Banner ad identifier + * @param {string} parameters.placementId - Placement identifier + * @param {object} [parameters.analyticsTags] - Pass additional analytics data + * @param {object} [networkParameters] - Parameters relevant to the network request + * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds) + * @returns {(true|Error)} + * @description User clicked a media banner + * @example + * constructorio.tracker.trackMediaImpressionClick( + * { + * bannerAdId: 'banner_ad_id', + * placementId: 'placement_id', + * }, + * ); + */ + trackMediaImpressionClick(parameters, networkParameters = {}) { + // Ensure required parameters are provided + if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { + const baseUrl = new URL(this.options.mediaServiceUrl); + + if (!baseUrl.hostname.startsWith('behavior') && !baseUrl.hostname.startsWith('dev-behavior')) { + baseUrl.hostname = `behavior.${baseUrl.hostname}`; + } + + const requestPath = `${baseUrl.toString()}v2/ad_behavioral_action/display_ad_click?`; + + const bodyParams = {}; + const { + bannerAdId, + placementId, + analyticsTags, + } = parameters; + + if (!helpers.isNil(bannerAdId)) { + bodyParams.banner_ad_id = bannerAdId; + } + + if (!helpers.isNil(placementId)) { + bodyParams.placement_id = placementId; + } + + if (!helpers.isNil(analyticsTags)) { + bodyParams.analytics_tags = analyticsTags; + } + + const requestURL = `${requestPath}${applyParamsAsString({}, this.options)}`; + const requestMethod = 'POST'; + const requestBody = applyParams(bodyParams, { ...this.options, requestMethod }); + + this.requests.queue( + requestURL, + requestMethod, + requestBody, + networkParameters, + ); + this.requests.send(); + + return true; + } + + this.requests.send(); + + return new Error('parameters are required of type object'); + } + /** * Send recommendation click event to API * diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index c43e1429..b159ec8e 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -444,5 +444,12 @@ declare class Tracker { }, networkParameters?: NetworkParameters ): true | Error; + trackMediaImpressionClick(parameters: { + bannerAdId: string; + placementId: string; + analyticsTags?: Record; + }, networkParameters?: NetworkParameters + ): true | Error; + on(messageType: string, callback: Function): true | Error; } From 6ddaeaa38a3edc05ff0fc954fd4b2069fa6d36fc Mon Sep 17 00:00:00 2001 From: Viktor Zavala Date: Wed, 4 Feb 2026 09:00:26 +0100 Subject: [PATCH 2/6] [REM-2574] Add a valid api key, banner id and placement id for display ads tracking tests --- cspell.json | 3 +- spec/src/modules/tracker.js | 116 +++++++----------------------------- 2 files changed, 23 insertions(+), 96 deletions(-) diff --git a/cspell.json b/cspell.json index b91c2462..dcd1af7b 100644 --- a/cspell.json +++ b/cspell.json @@ -49,6 +49,7 @@ "testdata", "Bytespider", "Timespans", - "googlequicksearchbox" + "googlequicksearchbox", + "IHFQD" ] } diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 19d6e746..b6c24e27 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -28,6 +28,7 @@ const utmParameters = 'utm_source=attentive&utm_medium=sms&utm_campaign=campaign const url = `http://localhost.test/path/name?query=term&category=cat&${utmParameters}`; const referrer = 'https://www.google.com/'; const canonicalUrl = 'https://localhost/'; +const testApiKeyWithAdPlacements = 'key_x6UnCVRZaJgIHFQD'; function validateOriginReferrer(requestParams) { expect(requestParams).to.have.property('origin_referrer').to.contain('localhost.test/path/name'); @@ -15513,8 +15514,8 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { describe('trackMediaImpressionView', () => { const requiredParameters = { - bannerAdId: 'banner_ad_id', - placementId: 'placement_id', + bannerAdId: 'AszgOLr3pCZheI0Bx7rNtSraeaN6o3IgNNWvUgan/LqMsf0pTVFaHqjDjWNj1Gz5+IfGOQOs6XOYcWVykjsYSphHF3j04TsXVAlkd2VorLK3dg39SsLiv8mOEVA6TcuBSXAmGLXZCyTmCRjD8JG6QXNEr5qWC073V6CwJRT/XnUYJ/8fiosWNIpNiv5z9VtocQLqILszRllLEMpuGFdXu2HS', + placementId: 'products-listings-page', }; const optionalParameters = { @@ -15523,7 +15524,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { it('Should respond with a valid response when required parameters are provided', (done) => { const { tracker } = new ConstructorIO({ - apiKey: testApiKey, + apiKey: testApiKeyWithAdPlacements, fetch: fetchSpy, mediaServiceUrl: 'https://media-cnstrc.com', ...requestQueueOptions, @@ -15562,7 +15563,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { it('Should respond with a valid response when required and optional parameters are provided', (done) => { const { tracker } = new ConstructorIO({ - apiKey: testApiKey, + apiKey: testApiKeyWithAdPlacements, fetch: fetchSpy, mediaServiceUrl: 'https://media-cnstrc.com', ...requestQueueOptions, @@ -15592,20 +15593,20 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); it('Should throw an error when invalid parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ apiKey: testApiKeyWithAdPlacements }); expect(tracker.trackMediaImpressionView([])).to.be.an('error'); }); it('Should throw an error when no parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ apiKey: testApiKeyWithAdPlacements }); expect(tracker.trackMediaImpressionView()).to.be.an('error'); }); it('Should send along origin_referrer query param if sendReferrerWithTrackingEvents is true', (done) => { const { tracker } = new ConstructorIO({ - apiKey: testApiKey, + apiKey: testApiKeyWithAdPlacements, fetch: fetchSpy, sendReferrerWithTrackingEvents: true, mediaServiceUrl: 'https://media-cnstrc.com', @@ -15633,7 +15634,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { it('Should not send along origin_referrer query param if sendReferrerWithTrackingEvents is false', (done) => { const { tracker } = new ConstructorIO({ - apiKey: testApiKey, + apiKey: testApiKeyWithAdPlacements, fetch: fetchSpy, sendReferrerWithTrackingEvents: false, mediaServiceUrl: 'https://media-cnstrc.com', @@ -15662,7 +15663,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', (done) => { const { tracker } = new ConstructorIO({ - apiKey: testApiKey, + apiKey: testApiKeyWithAdPlacements, mediaServiceUrl: 'https://media-cnstrc.com', ...requestQueueOptions, }); @@ -15679,7 +15680,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { it('Should be rejected when global network request timeout is provided and reached', (done) => { const { tracker } = new ConstructorIO({ - apiKey: testApiKey, + apiKey: testApiKeyWithAdPlacements, mediaServiceUrl: 'https://media-cnstrc.com', networkParameters: { timeout: 20, @@ -15697,87 +15698,12 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { ); }); } - - it('Should not encode body parameters', (done) => { - const specialCharacters = '+[]&'; - const userId = `user-id ${specialCharacters}`; - const bannerAdId = `banner_ad_id ${specialCharacters}`; - const { tracker } = new ConstructorIO({ - apiKey: testApiKey, - userId, - fetch: fetchSpy, - mediaServiceUrl: 'https://media-cnstrc.com', - ...requestQueueOptions, - }); - - tracker.on('success', (responseParams) => { - const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); - - // Request - expect(fetchSpy).to.have.been.called; - expect(requestParams).to.have.property('ui').to.equal(userId); - expect(requestParams) - .to.have.property('banner_ad_id') - .to.equal(bannerAdId); - - // Response - expect(responseParams).to.have.property('method').to.equal('POST'); - expect(responseParams).to.have.property('message').to.equal('ok'); - - done(); - }); - - expect( - tracker.trackMediaImpressionView({ ...requiredParameters, bannerAdId }), - ).to.equal(true); - }); - - it('Should properly transform non-breaking spaces in parameters', (done) => { - const breakingSpaces = '   '; - const userId = `user-id ${breakingSpaces} user-id`; - const bannerAdId = `banner_ad_id ${breakingSpaces} banner_ad_id`; - const bannerAdIdExpected = 'banner_ad_id banner_ad_id'; - const userIdExpected = 'user-id user-id'; - const { tracker } = new ConstructorIO({ - apiKey: testApiKey, - userId, - mediaServiceUrl: 'https://media-cnstrc.com', - fetch: fetchSpy, - ...requestQueueOptions, - }); - - tracker.on('success', (responseParams) => { - const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); - - // Request - expect(fetchSpy).to.have.been.called; - expect(requestParams).to.have.property('ui').to.equal(userIdExpected); - expect(requestParams) - .to.have.property('banner_ad_id') - .to.equal(bannerAdIdExpected); - - // Response - expect(responseParams).to.have.property('method').to.equal('POST'); - expect(responseParams).to.have.property('message').to.equal('ok'); - - done(); - }); - - expect( - tracker.trackMediaImpressionView({ - ...requiredParameters, - userId, - bannerAdId, - }), - ).to.equal(true); - }); }); describe('trackMediaImpressionClick', () => { - const testApiKeyWithAdPlacements = 'u7PNVQx-prod-en-us'; const requiredParameters = { bannerAdId: 'AszgOLr3pCZheI0Bx7rNtSraeaN6o3IgNNWvUgan/LqMsf0pTVFaHqjDjWNj1Gz5+IfGOQOs6XOYcWVykjsYSphHF3j04TsXVAlkd2VorLK3dg39SsLiv8mOEVA6TcuBSXAmGLXZCyTmCRjD8JG6QXNEr5qWC073V6CwJRT/XnUYJ/8fiosWNIpNiv5z9VtocQLqILszRllLEMpuGFdXu2HS', - placementId: 'home-page-top-banner', + placementId: 'products-listings-page', }; const optionalParameters = { @@ -15788,7 +15714,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { const { tracker } = new ConstructorIO({ apiKey: testApiKeyWithAdPlacements, fetch: fetchSpy, - mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + mediaServiceUrl: 'https://media-cnstrc.com', ...requestQueueOptions, }); @@ -15827,7 +15753,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { const { tracker } = new ConstructorIO({ apiKey: testApiKeyWithAdPlacements, fetch: fetchSpy, - mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + mediaServiceUrl: 'https://media-cnstrc.com', ...requestQueueOptions, }); @@ -15855,13 +15781,13 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); it('Should throw an error when invalid parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ apiKey: testApiKeyWithAdPlacements }); expect(tracker.trackMediaImpressionClick([])).to.be.an('error'); }); it('Should throw an error when no parameters are provided', () => { - const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + const { tracker } = new ConstructorIO({ apiKey: testApiKeyWithAdPlacements }); expect(tracker.trackMediaImpressionClick()).to.be.an('error'); }); @@ -15871,7 +15797,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { apiKey: testApiKeyWithAdPlacements, fetch: fetchSpy, sendReferrerWithTrackingEvents: true, - mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + mediaServiceUrl: 'https://media-cnstrc.com', ...requestQueueOptions, }); @@ -15899,7 +15825,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { apiKey: testApiKeyWithAdPlacements, fetch: fetchSpy, sendReferrerWithTrackingEvents: false, - mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + mediaServiceUrl: 'https://media-cnstrc.com', ...requestQueueOptions, }); @@ -15926,7 +15852,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { it('Should be rejected when network request timeout is provided and reached', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKeyWithAdPlacements, - mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + mediaServiceUrl: 'https://media-cnstrc.com', ...requestQueueOptions, }); @@ -15943,7 +15869,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { it('Should be rejected when global network request timeout is provided and reached', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKeyWithAdPlacements, - mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + mediaServiceUrl: 'https://media-cnstrc.com', networkParameters: { timeout: 20, }, @@ -15968,7 +15894,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { const { tracker } = new ConstructorIO({ apiKey: testApiKeyWithAdPlacements, userId, - mediaServiceUrl: 'https://dev-behavior.media-cnstrc.com', + mediaServiceUrl: 'https://media-cnstrc.com', fetch: fetchSpy, ...requestQueueOptions, }); From 7803878db746ebdebd906c9f9651bad215fd239b Mon Sep 17 00:00:00 2001 From: Viktor Zavala Date: Wed, 11 Feb 2026 18:23:21 +0100 Subject: [PATCH 3/6] [REM-2574] Update tests to fetch a valid display ads banner. Fix assertions. Extract behavior base URL helper function --- spec/src/modules/tracker.js | 100 +++++++++++++++++++++++++++++++----- src/modules/tracker.js | 12 +---- src/utils/helpers.js | 10 ++++ 3 files changed, 100 insertions(+), 22 deletions(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index b6c24e27..4224a67d 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -29,6 +29,7 @@ const url = `http://localhost.test/path/name?query=term&category=cat&${utmParame const referrer = 'https://www.google.com/'; const canonicalUrl = 'https://localhost/'; const testApiKeyWithAdPlacements = 'key_x6UnCVRZaJgIHFQD'; +const testPlacementId = 'home'; function validateOriginReferrer(requestParams) { expect(requestParams).to.have.property('origin_referrer').to.contain('localhost.test/path/name'); @@ -15513,15 +15514,22 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); describe('trackMediaImpressionView', () => { - const requiredParameters = { - bannerAdId: 'AszgOLr3pCZheI0Bx7rNtSraeaN6o3IgNNWvUgan/LqMsf0pTVFaHqjDjWNj1Gz5+IfGOQOs6XOYcWVykjsYSphHF3j04TsXVAlkd2VorLK3dg39SsLiv8mOEVA6TcuBSXAmGLXZCyTmCRjD8JG6QXNEr5qWC073V6CwJRT/XnUYJ/8fiosWNIpNiv5z9VtocQLqILszRllLEMpuGFdXu2HS', - placementId: 'products-listings-page', - }; + let requiredParameters; const optionalParameters = { analyticsTags: testAnalyticsTag, }; + before(async () => { + const response = await fetch(`https://display.media-cnstrc.com/display-ads?key=${testApiKeyWithAdPlacements}&placement_ids=${testPlacementId}`); + const data = await response.json(); + const ad = data.display_ads[testPlacementId]; + requiredParameters = { + bannerAdId: ad.banner_ad_id, + placementId: testPlacementId, + }; + }); + it('Should respond with a valid response when required parameters are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKeyWithAdPlacements, @@ -15551,7 +15559,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { // Response expect(responseParams).to.have.property('method').to.equal('POST'); - expect(responseParams).to.have.property('message'); + expect(responseParams).to.have.property('message').to.equal('ok'); done(); }); @@ -15580,7 +15588,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { // Response expect(responseParams).to.have.property('method').to.equal('POST'); - expect(responseParams).to.have.property('message'); + expect(responseParams).to.have.property('message').to.equal('ok'); done(); }); @@ -15660,6 +15668,36 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { ); }); + it('Should not encode body parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + const { tracker } = new ConstructorIO({ + apiKey: testApiKeyWithAdPlacements, + userId, + mediaServiceUrl: 'https://media-cnstrc.com', + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackMediaImpressionView(requiredParameters)).to.equal( + true, + ); + }); + if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', (done) => { const { tracker } = new ConstructorIO({ @@ -15701,15 +15739,23 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { }); describe('trackMediaImpressionClick', () => { - const requiredParameters = { - bannerAdId: 'AszgOLr3pCZheI0Bx7rNtSraeaN6o3IgNNWvUgan/LqMsf0pTVFaHqjDjWNj1Gz5+IfGOQOs6XOYcWVykjsYSphHF3j04TsXVAlkd2VorLK3dg39SsLiv8mOEVA6TcuBSXAmGLXZCyTmCRjD8JG6QXNEr5qWC073V6CwJRT/XnUYJ/8fiosWNIpNiv5z9VtocQLqILszRllLEMpuGFdXu2HS', - placementId: 'products-listings-page', - }; + let requiredParameters; const optionalParameters = { analyticsTags: testAnalyticsTag, }; + before(async () => { + const response = await fetch(`https://display.media-cnstrc.com/display-ads?key=${testApiKeyWithAdPlacements}&placement_ids=${testPlacementId}`); + const data = await response.json(); + const ad = data.display_ads[testPlacementId]; + + requiredParameters = { + bannerAdId: ad.banner_ad_id, + placementId: testPlacementId, + }; + }); + it('Should respond with a valid response when required parameters are provided', (done) => { const { tracker } = new ConstructorIO({ apiKey: testApiKeyWithAdPlacements, @@ -15739,7 +15785,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { // Response expect(responseParams).to.have.property('method').to.equal('POST'); - expect(responseParams).to.have.property('message'); + expect(responseParams).to.have.property('message').to.equal('ok'); done(); }); @@ -15768,7 +15814,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { // Response expect(responseParams).to.have.property('method').to.equal('POST'); - expect(responseParams).to.have.property('message'); + expect(responseParams).to.have.property('message').to.equal('ok'); done(); }); @@ -15848,6 +15894,36 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { ); }); + it('Should not encode body parameters', (done) => { + const specialCharacters = '+[]&'; + const userId = `user-id ${specialCharacters}`; + const { tracker } = new ConstructorIO({ + apiKey: testApiKeyWithAdPlacements, + userId, + mediaServiceUrl: 'https://media-cnstrc.com', + fetch: fetchSpy, + ...requestQueueOptions, + }); + + tracker.on('success', (responseParams) => { + const requestParams = helpers.extractBodyParamsFromFetch(fetchSpy); + + // Request + expect(fetchSpy).to.have.been.called; + expect(requestParams).to.have.property('ui').to.equal(userId); + + // Response + expect(responseParams).to.have.property('method').to.equal('POST'); + expect(responseParams).to.have.property('message').to.equal('ok'); + + done(); + }); + + expect(tracker.trackMediaImpressionClick(requiredParameters)).to.equal( + true, + ); + }); + if (!skipNetworkTimeoutTests) { it('Should be rejected when network request timeout is provided and reached', (done) => { const { tracker } = new ConstructorIO({ diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 24124048..a95f9bb1 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -1369,11 +1369,7 @@ class Tracker { trackMediaImpressionView(parameters, networkParameters = {}) { // Ensure parameters are provided (required) if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { - const baseUrl = new URL(this.options.mediaServiceUrl); - - if (!baseUrl.hostname.startsWith('behavior')) { - baseUrl.hostname = `behavior.${baseUrl.hostname}`; - } + const baseUrl = helpers.getBehaviorUrl(this.options.mediaServiceUrl); const requestPath = `${baseUrl.toString()}v2/ad_behavioral_action/display_ad_view?`; @@ -1439,11 +1435,7 @@ class Tracker { trackMediaImpressionClick(parameters, networkParameters = {}) { // Ensure required parameters are provided if (parameters && typeof parameters === 'object' && !Array.isArray(parameters)) { - const baseUrl = new URL(this.options.mediaServiceUrl); - - if (!baseUrl.hostname.startsWith('behavior') && !baseUrl.hostname.startsWith('dev-behavior')) { - baseUrl.hostname = `behavior.${baseUrl.hostname}`; - } + const baseUrl = helpers.getBehaviorUrl(this.options.mediaServiceUrl); const requestPath = `${baseUrl.toString()}v2/ad_behavioral_action/display_ad_click?`; diff --git a/src/utils/helpers.js b/src/utils/helpers.js index ed26f398..5a34c6c1 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -372,6 +372,16 @@ const utils = { }, truncateString: (string, maxLength) => string.slice(0, maxLength), + + getBehaviorUrl: (mediaServiceUrl) => { + const baseUrl = new URL(mediaServiceUrl); + + if (!baseUrl.hostname.startsWith('behavior')) { + baseUrl.hostname = `behavior.${baseUrl.hostname}`; + } + + return baseUrl; + }, }; module.exports = utils; From 1073534ad7de0a373a28a9818467fa6357d52f5c Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Thu, 12 Feb 2026 12:46:45 +0400 Subject: [PATCH 4/6] Move key to env variable --- cspell.json | 3 +-- spec/src/modules/tracker.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cspell.json b/cspell.json index dcd1af7b..b91c2462 100644 --- a/cspell.json +++ b/cspell.json @@ -49,7 +49,6 @@ "testdata", "Bytespider", "Timespans", - "googlequicksearchbox", - "IHFQD" + "googlequicksearchbox" ] } diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index 4224a67d..a76e98b4 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -17,6 +17,7 @@ chai.use(sinonChai); dotenv.config(); const testApiKey = process.env.TEST_REQUEST_API_KEY; +const testApiKeyWithAdPlacements = process.env.TEST_MEDIA_REQUEST_API_KEY; const clientVersion = 'cio-mocha'; const delayBetweenTests = 50; const bundled = process.env.BUNDLED === 'true'; @@ -28,7 +29,6 @@ const utmParameters = 'utm_source=attentive&utm_medium=sms&utm_campaign=campaign const url = `http://localhost.test/path/name?query=term&category=cat&${utmParameters}`; const referrer = 'https://www.google.com/'; const canonicalUrl = 'https://localhost/'; -const testApiKeyWithAdPlacements = 'key_x6UnCVRZaJgIHFQD'; const testPlacementId = 'home'; function validateOriginReferrer(requestParams) { From eda8c154c0e3c811a5db9de391b40ce59c556d57 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Thu, 12 Feb 2026 12:53:50 +0400 Subject: [PATCH 5/6] Spread instead of assign --- spec/src/modules/tracker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index a76e98b4..746e3ed5 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -15821,7 +15821,7 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect( tracker.trackMediaImpressionClick( - Object.assign(requiredParameters, optionalParameters), + { ...requiredParameters, ...optionalParameters }, ), ).to.equal(true); }); From f247712544c11f1ca3d8b7b1c3795f59cda4de5e Mon Sep 17 00:00:00 2001 From: Viktor Zavala Date: Thu, 12 Feb 2026 13:55:09 +0100 Subject: [PATCH 6/6] [REM-2574] Add media api key env variable to bundled test runs --- .github/workflows/run-tests-bundled.yml | 1 + .github/workflows/run-tests.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/run-tests-bundled.yml b/.github/workflows/run-tests-bundled.yml index 79d1ad28..24cfe00d 100644 --- a/.github/workflows/run-tests-bundled.yml +++ b/.github/workflows/run-tests-bundled.yml @@ -25,4 +25,5 @@ jobs: run: npm run test:bundled:parallel env: TEST_REQUEST_API_KEY: ${{ secrets.TEST_REQUEST_API_KEY }} + TEST_MEDIA_REQUEST_API_KEY: ${{ secrets.TEST_MEDIA_REQUEST_API_KEY }} SKIP_NETWORK_TIMEOUT_TESTS: true diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 448d80d0..2fbac4f6 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -25,4 +25,5 @@ jobs: run: npm run test:parallel env: TEST_REQUEST_API_KEY: ${{ secrets.TEST_REQUEST_API_KEY }} + TEST_MEDIA_REQUEST_API_KEY: ${{ secrets.TEST_MEDIA_REQUEST_API_KEY }} SKIP_NETWORK_TIMEOUT_TESTS: true