diff --git a/spec/src/modules/tracker.js b/spec/src/modules/tracker.js index fce802ad..518f3743 100644 --- a/spec/src/modules/tracker.js +++ b/spec/src/modules/tracker.js @@ -14539,4 +14539,264 @@ describe(`ConstructorIO - Tracker${bundledDescriptionSuffix}`, () => { expect(tracker.trackProductInsightsAgentAnswerFeedback(requiredParameters)).to.equal(true); }); }); + + describe('trackMediaImpressionView', () => { + const requiredParameters = { + bannerAdId: 'banner_ad_id', + placementId: 'placement_id', + }; + + const optionalParameters = { + analyticsTags: testAnalyticsTag, + }; + + it('Should respond with a valid response when required parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + 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('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('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.trackMediaImpressionView(requiredParameters)).to.equal( + true, + ); + }); + + it('Should respond with a valid response when required and optional parameters are provided', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + 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('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.trackMediaImpressionView( + 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.trackMediaImpressionView([])).to.be.an('error'); + }); + + it('Should throw an error when no parameters are provided', () => { + const { tracker } = new ConstructorIO({ apiKey: testApiKey }); + + 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, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: true, + mediaServiceUrl: 'https://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.trackMediaImpressionView(requiredParameters)).to.equal( + true, + ); + }); + + it('Should not send along origin_referrer query param if sendReferrerWithTrackingEvents is false', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + fetch: fetchSpy, + sendReferrerWithTrackingEvents: false, + mediaServiceUrl: 'https://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.trackMediaImpressionView(requiredParameters)).to.equal( + true, + ); + }); + + if (!skipNetworkTimeoutTests) { + it('Should be rejected when network request timeout is provided and reached', (done) => { + const { tracker } = new ConstructorIO({ + apiKey: testApiKey, + mediaServiceUrl: 'https://media-cnstrc.com', + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect( + tracker.trackMediaImpressionView(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: testApiKey, + mediaServiceUrl: 'https://media-cnstrc.com', + networkParameters: { + timeout: 20, + }, + ...requestQueueOptions, + }); + + tracker.on('error', ({ message }) => { + expect(message).to.equal(timeoutRejectionMessage); + done(); + }); + + expect(tracker.trackMediaImpressionView(requiredParameters)).to.equal( + true, + ); + }); + } + + 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); + }); + }); }); diff --git a/src/constructorio.js b/src/constructorio.js index 958f94cd..24ce07d8 100644 --- a/src/constructorio.js +++ b/src/constructorio.js @@ -40,6 +40,7 @@ class ConstructorIO { * @param {string} [parameters.serviceUrl='https://ac.cnstrc.com'] - API URL endpoint * @param {string} [parameters.quizzesServiceUrl='https://quizzes.cnstrc.com'] - Quizzes API URL endpoint * @param {string} [parameters.agentServiceUrl='https://agent.cnstrc.com'] - AI Shopping Agent API URL endpoint + * @param {string} [parameters.mediaServiceUrl='https://media-cnstrc.com'] - Media API URL endpoint * @param {string} [parameters.assistantServiceUrl='https://assistant.cnstrc.com'] - AI Shopping Assistant API URL endpoint @deprecated This parameter is deprecated and will be removed in a future version. Use parameters.agentServiceUrl instead. * @param {array} [parameters.segments] - User segments * @param {object} [parameters.testCells] - User test cells @@ -74,6 +75,7 @@ class ConstructorIO { quizzesServiceUrl, agentServiceUrl, assistantServiceUrl, + mediaServiceUrl, segments, testCells, clientId, @@ -121,6 +123,7 @@ class ConstructorIO { quizzesServiceUrl: (quizzesServiceUrl && quizzesServiceUrl.replace(/\/$/, '')) || 'https://quizzes.cnstrc.com', agentServiceUrl: (agentServiceUrl && agentServiceUrl.replace(/\/$/, '')) || 'https://agent.cnstrc.com', assistantServiceUrl: (assistantServiceUrl && assistantServiceUrl.replace(/\/$/, '')) || 'https://assistant.cnstrc.com', + mediaServiceUrl: (mediaServiceUrl && mediaServiceUrl.replace(/\/$/, '')) || 'https://media-cnstrc.com', sessionId: sessionId || session_id, clientId: clientId || client_id, userId, diff --git a/src/modules/tracker.js b/src/modules/tracker.js index 702e3a05..376b4385 100644 --- a/src/modules/tracker.js +++ b/src/modules/tracker.js @@ -1336,6 +1336,76 @@ class Tracker { return new Error('parameters are required of type object'); } + /** + * Send media impression view event to API + * + * @function trackMediaImpressionView + * @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 viewed a media banner + * @example + * constructorio.tracker.trackMediaImpressionView( + * { + * bannerAdId: 'banner_ad_id', + * placementId: 'placement_id', + * }, + * ); + */ + 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 requestPath = `${baseUrl.toString()}v2/ad_behavioral_action/display_ad_view?`; + + 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/index.d.ts b/src/types/index.d.ts index ce217566..f28d9461 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -47,6 +47,7 @@ export interface ConstructorClientOptions { serviceUrl?: string; quizzesServiceUrl?: string; agentServiceUrl?: string; + mediaServiceUrl?: string; assistantServiceUrl?: string; sessionId?: number; clientId?: string; diff --git a/src/types/tracker.d.ts b/src/types/tracker.d.ts index 60a21793..cf508543 100644 --- a/src/types/tracker.d.ts +++ b/src/types/tracker.d.ts @@ -436,5 +436,12 @@ declare class Tracker { networkParameters?: NetworkParameters ): true | Error; + trackMediaImpressionView(parameters: { + bannerAdId: string; + placementId: string; + analyticsTags?: Record; + }, networkParameters?: NetworkParameters + ): true | Error; + on(messageType: string, callback: Function): true | Error; }