From 603607b328555a7869fad7bc27703b74357edbbb Mon Sep 17 00:00:00 2001 From: Eyvaz <62054743+eyvazahmadzada@users.noreply.github.com> Date: Mon, 12 May 2025 12:23:02 +0200 Subject: [PATCH 1/6] V 0 29 (#31) * AGT-403: Add adType parameter to payload in report * AGT-403: Test for partner report, adType parameter * AGT-403: Test refactoring * AGT-403: Documentation updated * IntentIq Analytics Module: adUnitCode or placemetId to report (#9) * AGT-446: adUnitCode or placemetId to report * AGT-446: Description of changes, example * AGT-446: Fix documentation * AGT-446: Changes after review * Agt 409 full url in prebid modules (#8) * add sync * update full url * set url param * fix comment * fix issue fix sync mode * update url * update full url * add test * move browser_blacklist (#10) * add is optedOut (#11) * update date in FPD after sync (#12) * update sync logic for new user (#13) * fix unit tests * change version name * IntentIq ID: Support sourceMetaData and sourceMetaDataExternal (#17) * AGT-453: Support sourceMetaData * AGT-453: sourceMetaDataExternal * AGT-453: Documentation * AGT-453: Changes after review * AGT-453: Fix doc * Fix doc * AGT-453: Fix test * update test * update endpoint (#16) * update endpoint * update test * add tests * update test * fix conflict * fix conflict * add test for reportingServerAddress * add test for reportingServerAddress * AGT-498: support report method parameter (#22) * AGT-498: reportMethod * AGT-498: Tests and refactoring * AGT-498: method refactoring * AGT-498: get method refactoring * AGT-498: reportMethod parameter description (#23) * AGT-498: reportMethod * AGT-498: Tests and refactoring * AGT-498: method refactoring * AGT-498: get method refactoring * AGT-498: Documentation * Agt 452 server call counters (#18) * add counters * add unit tests and fixes * counters additions * clear counters when a request is made --------- Co-authored-by: Eyvaz Ahmadzada * add siloEnabled feature (#19) * add siloEnabled feature * add siloEnabled feature to analytics adapter * add unit tests for siloEnabled feature * add siloenabled to docs --------- Co-authored-by: Eyvaz Ahmadzada * fix unit test issues * AGT-455: Source parameter in requests (#28) * AGT-508: Spd parameter (#29) * AGT-508: Spd parameter * AGT-508: Move appendSpd to utils * AGT-499: Support partnerClientIdType and partnerClientId (#30) * AGT-499: partnerClientId and partnerClientIdType in requests * AGT-499: Clean comment * AGT-499: Refactoring * add groupchanged parameter (#25) * add groupchanged parameter * fix md file issue * add groupChanged example in docs * remove group from example * add unit tests and fix groupChanged issue --------- Co-authored-by: Eyvaz Ahmadzada Co-authored-by: DimaIntentIQ <139111483+DimaIntentIQ@users.noreply.github.com> * Agt 501 support additional params (#24) * additionalParams feature init * additional params add features * additionalparams fix analytics adapter * add unit tests for additionalParams * fix currentBrowser issue --------- Co-authored-by: Eyvaz Ahmadzada Co-authored-by: DimaIntentIQ <139111483+DimaIntentIQ@users.noreply.github.com> * fix linter error --------- Co-authored-by: dmytro-po Co-authored-by: DimaIntentIQ <139111483+DimaIntentIQ@users.noreply.github.com> Co-authored-by: dLepetynskyiIntentiq Co-authored-by: DimaIntentIQ Co-authored-by: Eyvaz Ahmadzada --- .../intentIqConstants/intentIqConstants.js | 18 +- .../intentIqUtils/handleAdditionalParams.js | 44 ++ libraries/intentIqUtils/intentIqConfig.js | 3 + libraries/intentIqUtils/urlUtils.js | 5 + modules/intentIqAnalyticsAdapter.js | 87 ++- modules/intentIqIdSystem.js | 203 +++++-- modules/intentIqIdSystem.md | 15 +- .../modules/intentIqAnalyticsAdapter_spec.js | 192 ++++++- test/spec/modules/intentIqIdSystem_spec.js | 513 +++++++++++++++++- 9 files changed, 999 insertions(+), 81 deletions(-) create mode 100644 libraries/intentIqUtils/handleAdditionalParams.js create mode 100644 libraries/intentIqUtils/intentIqConfig.js create mode 100644 libraries/intentIqUtils/urlUtils.js diff --git a/libraries/intentIqConstants/intentIqConstants.js b/libraries/intentIqConstants/intentIqConstants.js index a0f48caf97c..46a0ccec6e7 100644 --- a/libraries/intentIqConstants/intentIqConstants.js +++ b/libraries/intentIqConstants/intentIqConstants.js @@ -9,14 +9,11 @@ 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 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 +24,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 00000000000..e4bfa14c84f --- /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 00000000000..85c9111970b --- /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/urlUtils.js b/libraries/intentIqUtils/urlUtils.js new file mode 100644 index 00000000000..4cfb8273eab --- /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 2adc664d9e1..e58bb604bcd 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 5bc86dd439c..8d8c3a2c4e8 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -5,12 +5,13 @@ * @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'; @@ -18,13 +19,14 @@ 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 } 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 +47,14 @@ const encoderCH = { wow64: 7, fullVersionList: 8 }; +let sourceMetaData; +let sourceMetaDataExternal; + +let FIRST_PARTY_KEY_FINAL = FIRST_PARTY_KEY; +let FIRST_PARTY_DATA_KEY; +let callCount = 0; +let failCount = 0; +let noDataCount = 0; export let firstPartyData; @@ -82,12 +92,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 +128,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 +154,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 +220,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) { @@ -226,6 +292,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(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), storage, firstPartyData); +} + + /** @type {Submodule} */ export const intentIqIdSubmodule = { /** @@ -252,45 +342,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; + FIRST_PARTY_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; @@ -308,10 +407,10 @@ export const intentIqIdSubmodule = { 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)) { @@ -345,6 +444,10 @@ export const intentIqIdSubmodule = { 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); @@ -363,7 +466,7 @@ export const intentIqIdSubmodule = { firstPartyData.gppString = cmpData.gppString; firstPartyData.gdprString = cmpData.gdprString; shouldCallServer = true; - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); + storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); } else if (firstPartyData.isOptedOut) { partnerData.data = runtimeEids = { eids: [] }; @@ -377,7 +480,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,27 +489,34 @@ 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_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); } @@ -433,13 +543,15 @@ export const intentIqIdSubmodule = { 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) { @@ -456,7 +568,7 @@ export const intentIqIdSubmodule = { 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 @@ -490,6 +602,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 +621,7 @@ export const intentIqIdSubmodule = { callback(runtimeEids); firePartnerCallback() } + updateCountersAndStore(runtimeEids, allowedStorage, partnerData); storeFirstPartyData(); } else { callback(runtimeEids); @@ -512,6 +630,8 @@ export const intentIqIdSubmodule = { }, error: error => { logError(MODULE_NAME + ': ID fetch encountered an error', error); + failCount++; + updateCountersAndStore(runtimeEids, allowedStorage, partnerData); callback(runtimeEids); } }; @@ -519,6 +639,7 @@ export const intentIqIdSubmodule = { partnerData.wsrvcll = true; storeData(FIRST_PARTY_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 39bbb47256f..3cc801bbc6f 100644 --- a/modules/intentIqIdSystem.md +++ b/modules/intentIqIdSystem.md @@ -38,7 +38,7 @@ 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 trigered 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.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` | @@ -46,6 +46,13 @@ Please find below list of paramters that could be used in configuring Intent IQ | 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.iiqServerAddress | Optional | String | The base URL for the IntentIQ API server. If `iiqServerAddress` is provided in `configParams`, it will be used. | `"https://api.example.com"` | +| params.iiqPixelServerAddress | Optional | String | The base URL for the IntentIQ pixel synchronization server. If `iiqPixelServerAddress` is provided in `configParams`, it will be used. | `"https://sync.example.com"` | +| params.reportingServerAddress | Optional | String | The base URL for the IntentIQ reporting server. If `reportingServerAddress` is provided in `configParams`, it will be used.| `"https://reports.exmaple.com/report"` | +| 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.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.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` | ### Configuration example @@ -58,10 +65,12 @@ pbjs.setConfig({ partner: 123456, // valid partner id timeoutInMillis: 500, browserBlackList: "chrome", - callback: (data, group) => window.pbjs.requestBids(), + callback: (data) => window.pbjs.requestBids(), + groupChanged: (group) => console.log('Group is', group), manualWinReportEnabled: true, domainName: "currentDomain.com", - adUnitConfig: 1 // Extracting placementId strategy (adUnitCode or placementId order of priorities) + adUnitConfig: 1, // Extracting placementId strategy (adUnitCode or placementId order of priorities) + reportMethod: "GET" // Optional parameter }, storage: { type: "html5", diff --git a/test/spec/modules/intentIqAnalyticsAdapter_spec.js b/test/spec/modules/intentIqAnalyticsAdapter_spec.js index eccff1165cd..c88c84969ae 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 b2837f6e467..558e3c8c5e2 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -6,21 +6,22 @@ import { decryptData, handleClientHints, firstPartyData as moduleFPD, - isCMPStringTheSame, createPixelUrl + isCMPStringTheSame, createPixelUrl, translateMetadata } from '../../../modules/intentIqIdSystem'; import {storage, readData} 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 {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, NOT_YET_DEFINED, WITH_IIQ, WITHOUT_IIQ} from '../../../libraries/intentIqConstants/intentIqConstants.js'; const partner = 10; const pai = '11'; const pcid = '12'; +const sourceMetaData = '1.1.1.1'; 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 allConfigParams = { params: { partner, pai, pcid, sourceMetaData } }; const responseHeader = { 'Content-Type': 'application/json' } export const testClientHints = { @@ -53,6 +54,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 { @@ -180,6 +184,29 @@ describe('IntentIQ tests', function () { expect(intentIqIdSubmodule.decode(undefined)).to.equal(undefined); }); + it('should send AT=20 request and send source in it', function () { + intentIqIdSubmodule.getId({params: { + partner: 10, + browserBlackList: 'chrome' + } + }); + + 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; @@ -392,6 +419,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'; @@ -580,6 +693,35 @@ 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: pai, + pcid: pcid, + 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 () => { @@ -687,4 +829,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; + }); }); From 9fa8db32d15e870ef7193e5ae5336d6d6c352c88 Mon Sep 17 00:00:00 2001 From: Eyvaz <62054743+eyvazahmadzada@users.noreply.github.com> Date: Mon, 19 May 2025 09:16:04 +0200 Subject: [PATCH 2/6] fix unit tests (#32) Co-authored-by: Eyvaz Ahmadzada --- modules/intentIqIdSystem.js | 1 + test/spec/modules/intentIqIdSystem_spec.js | 37 ++++++++++++---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 8d8c3a2c4e8..31721e9da85 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -640,6 +640,7 @@ export const intentIqIdSubmodule = { partnerData.wsrvcll = true; storeData(FIRST_PARTY_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/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index 558e3c8c5e2..39c3849c66e 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -12,16 +12,17 @@ import {storage, readData} from '../../../libraries/intentIqUtils/storageUtils.j 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, WITHOUT_IIQ} from '../../../libraries/intentIqConstants/intentIqConstants.js'; +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 partnerClientId = '12'; +const partnerClientIdType = 0; const sourceMetaData = '1.1.1.1'; -const defaultConfigParams = { params: { partner: partner } }; -const paiConfigParams = { params: { partner: partner, pai: pai } }; -const pcidConfigParams = { params: { partner: partner, pcid: pcid } }; -const allConfigParams = { params: { partner, pai, pcid, sourceMetaData } }; +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 = { @@ -151,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, @@ -226,7 +227,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, @@ -240,7 +242,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, @@ -343,7 +346,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, @@ -358,7 +361,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, @@ -375,7 +378,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( @@ -708,8 +711,9 @@ describe('IntentIQ tests', function () { it('should make request to correct address with iiqPixelServerAddress parameter', function() { let wasCallbackCalled = false const callbackConfigParams = { params: { partner: partner, - pai: pai, - pcid: pcid, + pai, + partnerClientIdType, + partnerClientId, browserBlackList: 'Chrome', iiqPixelServerAddress: syncTestAPILink, callback: () => { @@ -819,8 +823,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 From b5da2c6cc05593374bbf44c938f4d7ccf13799ec Mon Sep 17 00:00:00 2001 From: DimaIntentIQ <139111483+DimaIntentIQ@users.noreply.github.com> Date: Tue, 20 May 2025 17:58:26 +0300 Subject: [PATCH 3/6] Agt 547 vr bug for module (#33) * save progress * fix calling server * add "dbsaved" parameter * add tests --- .../intentIqConstants/intentIqConstants.js | 1 + libraries/intentIqUtils/storageUtils.js | 13 ++++ modules/intentIqIdSystem.js | 59 +++++++++---------- test/spec/modules/intentIqIdSystem_spec.js | 42 ++++++++++++- 4 files changed, 84 insertions(+), 31 deletions(-) diff --git a/libraries/intentIqConstants/intentIqConstants.js b/libraries/intentIqConstants/intentIqConstants.js index 46a0ccec6e7..05a0bfb0885 100644 --- a/libraries/intentIqConstants/intentIqConstants.js +++ b/libraries/intentIqConstants/intentIqConstants.js @@ -11,6 +11,7 @@ export const EMPTY = 'EMPTY'; export const GVLID = '1323'; export const VERSION = 0.29; export const PREBID = 'pbjs'; +export const HOURS_24 = 86400000; export const INVALID_ID = 'INVALID_ID'; diff --git a/libraries/intentIqUtils/storageUtils.js b/libraries/intentIqUtils/storageUtils.js index 8e7bf1d12a5..338333ef3d1 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/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 31721e9da85..aa7a1f775e9 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -14,7 +14,7 @@ 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, @@ -22,7 +22,8 @@ import { CLIENT_HINTS_KEY, EMPTY, GVLID, - VERSION, INVALID_ID, SCREEN_PARAMS, SYNC_REFRESH_MILL, META_DATA_CONSTANT, PREBID + 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'; @@ -51,7 +52,7 @@ let sourceMetaData; let sourceMetaDataExternal; let FIRST_PARTY_KEY_FINAL = FIRST_PARTY_KEY; -let FIRST_PARTY_DATA_KEY; +let PARTNER_DATA_KEY; let callCount = 0; let failCount = 0; let noDataCount = 0; @@ -230,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). * @@ -312,7 +300,7 @@ function storeCounters(storage, partnerData) { partnerData.callCount = callCount; partnerData.failCount = failCount; partnerData.noDataCounter = noDataCount; - storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), storage, firstPartyData); + storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), storage, firstPartyData); } @@ -369,7 +357,7 @@ export const intentIqIdSubmodule = { sourceMetaData = isStr(configParams.sourceMetaData) ? translateMetadata(configParams.sourceMetaData) : ''; sourceMetaDataExternal = isNumber(configParams.sourceMetaDataExternal) ? configParams.sourceMetaDataExternal : undefined; let additionalParams = configParams.additionalParams ? configParams.additionalParams : undefined; - FIRST_PARTY_DATA_KEY = `${FIRST_PARTY_KEY}_${configParams.partner}`; + PARTNER_DATA_KEY = `${FIRST_PARTY_KEY}_${configParams.partner}`; const allowedStorage = defineStorageType(config.enabledStorageTypes); @@ -400,7 +388,6 @@ export const intentIqIdSubmodule = { pcid: firstPartyId, pcidDate: Date.now(), group: NOT_YET_DEFINED, - cttl: 0, uspString: EMPTY, gppString: EMPTY, gdprString: EMPTY, @@ -440,7 +427,7 @@ export const intentIqIdSubmodule = { }); } - const savedData = tryParse(readData(FIRST_PARTY_DATA_KEY, allowedStorage)) + const savedData = tryParse(readData(PARTNER_DATA_KEY, allowedStorage)) if (savedData) { partnerData = savedData; @@ -450,7 +437,7 @@ export const intentIqIdSubmodule = { 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); } } @@ -461,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_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); - storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); - } else if (firstPartyData.isOptedOut) { + 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() } @@ -517,7 +513,7 @@ export const intentIqIdSubmodule = { const storeFirstPartyData = () => { partnerData.eidl = runtimeEids?.eids?.length || -1 storeData(FIRST_PARTY_KEY_FINAL, JSON.stringify(firstPartyData), allowedStorage, firstPartyData); - storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); + storeData(PARTNER_DATA_KEY, JSON.stringify(partnerData), allowedStorage, firstPartyData); } const resp = function (callback) { @@ -527,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() @@ -536,8 +532,8 @@ 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; @@ -562,7 +558,7 @@ export const intentIqIdSubmodule = { respJson.data = partnerData.data = runtimeEids = { eids: [] }; const keysToRemove = [ - FIRST_PARTY_DATA_KEY, + PARTNER_DATA_KEY, CLIENT_HINTS_KEY ]; @@ -577,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() @@ -638,7 +637,7 @@ export const intentIqIdSubmodule = { 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}); diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index 39c3849c66e..c89e7993ba2 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -8,7 +8,7 @@ import { firstPartyData as moduleFPD, 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'; @@ -560,6 +560,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 () { From 08217e45c35201a402d99d9eb9e02539b9ffcacd Mon Sep 17 00:00:00 2001 From: dmytro-po Date: Wed, 21 May 2025 16:18:10 +0300 Subject: [PATCH 4/6] AGT-552: Additional query params (#34) * AGT-552: Additional query params * update documentation --------- Co-authored-by: DimaIntentIQ --- modules/intentIqIdSystem.md | 52 ++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/modules/intentIqIdSystem.md b/modules/intentIqIdSystem.md index 3cc801bbc6f..442c2a06158 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"` | @@ -40,19 +41,24 @@ Please find below list of paramters that could be used in configuring Intent IQ | 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 | `(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.iiqServerAddress | Optional | String | The base URL for the IntentIQ API server. If `iiqServerAddress` is provided in `configParams`, it will be used. | `"https://api.example.com"` | -| params.iiqPixelServerAddress | Optional | String | The base URL for the IntentIQ pixel synchronization server. If `iiqPixelServerAddress` is provided in `configParams`, it will be used. | `"https://sync.example.com"` | -| params.reportingServerAddress | Optional | String | The base URL for the IntentIQ reporting server. If `reportingServerAddress` is provided in `configParams`, it will be used.| `"https://reports.exmaple.com/report"` | -| 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.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.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.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 @@ -65,12 +71,28 @@ pbjs.setConfig({ partner: 123456, // valid partner id timeoutInMillis: 500, browserBlackList: "chrome", - callback: (data) => window.pbjs.requestBids(), + callback: (data) => {...}, // your logic here groupChanged: (group) => console.log('Group is', group), - manualWinReportEnabled: true, + manualWinReportEnabled: true, // Optional parameter domainName: "currentDomain.com", + gamObjectReference: googletag, // Optional parameter + gamParameterName: "intent_iq_group", // Optional parameter adUnitConfig: 1, // Extracting placementId strategy (adUnitCode or placementId order of priorities) - reportMethod: "GET" // Optional parameter + 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", From 37ad8ad10a4c9d86729c58d94198edbc321e9d85 Mon Sep 17 00:00:00 2001 From: DimaIntentIQ Date: Wed, 21 May 2025 17:42:54 +0300 Subject: [PATCH 5/6] Fix unit test for specific browser --- test/spec/modules/intentIqIdSystem_spec.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index c89e7993ba2..ccb255b4d15 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -11,7 +11,7 @@ import { 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 { 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; @@ -186,15 +186,19 @@ describe('IntentIQ tests', function () { }); it('should send AT=20 request and send source in it', function () { + const usedBrowser = 'chrome'; intentIqIdSubmodule.getId({params: { partner: 10, - browserBlackList: 'chrome' + browserBlackList: usedBrowser } }); - - const at20request = server.requests[0]; - expect(at20request.url).to.contain(`&source=${PREBID}`); - expect(at20request.url).to.contain(`at=20`); + 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`); + } }); From 53a6cace5da0a2233fa0a6d6b2140041aa197527 Mon Sep 17 00:00:00 2001 From: DimaIntentIQ Date: Fri, 23 May 2025 17:07:21 +0300 Subject: [PATCH 6/6] fix spelling --- modules/intentIqIdSystem.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/intentIqIdSystem.md b/modules/intentIqIdSystem.md index 442c2a06158..644fe07fcd2 100644 --- a/modules/intentIqIdSystem.md +++ b/modules/intentIqIdSystem.md @@ -39,7 +39,7 @@ Please find below list of parameters 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 | `(data) => console.log({ data })` | +| 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.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` |