diff --git a/Prebid.js b/Prebid.js new file mode 160000 index 00000000000..b121eda695f --- /dev/null +++ b/Prebid.js @@ -0,0 +1 @@ +Subproject commit b121eda695f82d8d2e6e8189f8fc6f861a3dbf5e diff --git a/libraries/intentIqConstants/intentIqConstants.js b/libraries/intentIqConstants/intentIqConstants.js index 0fa479a19ad..6bcf994d2b4 100644 --- a/libraries/intentIqConstants/intentIqConstants.js +++ b/libraries/intentIqConstants/intentIqConstants.js @@ -12,6 +12,7 @@ export const GVLID = "1323"; export const VERSION = 0.33; export const PREBID = "pbjs"; export const HOURS_24 = 86400000; +export const HOURS_72 = HOURS_24 * 3; export const INVALID_ID = "INVALID_ID"; diff --git a/libraries/intentIqUtils/getRefferer.js b/libraries/intentIqUtils/getRefferer.js index 20c6a6a5b47..2ed8a668a56 100644 --- a/libraries/intentIqUtils/getRefferer.js +++ b/libraries/intentIqUtils/getRefferer.js @@ -4,19 +4,25 @@ import { getWindowTop, logError, getWindowLocation, getWindowSelf } from '../../ * Determines if the script is running inside an iframe and retrieves the URL. * @return {string} The encoded vrref value representing the relevant URL. */ -export function getReferrer() { + +export function getCurrentUrl() { + let url = ''; try { - const url = getWindowSelf() === getWindowTop() - ? getWindowLocation().href - : getWindowTop().location.href; + if (getWindowSelf() === getWindowTop()) { + // top page + url = getWindowLocation().href || ''; + } else { + // iframe + url = getWindowTop().location.href || ''; + } if (url.length >= 50) { - const { origin } = new URL(url); - return origin; - } + return new URL(url).origin; + }; return url; } catch (error) { + // Handling access errors, such as cross-domain restrictions logError(`Error accessing location: ${error}`); return ''; } @@ -31,12 +37,12 @@ export function getReferrer() { * @return {string} The modified URL with appended `vrref` or `fui` parameters. */ export function appendVrrefAndFui(url, domainName) { - const fullUrl = encodeURIComponent(getReferrer()); + const fullUrl = getCurrentUrl(); if (fullUrl) { return (url += '&vrref=' + getRelevantRefferer(domainName, fullUrl)); } url += '&fui=1'; // Full Url Issue - url += '&vrref=' + encodeURIComponent(domainName || ''); + if (domainName) url += '&vrref=' + encodeURIComponent(domainName); return url; } @@ -47,10 +53,9 @@ export function appendVrrefAndFui(url, domainName) { * @return {string} The relevant referrer */ export function getRelevantRefferer(domainName, fullUrl) { - if (domainName && isDomainIncluded(fullUrl, domainName)) { - return fullUrl; - } - return domainName ? encodeURIComponent(domainName) : fullUrl; + return encodeURIComponent( + domainName && isDomainIncluded(fullUrl, domainName) ? fullUrl : (domainName || fullUrl) + ); } /** @@ -61,7 +66,7 @@ export function getRelevantRefferer(domainName, fullUrl) { */ export function isDomainIncluded(fullUrl, domainName) { try { - return fullUrl.includes(domainName); + return new URL(fullUrl).hostname === domainName; } catch (error) { logError(`Invalid URL provided: ${error}`); return false; diff --git a/libraries/intentIqUtils/getUnitPosition.js b/libraries/intentIqUtils/getUnitPosition.js new file mode 100644 index 00000000000..025a06a9964 --- /dev/null +++ b/libraries/intentIqUtils/getUnitPosition.js @@ -0,0 +1,17 @@ +export function getUnitPosition(pbjs, adUnitCode) { + const adUnits = pbjs?.adUnits; + if (!Array.isArray(adUnits) || !adUnitCode) return; + + for (let i = 0; i < adUnits.length; i++) { + const adUnit = adUnits[i]; + if (adUnit?.code !== adUnitCode) continue; + + const mediaTypes = adUnit?.mediaTypes; + if (!mediaTypes || typeof mediaTypes !== 'object') return; + + const firstKey = Object.keys(mediaTypes)[0]; + const pos = mediaTypes[firstKey]?.pos; + + return typeof pos === 'number' ? pos : undefined; + } +} diff --git a/libraries/intentIqUtils/intentIqConfig.js b/libraries/intentIqUtils/intentIqConfig.js index 3f2572f14fa..41c731f646b 100644 --- a/libraries/intentIqUtils/intentIqConfig.js +++ b/libraries/intentIqUtils/intentIqConfig.js @@ -1,3 +1,33 @@ -export const iiqServerAddress = (configParams, gdprDetected) => typeof configParams?.iiqServerAddress === 'string' ? configParams.iiqServerAddress : gdprDetected ? 'https://api-gdpr.intentiq.com' : 'https://api.intentiq.com' -export const iiqPixelServerAddress = (configParams, gdprDetected) => typeof configParams?.iiqPixelServerAddress === 'string' ? configParams.iiqPixelServerAddress : gdprDetected ? 'https://sync-gdpr.intentiq.com' : 'https://sync.intentiq.com' -export const reportingServerAddress = (reportEndpoint, gdprDetected) => reportEndpoint && typeof reportEndpoint === 'string' ? reportEndpoint : gdprDetected ? 'https://reports-gdpr.intentiq.com/report' : 'https://reports.intentiq.com/report' +const REGION_MAPPING = { + gdpr: true, + apac: true, + emea: true +}; + +function checkRegion(region) { + if (typeof region !== 'string') return ''; + const lower = region.toLowerCase(); + return REGION_MAPPING[lower] ? lower : ''; +} + +function buildServerAddress(baseName, region) { + const checkedRegion = checkRegion(region); + if (checkedRegion) return `https://${baseName}-${checkedRegion}.intentiq.com`; + return `https://${baseName}.intentiq.com`; +} + +export const getIiqServerAddress = (configParams = {}) => { + if (typeof configParams?.iiqServerAddress === 'string') return configParams.iiqServerAddress; + return buildServerAddress('api', configParams.region); +}; + +export const iiqPixelServerAddress = (configParams = {}) => { + if (typeof configParams?.iiqPixelServerAddress === 'string') return configParams.iiqPixelServerAddress; + return buildServerAddress('sync', configParams.region); +}; + +export const reportingServerAddress = (reportEndpoint, region) => { + if (reportEndpoint && typeof reportEndpoint === 'string') return reportEndpoint; + const host = buildServerAddress('reports', region); + return `${host}/report`; +}; diff --git a/libraries/intentIqUtils/urlUtils.js b/libraries/intentIqUtils/urlUtils.js index 4cfb8273eab..78579aeb67d 100644 --- a/libraries/intentIqUtils/urlUtils.js +++ b/libraries/intentIqUtils/urlUtils.js @@ -1,5 +1,7 @@ -export function appendSPData (url, firstPartyData) { - const spdParam = firstPartyData?.spd ? encodeURIComponent(typeof firstPartyData.spd === 'object' ? JSON.stringify(firstPartyData.spd) : firstPartyData.spd) : ''; - url += spdParam ? '&spd=' + spdParam : ''; - return url +export function appendSPData (url, partnerData) { + const spdParam = partnerData?.spd ? encodeURIComponent(typeof partnerData.spd === 'object' ? JSON.stringify(partnerData.spd) : partnerData.spd) : ''; + if (!spdParam) { + return url; + } + return `${url}&spd=${spdParam}`; }; diff --git a/modules/intentIqAnalyticsAdapter.js b/modules/intentIqAnalyticsAdapter.js index 946a13ae174..bf179d3e880 100644 --- a/modules/intentIqAnalyticsAdapter.js +++ b/modules/intentIqAnalyticsAdapter.js @@ -5,8 +5,9 @@ import { ajax } from '../src/ajax.js'; import { EVENTS } from '../src/constants.js'; import { detectBrowser } from '../libraries/intentIqUtils/detectBrowserUtils.js'; import { appendSPData } from '../libraries/intentIqUtils/urlUtils.js'; -import { appendVrrefAndFui, getReferrer } from '../libraries/intentIqUtils/getRefferer.js'; +import { appendVrrefAndFui, getCurrentUrl, getRelevantRefferer } from '../libraries/intentIqUtils/getRefferer.js'; import { getCmpData } from '../libraries/intentIqUtils/getCmpData.js'; +import { getUnitPosition } from '../libraries/intentIqUtils/getUnitPosition.js'; import { VERSION, PREBID, @@ -16,10 +17,12 @@ import { reportingServerAddress } from '../libraries/intentIqUtils/intentIqConfi import { handleAdditionalParams } from '../libraries/intentIqUtils/handleAdditionalParams.js'; import { gamPredictionReport } from '../libraries/intentIqUtils/gamPredictionReport.js'; import { defineABTestingGroup } from '../libraries/intentIqUtils/defineABTestingGroupUtils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; const MODULE_NAME = 'iiqAnalytics'; const analyticsType = 'endpoint'; const prebidVersion = '$prebid.version$'; +const pbjs = getGlobal(); export const REPORTER_ID = Date.now() + '_' + getRandom(0, 1000); let globalName; let identityGlobalName; @@ -70,10 +73,7 @@ const PARAMS_NAMES = { const DEFAULT_URL = 'https://reports.intentiq.com/report'; const getDataForDefineURL = () => { - const cmpData = getCmpData(); - const gdprDetected = cmpData.gdprString; - - return [iiqAnalyticsAnalyticsAdapter.initOptions.reportingServerAddress, gdprDetected]; + return [iiqAnalyticsAnalyticsAdapter.initOptions.reportingServerAddress, iiqAnalyticsAnalyticsAdapter.initOptions.region]; }; const getDefaultInitOptions = () => { @@ -92,7 +92,8 @@ const getDefaultInitOptions = () => { abPercentage: null, abTestUuid: null, additionalParams: null, - reportingServerAddress: '' + reportingServerAddress: '', + region: '' } } @@ -123,12 +124,13 @@ function initAdapterConfig(config) { const options = config?.options || {} iiqConfig = options - const { manualWinReportEnabled, gamPredictReporting, reportMethod, reportingServerAddress, adUnitConfig, partner, ABTestingConfigurationSource, browserBlackList, domainName, additionalParams } = options + const { manualWinReportEnabled, gamPredictReporting, reportMethod, reportingServerAddress, region, adUnitConfig, partner, ABTestingConfigurationSource, browserBlackList, domainName, additionalParams } = options iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = manualWinReportEnabled || false; iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod = parseReportingMethod(reportMethod); iiqAnalyticsAnalyticsAdapter.initOptions.gamPredictReporting = typeof gamPredictReporting === 'boolean' ? gamPredictReporting : false; iiqAnalyticsAnalyticsAdapter.initOptions.reportingServerAddress = typeof reportingServerAddress === 'string' ? reportingServerAddress : ''; + iiqAnalyticsAnalyticsAdapter.initOptions.region = typeof region === 'string' ? region : ''; iiqAnalyticsAnalyticsAdapter.initOptions.adUnitConfig = typeof adUnitConfig === 'number' ? adUnitConfig : 1; iiqAnalyticsAnalyticsAdapter.initOptions.configSource = ABTestingConfigurationSource; iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup = defineABTestingGroup(options); @@ -155,7 +157,7 @@ function receivePartnerData() { return false } iiqAnalyticsAnalyticsAdapter.initOptions.fpid = FPD - const { partnerData, clientsHints = '', actualABGroup } = window[identityGlobalName] + const { partnerData, clientHints = '', actualABGroup } = window[identityGlobalName] if (partnerData) { iiqAnalyticsAnalyticsAdapter.initOptions.dataIdsInitialized = true; @@ -172,7 +174,7 @@ function receivePartnerData() { if (actualABGroup) { iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup = actualABGroup; } - iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints = clientsHints; + iiqAnalyticsAnalyticsAdapter.initOptions.clientHints = clientHints; } catch (e) { logError(e); return false; @@ -268,9 +270,10 @@ function getRandom(start, end) { export function preparePayload(data) { const result = getDefaultDataObject(); + const fullUrl = getCurrentUrl(); result[PARAMS_NAMES.partnerId] = iiqAnalyticsAnalyticsAdapter.initOptions.partner; result[PARAMS_NAMES.prebidVersion] = prebidVersion; - result[PARAMS_NAMES.referrer] = getReferrer(); + result[PARAMS_NAMES.referrer] = getRelevantRefferer(iiqAnalyticsAnalyticsAdapter.initOptions.domainName, fullUrl); result[PARAMS_NAMES.terminationCause] = iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause; result[PARAMS_NAMES.clientType] = iiqAnalyticsAnalyticsAdapter.initOptions.clientType; result[PARAMS_NAMES.siteId] = iiqAnalyticsAnalyticsAdapter.initOptions.siteId; @@ -345,6 +348,15 @@ function prepareData(data, result) { if (data.status) { result.status = data.status; } + if (data.size) { + result.size = data.size; + } + if (typeof data.pos === 'number') { + result.pos = data.pos; + } else if (data.adUnitCode) { + const pos = getUnitPosition(pbjs, data.adUnitCode); + if (typeof pos === 'number') result.pos = pos; + } result.prebidAuctionId = data.auctionId || data.prebidAuctionId; @@ -410,6 +422,7 @@ function getDefaultDataObject() { function constructFullUrl(data) { const report = []; const reportMethod = iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod; + const partnerData = window[identityGlobalName]?.partnerData; const currentBrowserLowerCase = detectBrowser(); data = btoa(JSON.stringify(data)); report.push(data); @@ -432,11 +445,11 @@ function constructFullUrl(data) { '&source=' + PREBID + '&uh=' + - encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints) + + encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.clientHints) + (cmpData.uspString ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : '') + (cmpData.gppString ? '&gpp=' + encodeURIComponent(cmpData.gppString) : '') + (cmpData.gdprString ? '&gdpr_consent=' + encodeURIComponent(cmpData.gdprString) + '&gdpr=1' : '&gdpr=0'); - url = appendSPData(url, iiqAnalyticsAnalyticsAdapter.initOptions.fpid); + url = appendSPData(url, partnerData); url = appendVrrefAndFui(url, iiqAnalyticsAnalyticsAdapter.initOptions.domainName); if (reportMethod === 'POST') { diff --git a/modules/intentIqAnalyticsAdapter.md b/modules/intentIqAnalyticsAdapter.md index 42f6167c33b..c691496df59 100644 --- a/modules/intentIqAnalyticsAdapter.md +++ b/modules/intentIqAnalyticsAdapter.md @@ -45,11 +45,8 @@ pbjs.enableAnalytics({ provider: 'iiqAnalytics', options: { partner: 1177538, - manualWinReportEnabled: false, - reportMethod: "GET", - adUnitConfig: 1, + ABTestingConfigurationSource: 'IIQServer', domainName: "currentDomain.com", - gamPredictReporting: false } }); ``` @@ -90,7 +87,9 @@ 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. -adType: 'banner' // Specifies the type of ad served +adType: 'banner', // Specifies the type of ad served, +size: '320x250', // Size of adUnit item, +pos: 0 // The following values are defined in the ORTB 2.5 spec } ``` @@ -108,6 +107,8 @@ adType: 'banner' // Specifies the type of ad served | 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 | +| size | String | Size of adUnit item | '320x250' | No | +| pos | number | The pos field specifies the position of the adUnit on the page according to the OpenRTB 2.5 specification | 0 | No | To report the auction win, call the function as follows: diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index e10c19f1888..85d459d1c14 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -5,25 +5,25 @@ * @requires module:modules/userId */ -import {logError, isPlainObject, isStr, isNumber} from '../src/utils.js'; -import {ajax} from '../src/ajax.js'; -import {submodule} from '../src/hook.js' -import {detectBrowser} from '../libraries/intentIqUtils/detectBrowserUtils.js'; -import {appendSPData} from '../libraries/intentIqUtils/urlUtils.js'; +import { logError, isPlainObject, isStr, isNumber } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js' +import { detectBrowser } from '../libraries/intentIqUtils/detectBrowserUtils.js'; +import { appendSPData } from '../libraries/intentIqUtils/urlUtils.js'; import { isCHSupported } from '../libraries/intentIqUtils/chUtils.js' -import {appendVrrefAndFui} from '../libraries/intentIqUtils/getRefferer.js'; +import { appendVrrefAndFui } from '../libraries/intentIqUtils/getRefferer.js'; import { getCmpData } from '../libraries/intentIqUtils/getCmpData.js'; -import {readData, storeData, defineStorageType, removeDataByKey, tryParse} from '../libraries/intentIqUtils/storageUtils.js'; +import { readData, storeData, defineStorageType, removeDataByKey, tryParse } from '../libraries/intentIqUtils/storageUtils.js'; import { FIRST_PARTY_KEY, CLIENT_HINTS_KEY, EMPTY, GVLID, VERSION, INVALID_ID, SYNC_REFRESH_MILL, META_DATA_CONSTANT, PREBID, - HOURS_24, CH_KEYS + HOURS_72, CH_KEYS } from '../libraries/intentIqConstants/intentIqConstants.js'; -import {SYNC_KEY} from '../libraries/intentIqUtils/getSyncKey.js'; -import {iiqPixelServerAddress, iiqServerAddress} from '../libraries/intentIqUtils/intentIqConfig.js'; +import { SYNC_KEY } from '../libraries/intentIqUtils/getSyncKey.js'; +import { iiqPixelServerAddress, getIiqServerAddress } from '../libraries/intentIqUtils/intentIqConfig.js'; import { handleAdditionalParams } from '../libraries/intentIqUtils/handleAdditionalParams.js'; import { decryptData, encryptData } from '../libraries/intentIqUtils/cryptionUtils.js'; import { defineABTestingGroup } from '../libraries/intentIqUtils/defineABTestingGroupUtils.js'; @@ -81,7 +81,7 @@ function addUniquenessToUrl(url) { return url; } -function appendFirstPartyData (url, firstPartyData, partnerData) { +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) : ''; @@ -93,7 +93,7 @@ function verifyIdType(value) { return -1; } -function appendPartnersFirstParty (url, configParams) { +function appendPartnersFirstParty(url, configParams) { const partnerClientId = typeof configParams.partnerClientId === 'string' ? encodeURIComponent(configParams.partnerClientId) : ''; const partnerClientIdType = typeof configParams.partnerClientIdType === 'number' ? verifyIdType(configParams.partnerClientIdType) : -1; @@ -105,7 +105,7 @@ function appendPartnersFirstParty (url, configParams) { return url; } -function appendCMPData (url, cmpData) { +function appendCMPData(url, cmpData) { url += cmpData.uspString ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : ''; url += cmpData.gppString ? '&gpp=' + encodeURIComponent(cmpData.gppString) : ''; url += cmpData.gdprApplies @@ -114,7 +114,7 @@ function appendCMPData (url, cmpData) { return url } -function appendCounters (url) { +function appendCounters(url) { url += '&jaesc=' + encodeURIComponent(callCount); url += '&jafc=' + encodeURIComponent(failCount); url += '&jaensc=' + encodeURIComponent(noDataCount); @@ -146,7 +146,7 @@ function addMetaData(url, data) { return url + '&fbp=' + data; } -export function initializeGlobalIIQ (partnerId) { +export function initializeGlobalIIQ(partnerId) { if (!globalName || !window[globalName]) { globalName = `iiq_identity_${partnerId}` window[globalName] = {} @@ -171,7 +171,7 @@ export function createPixelUrl(firstPartyData, clientHints, configParams, partne url = appendCMPData(url, cmpData); url = addMetaData(url, sourceMetaDataExternal || sourceMetaData); url = handleAdditionalParams(browser, url, 0, configParams.additionalParams); - url = appendSPData(url, firstPartyData) + url = appendSPData(url, partnerData); url += '&source=' + PREBID; return url; } @@ -184,7 +184,7 @@ function sendSyncRequest(allowedStorage, url, partner, firstPartyData, newUser) const needToDoSync = (Date.now() - (firstPartyData?.date || firstPartyData?.sCal || Date.now())) > SYNC_REFRESH_MILL if (newUser || needToDoSync) { ajax(url, () => { - }, undefined, {method: 'GET', withCredentials: true}); + }, undefined, { method: 'GET', withCredentials: true }); if (firstPartyData?.date) { firstPartyData.date = Date.now() storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); @@ -193,7 +193,7 @@ function sendSyncRequest(allowedStorage, url, partner, firstPartyData, newUser) } else if (!lastSyncDate || lastSyncElapsedTime > SYNC_REFRESH_MILL) { storeData(SYNC_KEY(partner), Date.now() + '', allowedStorage); ajax(url, () => { - }, undefined, {method: 'GET', withCredentials: true}); + }, undefined, { method: 'GET', withCredentials: true }); } } @@ -208,9 +208,22 @@ export function setGamReporting(gamObjectReference, gamParameterName, userGroup, if (isBlacklisted) return; if (isPlainObject(gamObjectReference) && gamObjectReference.cmd) { gamObjectReference.cmd.push(() => { - gamObjectReference - .pubads() - .setTargeting(gamParameterName, userGroup); + if (typeof gamObjectReference.setConfig === 'function') { + const current = typeof gamObjectReference.getConfig === 'function' + ? gamObjectReference.getConfig('targeting') + : {}; + gamObjectReference.setConfig({ + targeting: { + ...(isPlainObject(current) ? current : {}), + [gamParameterName]: userGroup + } + }); + return; + } + const pubads = gamObjectReference?.pubads?.(); + if (pubads?.setTargeting) { + pubads.setTargeting(gamParameterName, userGroup); + } }); } } @@ -285,7 +298,7 @@ export const intentIqIdSubmodule = { * @returns {{intentIqId: {string}}|undefined} */ decode(value) { - return value && INVALID_ID !== value ? {'intentIqId': value} : undefined; + return value && INVALID_ID !== value ? { 'intentIqId': value } : undefined; }, /** @@ -431,7 +444,7 @@ export const intentIqIdSubmodule = { } } - function updateGlobalObj () { + function updateGlobalObj() { if (globalName) { window[globalName].partnerData = partnerData window[globalName].firstPartyData = firstPartyData @@ -442,8 +455,8 @@ export const intentIqIdSubmodule = { let hasPartnerData = !!Object.keys(partnerData).length; if (!isCMPStringTheSame(firstPartyData, cmpData) || - !firstPartyData.sCal || - (hasPartnerData && (!partnerData.cttl || !partnerData.date || Date.now() - partnerData.date > partnerData.cttl))) { + !firstPartyData.sCal || + (hasPartnerData && (!partnerData.cttl || !partnerData.date || Date.now() - partnerData.date > partnerData.cttl))) { firstPartyData.uspString = cmpData.uspString; firstPartyData.gppString = cmpData.gppString; firstPartyData.gdprString = cmpData.gdprString; @@ -454,7 +467,7 @@ export const intentIqIdSubmodule = { if (!shouldCallServer) { if (!hasPartnerData && !firstPartyData.isOptedOut) { shouldCallServer = true; - } else shouldCallServer = Date.now() > firstPartyData.sCal + HOURS_24; + } else shouldCallServer = Date.now() > firstPartyData.sCal + HOURS_72; } if (firstPartyData.isOptedOut) { @@ -498,7 +511,7 @@ export const intentIqIdSubmodule = { updateGlobalObj() // update global object before server request, to make sure analytical adapter will have it even if the server is "not in time" // use protocol relative urls for http or https - let url = `${iiqServerAddress(configParams, gdprDetected)}/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`; + let url = `${getIiqServerAddress(configParams)}/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=${configParams.partner}&pt=17&dpn=1`; url += configParams.pai ? '&pai=' + encodeURIComponent(configParams.pai) : ''; url = appendFirstPartyData(url, firstPartyData, partnerData); url = appendPartnersFirstParty(url, configParams); @@ -511,7 +524,7 @@ export const intentIqIdSubmodule = { url += actualABGroup ? '&testGroup=' + encodeURIComponent(actualABGroup) : ''; url = addMetaData(url, sourceMetaDataExternal || sourceMetaData); url = handleAdditionalParams(currentBrowserLowerCase, url, 1, additionalParams); - url = appendSPData(url, firstPartyData) + url = appendSPData(url, partnerData) url += '&source=' + PREBID; url += '&ABTestingConfigurationSource=' + configParams.ABTestingConfigurationSource url += '&abtg=' + encodeURIComponent(actualABGroup) @@ -528,6 +541,9 @@ export const intentIqIdSubmodule = { const resp = function (callback) { const callbacks = { success: response => { + if (rrttStrtTime && rrttStrtTime > 0) { + partnerData.rrtt = Date.now() - rrttStrtTime; + } const respJson = tryParse(response); // If response is a valid json and should save is true if (respJson) { @@ -542,7 +558,7 @@ export const intentIqIdSubmodule = { if (callbackTimeoutID) clearTimeout(callbackTimeoutID) if ('cttl' in respJson) { partnerData.cttl = respJson.cttl; - } else partnerData.cttl = HOURS_24; + } else partnerData.cttl = HOURS_72; if ('tc' in respJson) { partnerData.terminationCause = respJson.tc; @@ -588,7 +604,7 @@ export const intentIqIdSubmodule = { } else { // If data is a single string, assume it is an id with source intentiq.com if (respJson.data && typeof respJson.data === 'string') { - respJson.data = {eids: [respJson.data]} + respJson.data = { eids: [respJson.data] } } } partnerData.data = respJson.data; @@ -604,7 +620,7 @@ export const intentIqIdSubmodule = { if ('spd' in respJson) { // server provided data - firstPartyData.spd = respJson.spd; + partnerData.spd = respJson.spd; } if ('abTestUuid' in respJson) { @@ -620,10 +636,6 @@ export const intentIqIdSubmodule = { delete partnerData.gpr // remove prediction flag in case server doesn't provide it } - if (rrttStrtTime && rrttStrtTime > 0) { - partnerData.rrtt = Date.now() - rrttStrtTime; - } - if (respJson.data?.eids) { runtimeEids = respJson.data callback(respJson.data.eids); @@ -648,12 +660,13 @@ export const intentIqIdSubmodule = { callback(runtimeEids); } }; - rrttStrtTime = Date.now(); partnerData.wsrvcll = true; storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); clearCountersAndStore(allowedStorage, partnerData); + rrttStrtTime = Date.now(); + const sendAjax = uh => { if (uh) url += '&uh=' + encodeURIComponent(uh); ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); @@ -675,7 +688,7 @@ export const intentIqIdSubmodule = { sendAjax(''); } }; - const respObj = {callback: resp}; + const respObj = { callback: resp }; if (runtimeEids?.eids?.length) respObj.id = runtimeEids.eids; return respObj diff --git a/modules/intentIqIdSystem.md b/modules/intentIqIdSystem.md index 7597ba90fbf..2465f8cbf6f 100644 --- a/modules/intentIqIdSystem.md +++ b/modules/intentIqIdSystem.md @@ -56,6 +56,7 @@ Please find below list of parameters that could be used in configuring Intent IQ | params. ABTestingConfigurationSource| Optional | String | Determines how AB group will be defined. Possible values: `"IIQServer"` – group defined by IIQ server, `"percentage"` – generated group based on abPercentage, `"group"` – define group based on value provided by partner. | `IIQServer` | | params.abPercentage | Optional | Number | Percentage for A/B testing group. Default value is `95` | `95` | | params.group | Optional | String | Define group provided by partner, possible values: `"A"`, `"B"` | `"A"` | +| params.region | Optional | String | Optional region identifier used to automatically build server endpoints. When specified, region-specific endpoints will be used. (`gdpr`,`emea`, `apac`) | `"gdpr"` | | params.additionalParams | Optional | Array | This parameter allows sending additional custom key-value parameters with specific destination logic (sync, VR, winreport). Each custom parameter is defined as an object in the array. | `[ { parameterName: “abc”, parameterValue: 123, destination: [1,1,0] } ]` | | params.additionalParams [0].parameterName | Required | String | Name of the custom parameter. This will be sent as a query parameter. | `"abc"` | | params.additionalParams [0].parameterValue | Required | String / Number | Value to assign to the parameter. | `123` | @@ -72,6 +73,7 @@ pbjs.setConfig({ partner: 123456, // valid partner id timeoutInMillis: 500, browserBlackList: "chrome", + ABTestingConfigurationSource: 'IIQServer', callback: (data) => {...}, // your logic here groupChanged: (group) => console.log('Group is', group), domainName: "currentDomain.com", @@ -80,7 +82,8 @@ pbjs.setConfig({ sourceMetaData: "123.123.123.123", // Optional parameter sourceMetaDataExternal: 123456, // Optional parameter chTimeout: 10, // Optional parameter - abPercentage: 95 //Optional parameter + abPercentage: 95, // Optional parameter + region: "gdpr", // Optional parameter additionalParams: [ // Optional parameter { parameterName: "abc", diff --git a/test/spec/modules/intentIqAnalyticsAdapter_spec.js b/test/spec/modules/intentIqAnalyticsAdapter_spec.js index e93b6ec1915..8389ca4a644 100644 --- a/test/spec/modules/intentIqAnalyticsAdapter_spec.js +++ b/test/spec/modules/intentIqAnalyticsAdapter_spec.js @@ -2,8 +2,10 @@ import { expect } from "chai"; import iiqAnalyticsAnalyticsAdapter from "modules/intentIqAnalyticsAdapter.js"; import * as utils from "src/utils.js"; import { server } from "test/mocks/xhr.js"; +import { config } from "src/config.js"; import { EVENTS } from "src/constants.js"; import * as events from "src/events.js"; +import { getGlobal } from "../../../src/prebidGlobal.js"; import sinon from "sinon"; import { REPORTER_ID, @@ -20,7 +22,7 @@ import { } from "../../../libraries/intentIqConstants/intentIqConstants.js"; import * as detectBrowserUtils from "../../../libraries/intentIqUtils/detectBrowserUtils.js"; import { - getReferrer, + getCurrentUrl, appendVrrefAndFui, } from "../../../libraries/intentIqUtils/getRefferer.js"; import { @@ -29,7 +31,10 @@ import { gdprDataHandler, } from "../../../src/consentHandler.js"; +let getConfigStub; +let userIdConfigForTest; const partner = 10; +const identityName = `iiq_identity_${partner}` const defaultIdentityObject = { firstPartyData: { pcid: "f961ffb1-a0e1-4696-a9d2-a21d815bd344", @@ -41,8 +46,7 @@ const defaultIdentityObject = { sCal: Date.now() - 36000, isOptedOut: false, pid: "profile", - dbsaved: "true", - spd: "spd", + dbsaved: "true" }, partnerData: { abTestUuid: "abTestUuid", @@ -53,7 +57,7 @@ const defaultIdentityObject = { profile: "profile", wsrvcll: true, }, - clientHints: { + clientHints: JSON.stringify({ 0: '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"', 1: "?0", 2: '"macOS"', @@ -62,8 +66,30 @@ const defaultIdentityObject = { 6: '"15.6.1"', 7: "?0", 8: '"Chromium";v="142.0.7444.60", "Google Chrome";v="142.0.7444.60", "Not_A Brand";v="99.0.0.0"', - }, + }), }; +const regionCases = [ + { + name: 'default (no region)', + region: undefined, + expectedEndpoint: 'https://reports.intentiq.com/report' + }, + { + name: 'apac', + region: 'apac', + expectedEndpoint: 'https://reports-apac.intentiq.com/report' + }, + { + name: 'emea', + region: 'emea', + expectedEndpoint: 'https://reports-emea.intentiq.com/report' + }, + { + name: 'gdpr', + region: 'gdpr', + expectedEndpoint: 'https://reports-gdpr.intentiq.com/report' + } +] const version = VERSION; const REPORT_ENDPOINT = "https://reports.intentiq.com/report"; const REPORT_ENDPOINT_GDPR = "https://reports-gdpr.intentiq.com/report"; @@ -78,6 +104,22 @@ const getDefaultConfig = () => { } } +const getUserConfigWithReportingServerAddress = () => [ + { + 'name': 'intentIqId', + 'params': { + 'partner': partner, + 'unpack': null, + }, + 'storage': { + 'type': 'html5', + 'name': 'intentIqId', + 'expires': 60, + 'refreshInSeconds': 14400 + } + } +]; + const getWonRequest = () => ({ bidderCode: "pubmatic", width: 728, @@ -131,6 +173,11 @@ describe("IntentIQ tests all", function () { beforeEach(function () { logErrorStub = sinon.stub(utils, "logError"); sinon.stub(events, "getEvents").returns([]); + + if (config.getConfig && config.getConfig.restore) { + config.getConfig.restore(); + } + iiqAnalyticsAnalyticsAdapter.initOptions = { lsValueInitialized: false, partner: null, @@ -151,11 +198,12 @@ describe("IntentIQ tests all", function () { iiqAnalyticsAnalyticsAdapter.track.restore(); } sinon.spy(iiqAnalyticsAnalyticsAdapter, "track"); - window[`iiq_identity_${partner}`] = defaultIdentityObject; + window[identityName] = utils.deepClone(defaultIdentityObject); }); afterEach(function () { logErrorStub.restore(); + if (getConfigStub && getConfigStub.restore) getConfigStub.restore(); if (getWindowSelfStub) getWindowSelfStub.restore(); if (getWindowTopStub) getWindowTopStub.restore(); if (getWindowLocationStub) getWindowLocationStub.restore(); @@ -272,26 +320,52 @@ describe("IntentIQ tests all", function () { 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"); - const gdprStub = sinon - .stub(gdprDataHandler, "getConsentData") - .returns({ consentString: "gdprConsent" }); + it("should get pos from pbjs.adUnits when BID_WON has no pos", function () { + const pbjs = getGlobal(); + const prevAdUnits = pbjs.adUnits; - events.emit(EVENTS.BID_WON, getWonRequest()); + pbjs.adUnits = Array.isArray(pbjs.adUnits) ? pbjs.adUnits : []; + pbjs.adUnits.push({ code: "myVideoAdUnit", mediaTypes: { video: { pos: 777 } } }); + + enableAnalyticWithSpecialOptions({ manualWinReportEnabled: false }); + + events.emit(EVENTS.BID_WON, { + ...getWonRequest(), + adUnitCode: "myVideoAdUnit", + mediaType: "video" + }); - expect(server.requests.length).to.be.above(0); const request = server.requests[0]; + const payloadEncoded = new URL(request.url).searchParams.get("payload"); + const payloadDecoded = JSON.parse(atob(JSON.parse(payloadEncoded)[0])); - expect(request.url).to.contain(REPORT_ENDPOINT_GDPR); - gppStub.restore(); - uspStub.restore(); - gdprStub.restore(); + expect(payloadDecoded.pos).to.equal(777); + + pbjs.adUnits = prevAdUnits; + }); + + it("should get pos from reportExternalWin when present", function () { + enableAnalyticWithSpecialOptions({ manualWinReportEnabled: true }); + + const winPos = 999; + + window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin({ + adUnitCode: "myVideoAdUnit", + bidderCode: "appnexus", + cpm: 1.5, + currency: "USD", + mediaType: "video", + size: "300x250", + status: "rendered", + auctionId: "auc123", + pos: winPos + }); + + const request = server.requests[0]; + const payloadEncoded = new URL(request.url).searchParams.get("payload"); + const payloadDecoded = JSON.parse(atob(JSON.parse(payloadEncoded)[0])); + + expect(payloadDecoded.pos).to.equal(winPos); }); it("should initialize with default configurations", function () { @@ -319,6 +393,9 @@ describe("IntentIQ tests all", function () { }); it("should handle BID_WON event with default group configuration", function () { + const spdData = "server provided data"; + const expectedSpdEncoded = encodeURIComponent(spdData); + window[identityName].partnerData.spd = spdData; const wonRequest = getWonRequest(); events.emit(EVENTS.BID_WON, wonRequest); @@ -331,7 +408,7 @@ describe("IntentIQ tests all", function () { const payload = encodeURIComponent(JSON.stringify([base64String])); const expectedUrl = appendVrrefAndFui( REPORT_ENDPOINT + - `?pid=${partner}&mct=1&iiqid=${defaultIdentityObject.firstPartyData.pcid}&agid=${REPORTER_ID}&jsver=${version}&source=pbjs&uh=&gdpr=0&spd=spd`, + `?pid=${partner}&mct=1&iiqid=${defaultIdentityObject.firstPartyData.pcid}&agid=${REPORTER_ID}&jsver=${version}&source=pbjs&uh=${encodeURIComponent(window[identityName].clientHints)}&gdpr=0&spd=${expectedSpdEncoded}`, iiqAnalyticsAnalyticsAdapter.initOptions.domainName ); const urlWithPayload = expectedUrl + `&payload=${payload}`; @@ -379,6 +456,22 @@ describe("IntentIQ tests all", function () { gdprStub.restore(); }); + regionCases.forEach(({ name, region, expectedEndpoint }) => { + it(`should send request to region-specific report endpoint when region is "${name}"`, function () { + userIdConfigForTest = getUserConfigWithReportingServerAddress(); + getConfigStub = sinon.stub(config, "getConfig"); + getConfigStub.withArgs("userSync.userIds").callsFake(() => userIdConfigForTest); + + enableAnalyticWithSpecialOptions({ region }); + + events.emit(EVENTS.BID_WON, getWonRequest()); + + expect(server.requests.length).to.be.above(0); + const request = server.requests[0]; + expect(request.url).to.contain(expectedEndpoint); + }); + }); + it("should not send request if manualWinReportEnabled is true", function () { iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = true; events.emit(EVENTS.BID_WON, getWonRequest()); @@ -417,7 +510,7 @@ describe("IntentIQ tests all", function () { .stub(utils, "getWindowLocation") .returns({ href: "http://localhost:9876/" }); - const referrer = getReferrer(); + const referrer = getCurrentUrl(); expect(referrer).to.equal("http://localhost:9876/"); }); @@ -428,7 +521,7 @@ describe("IntentIQ tests all", function () { .stub(utils, "getWindowTop") .returns({ location: { href: "http://example.com/" } }); - const referrer = getReferrer(); + const referrer = getCurrentUrl(); expect(referrer).to.equal("http://example.com/"); }); @@ -440,7 +533,7 @@ describe("IntentIQ tests all", function () { .stub(utils, "getWindowTop") .throws(new Error("Access denied")); - const referrer = getReferrer(); + const referrer = getCurrentUrl(); expect(referrer).to.equal(""); expect(logErrorStub.calledOnce).to.be.true; expect(logErrorStub.firstCall.args[0]).to.contain( @@ -528,6 +621,36 @@ describe("IntentIQ tests all", function () { expect(request.url).to.include("general=Lee"); }); + it("should include domainName in both query and payload when fullUrl is empty (cross-origin)", function () { + const domainName = "mydomain-frame.com"; + + enableAnalyticWithSpecialOptions({ domainName }); + + getWindowTopStub = sinon.stub(utils, "getWindowTop").throws(new Error("cross-origin")); + + events.emit(EVENTS.BID_WON, getWonRequest()); + + const request = server.requests[0]; + + // Query contain vrref=domainName + const parsedUrl = new URL(request.url); + const vrrefParam = parsedUrl.searchParams.get("vrref"); + + // Payload contain vrref=domainName + const payloadEncoded = parsedUrl.searchParams.get("payload"); + const payloadDecoded = JSON.parse(atob(JSON.parse(payloadEncoded)[0])); + + expect(server.requests.length).to.be.above(0); + expect(vrrefParam).to.not.equal(null); + expect(decodeURIComponent(vrrefParam)).to.equal(domainName); + expect(parsedUrl.searchParams.get("fui")).to.equal("1"); + + expect(payloadDecoded).to.have.property("vrref"); + expect(decodeURIComponent(payloadDecoded.vrref)).to.equal(domainName); + + restoreReportList(); + }); + it("should not send additionalParams in report if value is too large", function () { const longVal = "x".repeat(5000000); @@ -550,8 +673,9 @@ describe("IntentIQ tests all", function () { it("should include spd parameter from LS in report URL", function () { const spdObject = { foo: "bar", value: 42 }; const expectedSpdEncoded = encodeURIComponent(JSON.stringify(spdObject)); - window[`iiq_identity_${partner}`].firstPartyData.spd = + window[identityName].firstPartyData.spd = JSON.stringify(spdObject); + window[identityName].partnerData.spd = spdObject; getWindowLocationStub = sinon .stub(utils, "getWindowLocation") @@ -568,7 +692,7 @@ describe("IntentIQ tests all", function () { it("should include spd parameter string from LS in report URL", function () { const spdData = "server provided data"; const expectedSpdEncoded = encodeURIComponent(spdData); - window[`iiq_identity_${partner}`].firstPartyData.spd = spdData; + window[identityName].partnerData.spd = spdData; getWindowLocationStub = sinon .stub(utils, "getWindowLocation") diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index 4b9afc38146..18dd0452943 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -123,6 +123,20 @@ const mockGAM = () => { }; }; +const regionCases = [ + { name: 'no region (default)', region: undefined, expected: 'https://api.intentiq.com' }, + { name: 'apac', region: 'apac', expected: 'https://api-apac.intentiq.com' }, + { name: 'emea', region: 'emea', expected: 'https://api-emea.intentiq.com' }, + { name: 'gdpr', region: 'gdpr', expected: 'https://api-gdpr.intentiq.com' } +]; + +const syncRegionCases = [ + { name: 'default', region: undefined, expected: 'https://sync.intentiq.com' }, + { name: 'apac', region: 'apac', expected: 'https://sync-apac.intentiq.com' }, + { name: 'emea', region: 'emea', expected: 'https://sync-emea.intentiq.com' }, + { name: 'gdpr', region: 'gdpr', expected: 'https://sync-gdpr.intentiq.com' }, +]; + describe('IntentIQ tests', function () { this.timeout(10000); let sandbox; @@ -597,7 +611,7 @@ describe('IntentIQ tests', function () { it('should send AT=20 request and send spd in it', async function () { const spdValue = { foo: 'bar', value: 42 }; const encodedSpd = encodeURIComponent(JSON.stringify(spdValue)); - localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({pcid: '123', spd: spdValue})); + localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, JSON.stringify({pcid: '123', spd: spdValue})); intentIqIdSubmodule.getId({params: { partner: 10, @@ -615,7 +629,7 @@ describe('IntentIQ tests', function () { it('should send AT=20 request and send spd string in it ', async function () { const spdValue = 'server provided data'; const encodedSpd = encodeURIComponent(spdValue); - localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({pcid: '123', spd: spdValue})); + localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, JSON.stringify({pcid: '123', spd: spdValue})); intentIqIdSubmodule.getId({params: { partner: 10, @@ -634,7 +648,7 @@ describe('IntentIQ tests', function () { const spdValue = { foo: 'bar', value: 42 }; const encodedSpd = encodeURIComponent(JSON.stringify(spdValue)); - localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ pcid: '123', spd: spdValue })); + localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, JSON.stringify({ pcid: '123', spd: spdValue })); const callBackSpy = sinon.spy(); const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; @@ -650,7 +664,7 @@ describe('IntentIQ tests', function () { it('should send spd string from firstPartyData in localStorage in at=39 request', async function () { const spdValue = 'spd string'; const encodedSpd = encodeURIComponent(spdValue); - localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ pcid: '123', spd: spdValue })); + localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, JSON.stringify({ pcid: '123', spd: spdValue })); const callBackSpy = sinon.spy(); const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; @@ -676,7 +690,7 @@ describe('IntentIQ tests', function () { JSON.stringify({ pid: 'test_pid', data: 'test_personid', ls: true, spd: spdValue }) ); - const storedLs = readData(FIRST_PARTY_KEY, ['html5', 'cookie'], storage); + const storedLs = readData(FIRST_PARTY_KEY + '_' + partner, ['html5', 'cookie'], storage); const parsedLs = JSON.parse(storedLs); expect(storedLs).to.not.be.null; @@ -738,7 +752,7 @@ describe('IntentIQ tests', function () { expect(result).to.equal('unknown'); }); - it("Should call the server for new partner if FPD has been updated by other partner, and 24 hours have not yet passed.", async () => { + it("Should call the server for new partner if FPD has been updated by other partner, and 72 hours have not yet passed.", async () => { const allowedStorage = ['html5'] const newPartnerId = 12345 const FPD = { @@ -759,7 +773,7 @@ describe('IntentIQ tests', function () { expect(request.url).contain("ProfilesEngineServlet?at=39") // server was called }) - it("Should NOT call the server if FPD has been updated user Opted Out, and 24 hours have not yet passed.", async () => { + it("Should NOT call the server if FPD has been updated user Opted Out, and 72 hours have not yet passed.", async () => { const allowedStorage = ['html5'] const newPartnerId = 12345 const FPD = { @@ -906,23 +920,15 @@ describe('IntentIQ tests', function () { expect(callbackArgument).to.deep.equal({ eids: [] }); // Ensure that runtimeEids was updated to { eids: [] } }); - it('should make request to correct address api-gdpr.intentiq.com if gdpr is detected', async function() { - const ENDPOINT_GDPR = 'https://api-gdpr.intentiq.com'; - mockConsentHandlers(uspData, gppData, gdprData); - const callBackSpy = sinon.spy(); - const submoduleCallback = intentIqIdSubmodule.getId({...defaultConfigParams}).callback; - - submoduleCallback(callBackSpy); - await waitForClientHints(); - const request = server.requests[0]; - - expect(request.url).to.contain(ENDPOINT_GDPR); - }); - it('should make request to correct address with iiqServerAddress parameter', async function() { - defaultConfigParams.params.iiqServerAddress = testAPILink + const customParams = { + params: { + ...defaultConfigParams.params, + iiqServerAddress: testAPILink + } + }; const callBackSpy = sinon.spy(); - const submoduleCallback = intentIqIdSubmodule.getId({...defaultConfigParams}).callback; + const submoduleCallback = intentIqIdSubmodule.getId({...customParams}).callback; submoduleCallback(callBackSpy); await waitForClientHints(); @@ -949,6 +955,57 @@ describe('IntentIQ tests', function () { const request = server.requests[0]; expect(request.url).to.contain(syncTestAPILink); }); + + regionCases.forEach(({ name, region, expected }) => { + it(`should use region-specific api endpoint when region is "${name}"`, async function () { + mockConsentHandlers(uspData, gppData, gdprData); // gdprApplies = true + + const callBackSpy = sinon.spy(); + const configWithRegion = { + params: { + ...defaultConfigParams.params, + region + } + }; + + const submoduleCallback = intentIqIdSubmodule.getId(configWithRegion).callback; + submoduleCallback(callBackSpy); + await waitForClientHints(); + + const request = server.requests[0]; + expect(request.url).to.contain(expected); + }); + }); + + syncRegionCases.forEach(({ name, region, expected }) => { + it(`should use region-specific sync endpoint when region is "${name}"`, async function () { + let wasCallbackCalled = false; + + const callbackConfigParams = { + params: { + partner, + pai, + partnerClientIdType, + partnerClientId, + browserBlackList: 'Chrome', + region, + callback: () => { + wasCallbackCalled = true; + } + } + }; + + mockConsentHandlers(uspData, gppData, gdprData); + + intentIqIdSubmodule.getId(callbackConfigParams); + + await waitForClientHints(); + + const request = server.requests[0]; + expect(request.url).to.contain(expected); + expect(wasCallbackCalled).to.equal(true); + }); + }); }); it('should get and save client hints to storage', async () => {