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; }