diff --git a/libraries/intentIqConstants/intentIqConstants.js b/libraries/intentIqConstants/intentIqConstants.js index 6dc16969d6c..a0f48caf97c 100644 --- a/libraries/intentIqConstants/intentIqConstants.js +++ b/libraries/intentIqConstants/intentIqConstants.js @@ -1,4 +1,5 @@ export const FIRST_PARTY_KEY = '_iiq_fdata'; + export const SUPPORTED_TYPES = ['html5', 'cookie'] export const WITH_IIQ = 'A'; @@ -8,4 +9,21 @@ export const BLACK_LIST = 'L'; export const CLIENT_HINTS_KEY = '_iiq_ch'; export const EMPTY = 'EMPTY'; export const GVLID = '1323'; -export const VERSION = 0.27; +export const VERSION = 0.28; + +export const VR_ENDPOINT = 'https://api.intentiq.com'; +export const GDPR_ENDPOINT = 'https://api-gdpr.intentiq.com'; +export const INVALID_ID = 'INVALID_ID'; + +export const SYNC_ENDPOINT = 'https://sync.intentiq.com' +export const GDPR_SYNC_ENDPOINT = 'https://sync-gdpr.intentiq.com' +export const SCREEN_PARAMS = { + 0: 'windowInnerHeight', + 1: 'windowInnerWidth', + 2: 'devicePixelRatio', + 3: 'windowScreenHeight', + 4: 'windowScreenWidth', + 5: 'language' +}; + +export const SYNC_REFRESH_MILL = 3600000; diff --git a/libraries/intentIqUtils/getRefferer.js b/libraries/intentIqUtils/getRefferer.js index 39fde70ac24..20c6a6a5b47 100644 --- a/libraries/intentIqUtils/getRefferer.js +++ b/libraries/intentIqUtils/getRefferer.js @@ -6,11 +6,16 @@ import { getWindowTop, logError, getWindowLocation, getWindowSelf } from '../../ */ export function getReferrer() { try { - if (getWindowSelf() === getWindowTop()) { - return encodeURIComponent(getWindowLocation().href); - } else { - return encodeURIComponent(getWindowTop().location.href); + const url = getWindowSelf() === getWindowTop() + ? getWindowLocation().href + : getWindowTop().location.href; + + if (url.length >= 50) { + const { origin } = new URL(url); + return origin; } + + return url; } catch (error) { logError(`Error accessing location: ${error}`); return ''; @@ -26,7 +31,7 @@ export function getReferrer() { * @return {string} The modified URL with appended `vrref` or `fui` parameters. */ export function appendVrrefAndFui(url, domainName) { - const fullUrl = getReferrer(); + const fullUrl = encodeURIComponent(getReferrer()); if (fullUrl) { return (url += '&vrref=' + getRelevantRefferer(domainName, fullUrl)); } diff --git a/libraries/intentIqUtils/getSyncKey.js b/libraries/intentIqUtils/getSyncKey.js new file mode 100644 index 00000000000..723a60e0059 --- /dev/null +++ b/libraries/intentIqUtils/getSyncKey.js @@ -0,0 +1 @@ +export const SYNC_KEY = (partner) => '_iiq_sync' + '_' + partner diff --git a/modules/intentIqAnalyticsAdapter.js b/modules/intentIqAnalyticsAdapter.js index 3cc0efbdb57..2adc664d9e1 100644 --- a/modules/intentIqAnalyticsAdapter.js +++ b/modules/intentIqAnalyticsAdapter.js @@ -55,7 +55,8 @@ const PARAMS_NAMES = { prebidVersion: 'pbjsver', partnerId: 'partnerId', firstPartyId: 'pcid', - placementId: 'placementId' + placementId: 'placementId', + adType: 'adType' }; let iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({defaultUrl: REPORT_ENDPOINT, analyticsType}), { @@ -129,7 +130,7 @@ function initReadLsIds() { iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause = pData.terminationCause iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs = pData.data; iiqAnalyticsAnalyticsAdapter.initOptions.eidl = pData.eidl || -1; - iiqAnalyticsAnalyticsAdapter.initOptions.ct = pData.ct || null; + iiqAnalyticsAnalyticsAdapter.initOptions.clientType = pData.clientType || null; iiqAnalyticsAnalyticsAdapter.initOptions.siteId = pData.siteId || null; iiqAnalyticsAnalyticsAdapter.initOptions.wsrvcll = pData.wsrvcll || false; iiqAnalyticsAnalyticsAdapter.initOptions.rrtt = pData.rrtt || null; @@ -188,7 +189,7 @@ export function preparePayload(data) { result[PARAMS_NAMES.referrer] = getReferrer(); result[PARAMS_NAMES.terminationCause] = iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause; result[PARAMS_NAMES.abTestGroup] = iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup; - result[PARAMS_NAMES.clientType] = iiqAnalyticsAnalyticsAdapter.initOptions.ct; + result[PARAMS_NAMES.clientType] = iiqAnalyticsAnalyticsAdapter.initOptions.clientType; result[PARAMS_NAMES.siteId] = iiqAnalyticsAnalyticsAdapter.initOptions.siteId; result[PARAMS_NAMES.wasServerCalled] = iiqAnalyticsAnalyticsAdapter.initOptions.wsrvcll; result[PARAMS_NAMES.requestRtt] = iiqAnalyticsAnalyticsAdapter.initOptions.rrtt; @@ -214,6 +215,8 @@ function fillEidsData(result) { } function prepareData (data, result) { + const adTypeValue = data.adType || data.mediaType; + if (data.bidderCode) { result.bidderCode = data.bidderCode; } @@ -235,30 +238,52 @@ function prepareData (data, result) { if (data.auctionId) { result.prebidAuctionId = data.auctionId; } - if (data.placementId) { - result.placementId = data.placementId; - } else { - // Simplified placementId determination - let placeIdFound = false; - if (data.params && Array.isArray(data.params)) { - for (let i = 0; i < data.params.length; i++) { - const param = data.params[i]; - if (param.placementId) { - result.placementId = param.placementId; - placeIdFound = true; - break; - } - } - } - if (!placeIdFound && data.adUnitCode) { - result.placementId = data.adUnitCode; - } + if (adTypeValue) { + result[PARAMS_NAMES.adType] = adTypeValue; + } + const iiqConfig = getIntentIqConfig(); + const adUnitConfig = iiqConfig.params?.adUnitConfig; + + switch (adUnitConfig) { + case 1: + // adUnitCode or placementId + result.placementId = data.adUnitCode || extractPlacementId(data) || ''; + break; + case 2: + // placementId or adUnitCode + result.placementId = extractPlacementId(data) || data.adUnitCode || ''; + break; + case 3: + // Only adUnitCode + result.placementId = data.adUnitCode || ''; + break; + case 4: + // Only placementId + result.placementId = extractPlacementId(data) || ''; + break; + default: + // Default (like in case #1) + result.placementId = data.adUnitCode || extractPlacementId(data) || ''; } result.biddingPlatformId = 1; result.partnerAuctionId = 'BW'; } +function extractPlacementId(data) { + if (data.placementId) { + return data.placementId; + } + if (data.params && Array.isArray(data.params)) { + for (let i = 0; i < data.params.length; i++) { + if (data.params[i].placementId) { + return data.params[i].placementId; + } + } + } + return null; +} + function getDefaultDataObject() { return { 'inbbl': false, diff --git a/modules/intentIqAnalyticsAdapter.md b/modules/intentIqAnalyticsAdapter.md index 2a3eece0576..2f601658a3d 100644 --- a/modules/intentIqAnalyticsAdapter.md +++ b/modules/intentIqAnalyticsAdapter.md @@ -60,7 +60,8 @@ currency: 'USD', // Currency for the CPM value. originalCpm: 1.5, // Original CPM value. originalCurrency: 'USD', // Original currency. status: 'rendered', // Auction status, e.g., 'rendered'. -placementId: 'div-1' // ID of the ad placement. +placementId: 'div-1', // ID of the ad placement. +adType: 'banner' // Specifies the type of ad served } ``` @@ -76,6 +77,7 @@ placementId: 'div-1' // ID of the ad placement. | originalCurrency | String | Currency of the original auction | USD | No | | status | String | Status of the impression. Leave empty or undefined if Prebid is not the bidding platform | rendered | No | | placementId | String | Unique identifier of the ad unit on the webpage that showed this ad | div-1 | No | +| adType | String | Specifies the type of ad served. Possible values: “banner“, “video“, “native“, “audio“. | banner | No | To report the auction win, call the function as follows: diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 548af86d772..f4a1b316adc 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -22,8 +22,9 @@ import { CLIENT_HINTS_KEY, EMPTY, GVLID, - VERSION, + VERSION, INVALID_ID, GDPR_ENDPOINT, VR_ENDPOINT, SYNC_ENDPOINT, SCREEN_PARAMS, GDPR_SYNC_ENDPOINT, SYNC_REFRESH_MILL } from '../libraries/intentIqConstants/intentIqConstants.js'; +import {SYNC_KEY} from '../libraries/intentIqUtils/getSyncKey.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -44,9 +45,7 @@ const encoderCH = { wow64: 7, fullVersionList: 8 }; -const INVALID_ID = 'INVALID_ID'; -const ENDPOINT = 'https://api.intentiq.com'; -const GDPR_ENDPOINT = 'https://api-gdpr.intentiq.com'; + export let firstPartyData; /** @@ -82,6 +81,89 @@ export function decryptData(encryptedText) { return bytes.toString(Utf8); } +function collectDeviceInfo() { + return { + windowInnerHeight: window.innerHeight, + windowInnerWidth: window.innerWidth, + devicePixelRatio: window.devicePixelRatio, + windowScreenHeight: window.screen.height, + windowScreenWidth: window.screen.width, + language: navigator.language + } +} + +function addUniquenessToUrl(url) { + url += '&tsrnd=' + Math.floor(Math.random() * 1000) + '_' + new Date().getTime(); + return url; +} + +function appendDeviceInfoToUrl(url, deviceInfo) { + const screenParamsString = Object.entries(SCREEN_PARAMS) + .map(([index, param]) => { + const value = (deviceInfo)[param]; + return `${index}:${value}`; + }) + .join(','); + + url += `&cz=${encodeURIComponent(screenParamsString)}`; + url += `&dw=${deviceInfo.windowScreenWidth}&dh=${deviceInfo.windowScreenHeight}&dpr=${deviceInfo.devicePixelRatio}&lan=${deviceInfo.language}`; + return url; +} + +function appendFirstPartyData (url, firstPartyData, partnerData) { + url += firstPartyData.pid ? '&pid=' + encodeURIComponent(firstPartyData.pid) : ''; + url += firstPartyData.pcid ? '&iiqidtype=2&iiqpcid=' + encodeURIComponent(firstPartyData.pcid) : ''; + url += firstPartyData.pcidDate ? '&iiqpciddate=' + encodeURIComponent(firstPartyData.pcidDate) : ''; + return url +} + +function appendCMPData (url, cmpData) { + url += cmpData.uspString ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : ''; + url += cmpData.gppString ? '&gpp=' + encodeURIComponent(cmpData.gppString) : ''; + url += cmpData.gdprApplies + ? '&gdpr_consent=' + encodeURIComponent(cmpData.gdprString) + '&gdpr=1' + : '&gdpr=0'; + return url +} + +export function createPixelUrl(firstPartyData, clientHints, configParams, partnerData, cmpData) { + const deviceInfo = collectDeviceInfo() + + let url = cmpData.gdprString ? GDPR_SYNC_ENDPOINT : SYNC_ENDPOINT; + url += '/profiles_engine/ProfilesEngineServlet?at=20&mi=10&secure=1' + url += '&dpi=' + configParams.partner; + url = appendFirstPartyData(url, firstPartyData, partnerData); + url = addUniquenessToUrl(url); + url += partnerData?.clientType ? '&idtype=' + partnerData.clientType : ''; + if (deviceInfo) url = appendDeviceInfoToUrl(url, deviceInfo) + url += VERSION ? '&jsver=' + VERSION : ''; + if (clientHints) url += '&uh=' + encodeURIComponent(clientHints); + url = appendVrrefAndFui(url, configParams.domainName); + url = appendCMPData(url, cmpData) + return url; +} + +function sendSyncRequest(allowedStorage, url, partner, firstPartyData, newUser) { + const lastSyncDate = Number(readData(SYNC_KEY(partner) || '', allowedStorage)) || false; + const lastSyncElapsedTime = Date.now() - lastSyncDate + + if (firstPartyData.isOptedOut) { + const needToDoSync = (Date.now() - (firstPartyData?.date || firstPartyData?.sCal || Date.now())) > SYNC_REFRESH_MILL + if (newUser || needToDoSync) { + ajax(url, () => { + }, undefined, {method: 'GET', withCredentials: true}); + if (firstPartyData?.date) { + firstPartyData.date = Date.now() + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + } + } + } else if (!lastSyncDate || lastSyncElapsedTime > SYNC_REFRESH_MILL) { + storeData(SYNC_KEY(partner), Date.now() + '', allowedStorage); + ajax(url, () => { + }, undefined, {method: 'GET', withCredentials: true}); + } +} + /** * Parse json if possible, else return null * @param data @@ -161,6 +243,7 @@ export const intentIqIdSubmodule = { decode(value) { return value && value != '' && INVALID_ID != value ? {'intentIqId': value} : undefined; }, + /** * performs action to obtain id and return a value in the callback's response argument * @function @@ -210,13 +293,7 @@ export const intentIqIdSubmodule = { const currentBrowserLowerCase = detectBrowser(); const browserBlackList = typeof configParams.browserBlackList === 'string' ? configParams.browserBlackList.toLowerCase() : ''; - - // Check if current browser is in blacklist - if (browserBlackList?.includes(currentBrowserLowerCase)) { - logError('User ID - intentIqId submodule: browser is in blacklist!'); - if (configParams.callback) configParams.callback('', BLACK_LIST); - return; - } + let newUser = false; if (!firstPartyData?.pcid) { const firstPartyId = generateGUID(); @@ -230,6 +307,7 @@ export const intentIqIdSubmodule = { gdprString: EMPTY, date: Date.now() }; + newUser = true; storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); } else if (!firstPartyData.pcidDate) { firstPartyData.pcidDate = Date.now(); @@ -284,7 +362,6 @@ export const intentIqIdSubmodule = { firstPartyData.uspString = cmpData.uspString; firstPartyData.gppString = cmpData.gppString; firstPartyData.gdprString = cmpData.gdprString; - firstPartyData.date = Date.now(); shouldCallServer = true; storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); @@ -297,6 +374,15 @@ export const intentIqIdSubmodule = { firePartnerCallback() } + // Check if current browser is in blacklist + if (browserBlackList?.includes(currentBrowserLowerCase)) { + logError('User ID - intentIqId submodule: browser is in blacklist! Data will be not provided.'); + if (configParams.callback) configParams.callback('', BLACK_LIST); + const url = createPixelUrl(firstPartyData, clientHints, configParams, partnerData, cmpData) + sendSyncRequest(allowedStorage, url, configParams.partner, firstPartyData, newUser) + return + } + if (!shouldCallServer) { if (isGroupB) runtimeEids = { eids: [] }; firePartnerCallback(); @@ -304,19 +390,13 @@ export const intentIqIdSubmodule = { } // use protocol relative urls for http or https - let url = `${gdprDetected ? GDPR_ENDPOINT : ENDPOINT}/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`; + let url = `${gdprDetected ? GDPR_ENDPOINT : VR_ENDPOINT}/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`; url += configParams.pcid ? '&pcid=' + encodeURIComponent(configParams.pcid) : ''; url += configParams.pai ? '&pai=' + encodeURIComponent(configParams.pai) : ''; - url += firstPartyData.pcid ? '&iiqidtype=2&iiqpcid=' + encodeURIComponent(firstPartyData.pcid) : ''; - url += firstPartyData.pid ? '&pid=' + encodeURIComponent(firstPartyData.pid) : ''; + url = appendFirstPartyData(url, firstPartyData, partnerData); url += (partnerData.cttl) ? '&cttl=' + encodeURIComponent(partnerData.cttl) : ''; url += (partnerData.rrtt) ? '&rrtt=' + encodeURIComponent(partnerData.rrtt) : ''; - url += firstPartyData.pcidDate ? '&iiqpciddate=' + encodeURIComponent(firstPartyData.pcidDate) : ''; - url += cmpData.uspString ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : ''; - url += cmpData.gppString ? '&gpp=' + encodeURIComponent(cmpData.gppString) : ''; - url += cmpData.gdprApplies - ? '&gdpr_consent=' + encodeURIComponent(cmpData.gdprString) + '&gdpr=1' - : '&gdpr=0'; + url = appendCMPData(url, cmpData) url += clientHints ? '&uh=' + encodeURIComponent(clientHints) : ''; url += VERSION ? '&jsver=' + VERSION : ''; url += firstPartyData?.group ? '&testGroup=' + encodeURIComponent(firstPartyData.group) : ''; @@ -403,7 +483,7 @@ export const intentIqIdSubmodule = { } if ('ct' in respJson) { - partnerData.ct = respJson.ct; + partnerData.clientType = respJson.ct; } if ('sid' in respJson) { diff --git a/modules/intentIqIdSystem.md b/modules/intentIqIdSystem.md index 44e4032e23d..39bbb47256f 100644 --- a/modules/intentIqIdSystem.md +++ b/modules/intentIqIdSystem.md @@ -38,13 +38,14 @@ Please find below list of paramters that could be used in configuring Intent IQ | params.partner | Required | Number | This is the partner ID value obtained from registering with IntentIQ. | `1177538` | | params.pcid | Optional | String | This is the partner cookie ID, it is a dynamic value attached to the request. | `"g3hC52b"` | | params.pai | Optional | String | This is the partner customer ID / advertiser ID, it is a dynamic value attached to the request. | `"advertiser1"` | -| params.callback | Required | Function | This is a callback which is trigered with data and AB group | `(data, group) => console.log({ data, group })` | +| params.callback | Optional | Function | This is a callback which is trigered with data and AB group | `(data, group) => console.log({ data, group })` | | params.timeoutInMillis | Optional | Number | This is the timeout in milliseconds, which defines the maximum duration before the callback is triggered. The default value is 500. | `450` | | params.browserBlackList | Optional |  String | This is the name of a browser that can be added to a blacklist. | `"chrome"` | -| params.manualWinReportEnabled | Optional | Boolean | This variable determines whether the bidWon event is triggered automatically. If set to false, the event will occur automatically, and manual reporting with reportExternalWin will be disabled. If set to true, the event will not occur automatically, allowing manual reporting through reportExternalWin. The default value is false. | `true`| +| params.manualWinReportEnabled | Optional | Boolean | This variable determines whether the bidWon event is triggered automatically. If set to false, the event will occur automatically, and manual reporting with reportExternalWin will be disabled. If set to true, the event will not occur automatically, allowing manual reporting through reportExternalWin. The default value is false. | `true` | | params.domainName | Optional | String | Specifies the domain of the page in which the IntentIQ object is currently running and serving the impression. This domain will be used later in the revenue reporting breakdown by domain. For example, cnn.com. It identifies the primary source of requests to the IntentIQ servers, even within nested web pages. | `"currentDomain.com"` | -| params.gamObjectReference | Optional | Object | This is a reference to the Google Ad Manager (GAM) object, which will be used to set targeting. If this parameter is not provided, the group reporting will not be configured. | `googletag` | +| params.gamObjectReference | Optional | Object | This is a reference to the Google Ad Manager (GAM) object, which will be used to set targeting. If this parameter is not provided, the group reporting will not be configured. | `googletag` | | params.gamParameterName | Optional | String | The name of the targeting parameter that will be used to pass the group. If not specified, the default value is `intent_iq_group`. | `"intent_iq_group"` | +| params.adUnitConfig | Optional | Number | Determines how the placementId parameter is extracted in the report (default is 1). Possible values: 1 – adUnitCode first, 2 – placementId first, 3 – only adUnitCode, 4 – only placementId | `1` | ### Configuration example @@ -60,6 +61,7 @@ pbjs.setConfig({ callback: (data, group) => window.pbjs.requestBids(), manualWinReportEnabled: true, domainName: "currentDomain.com", + adUnitConfig: 1 // Extracting placementId strategy (adUnitCode or placementId order of priorities) }, storage: { type: "html5", diff --git a/test/spec/modules/intentIqAnalyticsAdapter_spec.js b/test/spec/modules/intentIqAnalyticsAdapter_spec.js index 26a70ded14e..eccff1165cd 100644 --- a/test/spec/modules/intentIqAnalyticsAdapter_spec.js +++ b/test/spec/modules/intentIqAnalyticsAdapter_spec.js @@ -21,12 +21,13 @@ const REPORT_ENDPOINT_GDPR = 'https://reports-gdpr.intentiq.com/report'; const storage = getStorageManager({ moduleType: 'analytics', moduleName: 'iiqAnalytics' }); -const USERID_CONFIG = [ +const getUserConfig = () => [ { 'name': 'intentIqId', 'params': { 'partner': partner, 'unpack': null, + 'manualWinReportEnabled': false }, 'storage': { 'type': 'html5', @@ -58,7 +59,6 @@ let wonRequest = { 'responseTimestamp': 1669644710345, 'requestTimestamp': 1669644710109, 'bidder': 'testbidder', - 'adUnitCode': 'addUnitCode', 'timeToRespond': 236, 'pbLg': '5.00', 'pbMg': '5.00', @@ -79,7 +79,7 @@ describe('IntentIQ tests all', function () { beforeEach(function () { logErrorStub = sinon.stub(utils, 'logError'); - sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(getUserConfig()); sinon.stub(events, 'getEvents').returns([]); iiqAnalyticsAnalyticsAdapter.enableAnalytics({ provider: 'iiqAnalytics', @@ -134,6 +134,45 @@ describe('IntentIQ tests all', function () { expect(request.url).to.contain('iiqid=f961ffb1-a0e1-4696-a9d2-a21d815bd344'); }); + it('should include adType in payload when present in BID_WON event', function () { + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); + const bidWonEvent = { ...wonRequest, mediaType: 'video' }; + + events.emit(EVENTS.BID_WON, bidWonEvent); + + const request = server.requests[0]; + const urlParams = new URL(request.url); + const payloadEncoded = urlParams.searchParams.get('payload'); + const payloadDecoded = JSON.parse(atob(JSON.parse(payloadEncoded)[0])); + + expect(server.requests.length).to.be.above(0); + expect(payloadDecoded).to.have.property('adType', bidWonEvent.mediaType); + }); + + it('should include adType in payload when present in reportExternalWin event', function () { + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); + const externalWinEvent = { cpm: 1, currency: 'USD', adType: 'banner' }; + const [userConfig] = getUserConfig(); + userConfig.params.manualWinReportEnabled = true; + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); + + const partnerId = userConfig.params.partner; + + events.emit(EVENTS.BID_REQUESTED); + + window[`intentIqAnalyticsAdapter_${partnerId}`].reportExternalWin(externalWinEvent); + + const request = server.requests[0]; + const urlParams = new URL(request.url); + const payloadEncoded = urlParams.searchParams.get('payload'); + const payloadDecoded = JSON.parse(atob(JSON.parse(payloadEncoded)[0])); + + expect(server.requests.length).to.be.above(0); + expect(payloadDecoded).to.have.property('adType', externalWinEvent.adType); + }); + it('should send report to report-gdpr address if gdpr is detected', function () { const gppStub = sinon.stub(gppDataHandler, 'getConsentData').returns({ gppString: '{"key1":"value1","key2":"value2"}' }); const uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns('1NYN'); @@ -250,7 +289,7 @@ describe('IntentIQ tests all', function () { getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); const referrer = getReferrer(); - expect(referrer).to.equal(encodeURIComponent('http://localhost:9876/')); + expect(referrer).to.equal('http://localhost:9876/'); }); it('should return window.top.location.href when window.self !== window.top and access is successful', function () { @@ -260,7 +299,7 @@ describe('IntentIQ tests all', function () { const referrer = getReferrer(); - expect(referrer).to.equal(encodeURIComponent('http://example.com/')); + expect(referrer).to.equal('http://example.com/'); }); it('should return an empty string and log an error when accessing window.top.location.href throws an error', function () { @@ -275,7 +314,7 @@ describe('IntentIQ tests all', function () { }); it('should not send request if the browser is in blacklist (chrome)', function () { - const USERID_CONFIG_BROWSER = [...USERID_CONFIG]; + const USERID_CONFIG_BROWSER = [...getUserConfig()]; USERID_CONFIG_BROWSER[0].params.browserBlackList = 'ChrOmE'; config.getConfig.restore(); @@ -289,7 +328,7 @@ describe('IntentIQ tests all', function () { }); it('should send request if the browser is not in blacklist (safari)', function () { - const USERID_CONFIG_BROWSER = [...USERID_CONFIG]; + const USERID_CONFIG_BROWSER = [...getUserConfig()]; USERID_CONFIG_BROWSER[0].params.browserBlackList = 'chrome,firefox'; config.getConfig.restore(); @@ -367,4 +406,79 @@ describe('IntentIQ tests all', function () { } }); }); + + const adUnitConfigTests = [ + { + adUnitConfig: 1, + description: 'should extract adUnitCode first (adUnitConfig = 1)', + event: { adUnitCode: 'adUnitCode-123', placementId: 'placementId-456' }, + expectedPlacementId: 'adUnitCode-123' + }, + { + adUnitConfig: 1, + description: 'should extract placementId if there is no adUnitCode (adUnitConfig = 1)', + event: { placementId: 'placementId-456' }, + expectedPlacementId: 'placementId-456' + }, + { + adUnitConfig: 2, + description: 'should extract placementId first (adUnitConfig = 2)', + event: { adUnitCode: 'adUnitCode-123', placementId: 'placementId-456' }, + expectedPlacementId: 'placementId-456' + }, + { + adUnitConfig: 2, + description: 'should extract adUnitCode if there is no placementId (adUnitConfig = 2)', + event: { adUnitCode: 'adUnitCode-123', }, + expectedPlacementId: 'adUnitCode-123' + }, + { + adUnitConfig: 3, + description: 'should extract only adUnitCode (adUnitConfig = 3)', + event: { adUnitCode: 'adUnitCode-123', placementId: 'placementId-456' }, + expectedPlacementId: 'adUnitCode-123' + }, + { + adUnitConfig: 4, + description: 'should extract only placementId (adUnitConfig = 4)', + event: { adUnitCode: 'adUnitCode-123', placementId: 'placementId-456' }, + expectedPlacementId: 'placementId-456' + }, + { + adUnitConfig: 1, + description: 'should return empty placementId if neither adUnitCode or placementId exist', + event: {}, + expectedPlacementId: '' + }, + { + adUnitConfig: 1, + description: 'should extract placementId from params array if no top-level adUnitCode or placementId exist (adUnitConfig = 1)', + event: { + params: [{ someKey: 'value' }, { placementId: 'nested-placementId' }] + }, + expectedPlacementId: 'nested-placementId' + } + ]; + + adUnitConfigTests.forEach(({ adUnitConfig, description, event, expectedPlacementId }) => { + it(description, function () { + const [userConfig] = getUserConfig(); + userConfig.params.adUnitConfig = adUnitConfig; + + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); + + const testEvent = { ...wonRequest, ...event }; + events.emit(EVENTS.BID_WON, testEvent); + + const request = server.requests[0]; + const urlParams = new URL(request.url); + const encodedPayload = urlParams.searchParams.get('payload'); + const decodedPayload = JSON.parse(atob(JSON.parse(encodedPayload)[0])); + + expect(server.requests.length).to.be.above(0); + expect(encodedPayload).to.exist; + expect(decodedPayload).to.have.property('placementId', expectedPlacementId); + }); + }); }); diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index d4220710c1f..b2837f6e467 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -1,7 +1,13 @@ import { expect } from 'chai'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; -import { intentIqIdSubmodule, decryptData, handleClientHints, firstPartyData as moduleFPD } from '../../../modules/intentIqIdSystem'; +import { + intentIqIdSubmodule, + decryptData, + handleClientHints, + firstPartyData as moduleFPD, + isCMPStringTheSame, createPixelUrl +} from '../../../modules/intentIqIdSystem'; import {storage, readData} from '../../../libraries/intentIqUtils/storageUtils.js'; import { gppDataHandler, uspDataHandler, gdprDataHandler } from '../../../src/consentHandler'; import { clearAllCookies } from '../../helpers/cookies'; @@ -589,6 +595,85 @@ describe('IntentIQ tests', function () { expect(savedClientHints).to.equal(expectedClientHints); }); + it('should return true if CMP strings are the same', function () { + const fpData = { gdprString: '123', gppString: '456', uspString: '789' }; + const cmpData = { gdprString: '123', gppString: '456', uspString: '789' }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.true; + }); + + it('should return false if gdprString is different', function () { + const fpData = { gdprString: '123', gppString: '456', uspString: '789' }; + const cmpData = { gdprString: '321', gppString: '456', uspString: '789' }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.false; + }); + + it('should return false if gppString is different', function () { + const fpData = { gdprString: '123', gppString: '456', uspString: '789' }; + const cmpData = { gdprString: '123', gppString: '654', uspString: '789' }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.false; + }); + + it('should return false if uspString is different', function () { + const fpData = { gdprString: '123', gppString: '456', uspString: '789' }; + const cmpData = { gdprString: '123', gppString: '456', uspString: '987' }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.false; + }); + + it('should return false if one of the properties is missing in fpData', function () { + const fpData = { gdprString: '123', gppString: '456' }; + const cmpData = { gdprString: '123', gppString: '456', uspString: '789' }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.false; + }); + + it('should return false if one of the properties is missing in cmpData', function () { + const fpData = { gdprString: '123', gppString: '456', uspString: '789' }; + const cmpData = { gdprString: '123', gppString: '456' }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.false; + }); + + it('should return true if both objects are empty', function () { + const fpData = {}; + const cmpData = {}; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.true; + }); + + it('should return false if one object is empty and another is not', function () { + const fpData = {}; + const cmpData = { gdprString: '123', gppString: '456', uspString: '789' }; + + expect(isCMPStringTheSame(fpData, cmpData)).to.be.false; + }); + + it('should add clientHints to the URL if provided', function () { + const firstPartyData = {}; + const clientHints = 'exampleClientHints'; + const configParams = { partner: 'testPartner', domainName: 'example.com' }; + const partnerData = {}; + const cmpData = {}; + + const url = createPixelUrl(firstPartyData, clientHints, configParams, partnerData, cmpData); + + expect(url).to.include(`&uh=${encodeURIComponent(clientHints)}`); + }); + + it('should not add clientHints to the URL if not provided', function () { + const firstPartyData = {}; + const configParams = { partner: 'testPartner', domainName: 'example.com' }; + const partnerData = {}; + const cmpData = {}; + + const url = createPixelUrl(firstPartyData, undefined, configParams, partnerData, cmpData); + + expect(url).to.not.include('&uh='); + }); + it('should run callback from params', async () => { let wasCallbackCalled = false const callbackConfigParams = { params: { partner: partner,