diff --git a/libraries/intentIqConstants/intentIqConstants.js b/libraries/intentIqConstants/intentIqConstants.js index 945bcfad4ad..0992f4c26b3 100644 --- a/libraries/intentIqConstants/intentIqConstants.js +++ b/libraries/intentIqConstants/intentIqConstants.js @@ -9,21 +9,12 @@ export const BLACK_LIST = 'L'; export const CLIENT_HINTS_KEY = '_iiq_ch'; export const EMPTY = 'EMPTY'; export const GVLID = '1323'; -export const VERSION = 0.31; +export const VERSION = 0.32; export const PREBID = 'pbjs'; export const HOURS_24 = 86400000; export const INVALID_ID = 'INVALID_ID'; -export const SCREEN_PARAMS = { - 0: 'windowInnerHeight', - 1: 'windowInnerWidth', - 2: 'devicePixelRatio', - 3: 'windowScreenHeight', - 4: 'windowScreenWidth', - 5: 'language' -}; - export const SYNC_REFRESH_MILL = 3600000; export const META_DATA_CONSTANT = 256; diff --git a/libraries/intentIqUtils/gamPredictionReport.js b/libraries/intentIqUtils/gamPredictionReport.js new file mode 100644 index 00000000000..09db065f494 --- /dev/null +++ b/libraries/intentIqUtils/gamPredictionReport.js @@ -0,0 +1,97 @@ +import { getEvents } from '../../src/events.js'; +import { logError } from '../../src/utils.js'; + +export function gamPredictionReport (gamObjectReference, sendData) { + try { + if (!gamObjectReference || !sendData) logError('Failed to get gamPredictionReport, required data is missed'); + const getSlotTargeting = (slot) => { + const kvs = {}; + try { + (slot.getTargetingKeys() || []).forEach((k) => { + kvs[k] = slot.getTargeting(k); + }); + } catch (e) { + logError('Failed to get targeting keys: ' + e); + } + return kvs; + }; + + const extractWinData = (gamEvent) => { + const slot = gamEvent.slot; + const targeting = getSlotTargeting(slot); + + const dataToSend = { + placementId: slot.getSlotElementId && slot.getSlotElementId(), + adUnitPath: slot.getAdUnitPath && slot.getAdUnitPath(), + bidderCode: targeting.hb_bidder ? targeting.hb_bidder[0] : null, + biddingPlatformId: 5 + }; + + if (dataToSend.placementId) { + // TODO check auto subscription to prebid events + const bidWonEvents = getEvents().filter((ev) => ev.eventType === 'bidWon'); + if (bidWonEvents.length) { + for (let i = bidWonEvents.length - 1; i >= 0; i--) { + const element = bidWonEvents[i]; + if ( + dataToSend.placementId === element.id && + targeting.hb_adid && + targeting.hb_adid[0] === element.args.adId + ) { + return; // don't send report if there was bidWon event earlier + } + } + } + const endEvents = getEvents().filter((ev) => ev.eventType === 'auctionEnd'); + if (endEvents.length) { + for (let i = endEvents.length - 1; i >= 0; i--) { + // starting from the last event + const element = endEvents[i]; + if (element.args?.adUnitCodes?.includes(dataToSend.placementId)) { + const defineRelevantData = (bid) => { + dataToSend.cpm = bid.cpm + 0.01; // add one cent to the cpm + dataToSend.currency = bid.currency; + dataToSend.originalCpm = bid.originalCpm; + dataToSend.originalCurrency = bid.originalCurrency; + dataToSend.status = bid.status; + dataToSend.prebidAuctionId = element.args?.auctionId; + }; + if (dataToSend.bidderCode) { + const relevantBid = element.args?.bidsReceived.find( + (item) => + item.bidder === dataToSend.bidderCode && + item.adUnitCode === dataToSend.placementId + ); + if (relevantBid) { + defineRelevantData(relevantBid); + break; + } + } else { + let highestBid = 0; + element.args?.bidsReceived.forEach((bid) => { + if (bid.adUnitCode === dataToSend.placementId && bid.cpm > highestBid) { + highestBid = bid.cpm; + defineRelevantData(bid); + } + }); + break; + } + } + } + } + } + return dataToSend; + }; + gamObjectReference.cmd.push(() => { + gamObjectReference.pubads().addEventListener('slotRenderEnded', (event) => { + if (event.isEmpty) return; + const data = extractWinData(event); + if (data?.cpm) { + sendData(data); + } + }); + }); + } catch (error) { + this.logger.error('Failed to subscribe to GAM: ' + error); + } +}; diff --git a/libraries/intentIqUtils/intentIqConfig.js b/libraries/intentIqUtils/intentIqConfig.js index 85c9111970b..3f2572f14fa 100644 --- a/libraries/intentIqUtils/intentIqConfig.js +++ b/libraries/intentIqUtils/intentIqConfig.js @@ -1,3 +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' +export const reportingServerAddress = (reportEndpoint, gdprDetected) => reportEndpoint && typeof reportEndpoint === 'string' ? reportEndpoint : gdprDetected ? 'https://reports-gdpr.intentiq.com/report' : 'https://reports.intentiq.com/report' diff --git a/modules/intentIqAnalyticsAdapter.js b/modules/intentIqAnalyticsAdapter.js index f1322d8a982..06c9bcb28b4 100644 --- a/modules/intentIqAnalyticsAdapter.js +++ b/modules/intentIqAnalyticsAdapter.js @@ -1,26 +1,39 @@ -import {logError, logInfo} from '../src/utils.js'; +import { isPlainObject, logError, logInfo } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; -import {ajax} from '../src/ajax.js'; -import {getStorageManager} from '../src/storageManager.js'; -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, PREBID} from '../libraries/intentIqConstants/intentIqConstants.js'; -import {readData, defineStorageType} from '../libraries/intentIqUtils/storageUtils.js'; -import {reportingServerAddress} from '../libraries/intentIqUtils/intentIqConfig.js'; +import { ajax } from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; +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, + 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'; +import { gamPredictionReport } from '../libraries/intentIqUtils/gamPredictionReport.js'; -const MODULE_NAME = 'iiqAnalytics' +const MODULE_NAME = 'iiqAnalytics'; const analyticsType = 'endpoint'; -const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}); +const storage = getStorageManager({ + moduleType: MODULE_TYPE_ANALYTICS, + moduleName: MODULE_NAME +}); const prebidVersion = '$prebid.version$'; export const REPORTER_ID = Date.now() + '_' + getRandom(0, 1000); const allowedStorage = defineStorageType(config.enabledStorageTypes); +let globalName; +let alreadySubscribedOnGAM = false; +let reportList = {}; +let cleanReportsID; const PARAMS_NAMES = { abTestGroup: 'abGroup', @@ -61,20 +74,19 @@ const PARAMS_NAMES = { }; function getIntentIqConfig() { - return config.getConfig('userSync.userIds')?.find(m => m.name === 'intentIqId'); + return config.getConfig('userSync.userIds')?.find((m) => m.name === 'intentIqId'); } -const DEFAULT_URL = 'https://reports.intentiq.com/report' +const DEFAULT_URL = 'https://reports.intentiq.com/report'; const getDataForDefineURL = () => { - const iiqConfig = getIntentIqConfig() const cmpData = getCmpData(); const gdprDetected = cmpData.gdprString; - return [iiqConfig, gdprDetected] -} + return [iiqAnalyticsAnalyticsAdapter.initOptions.reportingServerAddress, gdprDetected]; +}; -const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({url: DEFAULT_URL, analyticsType}), { +const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({ url: DEFAULT_URL, analyticsType }), { initOptions: { lsValueInitialized: false, partner: null, @@ -87,15 +99,22 @@ const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({url: DEFAULT_URL, an domainName: null, siloEnabled: false, reportMethod: null, - additionalParams: null + additionalParams: null, + reportingServerAddress: '' }, - track({eventType, args}) { + track({ eventType, args }) { switch (eventType) { case BID_WON: bidWon(args); break; case BID_REQUESTED: + checkAndInitConfig(); defineGlobalVariableName(); + if (!alreadySubscribedOnGAM && shouldSubscribeOnGAM()) { + alreadySubscribedOnGAM = true; + const iiqConfig = getIntentIqConfig(); + gamPredictionReport(iiqConfig?.params?.gamObjectReference, bidWon); + } break; default: break; @@ -104,29 +123,34 @@ const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({url: DEFAULT_URL, an }); // Events needed -const { - BID_WON, - BID_REQUESTED -} = EVENTS; +const { BID_WON, BID_REQUESTED } = EVENTS; -function initAdapterConfig() { +function initAdapterConfig(config) { if (iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized) return; - const iiqConfig = getIntentIqConfig() + const iiqIdSystemConfig = getIntentIqConfig(); - if (iiqConfig) { + if (iiqIdSystemConfig) { + const { manualWinReportEnabled, gamPredictReporting, reportMethod, reportingServerAddress: reportEndpoint, adUnitConfig } = config?.options || {} iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized = true; iiqAnalyticsAnalyticsAdapter.initOptions.partner = - iiqConfig.params?.partner && !isNaN(iiqConfig.params.partner) ? iiqConfig.params.partner : -1; + iiqIdSystemConfig.params?.partner && !isNaN(iiqIdSystemConfig.params.partner) ? iiqIdSystemConfig.params.partner : -1; iiqAnalyticsAnalyticsAdapter.initOptions.browserBlackList = - typeof iiqConfig.params?.browserBlackList === 'string' ? iiqConfig.params.browserBlackList.toLowerCase() : ''; - iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = iiqConfig.params?.manualWinReportEnabled || false; - iiqAnalyticsAnalyticsAdapter.initOptions.domainName = iiqConfig.params?.domainName || ''; + typeof iiqIdSystemConfig.params?.browserBlackList === 'string' + ? iiqIdSystemConfig.params.browserBlackList.toLowerCase() + : ''; + iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = + manualWinReportEnabled || false; + iiqAnalyticsAnalyticsAdapter.initOptions.domainName = iiqIdSystemConfig.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; + typeof iiqIdSystemConfig.params?.siloEnabled === 'boolean' ? iiqIdSystemConfig.params.siloEnabled : false; + iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod = parseReportingMethod(reportMethod); + iiqAnalyticsAnalyticsAdapter.initOptions.additionalParams = iiqIdSystemConfig.params?.additionalParams || null; + iiqAnalyticsAnalyticsAdapter.initOptions.gamPredictReporting = typeof gamPredictReporting === 'boolean' ? gamPredictReporting : false; + iiqAnalyticsAnalyticsAdapter.initOptions.reportingServerAddress = typeof reportEndpoint === 'string' ? reportEndpoint : ''; + iiqAnalyticsAnalyticsAdapter.initOptions.adUnitConfig = typeof adUnitConfig === 'number' ? adUnitConfig : 1; } else { + logError('IIQ ANALYTICS -> there is no initialized intentIqIdSystem module') iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized = false; iiqAnalyticsAnalyticsAdapter.initOptions.partner = -1; iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod = 'GET'; @@ -136,20 +160,31 @@ function initAdapterConfig() { function initReadLsIds() { try { iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs = null; - iiqAnalyticsAnalyticsAdapter.initOptions.fpid = JSON.parse(readData( - `${FIRST_PARTY_KEY}${iiqAnalyticsAnalyticsAdapter.initOptions.siloEnabled ? '_p_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner : ''}`, - 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; } - const partnerData = readData(FIRST_PARTY_KEY + '_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner, allowedStorage, storage); + const partnerData = readData( + FIRST_PARTY_KEY + '_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner, + allowedStorage, + storage + ); const clientsHints = readData(CLIENT_HINTS_KEY, allowedStorage, storage) || ''; if (partnerData) { iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized = true; const pData = JSON.parse(partnerData); - iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause = pData.terminationCause + iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause = pData.terminationCause; iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs = pData.data; iiqAnalyticsAnalyticsAdapter.initOptions.eidl = pData.eidl || -1; iiqAnalyticsAnalyticsAdapter.initOptions.clientType = pData.clientType || null; @@ -158,34 +193,79 @@ function initReadLsIds() { iiqAnalyticsAnalyticsAdapter.initOptions.rrtt = pData.rrtt || null; } - iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints = clientsHints + iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints = clientsHints; } catch (e) { - logError(e) + logError(e); } } -function bidWon(args, isReportExternal) { +function shouldSubscribeOnGAM() { + const iiqConfig = getIntentIqConfig(); + if (!iiqConfig?.params?.gamObjectReference || !isPlainObject(iiqConfig.params.gamObjectReference)) return false; + const partnerDataFromLS = readData( + FIRST_PARTY_KEY + '_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner, + allowedStorage, + storage + ); + + if (partnerDataFromLS) { + const partnerData = JSON.parse(partnerDataFromLS); + return partnerData.gpr || (!('gpr' in partnerData) && iiqAnalyticsAnalyticsAdapter.initOptions.gamPredictReporting); + } + return false; +} + +function shouldSendReport(isReportExternal) { + return ( + (isReportExternal && + iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled && + !shouldSubscribeOnGAM()) || + (!isReportExternal && !iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled) + ); +} + +export function restoreReportList() { + reportList = {}; +} + +function checkAndInitConfig() { if (!iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized) { initAdapterConfig(); } +} - if (isNaN(iiqAnalyticsAnalyticsAdapter.initOptions.partner) || iiqAnalyticsAnalyticsAdapter.initOptions.partner === -1) return; +function bidWon(args, isReportExternal) { + checkAndInitConfig(); + if ( + isNaN(iiqAnalyticsAnalyticsAdapter.initOptions.partner) || + iiqAnalyticsAnalyticsAdapter.initOptions.partner === -1 + ) { + return; + } const currentBrowserLowerCase = detectBrowser(); if (iiqAnalyticsAnalyticsAdapter.initOptions.browserBlackList?.includes(currentBrowserLowerCase)) { logError('IIQ ANALYTICS -> Browser is in blacklist!'); return; } - if (iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized && !iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized) { + if ( + iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized && + !iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized + ) { initReadLsIds(); } - if ((isReportExternal && iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled) || (!isReportExternal && !iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled)) { - const { url, method, payload } = constructFullUrl(preparePayload(args, true)); + if (shouldSendReport(isReportExternal)) { + const preparedPayload = preparePayload(args, true); + if (!preparedPayload) return false; + const { url, method, payload } = constructFullUrl(preparedPayload); if (method === 'POST') { - ajax(url, undefined, payload, {method, contentType: 'application/x-www-form-urlencoded'}); + ajax(url, undefined, payload, { + method, + contentType: 'application/x-www-form-urlencoded' + }); } else { - ajax(url, undefined, null, {method}); + ajax(url, undefined, null, { method }); } logInfo('IIQ ANALYTICS -> BID WON'); return true; @@ -214,17 +294,18 @@ function defineGlobalVariableName() { const iiqConfig = getIntentIqConfig(); const partnerId = iiqConfig?.params?.partner || 0; + globalName = `intentIqAnalyticsAdapter_${partnerId}`; - window[`intentIqAnalyticsAdapter_${partnerId}`] = { reportExternalWin }; + window[globalName] = { reportExternalWin }; } function getRandom(start, end) { - return Math.floor((Math.random() * (end - start + 1)) + start); + return Math.floor(Math.random() * (end - start + 1) + start); } export function preparePayload(data) { const result = getDefaultDataObject(); - readData(FIRST_PARTY_KEY + '_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner, allowedStorage, storage); + result[PARAMS_NAMES.partnerId] = iiqAnalyticsAnalyticsAdapter.initOptions.partner; result[PARAMS_NAMES.prebidVersion] = prebidVersion; result[PARAMS_NAMES.referrer] = getReferrer(); @@ -238,11 +319,27 @@ export function preparePayload(data) { result[PARAMS_NAMES.isInTestGroup] = iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup === 'A'; result[PARAMS_NAMES.agentId] = REPORTER_ID; - if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid?.pcid) result[PARAMS_NAMES.firstPartyId] = encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pcid); - if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid?.pid) result[PARAMS_NAMES.profile] = encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pid) - + if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid?.pcid) { + result[PARAMS_NAMES.firstPartyId] = encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pcid); + } + if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid?.pid) { + result[PARAMS_NAMES.profile] = encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pid); + } prepareData(data, result); + if (!reportList[result.placementId] || !reportList[result.placementId][result.prebidAuctionId]) { + reportList[result.placementId] = reportList[result.placementId] + ? { ...reportList[result.placementId], [result.prebidAuctionId]: 1 } + : { [result.prebidAuctionId]: 1 }; + cleanReportsID = setTimeout(() => { + if (cleanReportsID) clearTimeout(cleanReportsID); + restoreReportList(); + }, 1500); // clear object in 1.5 second after defining reporting list + } else { + logError('Duplication detected, report will be not sent'); + return; + } + fillEidsData(result); return result; @@ -250,12 +347,13 @@ export function preparePayload(data) { function fillEidsData(result) { if (iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized) { - result[PARAMS_NAMES.hadEidsInLocalStorage] = iiqAnalyticsAnalyticsAdapter.initOptions.eidl && iiqAnalyticsAnalyticsAdapter.initOptions.eidl > 0; + result[PARAMS_NAMES.hadEidsInLocalStorage] = + iiqAnalyticsAnalyticsAdapter.initOptions.eidl && iiqAnalyticsAnalyticsAdapter.initOptions.eidl > 0; result[PARAMS_NAMES.auctionEidsLength] = iiqAnalyticsAnalyticsAdapter.initOptions.eidl || -1; } } -function prepareData (data, result) { +function prepareData(data, result) { const adTypeValue = data.adType || data.mediaType; if (data.bidderCode) { @@ -276,16 +374,14 @@ function prepareData (data, result) { if (data.status) { result.status = data.status; } - if (data.auctionId) { - result.prebidAuctionId = data.auctionId; - } + + result.prebidAuctionId = data.auctionId || data.prebidAuctionId; + if (adTypeValue) { result[PARAMS_NAMES.adType] = adTypeValue; } - const iiqConfig = getIntentIqConfig(); - const adUnitConfig = iiqConfig.params?.adUnitConfig; - switch (adUnitConfig) { + switch (iiqAnalyticsAnalyticsAdapter.initOptions.adUnitConfig) { case 1: // adUnitCode or placementId result.placementId = data.adUnitCode || extractPlacementId(data) || ''; @@ -307,7 +403,7 @@ function prepareData (data, result) { result.placementId = data.adUnitCode || extractPlacementId(data) || ''; } - result.biddingPlatformId = 1; + result.biddingPlatformId = data.biddingPlatformId || 1; result.partnerAuctionId = 'BW'; } @@ -327,18 +423,18 @@ function extractPlacementId(data) { function getDefaultDataObject() { return { - 'inbbl': false, - 'pbjsver': prebidVersion, - 'partnerAuctionId': 'BW', - 'reportSource': 'pbjs', - 'abGroup': 'U', - 'jsversion': VERSION, - 'partnerId': -1, - 'biddingPlatformId': 1, - 'idls': false, - 'ast': -1, - 'aeidln': -1 - } + inbbl: false, + pbjsver: prebidVersion, + partnerAuctionId: 'BW', + reportSource: 'pbjs', + abGroup: 'U', + jsversion: VERSION, + partnerId: -1, + biddingPlatformId: 1, + idls: false, + ast: -1, + aeidln: -1 + }; } function constructFullUrl(data) { @@ -351,27 +447,38 @@ function constructFullUrl(data) { const cmpData = getCmpData(); const baseUrl = reportingServerAddress(...getDataForDefineURL()); - let url = baseUrl + '?pid=' + iiqAnalyticsAnalyticsAdapter.initOptions.partner + - '&mct=1' + - ((iiqAnalyticsAnalyticsAdapter.initOptions?.fpid) - ? '&iiqid=' + encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pcid) : '') + - '&agid=' + REPORTER_ID + - '&jsver=' + VERSION + - '&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) + let url = + baseUrl + + '?pid=' + + iiqAnalyticsAnalyticsAdapter.initOptions.partner + + '&mct=1' + + (iiqAnalyticsAnalyticsAdapter.initOptions?.fpid + ? '&iiqid=' + encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pcid) + : '') + + '&agid=' + + REPORTER_ID + + '&jsver=' + + VERSION + + '&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); 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); + url = handleAdditionalParams( + currentBrowserLowerCase, + url, + 2, + iiqAnalyticsAnalyticsAdapter.initOptions.additionalParams + ); return { url, method: 'GET' }; } @@ -379,6 +486,7 @@ iiqAnalyticsAnalyticsAdapter.originEnableAnalytics = iiqAnalyticsAnalyticsAdapte iiqAnalyticsAnalyticsAdapter.enableAnalytics = function (myConfig) { iiqAnalyticsAnalyticsAdapter.originEnableAnalytics(myConfig); // call the base class function + initAdapterConfig(myConfig) }; adapterManager.registerAnalyticsAdapter({ adapter: iiqAnalyticsAnalyticsAdapter, diff --git a/modules/intentIqAnalyticsAdapter.md b/modules/intentIqAnalyticsAdapter.md index 2f601658a3d..9389cb7d8ee 100644 --- a/modules/intentIqAnalyticsAdapter.md +++ b/modules/intentIqAnalyticsAdapter.md @@ -4,45 +4,61 @@ Module Name: iiqAnalytics Module Type: Analytics Adapter Maintainer: julian@intentiq.com -# Description +### Description By using this Intent IQ adapter, you will be able to obtain comprehensive analytics and metrics regarding the performance of the Intent IQ Unified ID module. This includes how the module impacts your revenue, CPMs, and fill rates related to bidders and domains. -## Intent IQ Universal ID Registration +#### Intent IQ Universal ID Registration No registration for this module is required. -## Intent IQ Universal IDConfiguration +#### Intent IQ Universal ID Configuration -IMPORTANT: only effective when Intent IQ Universal ID module is installed and configured. [(How-To)](https://docs.prebid.org/dev-docs/modules/userid-submodules/intentiq.html) +**IMPORTANT**: only effective when Intent IQ Universal ID module be installed and configured. [(How-To)](https://docs.prebid.org/dev-docs/modules/userid-submodules/intentiq.html) + +### Analytics Options + +{: .table .table-bordered .table-striped } +| Parameter | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| options.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. | `false` | +| options.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"` | +| options.reportingServerAddress | Optional | String | The base URL for the IntentIQ reporting server. If parameter is provided in `configParams`, it will be used. | `"https://domain.com"` | +| options.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` | +| options.gamPredictReporting | Optional | Boolean | This variable controls whether the GAM prediction logic is enabled or disabled. The main purpose of this logic is to extract information from a rendered GAM slot when no Prebid bidWon event is available. In that case, we take the highest CPM from the current auction and add 0.01 to that value. | `false` | #### Example Configuration ```js pbjs.enableAnalytics({ - provider: 'iiqAnalytics' + provider: 'iiqAnalytics', + options: { + manualWinReportEnabled: false, + reportMethod: "GET", + adUnitConfig: 1, + gamPredictReporting: false + } }); ``` - ### Manual Report Trigger with reportExternalWin The reportExternalWin function allows for manual reporting, meaning that reports will not be sent automatically but only when triggered manually. To enable this manual reporting functionality, you must set the manualWinReportEnabled parameter in Intent IQ Unified ID module configuration is true. Once enabled, reports can be manually triggered using the reportExternalWin function. - ### Calling the reportExternalWin Function To call the reportExternalWin function, you need to pass the partner_id parameter as shown in the example below: ```js -window.intentIqAnalyticsAdapter_[partner_id].reportExternalWin() +window.intentIqAnalyticsAdapter_[partner_id].reportExternalWin(reportData) ``` + Example use with Partner ID = 123455 ```js -window.intentIqAnalyticsAdapter_123455.reportExternalWin() +window.intentIqAnalyticsAdapter_123455.reportExternalWin(reportData) ``` ### Function Parameters @@ -60,11 +76,12 @@ currency: 'USD', // Currency for the CPM value. originalCpm: 1.5, // Original CPM value. originalCurrency: 'USD', // Original currency. status: 'rendered', // Auction status, e.g., 'rendered'. -placementId: 'div-1', // ID of the ad placement. +placementId: 'div-1' // ID of the ad placement. adType: 'banner' // Specifies the type of ad served } ``` +{: .table .table-bordered .table-striped } | Field | Data Type | Description | Example | Mandatory | |--------------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|-----------| | biddingPlatformId | Integer | Specify the platform in which this ad impression was rendered – 1 – Prebid, 2 – Amazon, 3 – Google, 4 – Open RTB (including your local Prebid server) | 1 | Yes | @@ -79,7 +96,6 @@ adType: 'banner' // Specifies the type of ad served | placementId | String | Unique identifier of the ad unit on the webpage that showed this ad | div-1 | No | | adType | String | Specifies the type of ad served. Possible values: “banner“, “video“, “native“, “audio“. | banner | No | - To report the auction win, call the function as follows: ```js diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index df26b3ce2b3..53755afa050 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -5,7 +5,7 @@ * @requires module:modules/userId */ -import {logError, isPlainObject, isStr, isNumber, getWinDimensions} from '../src/utils.js'; +import {logError, isPlainObject, isStr, isNumber} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js' import {detectBrowser} from '../libraries/intentIqUtils/detectBrowserUtils.js'; @@ -21,7 +21,7 @@ import { CLIENT_HINTS_KEY, EMPTY, GVLID, - VERSION, INVALID_ID, SCREEN_PARAMS, SYNC_REFRESH_MILL, META_DATA_CONSTANT, PREBID, + VERSION, INVALID_ID, SYNC_REFRESH_MILL, META_DATA_CONSTANT, PREBID, HOURS_24, CH_KEYS } from '../libraries/intentIqConstants/intentIqConstants.js'; import {SYNC_KEY} from '../libraries/intentIqUtils/getSyncKey.js'; @@ -73,36 +73,11 @@ function generateGUID() { return guid; } -function collectDeviceInfo() { - const windowDimensions = getWinDimensions(); - return { - windowInnerHeight: windowDimensions.innerHeight, - windowInnerWidth: windowDimensions.innerWidth, - devicePixelRatio: windowDimensions.devicePixelRatio, - windowScreenHeight: windowDimensions.screen.height, - windowScreenWidth: windowDimensions.screen.width, - language: navigator.language - } -} - function addUniquenessToUrl(url) { url += '&tsrnd=' + Math.floor(Math.random() * 1000) + '_' + new Date().getTime(); return url; } -function appendDeviceInfoToUrl(url, deviceInfo) { - const screenParamsString = Object.entries(SCREEN_PARAMS) - .map(([index, param]) => { - const value = (deviceInfo)[param]; - return `${index}:${value}`; - }) - .join(','); - - url += `&cz=${encodeURIComponent(screenParamsString)}`; - url += `&dw=${deviceInfo.windowScreenWidth}&dh=${deviceInfo.windowScreenHeight}&dpr=${deviceInfo.devicePixelRatio}&lan=${deviceInfo.language}`; - return url; -} - function appendFirstPartyData (url, firstPartyData, partnerData) { url += firstPartyData.pid ? '&pid=' + encodeURIComponent(firstPartyData.pid) : ''; url += firstPartyData.pcid ? '&iiqidtype=2&iiqpcid=' + encodeURIComponent(firstPartyData.pcid) : ''; @@ -169,7 +144,6 @@ function addMetaData(url, data) { } export function createPixelUrl(firstPartyData, clientHints, configParams, partnerData, cmpData) { - const deviceInfo = collectDeviceInfo(); const browser = detectBrowser(); let url = iiqPixelServerAddress(configParams, cmpData.gdprString); @@ -179,7 +153,6 @@ export function createPixelUrl(firstPartyData, clientHints, configParams, partne url = appendPartnersFirstParty(url, configParams); url = addUniquenessToUrl(url); url += partnerData?.clientType ? '&idtype=' + partnerData.clientType : ''; - if (deviceInfo) url = appendDeviceInfoToUrl(url, deviceInfo); url += VERSION ? '&jsver=' + VERSION : ''; if (clientHints) url += '&uh=' + encodeURIComponent(clientHints); url = appendVrrefAndFui(url, configParams.domainName); @@ -219,7 +192,8 @@ function sendSyncRequest(allowedStorage, url, partner, firstPartyData, newUser) * @param {string} gamParameterName - The name of the GAM targeting parameter where the group value will be stored. * @param {string} userGroup - The A/B testing group assigned to the user (e.g., 'A', 'B', or a custom value). */ -export function setGamReporting(gamObjectReference, gamParameterName, userGroup) { +export function setGamReporting(gamObjectReference, gamParameterName, userGroup, isBlacklisted = false) { + if (isBlacklisted) return; if (isPlainObject(gamObjectReference) && gamObjectReference.cmd) { gamObjectReference.cmd.push(() => { gamObjectReference @@ -350,7 +324,12 @@ export const intentIqIdSubmodule = { const gdprDetected = cmpData.gdprString; firstPartyData = tryParse(readData(FIRST_PARTY_KEY_FINAL, allowedStorage)); const isGroupB = firstPartyData?.group === WITHOUT_IIQ; - setGamReporting(gamObjectReference, gamParameterName, firstPartyData?.group); + const currentBrowserLowerCase = detectBrowser(); + const browserBlackList = typeof configParams.browserBlackList === 'string' ? configParams.browserBlackList.toLowerCase() : ''; + const isBlacklisted = browserBlackList?.includes(currentBrowserLowerCase); + let newUser = false; + + setGamReporting(gamObjectReference, gamParameterName, firstPartyData?.group, isBlacklisted); if (groupChanged) groupChanged(firstPartyData?.group || NOT_YET_DEFINED); @@ -359,10 +338,6 @@ export const intentIqIdSubmodule = { }, configParams.timeoutInMillis || 500 ); - const currentBrowserLowerCase = detectBrowser(); - const browserBlackList = typeof configParams.browserBlackList === 'string' ? configParams.browserBlackList.toLowerCase() : ''; - let newUser = false; - if (!firstPartyData?.pcid) { const firstPartyId = generateGUID(); firstPartyData = { @@ -478,7 +453,7 @@ export const intentIqIdSubmodule = { } // Check if current browser is in blacklist - if (browserBlackList?.includes(currentBrowserLowerCase)) { + if (isBlacklisted) { logError('User ID - intentIqId submodule: browser is in blacklist! Data will be not provided.'); if (configParams.callback) configParams.callback(''); @@ -617,6 +592,12 @@ export const intentIqIdSubmodule = { // server provided data firstPartyData.spd = respJson.spd; } + if ('gpr' in respJson) { + // GAM prediction reporting + partnerData.gpr = respJson.gpr; + } else { + delete partnerData.gpr // remove prediction flag in case server doesn't provide it + } if (rrttStrtTime && rrttStrtTime > 0) { partnerData.rrtt = Date.now() - rrttStrtTime; diff --git a/modules/intentIqIdSystem.md b/modules/intentIqIdSystem.md index 42acf8a0600..bf561649566 100644 --- a/modules/intentIqIdSystem.md +++ b/modules/intentIqIdSystem.md @@ -15,7 +15,7 @@ By leveraging the Intent IQ identity graph, our module helps publishers, SSPs, a ## Registration 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 +check our [documentation](https://pbmodule.documents.intentiq.com/) to get more information about our solution and how to utilze it's full potential ## Integration @@ -43,24 +43,20 @@ Please find below list of parameters that could be used in configuring Intent IQ | 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` | | 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.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.chTimeout | Optional | Number | Maximum time (in milliseconds) to wait for Client Hints from the browser before sending request. Default value is `10ms` | `30` | -| 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]` | +| params.additionalParams | Optional | Array | This parameter allows sending additional custom key-value parameters with specific destination logic (sync, VR, winreport). Each custom parameter is defined as an object in the array. | `[ { parameterName: “abc”, parameterValue: 123, destination: [1,1,0] } ]` | +| params.additionalParams [0].parameterName | Required | String | Name of the custom parameter. This will be sent as a query parameter. | `"abc"` | +| params.additionalParams [0].parameterValue | Required | String / Number | Value to assign to the parameter. | `123` | +| params.additionalParams [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 @@ -75,15 +71,13 @@ pbjs.setConfig({ browserBlackList: "chrome", callback: (data) => {...}, // your logic here groupChanged: (group) => console.log('Group is', group), - 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) sourceMetaData: "123.123.123.123", // Optional parameter sourceMetaDataExternal: 123456, // Optional parameter - reportMethod: "GET", // Optional parameter - additionalParameters: [ // Optional parameter + chTimeout: 10, // Optional parameter + additionalParams: [ // Optional parameter { parameterName: "abc", parameterValue: 123, diff --git a/test/spec/modules/intentIqAnalyticsAdapter_spec.js b/test/spec/modules/intentIqAnalyticsAdapter_spec.js index 76ecabf3460..664c6041ec8 100644 --- a/test/spec/modules/intentIqAnalyticsAdapter_spec.js +++ b/test/spec/modules/intentIqAnalyticsAdapter_spec.js @@ -7,10 +7,10 @@ import { EVENTS } from 'src/constants.js'; import * as events from 'src/events.js'; import { getStorageManager } from 'src/storageManager.js'; import sinon from 'sinon'; -import { REPORTER_ID, preparePayload } from '../../../modules/intentIqAnalyticsAdapter.js'; -import {FIRST_PARTY_KEY, PREBID, VERSION} from '../../../libraries/intentIqConstants/intentIqConstants.js'; +import { REPORTER_ID, preparePayload, restoreReportList } from '../../../modules/intentIqAnalyticsAdapter.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 { getReferrer, appendVrrefAndFui } from '../../../libraries/intentIqUtils/getRefferer.js'; import { gppDataHandler, uspDataHandler, gdprDataHandler } from '../../../src/consentHandler.js'; const partner = 10; @@ -22,6 +22,8 @@ const REPORT_SERVER_ADDRESS = 'https://test-reports.intentiq.com/report'; const storage = getStorageManager({ moduleType: 'analytics', moduleName: 'iiqAnalytics' }); +const randomVal = () => Math.floor(Math.random() * 100000) + 1 + const getUserConfig = () => [ { 'name': 'intentIqId', @@ -45,8 +47,6 @@ const getUserConfigWithReportingServerAddress = () => [ 'params': { 'partner': partner, 'unpack': null, - 'manualWinReportEnabled': false, - 'reportingServerAddress': REPORT_SERVER_ADDRESS }, 'storage': { 'type': 'html5', @@ -57,7 +57,7 @@ const getUserConfigWithReportingServerAddress = () => [ } ]; -const wonRequest = { +const getWonRequest = () => ({ 'bidderCode': 'pubmatic', 'width': 728, 'height': 90, @@ -65,7 +65,7 @@ const wonRequest = { 'adId': '23caeb34c55da51', 'requestId': '87615b45ca4973', 'transactionId': '5e69fd76-8c86-496a-85ce-41ae55787a50', - 'auctionId': '0cbd3a43-ff45-47b8-b002-16d3946b23bf', + 'auctionId': '0cbd3a43-ff45-47b8-b002-16d3946b23bf-' + randomVal(), 'mediaType': 'banner', 'source': 'client', 'cpm': 5, @@ -87,7 +87,15 @@ const wonRequest = { 'pbCg': '', 'size': '728x90', 'status': 'rendered' -}; +}); + +const enableAnalyticWithSpecialOptions = (options) => { + iiqAnalyticsAnalyticsAdapter.disableAnalytics() + iiqAnalyticsAnalyticsAdapter.enableAnalytics({ + provider: 'iiqAnalytics', + options + }) +} describe('IntentIQ tests all', function () { let logErrorStub; @@ -138,8 +146,11 @@ describe('IntentIQ tests all', function () { }); it('should send POST request with payload in request body if reportMethod is POST', function () { + enableAnalyticWithSpecialOptions({ + reportMethod: 'POST' + }) const [userConfig] = getUserConfig(); - userConfig.params.reportMethod = 'POST'; + const wonRequest = getWonRequest(); config.getConfig.restore(); sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); @@ -149,6 +160,7 @@ describe('IntentIQ tests all', function () { events.emit(EVENTS.BID_WON, wonRequest); const request = server.requests[0]; + restoreReportList(); const expectedData = preparePayload(wonRequest); const expectedPayload = `["${btoa(JSON.stringify(expectedData))}"]`; @@ -159,6 +171,7 @@ describe('IntentIQ tests all', function () { it('should send GET request with payload in query string if reportMethod is NOT provided', function () { const [userConfig] = getUserConfig(); + const wonRequest = getWonRequest(); config.getConfig.restore(); sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); @@ -173,6 +186,7 @@ describe('IntentIQ tests all', function () { const payloadEncoded = url.searchParams.get('payload'); const decoded = JSON.parse(atob(JSON.parse(payloadEncoded)[0])); + restoreReportList(); const expected = preparePayload(wonRequest); expect(decoded.partnerId).to.equal(expected.partnerId); @@ -182,9 +196,9 @@ describe('IntentIQ tests all', function () { it('IIQ Analytical Adapter bid win report', function () { localStorage.setItem(FIRST_PARTY_KEY, defaultData); - getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({href: 'http://localhost:9876'}); + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876' }); const expectedVrref = getWindowLocationStub().href; - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -200,7 +214,7 @@ describe('IntentIQ tests all', function () { it('should include adType in payload when present in BID_WON event', function () { localStorage.setItem(FIRST_PARTY_KEY, defaultData); getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); - const bidWonEvent = { ...wonRequest, mediaType: 'video' }; + const bidWonEvent = { ...getWonRequest(), mediaType: 'video' }; events.emit(EVENTS.BID_WON, bidWonEvent); @@ -214,10 +228,10 @@ describe('IntentIQ tests all', function () { }); it('should include adType in payload when present in reportExternalWin event', function () { + enableAnalyticWithSpecialOptions({ manualWinReportEnabled: true }) getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); const externalWinEvent = { cpm: 1, currency: 'USD', adType: 'banner' }; const [userConfig] = getUserConfig(); - userConfig.params.manualWinReportEnabled = true; config.getConfig.restore(); sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); @@ -241,7 +255,7 @@ describe('IntentIQ tests all', function () { const uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns('1NYN'); const gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns({ consentString: 'gdprConsent' }); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -260,7 +274,7 @@ describe('IntentIQ tests all', function () { localStorage.setItem(FIRST_PARTY_KEY, '{"pcid":"testpcid", "group": "B"}'); const expectedVrref = encodeURIComponent('http://localhost:9876/'); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -273,11 +287,13 @@ describe('IntentIQ tests all', function () { it('should handle BID_WON event with default group configuration', function () { localStorage.setItem(FIRST_PARTY_KEY, defaultData); const defaultDataObj = JSON.parse(defaultData) + const wonRequest = getWonRequest(); events.emit(EVENTS.BID_WON, wonRequest); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; + restoreReportList() const dataToSend = preparePayload(wonRequest); const base64String = btoa(JSON.stringify(dataToSend)); const payload = encodeURIComponent(JSON.stringify([base64String])); @@ -301,7 +317,7 @@ describe('IntentIQ tests all', function () { getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -317,14 +333,14 @@ describe('IntentIQ tests all', function () { it('should not send request if manualWinReportEnabled is true', function () { iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = true; - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.equal(1); }); it('should read data from local storage', function () { localStorage.setItem(FIRST_PARTY_KEY, '{"group": "A"}'); localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, '{"data":"testpcid", "eidl": 10}'); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs).to.equal('testpcid'); expect(iiqAnalyticsAnalyticsAdapter.initOptions.eidl).to.equal(10); expect(iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup).to.equal('A'); @@ -333,18 +349,18 @@ describe('IntentIQ tests all', function () { it('should handle initialization values from local storage', function () { localStorage.setItem(FIRST_PARTY_KEY, '{"pcid":"testpcid", "group": "B"}'); localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, '{"data":"testpcid"}'); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup).to.equal('B'); expect(iiqAnalyticsAnalyticsAdapter.initOptions.fpid).to.be.not.null; }); it('should handle reportExternalWin', function () { events.emit(EVENTS.BID_REQUESTED); - iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = true; + iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = false; localStorage.setItem(FIRST_PARTY_KEY, '{"pcid":"testpcid", "group": "B"}'); localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, '{"data":"testpcid"}'); expect(window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin).to.be.a('function'); - expect(window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin({cpm: 1, currency: 'USD'})).to.equal(false); + expect(window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin({ cpm: 1, currency: 'USD' })).to.equal(false); }); it('should return window.location.href when window.self === window.top', function () { @@ -387,7 +403,7 @@ describe('IntentIQ tests all', function () { detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('chrome'); localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.equal(0); }); @@ -401,7 +417,7 @@ describe('IntentIQ tests all', function () { detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('safari'); localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -419,9 +435,10 @@ describe('IntentIQ tests all', function () { config.getConfig.restore(); sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG_BROWSER); detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('safari'); + enableAnalyticWithSpecialOptions({ reportingServerAddress: REPORT_SERVER_ADDRESS }) localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -431,7 +448,7 @@ describe('IntentIQ tests all', function () { it('should include source parameter in report URL', function () { localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify(defaultData)); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; expect(server.requests.length).to.be.above(0); @@ -447,7 +464,7 @@ describe('IntentIQ tests all', function () { 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); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -466,7 +483,7 @@ describe('IntentIQ tests all', function () { sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(userConfig); localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; expect(request.url).to.include('general=Lee'); @@ -485,7 +502,7 @@ describe('IntentIQ tests all', function () { sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(userConfig); localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; expect(request.url).not.to.include('general'); @@ -494,10 +511,10 @@ describe('IntentIQ tests all', function () { const spdObject = { foo: 'bar', value: 42 }; const expectedSpdEncoded = encodeURIComponent(JSON.stringify(spdObject)); - localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({...defaultData, spd: 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); + events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; @@ -509,10 +526,10 @@ describe('IntentIQ tests all', function () { const spdObject = 'server provided data'; const expectedSpdEncoded = encodeURIComponent(spdObject); - localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({...defaultData, spd: 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); + events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; @@ -520,6 +537,99 @@ describe('IntentIQ tests all', function () { expect(request.url).to.include(`&spd=${expectedSpdEncoded}`); }); + describe('GAM prediction reporting', function () { + function createMockGAM() { + const listeners = {}; + return { + cmd: [], + pubads: () => ({ + addEventListener: (name, cb) => { + listeners[name] = cb; + } + }), + _listeners: listeners + }; + } + + function withConfigGamPredict(gamObj) { + const [userConfig] = getUserConfig(); + userConfig.params.gamObjectReference = gamObj; + userConfig.params.gamPredictReporting = true; + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); + } + + it('should subscribe to GAM and send report on slotRenderEnded without prior bidWon', function () { + const gam = createMockGAM(); + withConfigGamPredict(gam); + + // enable subscription by LS flag + localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, JSON.stringify({ gpr: true })); + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + + // provide recent auctionEnd with matching bid to enrich payload + events.getEvents.restore(); + sinon.stub(events, 'getEvents').returns([ + { + eventType: 'auctionEnd', args: { + auctionId: 'auc-1', + adUnitCodes: ['ad-unit-1'], + bidsReceived: [{ bidder: 'pubmatic', adUnitCode: 'ad-unit-1', cpm: 1, currency: 'USD', originalCpm: 1, originalCurrency: 'USD', status: 'rendered' }] + } + } + ]); + + // trigger adapter to subscribe + events.emit(EVENTS.BID_REQUESTED); + + // execute GAM cmd to register listener + gam.cmd.forEach(fn => fn()); + + // simulate slotRenderEnded + const slot = { + getSlotElementId: () => 'ad-unit-1', + getAdUnitPath: () => '/123/foo', + getTargetingKeys: () => ['hb_bidder', 'hb_adid'], + getTargeting: (k) => k === 'hb_bidder' ? ['pubmatic'] : k === 'hb_adid' ? ['ad123'] : [] + }; + if (gam._listeners['slotRenderEnded']) { + gam._listeners['slotRenderEnded']({ isEmpty: false, slot }); + } + + expect(server.requests.length).to.be.above(0); + }); + + it('should NOT send report if a matching bidWon already exists', function () { + const gam = createMockGAM(); + withConfigGamPredict(gam); + + localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, JSON.stringify({ gpr: true })); + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + + // provide prior bidWon matching placementId and hb_adid + events.getEvents.restore(); + sinon.stub(events, 'getEvents').returns([ + { eventType: 'bidWon', args: { adId: 'ad123' }, id: 'ad-unit-1' } + ]); + + events.emit(EVENTS.BID_REQUESTED); + gam.cmd.forEach(fn => fn()); + + const slot = { + getSlotElementId: () => 'ad-unit-1', + getAdUnitPath: () => '/123/foo', + getTargetingKeys: () => ['hb_bidder', 'hb_adid'], + getTargeting: (k) => k === 'hb_bidder' ? ['pubmatic'] : k === 'hb_adid' ? ['ad123'] : [] + }; + + const initialRequests = server.requests.length; + if (gam._listeners['slotRenderEnded']) { + gam._listeners['slotRenderEnded']({ isEmpty: false, slot }); + } + expect(server.requests.length).to.equal(initialRequests); + }); + }); + const testCasesVrref = [ { description: 'domainName matches window.top.location.href', @@ -636,12 +746,12 @@ describe('IntentIQ tests all', function () { adUnitConfigTests.forEach(({ adUnitConfig, description, event, expectedPlacementId }) => { it(description, function () { const [userConfig] = getUserConfig(); - userConfig.params.adUnitConfig = adUnitConfig; + enableAnalyticWithSpecialOptions({ adUnitConfig }) config.getConfig.restore(); sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); - const testEvent = { ...wonRequest, ...event }; + const testEvent = { ...getWonRequest(), ...event }; events.emit(EVENTS.BID_WON, testEvent); const request = server.requests[0]; diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index 00ca52bf80a..42cff5c2582 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -393,6 +393,46 @@ describe('IntentIQ tests', function () { expect(targetingKeys).to.include(customParamName); }); + it('should NOT call GAM setTargeting when current browser is in browserBlackList', function () { + const usedBrowser = 'chrome'; + const gam = mockGAM(); + const pa = gam.pubads(); + sinon.stub(gam, 'pubads').returns(pa); + + const originalSetTargeting = pa.setTargeting; + let setTargetingCalls = 0; + pa.setTargeting = function (...args) { + setTargetingCalls++; + return originalSetTargeting.apply(this, args); + }; + + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ + pcid: 'pcid-1', + pcidDate: Date.now(), + group: 'A', + isOptedOut: false, + date: Date.now(), + sCal: Date.now() + })); + + const cfg = { + params: { + partner, + gamObjectReference: gam, + gamParameterName: 'custom_gam_param', + browserBlackList: usedBrowser + } + }; + + intentIqIdSubmodule.getId(cfg); + gam.cmd.forEach(fn => fn()); + const currentBrowserLowerCase = detectBrowser(); + if (currentBrowserLowerCase === usedBrowser) { + expect(setTargetingCalls).to.equal(0); + expect(pa.getTargetingKeys()).to.not.include('custom_gam_param'); + } + }); + it('should not throw Uncaught TypeError when IntentIQ endpoint returns empty response', async function () { const callBackSpy = sinon.spy(); const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback;