diff --git a/libraries/intentIqConstants/intentIqConstants.js b/libraries/intentIqConstants/intentIqConstants.js index a0f48caf97c2..05a0bfb08852 100644 --- a/libraries/intentIqConstants/intentIqConstants.js +++ b/libraries/intentIqConstants/intentIqConstants.js @@ -9,14 +9,12 @@ export const BLACK_LIST = 'L'; export const CLIENT_HINTS_KEY = '_iiq_ch'; export const EMPTY = 'EMPTY'; export const GVLID = '1323'; -export const VERSION = 0.28; +export const VERSION = 0.29; +export const PREBID = 'pbjs'; +export const HOURS_24 = 86400000; -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', @@ -27,3 +25,14 @@ export const SCREEN_PARAMS = { }; export const SYNC_REFRESH_MILL = 3600000; +export const META_DATA_CONSTANT = 256; + +export const MAX_REQUEST_LENGTH = { + // https://www.geeksforgeeks.org/maximum-length-of-a-url-in-different-browsers/ + chrome: 2097152, + safari: 80000, + opera: 2097152, + edge: 2048, + firefox: 65536, + ie: 2048 +}; diff --git a/libraries/intentIqUtils/handleAdditionalParams.js b/libraries/intentIqUtils/handleAdditionalParams.js new file mode 100644 index 000000000000..e4bfa14c84fe --- /dev/null +++ b/libraries/intentIqUtils/handleAdditionalParams.js @@ -0,0 +1,44 @@ +import { MAX_REQUEST_LENGTH } from "../intentIqConstants/intentIqConstants.js"; + +/** + * Appends additional parameters to a URL if they are valid and applicable for the given request destination. + * + * @param {string} browser - The name of the current browser; used to look up the maximum URL length. + * @param {string} url - The base URL to which additional parameters may be appended. + * @param {(string|number)} requestTo - The destination identifier; used as an index to check if a parameter applies. + * @param {Array} additionalParams - An array of parameter objects to append. + * Each parameter object should have the following properties: + * - `parameterName` {string}: The name of the parameter. + * - `parameterValue` {*}: The value of the parameter. + * - `destination` {Object|Array}: An object or array indicating the applicable destinations. Sync = 0, VR = 1, reporting = 2 + * + * @return {string} The resulting URL with additional parameters appended if valid; otherwise, the original URL. + */ +export function handleAdditionalParams(browser, url, requestTo, additionalParams) { + let queryString = ''; + + if (!Array.isArray(additionalParams)) return url; + + for (let i = 0; i < additionalParams.length; i++) { + const param = additionalParams[i]; + + if ( + typeof param !== 'object' || + !param.parameterName || + !param.parameterValue || + !param.destination || + !Array.isArray(param.destination) + ) { + continue; + } + + if (param.destination[requestTo]) { + queryString += `&agp_${encodeURIComponent(param.parameterName)}=${param.parameterValue}`; + } + } + + const maxLength = MAX_REQUEST_LENGTH[browser] ?? 2048; + if ((url.length + queryString.length) > maxLength) return url; + + return url + queryString; +} diff --git a/libraries/intentIqUtils/intentIqConfig.js b/libraries/intentIqUtils/intentIqConfig.js new file mode 100644 index 000000000000..85c9111970b1 --- /dev/null +++ b/libraries/intentIqUtils/intentIqConfig.js @@ -0,0 +1,3 @@ +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 = (configParams, gdprDetected) => typeof configParams?.params?.reportingServerAddress === 'string' ? configParams.params.reportingServerAddress : gdprDetected ? 'https://reports-gdpr.intentiq.com/report' : 'https://reports.intentiq.com/report' diff --git a/libraries/intentIqUtils/storageUtils.js b/libraries/intentIqUtils/storageUtils.js index 8e7bf1d12a5c..338333ef3d1e 100644 --- a/libraries/intentIqUtils/storageUtils.js +++ b/libraries/intentIqUtils/storageUtils.js @@ -87,3 +87,16 @@ export function defineStorageType(params) { const filteredArr = params.filter(item => SUPPORTED_TYPES.includes(item)); return filteredArr.length ? filteredArr : ['html5']; } + +/** + * Parse json if possible, else return null + * @param data + */ +export function tryParse(data) { + try { + return JSON.parse(data); + } catch (err) { + logError(err); + return null; + } +} diff --git a/libraries/intentIqUtils/urlUtils.js b/libraries/intentIqUtils/urlUtils.js new file mode 100644 index 000000000000..4cfb8273eabb --- /dev/null +++ b/libraries/intentIqUtils/urlUtils.js @@ -0,0 +1,5 @@ +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 +}; diff --git a/modules/intentIqAnalyticsAdapter.js b/modules/intentIqAnalyticsAdapter.js index 2adc664d9e13..e58bb604bcd0 100644 --- a/modules/intentIqAnalyticsAdapter.js +++ b/modules/intentIqAnalyticsAdapter.js @@ -7,15 +7,16 @@ import {config} from '../src/config.js'; import {EVENTS} from '../src/constants.js'; import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; import {detectBrowser} from '../libraries/intentIqUtils/detectBrowserUtils.js'; +import {appendSPData} from '../libraries/intentIqUtils/urlUtils.js'; import {appendVrrefAndFui, getReferrer} from '../libraries/intentIqUtils/getRefferer.js'; import {getCmpData} from '../libraries/intentIqUtils/getCmpData.js' -import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, VERSION} from '../libraries/intentIqConstants/intentIqConstants.js'; +import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, VERSION, PREBID} from '../libraries/intentIqConstants/intentIqConstants.js'; import {readData, defineStorageType} from '../libraries/intentIqUtils/storageUtils.js'; +import {reportingServerAddress} from '../libraries/intentIqUtils/intentIqConfig.js'; +import { handleAdditionalParams } from '../libraries/intentIqUtils/handleAdditionalParams.js'; const MODULE_NAME = 'iiqAnalytics' const analyticsType = 'endpoint'; -const REPORT_ENDPOINT = 'https://reports.intentiq.com/report'; -const REPORT_ENDPOINT_GDPR = 'https://reports-gdpr.intentiq.com/report'; const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}); const prebidVersion = '$prebid.version$'; export const REPORTER_ID = Date.now() + '_' + getRandom(0, 1000); @@ -59,7 +60,21 @@ const PARAMS_NAMES = { adType: 'adType' }; -let iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({defaultUrl: REPORT_ENDPOINT, analyticsType}), { +function getIntentIqConfig() { + return config.getConfig('userSync.userIds')?.find(m => m.name === 'intentIqId'); +} + +const DEFAULT_URL = 'https://reports.intentiq.com/report' + +const getDataForDefineURL = () => { + const iiqConfig = getIntentIqConfig() + const cmpData = getCmpData(); + const gdprDetected = cmpData.gdprString; + + return [iiqConfig, gdprDetected] +} + +let iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({url: DEFAULT_URL, analyticsType}), { initOptions: { lsValueInitialized: false, partner: null, @@ -69,7 +84,10 @@ let iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({defaultUrl: REPORT_END eidl: null, lsIdsInitialized: false, manualWinReportEnabled: false, - domainName: null + domainName: null, + siloEnabled: false, + reportMethod: null, + additionalParams: null }, track({eventType, args}) { switch (eventType) { @@ -91,11 +109,7 @@ const { BID_REQUESTED } = EVENTS; -function getIntentIqConfig() { - return config.getConfig('userSync.userIds')?.find(m => m.name === 'intentIqId'); -} - -function initLsValues() { +function initAdapterConfig() { if (iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized) return; let iiqConfig = getIntentIqConfig() @@ -108,16 +122,24 @@ function initLsValues() { typeof iiqConfig.params?.browserBlackList === 'string' ? iiqConfig.params.browserBlackList.toLowerCase() : ''; iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = iiqConfig.params?.manualWinReportEnabled || false; iiqAnalyticsAnalyticsAdapter.initOptions.domainName = iiqConfig.params?.domainName || ''; + iiqAnalyticsAnalyticsAdapter.initOptions.siloEnabled = + typeof iiqConfig.params?.siloEnabled === 'boolean' ? iiqConfig.params.siloEnabled : false; + iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod = parseReportingMethod(iiqConfig.params?.reportMethod); + iiqAnalyticsAnalyticsAdapter.initOptions.additionalParams = iiqConfig.params?.additionalParams || null; } else { iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized = false; iiqAnalyticsAnalyticsAdapter.initOptions.partner = -1; + iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod = 'GET'; } } function initReadLsIds() { try { iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs = null; - iiqAnalyticsAnalyticsAdapter.initOptions.fpid = JSON.parse(readData(FIRST_PARTY_KEY, allowedStorage, storage)); + iiqAnalyticsAnalyticsAdapter.initOptions.fpid = JSON.parse(readData( + `${FIRST_PARTY_KEY}${iiqAnalyticsAnalyticsAdapter.initOptions.siloEnabled ? '_p_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner : ''}`, + allowedStorage, storage + )); if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid) { iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup = iiqAnalyticsAnalyticsAdapter.initOptions.fpid.group; } @@ -144,7 +166,7 @@ function initReadLsIds() { function bidWon(args, isReportExternal) { if (!iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized) { - initLsValues(); + initAdapterConfig(); } if (isNaN(iiqAnalyticsAnalyticsAdapter.initOptions.partner) || iiqAnalyticsAnalyticsAdapter.initOptions.partner == -1) return; @@ -159,13 +181,32 @@ function bidWon(args, isReportExternal) { initReadLsIds(); } if ((isReportExternal && iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled) || (!isReportExternal && !iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled)) { - ajax(constructFullUrl(preparePayload(args, true)), undefined, null, {method: 'GET'}); - logInfo('IIQ ANALYTICS -> BID WON') + const { url, method, payload } = constructFullUrl(preparePayload(args, true)); + if (method === 'POST') { + ajax(url, undefined, payload, {method, contentType: 'application/x-www-form-urlencoded'}); + } else { + ajax(url, undefined, null, {method}); + } + logInfo('IIQ ANALYTICS -> BID WON'); return true; } return false; } +function parseReportingMethod(reportMethod) { + if (typeof reportMethod === 'string') { + switch (reportMethod.toUpperCase()) { + case 'GET': + return 'GET'; + case 'POST': + return 'POST'; + default: + return 'GET'; + } + } + return 'GET'; +} + function defineGlobalVariableName() { function reportExternalWin(args) { return bidWon(args, true); @@ -302,12 +343,13 @@ function getDefaultDataObject() { function constructFullUrl(data) { let report = []; + const reportMethod = iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod; + const currentBrowserLowerCase = detectBrowser(); data = btoa(JSON.stringify(data)); report.push(data); const cmpData = getCmpData(); - const gdprDetected = cmpData.gdprString; - const baseUrl = gdprDetected ? REPORT_ENDPOINT_GDPR : REPORT_ENDPOINT; + const baseUrl = reportingServerAddress(...getDataForDefineURL()); let url = baseUrl + '?pid=' + iiqAnalyticsAnalyticsAdapter.initOptions.partner + '&mct=1' + @@ -315,17 +357,22 @@ function constructFullUrl(data) { ? '&iiqid=' + encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pcid) : '') + '&agid=' + REPORTER_ID + '&jsver=' + VERSION + - '&source=pbjs' + - '&payload=' + JSON.stringify(report) + + '&source=' + PREBID + '&uh=' + encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints) + (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 = appendVrrefAndFui(url, iiqAnalyticsAnalyticsAdapter.initOptions.domainName); - return url; + + if (reportMethod === 'POST') { + return { url, method: 'POST', payload: JSON.stringify(report) }; + } + url += '&payload=' + encodeURIComponent(JSON.stringify(report)); + url = handleAdditionalParams(currentBrowserLowerCase, url, 2, iiqAnalyticsAnalyticsAdapter.initOptions.additionalParams); + return { url, method: 'GET' }; } iiqAnalyticsAnalyticsAdapter.originEnableAnalytics = iiqAnalyticsAnalyticsAdapter.enableAnalytics; diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 5bc86dd439c1..aa7a1f775e9b 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -5,26 +5,29 @@ * @requires module:modules/userId */ -import {logError, isPlainObject, getWinDimensions} from '../src/utils.js'; +import {logError, isPlainObject, isStr, isNumber, getWinDimensions} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js' import AES from 'crypto-js/aes.js'; import Utf8 from 'crypto-js/enc-utf8.js'; import {detectBrowser} from '../libraries/intentIqUtils/detectBrowserUtils.js'; +import {appendSPData} from '../libraries/intentIqUtils/urlUtils.js'; import {appendVrrefAndFui} from '../libraries/intentIqUtils/getRefferer.js'; import { getCmpData } from '../libraries/intentIqUtils/getCmpData.js'; -import {readData, storeData, defineStorageType, removeDataByKey} from '../libraries/intentIqUtils/storageUtils.js'; +import {readData, storeData, defineStorageType, removeDataByKey, tryParse} from '../libraries/intentIqUtils/storageUtils.js'; import { FIRST_PARTY_KEY, WITH_IIQ, WITHOUT_IIQ, NOT_YET_DEFINED, - BLACK_LIST, CLIENT_HINTS_KEY, EMPTY, GVLID, - VERSION, INVALID_ID, GDPR_ENDPOINT, VR_ENDPOINT, SYNC_ENDPOINT, SCREEN_PARAMS, GDPR_SYNC_ENDPOINT, SYNC_REFRESH_MILL + VERSION, INVALID_ID, SCREEN_PARAMS, SYNC_REFRESH_MILL, META_DATA_CONSTANT, PREBID, + HOURS_24 } from '../libraries/intentIqConstants/intentIqConstants.js'; import {SYNC_KEY} from '../libraries/intentIqUtils/getSyncKey.js'; +import {iiqPixelServerAddress, iiqServerAddress} from '../libraries/intentIqUtils/intentIqConfig.js'; +import { handleAdditionalParams } from '../libraries/intentIqUtils/handleAdditionalParams.js'; /** * @typedef {import('../modules/userId/index.js').Submodule} Submodule @@ -45,6 +48,14 @@ const encoderCH = { wow64: 7, fullVersionList: 8 }; +let sourceMetaData; +let sourceMetaDataExternal; + +let FIRST_PARTY_KEY_FINAL = FIRST_PARTY_KEY; +let PARTNER_DATA_KEY; +let callCount = 0; +let failCount = 0; +let noDataCount = 0; export let firstPartyData; @@ -82,12 +93,13 @@ export function decryptData(encryptedText) { } function collectDeviceInfo() { + const windowDimensions = getWinDimensions(); return { - windowInnerHeight: getWinDimensions().innerHeight, - windowInnerWidth: getWinDimensions().innerWidth, - devicePixelRatio: window.devicePixelRatio, - windowScreenHeight: window.screen.height, - windowScreenWidth: window.screen.width, + windowInnerHeight: windowDimensions.innerHeight, + windowInnerWidth: windowDimensions.innerWidth, + devicePixelRatio: windowDimensions.devicePixelRatio, + windowScreenHeight: windowDimensions.screen.height, + windowScreenWidth: windowDimensions.screen.width, language: navigator.language } } @@ -117,6 +129,23 @@ function appendFirstPartyData (url, firstPartyData, partnerData) { return url } +function verifyIdType(value) { + if (value === 0 || value === 1 || value === 3 || value === 4) return value; + return -1; +} + +function appendPartnersFirstParty (url, configParams) { + let partnerClientId = typeof configParams.partnerClientId === 'string' ? encodeURIComponent(configParams.partnerClientId) : ''; + let partnerClientIdType = typeof configParams.partnerClientIdType === 'number' ? verifyIdType(configParams.partnerClientIdType) : -1; + + if (partnerClientIdType === -1) return url; + if (partnerClientId !== '') { + url = url + '&pcid=' + partnerClientId; + url = url + '&idtype=' + partnerClientIdType; + } + return url; +} + function appendCMPData (url, cmpData) { url += cmpData.uspString ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : ''; url += cmpData.gppString ? '&gpp=' + encodeURIComponent(cmpData.gppString) : ''; @@ -126,20 +155,58 @@ function appendCMPData (url, cmpData) { return url } +function appendCounters (url) { + url += '&jaesc=' + encodeURIComponent(callCount); + url += '&jafc=' + encodeURIComponent(failCount); + url += '&jaensc=' + encodeURIComponent(noDataCount); + return url +} + +/** + * Translate and validate sourceMetaData + */ +export function translateMetadata(data) { + try { + const d = data.split('.'); + return ( + ((+d[0] * META_DATA_CONSTANT + +d[1]) * META_DATA_CONSTANT + +d[2]) * META_DATA_CONSTANT + + +d[3] + ); + } catch (e) { + return NaN; + } +} + +/** + * Add sourceMetaData to URL if valid + */ +function addMetaData(url, data) { + if (typeof data !== 'number' || isNaN(data)) { + return url; + } + return url + '&fbp=' + data; +} + export function createPixelUrl(firstPartyData, clientHints, configParams, partnerData, cmpData) { - const deviceInfo = collectDeviceInfo() + const deviceInfo = collectDeviceInfo(); + const browser = detectBrowser(); - let url = cmpData.gdprString ? GDPR_SYNC_ENDPOINT : SYNC_ENDPOINT; - url += '/profiles_engine/ProfilesEngineServlet?at=20&mi=10&secure=1' + let url = iiqPixelServerAddress(configParams, cmpData.gdprString); + url += '/profiles_engine/ProfilesEngineServlet?at=20&mi=10&secure=1'; url += '&dpi=' + configParams.partner; url = appendFirstPartyData(url, firstPartyData, partnerData); + url = appendPartnersFirstParty(url, configParams); url = addUniquenessToUrl(url); url += partnerData?.clientType ? '&idtype=' + partnerData.clientType : ''; - if (deviceInfo) url = appendDeviceInfoToUrl(url, deviceInfo) + 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) + url = appendCMPData(url, cmpData); + url = addMetaData(url, sourceMetaDataExternal || sourceMetaData); + url = handleAdditionalParams(browser, url, 0, configParams.additionalParams); + url = appendSPData(url, firstPartyData) + url += '&source=' + PREBID; return url; } @@ -154,7 +221,7 @@ function sendSyncRequest(allowedStorage, url, partner, firstPartyData, newUser) }, undefined, {method: 'GET', withCredentials: true}); if (firstPartyData?.date) { firstPartyData.date = Date.now() - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); } } } else if (!lastSyncDate || lastSyncElapsedTime > SYNC_REFRESH_MILL) { @@ -164,19 +231,6 @@ function sendSyncRequest(allowedStorage, url, partner, firstPartyData, newUser) } } -/** - * Parse json if possible, else return null - * @param data - */ -function tryParse(data) { - try { - return JSON.parse(data); - } catch (err) { - logError(err); - return null; - } -} - /** * Configures and updates A/B testing group in Google Ad Manager (GAM). * @@ -226,6 +280,30 @@ export function isCMPStringTheSame(fpData, cmpData) { return firstPartyDataCPString === cmpDataString; } +function updateCountersAndStore(runtimeEids, allowedStorage, partnerData) { + if (!runtimeEids?.eids?.length) { + noDataCount++; + } else { + callCount++; + } + storeCounters(allowedStorage, partnerData); +} + +function clearCountersAndStore(allowedStorage, partnerData) { + callCount = 0; + failCount = 0; + noDataCount = 0; + storeCounters(allowedStorage, partnerData); +} + +function storeCounters(storage, partnerData) { + partnerData.callCount = callCount; + partnerData.failCount = failCount; + partnerData.noDataCounter = noDataCount; + storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), storage, firstPartyData); +} + + /** @type {Submodule} */ export const intentIqIdSubmodule = { /** @@ -252,45 +330,54 @@ export const intentIqIdSubmodule = { */ getId(config) { const configParams = (config?.params) || {}; + + const firePartnerCallback = () => { + if (configParams.callback && !callbackFired) { + callbackFired = true; + if (callbackTimeoutID) clearTimeout(callbackTimeoutID); + if (isGroupB) runtimeEids = { eids: [] }; + configParams.callback(runtimeEids); + } + } + + if (typeof configParams.partner !== 'number') { + logError('User ID - intentIqId submodule requires a valid partner to be defined'); + firePartnerCallback() + return; + } + let decryptedData, callbackTimeoutID; let callbackFired = false; let runtimeEids = { eids: [] }; let gamObjectReference = isPlainObject(configParams.gamObjectReference) ? configParams.gamObjectReference : undefined; let gamParameterName = configParams.gamParameterName ? configParams.gamParameterName : 'intent_iq_group'; + let groupChanged = typeof configParams.groupChanged === 'function' ? configParams.groupChanged : undefined; + let siloEnabled = typeof configParams.siloEnabled === 'boolean' ? configParams.siloEnabled : false; + sourceMetaData = isStr(configParams.sourceMetaData) ? translateMetadata(configParams.sourceMetaData) : ''; + sourceMetaDataExternal = isNumber(configParams.sourceMetaDataExternal) ? configParams.sourceMetaDataExternal : undefined; + let additionalParams = configParams.additionalParams ? configParams.additionalParams : undefined; + PARTNER_DATA_KEY = `${FIRST_PARTY_KEY}_${configParams.partner}`; const allowedStorage = defineStorageType(config.enabledStorageTypes); let rrttStrtTime = 0; let partnerData = {}; let shouldCallServer = false; - const FIRST_PARTY_DATA_KEY = `${FIRST_PARTY_KEY}_${configParams.partner}`; + FIRST_PARTY_KEY_FINAL = `${FIRST_PARTY_KEY}${siloEnabled ? '_p_' + configParams.partner : ''}`; const cmpData = getCmpData(); const gdprDetected = cmpData.gdprString; - firstPartyData = tryParse(readData(FIRST_PARTY_KEY, allowedStorage)); + firstPartyData = tryParse(readData(FIRST_PARTY_KEY_FINAL, allowedStorage)); const isGroupB = firstPartyData?.group === WITHOUT_IIQ; - setGamReporting(gamObjectReference, gamParameterName, firstPartyData?.group) + setGamReporting(gamObjectReference, gamParameterName, firstPartyData?.group); - const firePartnerCallback = () => { - if (configParams.callback && !callbackFired) { - callbackFired = true; - if (callbackTimeoutID) clearTimeout(callbackTimeoutID); - if (isGroupB) runtimeEids = { eids: [] }; - configParams.callback(runtimeEids, firstPartyData?.group || NOT_YET_DEFINED); - } - } + if (groupChanged) groupChanged(firstPartyData?.group || NOT_YET_DEFINED); callbackTimeoutID = setTimeout(() => { firePartnerCallback(); }, configParams.timeoutInMillis || 500 ); - if (typeof configParams.partner !== 'number') { - logError('User ID - intentIqId submodule requires a valid partner to be defined'); - firePartnerCallback() - return; - } - const currentBrowserLowerCase = detectBrowser(); const browserBlackList = typeof configParams.browserBlackList === 'string' ? configParams.browserBlackList.toLowerCase() : ''; let newUser = false; @@ -301,17 +388,16 @@ export const intentIqIdSubmodule = { pcid: firstPartyId, pcidDate: Date.now(), group: NOT_YET_DEFINED, - cttl: 0, uspString: EMPTY, gppString: EMPTY, gdprString: EMPTY, date: Date.now() }; newUser = true; - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); } else if (!firstPartyData.pcidDate) { firstPartyData.pcidDate = Date.now(); - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); } if (gdprDetected && !('isOptedOut' in firstPartyData)) { @@ -341,13 +427,17 @@ export const intentIqIdSubmodule = { }); } - const savedData = tryParse(readData(FIRST_PARTY_DATA_KEY, allowedStorage)) + const savedData = tryParse(readData(PARTNER_DATA_KEY, allowedStorage)) if (savedData) { partnerData = savedData; + if (typeof partnerData.callCount === 'number') callCount = partnerData.callCount; + if (typeof partnerData.failCount === 'number') failCount = partnerData.failCount; + if (typeof partnerData.noDataCounter === 'number') noDataCount = partnerData.noDataCounter; + if (partnerData.wsrvcll) { partnerData.wsrvcll = false; - storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); } } @@ -358,14 +448,23 @@ export const intentIqIdSubmodule = { } } - if (!firstPartyData.cttl || Date.now() - firstPartyData.date > firstPartyData.cttl || !isCMPStringTheSame(firstPartyData, cmpData)) { + if (!isCMPStringTheSame(firstPartyData, cmpData) || + !firstPartyData.sCal || + (savedData && (!partnerData.cttl || !partnerData.date || Date.now() - partnerData.date > partnerData.cttl))) { firstPartyData.uspString = cmpData.uspString; firstPartyData.gppString = cmpData.gppString; firstPartyData.gdprString = cmpData.gdprString; shouldCallServer = true; - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); - storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); - } else if (firstPartyData.isOptedOut) { + storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + } + if (!shouldCallServer) { + if (!savedData && !firstPartyData.isOptedOut) { + shouldCallServer = true; + } else shouldCallServer = Date.now() > firstPartyData.sCal + HOURS_24; + } + + if (firstPartyData.isOptedOut) { partnerData.data = runtimeEids = { eids: [] }; firePartnerCallback() } @@ -377,7 +476,7 @@ export const intentIqIdSubmodule = { // 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); + if (configParams.callback) configParams.callback(''); const url = createPixelUrl(firstPartyData, clientHints, configParams, partnerData, cmpData) sendSyncRequest(allowedStorage, url, configParams.partner, firstPartyData, newUser) return @@ -386,28 +485,35 @@ export const intentIqIdSubmodule = { if (!shouldCallServer) { if (isGroupB) runtimeEids = { eids: [] }; firePartnerCallback(); + updateCountersAndStore(runtimeEids, allowedStorage, partnerData); return { id: runtimeEids.eids }; } // use protocol relative urls for http or https - 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) : ''; + let url = `${iiqServerAddress(configParams, gdprDetected)}/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); url += (partnerData.cttl) ? '&cttl=' + encodeURIComponent(partnerData.cttl) : ''; url += (partnerData.rrtt) ? '&rrtt=' + encodeURIComponent(partnerData.rrtt) : ''; - url = appendCMPData(url, cmpData) + url = appendCMPData(url, cmpData); + url += '&japs=' + encodeURIComponent(configParams.siloEnabled === true); + url = appendCounters(url); url += clientHints ? '&uh=' + encodeURIComponent(clientHints) : ''; url += VERSION ? '&jsver=' + VERSION : ''; url += firstPartyData?.group ? '&testGroup=' + encodeURIComponent(firstPartyData.group) : ''; + url = addMetaData(url, sourceMetaDataExternal || sourceMetaData); + url = handleAdditionalParams(currentBrowserLowerCase, url, 1, additionalParams); + url = appendSPData(url, firstPartyData) + url += '&source=' + PREBID; // Add vrref and fui to the URL url = appendVrrefAndFui(url, configParams.domainName); const storeFirstPartyData = () => { partnerData.eidl = runtimeEids?.eids?.length || -1 - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); - storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); } const resp = function (callback) { @@ -417,7 +523,7 @@ export const intentIqIdSubmodule = { // If response is a valid json and should save is true if (respJson) { partnerData.date = Date.now(); - firstPartyData.date = Date.now(); + firstPartyData.sCal = Date.now(); const defineEmptyDataAndFireCallback = () => { respJson.data = partnerData.data = runtimeEids = { eids: [] }; storeFirstPartyData() @@ -426,20 +532,22 @@ export const intentIqIdSubmodule = { } if (callbackTimeoutID) clearTimeout(callbackTimeoutID) if ('cttl' in respJson) { - firstPartyData.cttl = respJson.cttl; - } else firstPartyData.cttl = 86400000; + partnerData.cttl = respJson.cttl; + } else partnerData.cttl = HOURS_24; if ('tc' in respJson) { partnerData.terminationCause = respJson.tc; if (respJson.tc == 41) { firstPartyData.group = WITHOUT_IIQ; - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + if (groupChanged) groupChanged(firstPartyData.group); defineEmptyDataAndFireCallback(); if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group); return } else { firstPartyData.group = WITH_IIQ; if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group); + if (groupChanged) groupChanged(firstPartyData.group); } } if ('isOptedOut' in respJson) { @@ -450,13 +558,13 @@ export const intentIqIdSubmodule = { respJson.data = partnerData.data = runtimeEids = { eids: [] }; const keysToRemove = [ - FIRST_PARTY_DATA_KEY, + PARTNER_DATA_KEY, CLIENT_HINTS_KEY ]; keysToRemove.forEach(key => removeDataByKey(key, allowedStorage)); - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); firePartnerCallback(); callback(runtimeEids); return @@ -465,6 +573,9 @@ export const intentIqIdSubmodule = { if ('pid' in respJson) { firstPartyData.pid = respJson.pid; } + if ('dbsaved' in respJson) { + firstPartyData.dbsaved = respJson.dbsaved; + } if ('ls' in respJson) { if (respJson.ls === false) { defineEmptyDataAndFireCallback() @@ -490,6 +601,11 @@ export const intentIqIdSubmodule = { partnerData.siteId = respJson.sid; } + if ('spd' in respJson) { + // server provided data + firstPartyData.spd = respJson.spd; + } + if (rrttStrtTime && rrttStrtTime > 0) { partnerData.rrtt = Date.now() - rrttStrtTime; } @@ -504,6 +620,7 @@ export const intentIqIdSubmodule = { callback(runtimeEids); firePartnerCallback() } + updateCountersAndStore(runtimeEids, allowedStorage, partnerData); storeFirstPartyData(); } else { callback(runtimeEids); @@ -512,13 +629,17 @@ export const intentIqIdSubmodule = { }, error: error => { logError(MODULE_NAME + ': ID fetch encountered an error', error); + failCount++; + updateCountersAndStore(runtimeEids, allowedStorage, partnerData); callback(runtimeEids); } }; rrttStrtTime = Date.now(); partnerData.wsrvcll = true; - storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + clearCountersAndStore(allowedStorage, partnerData); + ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true}); }; const respObj = {callback: resp}; diff --git a/modules/intentIqIdSystem.md b/modules/intentIqIdSystem.md index 39bbb47256ff..644fe07fcd29 100644 --- a/modules/intentIqIdSystem.md +++ b/modules/intentIqIdSystem.md @@ -10,16 +10,16 @@ gpp_sids: usnat By leveraging the Intent IQ identity graph, our module helps publishers, SSPs, and DSPs overcome the challenges of monetizing cookie-less inventory and preparing for a future without 3rd-party cookies. Our solution implements 1st-party data clustering and provides Intent IQ person IDs with over 90% coverage and unmatched accuracy in supported countries while remaining privacy-friendly and CCPA compliant. This results in increased CPMs, higher fill rates, and, ultimately, lifting overall revenue -# All you need is a few basic steps to start using our solution. +# All you need is a few basic steps to start using our solution ## Registration -Navigate to [our portal ](https://www.intentiq.com/) and contact our team for partner ID. +Navigate to [our portal](https://www.intentiq.com/) and contact our team for partner ID. check our [documentation](https://pbmodule.documents.intentiq.com/) to get more information about our solution and how utilze it's full potential ## Integration -``` +```bash gulp build –modules=intentIqIdSystem ``` @@ -29,8 +29,9 @@ We recommend including the Intent IQ Analytics adapter module for improved visib ### Parameters -Please find below list of paramters that could be used in configuring Intent IQ Universal ID module +Please find below list of parameters that could be used in configuring Intent IQ Universal ID module +{: .table .table-bordered .table-striped } | Param under userSync.userIds[] | Scope | Type | Description | Example | | ------------------------------ | -------- |----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------| | name | Required | String | The name of this module: "intentIqId" | `"intentIqId"` | @@ -38,14 +39,26 @@ 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 | Optional | 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 triggered with data | `(data) => console.log({ data })` | | 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.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.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.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` | +| 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` | +| params.sourceMetaData | Optional | String | This metadata can be provided by the partner and will be included in the requests URL as a query parameter | `"123.123.123.123"` | +| params.sourceMetaDataExternal | Optional | Number | This metadata can be provided by the partner and will be included in the requests URL as a query parameter | `123456` | +| params.iiqServerAddress | Optional | String | The base URL for the IntentIQ API server. If parameter is provided in `configParams`, it will be used. | `"https://domain.com"` | +| params.iiqPixelServerAddress | Optional | String | The base URL for the IntentIQ pixel synchronization server. If parameter is provided in `configParams`, it will be used. | `"https://domain.com"` | +| params.reportingServerAddress | Optional | String | The base URL for the IntentIQ reporting server. If parameter is provided in `configParams`, it will be used. | `"https://domain.com"` | +| params.reportMethod | Optional | String | Defines the HTTP method used to send the analytics report. If set to `"POST"`, the report payload will be sent in the body of the request. If set to `"GET"` (default), the payload will be included as a query parameter in the request URL. |`"GET"` | +| params.siloEnabled | Optional | Boolean | Determines if first-party data is stored in a siloed storage key. When set to `true`, first-party data is stored under a modified key that appends `_p_` plus the partner value rather than using the default storage key. The default value is `false`. | `true` | +| params.groupChanged | Optional | Function | A callback that is triggered every time the user’s A/B group is set or updated. |`(group) => console.log('Group changed:', group)` | +| params.additionalParameters | 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.additionalParameters [0].parameterName | Required | String | Name of the custom parameter. This will be sent as a query parameter. | `"abc"` | +| params.additionalParameters [0].parameterValue | Required | String / Number | Value to assign to the parameter. | `123` | +| params.additionalParameters [0].destination | Required | Array | Array of numbers either `1` or `0`. Controls where this parameter is sent `[sendWithSync, sendWithVr, winreport]`. | `[1, 0, 0]` | ### Configuration example @@ -58,10 +71,28 @@ pbjs.setConfig({ partner: 123456, // valid partner id timeoutInMillis: 500, browserBlackList: "chrome", - callback: (data, group) => window.pbjs.requestBids(), - manualWinReportEnabled: true, + callback: (data) => {...}, // your logic here + groupChanged: (group) => console.log('Group is', group), + manualWinReportEnabled: true, // Optional parameter domainName: "currentDomain.com", - adUnitConfig: 1 // Extracting placementId strategy (adUnitCode or placementId order of priorities) + gamObjectReference: googletag, // Optional parameter + gamParameterName: "intent_iq_group", // Optional parameter + adUnitConfig: 1, // Extracting placementId strategy (adUnitCode or placementId order of priorities) + sourceMetaData: "123.123.123.123", // Optional parameter + sourceMetaDataExternal: 123456, // Optional parameter + reportMethod: "GET", // Optional parameter + additionalParameters: [ // Optional parameter + { + parameterName: "abc", + parameterValue: 123, + destination: [1, 1, 0] // sendWithSync: true, sendWithVr: true, winreport: false + }, + { + parameterName: "xyz", + parameterValue: 111, + destination: [0, 1, 1] // sendWithSync: false, sendWithVr: true, winreport: true + } + ] }, storage: { type: "html5", diff --git a/test/spec/modules/intentIqAnalyticsAdapter_spec.js b/test/spec/modules/intentIqAnalyticsAdapter_spec.js index eccff1165cdf..c88c84969ae3 100644 --- a/test/spec/modules/intentIqAnalyticsAdapter_spec.js +++ b/test/spec/modules/intentIqAnalyticsAdapter_spec.js @@ -8,7 +8,7 @@ import * as events from 'src/events.js'; import { getStorageManager } from 'src/storageManager.js'; import sinon from 'sinon'; import { REPORTER_ID, preparePayload } from '../../../modules/intentIqAnalyticsAdapter'; -import {FIRST_PARTY_KEY, VERSION} from '../../../libraries/intentIqConstants/intentIqConstants.js'; +import {FIRST_PARTY_KEY, PREBID, VERSION} from '../../../libraries/intentIqConstants/intentIqConstants.js'; import * as detectBrowserUtils from '../../../libraries/intentIqUtils/detectBrowserUtils.js'; import {getReferrer, appendVrrefAndFui} from '../../../libraries/intentIqUtils/getRefferer.js'; import { gppDataHandler, uspDataHandler, gdprDataHandler } from '../../../src/consentHandler.js'; @@ -18,6 +18,8 @@ const defaultData = '{"pcid":"f961ffb1-a0e1-4696-a9d2-a21d815bd344", "group": "A const version = VERSION; const REPORT_ENDPOINT = 'https://reports.intentiq.com/report'; const REPORT_ENDPOINT_GDPR = 'https://reports-gdpr.intentiq.com/report'; +const REPORT_SERVER_ADDRESS = 'https://test-reports.intentiq.com/report'; + const storage = getStorageManager({ moduleType: 'analytics', moduleName: 'iiqAnalytics' }); @@ -27,7 +29,25 @@ const getUserConfig = () => [ 'params': { 'partner': partner, 'unpack': null, - 'manualWinReportEnabled': false + 'manualWinReportEnabled': false, + }, + 'storage': { + 'type': 'html5', + 'name': 'intentIqId', + 'expires': 60, + 'refreshInSeconds': 14400 + } + } +]; + +const getUserConfigWithReportingServerAddress = () => [ + { + 'name': 'intentIqId', + 'params': { + 'partner': partner, + 'unpack': null, + 'manualWinReportEnabled': false, + 'reportingServerAddress': REPORT_SERVER_ADDRESS }, 'storage': { 'type': 'html5', @@ -118,18 +138,62 @@ describe('IntentIQ tests all', function () { server.reset(); }); - it('IIQ Analytical Adapter bid win report', function () { + it('should send POST request with payload in request body if reportMethod is POST', function () { + const [userConfig] = getUserConfig(); + userConfig.params.reportMethod = 'POST'; + + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); + localStorage.setItem(FIRST_PARTY_KEY, defaultData); - getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({href: 'http://localhost:9876/'}); - const expectedVrref = encodeURIComponent(getWindowLocationStub().href); + + events.emit(EVENTS.BID_WON, wonRequest); + + const request = server.requests[0]; + + const expectedData = preparePayload(wonRequest); + const expectedPayload = `["${btoa(JSON.stringify(expectedData))}"]`; + + expect(request.method).to.equal('POST'); + expect(request.requestBody).to.equal(expectedPayload); + }); + it('should send GET request with payload in query string if reportMethod is NOT provided', function () { + const [userConfig] = getUserConfig(); + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); + + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + events.emit(EVENTS.BID_WON, wonRequest); + + const request = server.requests[0]; + + expect(request.method).to.equal('GET'); + + const url = new URL(request.url); + const payloadEncoded = url.searchParams.get('payload'); + const decoded = JSON.parse(atob(JSON.parse(payloadEncoded)[0])); + + const expected = preparePayload(wonRequest); + + expect(decoded.partnerId).to.equal(expected.partnerId); + expect(decoded.adType).to.equal(expected.adType); + expect(decoded.prebidAuctionId).to.equal(expected.prebidAuctionId); + }); + + it('IIQ Analytical Adapter bid win report', function () { + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({href: 'http://localhost:9876'}); + const expectedVrref = getWindowLocationStub().href; events.emit(EVENTS.BID_WON, wonRequest); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; + const parsedUrl = new URL(request.url); + const vrref = parsedUrl.searchParams.get('vrref'); expect(request.url).to.contain(REPORT_ENDPOINT + '?pid=' + partner + '&mct=1'); expect(request.url).to.contain(`&jsver=${version}`); - expect(request.url).to.contain(`&vrref=${expectedVrref}`); + expect(`&vrref=${decodeURIComponent(vrref)}`).to.contain(`&vrref=${expectedVrref}`); expect(request.url).to.contain('&payload='); expect(request.url).to.contain('iiqid=f961ffb1-a0e1-4696-a9d2-a21d815bd344'); }); @@ -217,11 +281,13 @@ describe('IntentIQ tests all', function () { const request = server.requests[0]; const dataToSend = preparePayload(wonRequest); const base64String = btoa(JSON.stringify(dataToSend)); - const payload = `[%22${base64String}%22]`; + const payload = encodeURIComponent(JSON.stringify([base64String])); const expectedUrl = appendVrrefAndFui(REPORT_ENDPOINT + - `?pid=${partner}&mct=1&iiqid=${defaultDataObj.pcid}&agid=${REPORTER_ID}&jsver=${version}&source=pbjs&payload=${payload}&uh=&gdpr=0`, iiqAnalyticsAnalyticsAdapter.initOptions.domainName + `?pid=${partner}&mct=1&iiqid=${defaultDataObj.pcid}&agid=${REPORTER_ID}&jsver=${version}&source=pbjs&uh=&gdpr=0`, iiqAnalyticsAnalyticsAdapter.initOptions.domainName ); - expect(request.url).to.equal(expectedUrl); + const urlWithPayload = expectedUrl + `&payload=${payload}`; + + expect(request.url).to.equal(urlWithPayload); expect(dataToSend.pcid).to.equal(defaultDataObj.pcid) }); @@ -347,6 +413,114 @@ describe('IntentIQ tests all', function () { expect(request.url).to.contain('iiqid=f961ffb1-a0e1-4696-a9d2-a21d815bd344'); }); + it('should send request in reportingServerAddress no gdpr', function () { + const USERID_CONFIG_BROWSER = [...getUserConfigWithReportingServerAddress()]; + USERID_CONFIG_BROWSER[0].params.browserBlackList = 'chrome,firefox'; + + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG_BROWSER); + detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('safari'); + + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + events.emit(EVENTS.BID_WON, wonRequest); + + expect(server.requests.length).to.be.above(0); + const request = server.requests[0]; + expect(request.url).to.contain(REPORT_SERVER_ADDRESS); + }); + + it('should include source parameter in report URL', function () { + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify(defaultData)); + + events.emit(EVENTS.BID_WON, wonRequest); + const request = server.requests[0]; + + expect(server.requests.length).to.be.above(0); + expect(request.url).to.include(`&source=${PREBID}`); + }); + + it('should use correct key if siloEnabled is true', function () { + const siloEnabled = true; + const USERID_CONFIG = [...getUserConfig()]; + USERID_CONFIG[0].params.siloEnabled = siloEnabled; + + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG); + + localStorage.setItem(FIRST_PARTY_KEY, `${FIRST_PARTY_KEY}${siloEnabled ? '_p_' + partner : ''}`); + events.emit(EVENTS.BID_WON, wonRequest); + + expect(server.requests.length).to.be.above(0); + const request = server.requests[0]; + expect(request.url).to.contain(REPORT_ENDPOINT + '?pid=' + partner + '&mct=1'); + }); + + it('should send additionalParams in report if valid and small enough', function () { + const userConfig = getUserConfig(); + userConfig[0].params.additionalParams = [{ + parameterName: 'general', + parameterValue: 'Lee', + destination: [0, 0, 1] + }]; + + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(userConfig); + + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + events.emit(EVENTS.BID_WON, wonRequest); + + const request = server.requests[0]; + expect(request.url).to.include('general=Lee'); + }); + + it('should not send additionalParams in report if value is too large', function () { + const longVal = 'x'.repeat(5000000); + const userConfig = getUserConfig(); + userConfig[0].params.additionalParams = [{ + parameterName: 'general', + parameterValue: longVal, + destination: [0, 0, 1] + }]; + + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(userConfig); + + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + events.emit(EVENTS.BID_WON, wonRequest); + + const request = server.requests[0]; + expect(request.url).not.to.include('general'); + }); + it('should include spd parameter from LS in report URL', function () { + const spdObject = { foo: 'bar', value: 42 }; + const expectedSpdEncoded = encodeURIComponent(JSON.stringify(spdObject)); + + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({...defaultData, spd: spdObject})); + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); + + events.emit(EVENTS.BID_WON, wonRequest); + + const request = server.requests[0]; + + expect(server.requests.length).to.be.above(0); + expect(request.url).to.include(`&spd=${expectedSpdEncoded}`); + }); + + it('should include spd parameter string from LS in report URL', function () { + const spdObject = 'server provided data'; + const expectedSpdEncoded = encodeURIComponent(spdObject); + + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({...defaultData, spd: spdObject})); + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); + + events.emit(EVENTS.BID_WON, wonRequest); + + const request = server.requests[0]; + + expect(server.requests.length).to.be.above(0); + expect(request.url).to.include(`&spd=${expectedSpdEncoded}`); + }); + const testCasesVrref = [ { description: 'domainName matches window.top.location.href', diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index b2837f6e467e..ccb255b4d158 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -6,21 +6,23 @@ import { decryptData, handleClientHints, firstPartyData as moduleFPD, - isCMPStringTheSame, createPixelUrl + isCMPStringTheSame, createPixelUrl, translateMetadata } from '../../../modules/intentIqIdSystem'; -import {storage, readData} from '../../../libraries/intentIqUtils/storageUtils.js'; +import { storage, readData, storeData } from '../../../libraries/intentIqUtils/storageUtils.js'; import { gppDataHandler, uspDataHandler, gdprDataHandler } from '../../../src/consentHandler'; import { clearAllCookies } from '../../helpers/cookies'; -import { detectBrowserFromUserAgent, detectBrowserFromUserAgentData } from '../../../libraries/intentIqUtils/detectBrowserUtils'; -import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, NOT_YET_DEFINED, WITH_IIQ} from '../../../libraries/intentIqConstants/intentIqConstants.js'; +import { detectBrowser, detectBrowserFromUserAgent, detectBrowserFromUserAgentData } from '../../../libraries/intentIqUtils/detectBrowserUtils'; +import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, NOT_YET_DEFINED, PREBID, WITH_IIQ, WITHOUT_IIQ} from '../../../libraries/intentIqConstants/intentIqConstants.js'; const partner = 10; const pai = '11'; -const pcid = '12'; -const defaultConfigParams = { params: { partner: partner } }; -const paiConfigParams = { params: { partner: partner, pai: pai } }; -const pcidConfigParams = { params: { partner: partner, pcid: pcid } }; -const allConfigParams = { params: { partner: partner, pai: pai, pcid: pcid } }; +const partnerClientId = '12'; +const partnerClientIdType = 0; +const sourceMetaData = '1.1.1.1'; +const defaultConfigParams = { params: { partner } }; +const paiConfigParams = { params: { partner, pai } }; +const pcidConfigParams = { params: { partner, partnerClientIdType, partnerClientId } }; +const allConfigParams = { params: { partner, pai, partnerClientIdType, partnerClientId, sourceMetaData } }; const responseHeader = { 'Content-Type': 'application/json' } export const testClientHints = { @@ -53,6 +55,9 @@ export const testClientHints = { wow64: false }; +const testAPILink = 'https://new-test-api.intentiq.com' +const syncTestAPILink = 'https://new-test-sync.intentiq.com' + const mockGAM = () => { const targetingObject = {}; return { @@ -147,7 +152,7 @@ describe('IntentIQ tests', function () { let submoduleCallback = intentIqIdSubmodule.getId({ ...allConfigParams, enabledStorageTypes: ['cookie'] }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&pai=11&iiqidtype=2&iiqpcid='); + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pai=11&iiqidtype=2&iiqpcid='); request.respond( 200, responseHeader, @@ -180,6 +185,33 @@ describe('IntentIQ tests', function () { expect(intentIqIdSubmodule.decode(undefined)).to.equal(undefined); }); + it('should send AT=20 request and send source in it', function () { + const usedBrowser = 'chrome'; + intentIqIdSubmodule.getId({params: { + partner: 10, + browserBlackList: usedBrowser + } + }); + const currentBrowserLowerCase = detectBrowser(); + + if (currentBrowserLowerCase === usedBrowser) { + const at20request = server.requests[0]; + expect(at20request.url).to.contain(`&source=${PREBID}`); + expect(at20request.url).to.contain(`at=20`); + } + }); + + + it('should send at=39 request and send source in it', function () { + const callBackSpy = sinon.spy(); + const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + + submoduleCallback(callBackSpy); + const request = server.requests[0]; + + expect(request.url).to.contain(`&source=${PREBID}`); + }); + it('should call the IntentIQ endpoint with only partner, pai', function () { let callBackSpy = sinon.spy(); let submoduleCallback = intentIqIdSubmodule.getId(paiConfigParams).callback; @@ -199,7 +231,8 @@ describe('IntentIQ tests', function () { let submoduleCallback = intentIqIdSubmodule.getId(pcidConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&iiqidtype=2&iiqpcid='); + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1'); + expect(request.url).to.contain('&pcid=12'); request.respond( 200, responseHeader, @@ -213,7 +246,8 @@ describe('IntentIQ tests', function () { let submoduleCallback = intentIqIdSubmodule.getId(allConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&pai=11&iiqidtype=2&iiqpcid='); + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pai=11&iiqidtype=2&iiqpcid='); + expect(request.url).to.contain('&pcid=12'); request.respond( 200, responseHeader, @@ -316,7 +350,7 @@ describe('IntentIQ tests', function () { let submoduleCallback = intentIqIdSubmodule.getId(allConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&pai=11&iiqidtype=2&iiqpcid='); + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pai=11&iiqidtype=2&iiqpcid='); request.respond( 200, responseHeader, @@ -331,7 +365,7 @@ describe('IntentIQ tests', function () { let submoduleCallback = intentIqIdSubmodule.getId(allConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&pai=11&iiqidtype=2&iiqpcid='); + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pai=11&iiqidtype=2&iiqpcid='); request.respond( 200, responseHeader, @@ -348,7 +382,7 @@ describe('IntentIQ tests', function () { submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pcid=12&pai=11&iiqidtype=2&iiqpcid='); + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39&mi=10&dpi=10&pt=17&dpn=1&pai=11&iiqidtype=2&iiqpcid='); expect(request.url).to.contain('cttl=' + testLSValue.cttl); expect(request.url).to.contain('rrtt=' + testLSValue.rrtt); request.respond( @@ -392,6 +426,92 @@ describe('IntentIQ tests', function () { expect(logErrorStub.called).to.be.true; }); + it('should send AT=20 request and send spd in it', function () { + const spdValue = { foo: 'bar', value: 42 }; + const encodedSpd = encodeURIComponent(JSON.stringify(spdValue)); + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({pcid: '123', spd: spdValue})); + + intentIqIdSubmodule.getId({params: { + partner: 10, + browserBlackList: 'chrome' + } + }); + + const at20request = server.requests[0]; + expect(at20request.url).to.contain(`&spd=${encodedSpd}`); + expect(at20request.url).to.contain(`at=20`); + }); + + it('should send AT=20 request and send spd string in it ', function () { + const spdValue = 'server provided data'; + const encodedSpd = encodeURIComponent(spdValue); + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({pcid: '123', spd: spdValue})); + + intentIqIdSubmodule.getId({params: { + partner: 10, + browserBlackList: 'chrome' + } + }); + + const at20request = server.requests[0]; + expect(at20request.url).to.contain(`&spd=${encodedSpd}`); + expect(at20request.url).to.contain(`at=20`); + }); + + it('should send spd from firstPartyData in localStorage in at=39 request', function () { + const spdValue = { foo: 'bar', value: 42 }; + const encodedSpd = encodeURIComponent(JSON.stringify(spdValue)); + + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ pcid: '123', spd: spdValue })); + + const callBackSpy = sinon.spy(); + const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + + submoduleCallback(callBackSpy); + const request = server.requests[0]; + + expect(request.url).to.contain(`&spd=${encodedSpd}`); + expect(request.url).to.contain(`at=39`); + }); + + it('should send spd string from firstPartyData in localStorage in at=39 request', function () { + const spdValue = 'spd string'; + const encodedSpd = encodeURIComponent(spdValue); + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ pcid: '123', spd: spdValue })); + + const callBackSpy = sinon.spy(); + const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + + submoduleCallback(callBackSpy); + const request = server.requests[0]; + + expect(request.url).to.contain(`&spd=${encodedSpd}`); + expect(request.url).to.contain(`at=39`); + }); + + it('should save spd to firstPartyData in localStorage if present in response', function () { + const spdValue = { foo: 'bar', value: 42 }; + let callBackSpy = sinon.spy(); + const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + + submoduleCallback(callBackSpy); + const request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ pid: 'test_pid', data: 'test_personid', ls: true, spd: spdValue }) + ); + + const storedLs = readData(FIRST_PARTY_KEY, ['html5', 'cookie'], storage); + const parsedLs = JSON.parse(storedLs); + + expect(storedLs).to.not.be.null; + expect(callBackSpy.calledOnce).to.be.true; + expect(parsedLs).to.have.property('spd'); + expect(parsedLs.spd).to.deep.equal(spdValue); + }); + describe('detectBrowserFromUserAgent', function () { it('should detect Chrome browser', function () { const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'; @@ -444,6 +564,46 @@ describe('IntentIQ tests', function () { const result = detectBrowserFromUserAgentData(userAgentData); 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.", () => { + const allowedStorage = ['html5'] + const newPartnerId = 12345 + const FPD = { + pcid: 'c869aa1f-fe40-47cb-810f-4381fec28fc9', + pcidDate: 1747720820757, + group: 'A', + sCal: Date.now(), + gdprString: null, + gppString: null, + uspString: null + }; + + storeData(FIRST_PARTY_KEY, JSON.stringify(FPD), allowedStorage, storage) + const callBackSpy = sinon.spy() + const submoduleCallback = intentIqIdSubmodule.getId({...allConfigParams, params: {...allConfigParams.params, partner: newPartnerId}}).callback; + submoduleCallback(callBackSpy); + const request = server.requests[0]; + 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.", () => { + const allowedStorage = ['html5'] + const newPartnerId = 12345 + const FPD = { + pcid: 'c869aa1f-fe40-47cb-810f-4381fec28fc9', + pcidDate: 1747720820757, + group: 'A', + isOptedOut: true, + sCal: Date.now(), + gdprString: null, + gppString: null, + uspString: null + }; + + storeData(FIRST_PARTY_KEY, JSON.stringify(FPD), allowedStorage, storage) + const returnedObject = intentIqIdSubmodule.getId({...allConfigParams, params: {...allConfigParams.params, partner: newPartnerId}}); + expect(returnedObject.callback).to.be.undefined + expect(server.requests.length).to.equal(0) // no server requests + }) }); describe('IntentIQ consent management within getId', function () { @@ -580,6 +740,36 @@ describe('IntentIQ tests', function () { expect(request.url).to.contain(ENDPOINT_GDPR); }); + + it('should make request to correct address with iiqServerAddress parameter', function() { + defaultConfigParams.params.iiqServerAddress = testAPILink + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId({...defaultConfigParams}).callback; + + submoduleCallback(callBackSpy); + let request = server.requests[0]; + + expect(request.url).to.contain(testAPILink); + }); + + it('should make request to correct address with iiqPixelServerAddress parameter', function() { + let wasCallbackCalled = false + const callbackConfigParams = { params: { partner: partner, + pai, + partnerClientIdType, + partnerClientId, + browserBlackList: 'Chrome', + iiqPixelServerAddress: syncTestAPILink, + callback: () => { + wasCallbackCalled = true + } } }; + + intentIqIdSubmodule.getId({...callbackConfigParams}); + + let request = server.requests[0]; + + expect(request.url).to.contain(syncTestAPILink); + }); }); it('should get and save client hints to storage', async () => { @@ -677,8 +867,9 @@ describe('IntentIQ tests', function () { it('should run callback from params', async () => { let wasCallbackCalled = false const callbackConfigParams = { params: { partner: partner, - pai: pai, - pcid: pcid, + pai, + partnerClientIdType, + partnerClientId, browserBlackList: 'Chrome', callback: () => { wasCallbackCalled = true @@ -687,4 +878,369 @@ describe('IntentIQ tests', function () { await intentIqIdSubmodule.getId(callbackConfigParams); expect(wasCallbackCalled).to.equal(true); }); + + it('should send sourceMetaData in AT=39 if it exists in configParams', function () { + let translatedMetaDataValue = translateMetadata(sourceMetaData) + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(allConfigParams).callback; + submoduleCallback(callBackSpy); + + let request = server.requests[0]; + + expect(request.url).to.include('?at=39') + expect(request.url).to.include(`fbp=${translatedMetaDataValue}`) + }); + + it('should NOT send sourceMetaData and sourceMetaDataExternal in AT=39 if it is undefined', function () { + let callBackSpy = sinon.spy(); + const configParams = { params: {...allConfigParams.params, sourceMetaData: undefined} }; + let submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + + let request = server.requests[0]; + + expect(request.url).to.include('?at=39') + expect(request.url).not.to.include('fbp=') + }); + + it('should NOT send sourceMetaData in AT=39 if value is NAN', function () { + let callBackSpy = sinon.spy(); + const configParams = { params: {...allConfigParams.params, sourceMetaData: NaN} }; + let submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + + let request = server.requests[0]; + + expect(request.url).to.include('?at=39') + expect(request.url).not.to.include('fbp=') + }); + + it('should send sourceMetaData in AT=20 if it exists in configParams', function () { + let translatedMetaDataValue = translateMetadata(sourceMetaData) + const configParams = { params: {...allConfigParams.params, browserBlackList: 'chrome'} }; + + intentIqIdSubmodule.getId(configParams); + let request = server.requests[0]; + + expect(request.url).to.include('?at=20'); + expect(request.url).to.include(`fbp=${translatedMetaDataValue}`) + }); + + it('should NOT send sourceMetaData in AT=20 if value is NAN', function () { + const configParams = { params: {...allConfigParams.params, sourceMetaData: NaN, browserBlackList: 'chrome'} }; + + intentIqIdSubmodule.getId(configParams); + let request = server.requests[0]; + + expect(request.url).to.include('?at=20'); + expect(request.url).to.not.include('&fbp='); + }); + + it('should send pcid and idtype in AT=20 if it provided in config', function () { + let partnerClientId = 'partnerClientId 123'; + let partnerClientIdType = 0; + const configParams = { params: {...allConfigParams.params, browserBlackList: 'chrome', partnerClientId, partnerClientIdType} }; + + intentIqIdSubmodule.getId(configParams); + let request = server.requests[0]; + + expect(request.url).to.include('?at=20'); + expect(request.url).to.include(`&pcid=${encodeURIComponent(partnerClientId)}`); + expect(request.url).to.include(`&idtype=${partnerClientIdType}`); + }); + + it('should NOT send pcid and idtype in AT=20 if partnerClientId is NOT a string', function () { + let partnerClientId = 123; + let partnerClientIdType = 0; + const configParams = { params: {...allConfigParams.params, browserBlackList: 'chrome', partnerClientId, partnerClientIdType} }; + + intentIqIdSubmodule.getId(configParams); + let request = server.requests[0]; + + expect(request.url).to.include('?at=20'); + expect(request.url).not.to.include(`&pcid=`); + expect(request.url).not.to.include(`&idtype=`); + }); + + it('should NOT send pcid and idtype in AT=20 if partnerClientIdType is NOT a number', function () { + let partnerClientId = 'partnerClientId 123'; + let partnerClientIdType = 'wrong'; + const configParams = { params: {...allConfigParams.params, browserBlackList: 'chrome', partnerClientId, partnerClientIdType} }; + + intentIqIdSubmodule.getId(configParams); + let request = server.requests[0]; + + expect(request.url).to.include('?at=20'); + expect(request.url).not.to.include(`&pcid=`); + expect(request.url).not.to.include(`&idtype=`); + }); + + it('should send partnerClientId and partnerClientIdType in AT=39 if it provided in config', function () { + let partnerClientId = 'partnerClientId 123'; + let partnerClientIdType = 0; + let callBackSpy = sinon.spy(); + const configParams = { params: {...allConfigParams.params, partnerClientId, partnerClientIdType} }; + let submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + + let request = server.requests[0]; + + expect(request.url).to.include('?at=39') + expect(request.url).to.include(`&pcid=${encodeURIComponent(partnerClientId)}`); + expect(request.url).to.include(`&idtype=${partnerClientIdType}`); + }); + + it('should NOT send partnerClientId and partnerClientIdType in AT=39 if partnerClientId is not a string', function () { + let partnerClientId = 123; + let partnerClientIdType = 0; + let callBackSpy = sinon.spy(); + const configParams = { params: {...allConfigParams.params, partnerClientId, partnerClientIdType} }; + let submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + + let request = server.requests[0]; + + expect(request.url).to.include('?at=39') + expect(request.url).not.to.include(`&pcid=${partnerClientId}`); + expect(request.url).not.to.include(`&idtype=${partnerClientIdType}`); + }); + + it('should NOT send partnerClientId and partnerClientIdType in AT=39 if partnerClientIdType is not a number', function () { + let partnerClientId = 'partnerClientId-123'; + let partnerClientIdType = 'wrong'; + let callBackSpy = sinon.spy(); + const configParams = { params: {...allConfigParams.params, partnerClientId, partnerClientIdType} }; + let submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + + let request = server.requests[0]; + + expect(request.url).to.include('?at=39') + expect(request.url).not.to.include(`&pcid=${partnerClientId}`); + expect(request.url).not.to.include(`&idtype=${partnerClientIdType}`); + }); + + it('should NOT send sourceMetaData in AT=20 if sourceMetaDataExternal provided', function () { + const configParams = { params: {...allConfigParams.params, browserBlackList: 'chrome', sourceMetaDataExternal: 123} }; + + intentIqIdSubmodule.getId(configParams); + let request = server.requests[0]; + + expect(request.url).to.include('?at=20'); + expect(request.url).to.include('&fbp=123'); + }); + + it('should store first party data under the silo key when siloEnabled is true', function () { + const configParams = { params: {...allConfigParams.params, siloEnabled: true} }; + + intentIqIdSubmodule.getId(configParams); + const expectedKey = FIRST_PARTY_KEY + '_p_' + configParams.params.partner; + const storedData = localStorage.getItem(expectedKey); + expect(storedData).to.be.a('string'); + expect(localStorage.getItem(FIRST_PARTY_KEY)).to.be.null; + + const parsed = JSON.parse(storedData); + expect(parsed).to.have.property('pcid'); + }); + + it('should send siloEnabled value in the request', function () { + let callBackSpy = sinon.spy(); + const configParams = { params: {...allConfigParams.params, siloEnabled: true} }; + let submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + + let request = server.requests[0]; + + expect(request.url).to.contain(`&japs=${configParams.params.siloEnabled}`); + }); + + it('should increment callCount when valid eids are returned', function () { + const firstPartyDataKey = '_iiq_fdata_' + partner; + const partnerData = { callCount: 0, failCount: 0, noDataCounter: 0 }; + localStorage.setItem(firstPartyDataKey, JSON.stringify(partnerData)); + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ pcid: 'abc', cttl: 9999999, group: 'with_iiq' })); + + const responseData = { data: { eids: ['abc'] }, ls: true }; + + const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + const callBackSpy = sinon.spy(); + + submoduleCallback(callBackSpy); + const request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify(responseData)); + + const updatedData = JSON.parse(localStorage.getItem(firstPartyDataKey)); + expect(updatedData.callCount).to.equal(1); + }); + + it('should increment failCount when request fails', function () { + const firstPartyDataKey = '_iiq_fdata_' + partner; + const partnerData = { callCount: 0, failCount: 0, noDataCounter: 0 }; + localStorage.setItem(firstPartyDataKey, JSON.stringify(partnerData)); + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ pcid: 'abc', cttl: 9999999, group: 'with_iiq' })); + + const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + const callBackSpy = sinon.spy(); + + submoduleCallback(callBackSpy); + const request = server.requests[0]; + request.respond(503, responseHeader, 'Service Unavailable'); + + const updatedData = JSON.parse(localStorage.getItem(firstPartyDataKey)); + expect(updatedData.failCount).to.equal(1); + }); + + it('should increment noDataCounter when eids are empty', function () { + const firstPartyDataKey = '_iiq_fdata_' + partner; + const partnerData = { callCount: 0, failCount: 0, noDataCounter: 0 }; + localStorage.setItem(firstPartyDataKey, JSON.stringify(partnerData)); + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ pcid: 'abc', cttl: 9999999, group: 'with_iiq' })); + + const responseData = { data: { eids: [] }, ls: true }; + + const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + const callBackSpy = sinon.spy(); + + submoduleCallback(callBackSpy); + const request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify(responseData)); + + const updatedData = JSON.parse(localStorage.getItem(firstPartyDataKey)); + expect(updatedData.noDataCounter).to.equal(1); + }); + + it('should send additional parameters in sync request due to configuration', function () { + const configParams = { + params: { + ...defaultConfigParams.params, + browserBlackList: 'chrome', + additionalParams: [{ + parameterName: 'general', + parameterValue: 'Lee', + destination: [1, 0, 0] + }] + } + }; + + intentIqIdSubmodule.getId(configParams); + const syncRequest = server.requests[0]; + + expect(syncRequest.url).to.include('general=Lee'); + }); + it('should send additionalParams in VR request', function () { + const configParams = { + params: { + ...defaultConfigParams.params, + additionalParams: [{ + parameterName: 'general', + parameterValue: 'Lee', + destination: [0, 1, 0] + }] + } + }; + + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + const vrRequest = server.requests[0]; + + expect(vrRequest.url).to.include('general=Lee'); + }); + + it('should not send additionalParams in case it is not an array', function () { + const configParams = { + params: { + ...defaultConfigParams.params, + additionalParams: { + parameterName: 'general', + parameterValue: 'Lee', + destination: [0, 1, 0] + } + } + }; + + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + const vrRequest = server.requests[0]; + + expect(vrRequest.url).not.to.include('general='); + }); + + it('should not send additionalParams in case request url is too long', function () { + const longValue = 'x'.repeat(5000000); // simulate long parameter + const configParams = { + params: { + ...defaultConfigParams.params, + additionalParams: [{ + parameterName: 'general', + parameterValue: longValue, + destination: [0, 1, 0] + }] + } + }; + + let callBackSpy = sinon.spy(); + let submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + const vrRequest = server.requests[0]; + + expect(vrRequest.url).not.to.include('general='); + }); + + it('should call groupChanged with "withoutIIQ" when terminationCause is 41', function () { + const groupChangedSpy = sinon.spy(); + const callBackSpy = sinon.spy(); + const configParams = { + params: { + ...defaultConfigParams.params, + groupChanged: groupChangedSpy + } + }; + + const submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + + const request = server.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({ + tc: 41, + isOptedOut: false, + data: { eids: [] } + }) + ); + + expect(callBackSpy.calledOnce).to.be.true; + expect(groupChangedSpy.calledWith(WITHOUT_IIQ)).to.be.true; + }); + + it('should call groupChanged with "withIIQ" when terminationCause is NOT 41', function () { + const groupChangedSpy = sinon.spy(); + const callBackSpy = sinon.spy(); + const configParams = { + params: { + ...defaultConfigParams.params, + groupChanged: groupChangedSpy + } + }; + + const submoduleCallback = intentIqIdSubmodule.getId(configParams).callback; + submoduleCallback(callBackSpy); + + const request = server.requests[0]; + request.respond( + 200, + responseHeader, + JSON.stringify({ + tc: 35, + isOptedOut: false, + data: { eids: [] } + }) + ); + + expect(callBackSpy.calledOnce).to.be.true; + expect(groupChangedSpy.calledWith(WITH_IIQ)).to.be.true; + }); });