diff --git a/libraries/intentIqConstants/intentIqConstants.js b/libraries/intentIqConstants/intentIqConstants.js index 2cc9acc1844..7f3b6fd94f7 100644 --- a/libraries/intentIqConstants/intentIqConstants.js +++ b/libraries/intentIqConstants/intentIqConstants.js @@ -7,4 +7,4 @@ export const OPT_OUT = 'O'; export const BLACK_LIST = 'L'; export const CLIENT_HINTS_KEY = '_iiq_ch'; export const EMPTY = 'EMPTY' -export const VERSION = 0.25 +export const VERSION = 0.26 diff --git a/libraries/intentIqUtils/detectBrowserUtils.js b/libraries/intentIqUtils/detectBrowserUtils.js index c7004c77ae9..37a935bda28 100644 --- a/libraries/intentIqUtils/detectBrowserUtils.js +++ b/libraries/intentIqUtils/detectBrowserUtils.js @@ -64,7 +64,7 @@ export function detectBrowserFromUserAgent(userAgent) { /** * Detects the browser from the NavigatorUAData object - * @param {NavigatorUAData} userAgentData - The user agent data object from the browser + * @param {Object} userAgentData - The user agent data object from the browser * @return {string} The name of the detected browser or 'unknown' if unable to detect */ export function detectBrowserFromUserAgentData(userAgentData) { diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 4a7d0f47b18..48210e49c19 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -5,7 +5,7 @@ * @requires module:modules/userId */ -import {logError, logInfo} from '../src/utils.js'; +import {logError, logInfo, isPlainObject} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js'; @@ -159,6 +159,23 @@ function tryParse(data) { } } +/** + * Configures and updates A/B testing group in Google Ad Manager (GAM). + * + * @param {object} gamObjectReference - Reference to the GAM object, expected to have a `cmd` queue and `pubads()` API. + * @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) { + if (isPlainObject(gamObjectReference) && Array.isArray(gamObjectReference.cmd)) { + gamObjectReference.cmd.push(() => { + gamObjectReference + .pubads() + .setTargeting(gamParameterName, userGroup || NOT_YET_DEFINED); + }); + } +} + /** * Processes raw client hints data into a structured format. * @param {object} clientHints - Raw client hints data @@ -218,11 +235,14 @@ export const intentIqIdSubmodule = { 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'; const allowedStorage = defineStorageType(config.enabledStorageTypes); let firstPartyData = tryParse(readData(FIRST_PARTY_KEY, allowedStorage)); const isGroupB = firstPartyData?.group === WITHOUT_IIQ; + setGamReporting(gamObjectReference, gamParameterName, firstPartyData?.group) const firePartnerCallback = () => { if (configParams.callback && !callbackFired) { @@ -403,9 +423,11 @@ export const intentIqIdSubmodule = { firstPartyData.group = WITHOUT_IIQ; storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), allowedStorage); defineEmptyDataAndFireCallback(); + if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group); return } else { firstPartyData.group = WITH_IIQ; + if (gamObjectReference) setGamReporting(gamObjectReference, gamParameterName, firstPartyData.group); } } if ('isOptedOut' in respJson) { diff --git a/modules/intentIqIdSystem.md b/modules/intentIqIdSystem.md index 34fd495d625..0103d403503 100644 --- a/modules/intentIqIdSystem.md +++ b/modules/intentIqIdSystem.md @@ -43,6 +43,8 @@ Please find below list of paramters that could be used in configuring Intent IQ | 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"` | ### Configuration example diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index b95c3603e3a..356ddcb6e2a 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -2,12 +2,12 @@ import { expect } from 'chai'; import { intentIqIdSubmodule, storage } from 'modules/intentIqIdSystem.js'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; -import { decryptData, handleClientHints, readData } from '../../../modules/intentIqIdSystem'; +import { decryptData, handleClientHints, readData, setGamReporting } from '../../../modules/intentIqIdSystem'; import {getGppValue} from '../../../libraries/intentIqUtils/getGppValue.js'; import { gppDataHandler, uspDataHandler } from '../../../src/consentHandler'; import { clearAllCookies } from '../../helpers/cookies'; import { detectBrowserFromUserAgent, detectBrowserFromUserAgentData } from '../../../libraries/intentIqUtils/detectBrowserUtils'; -import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY} from '../../../libraries/intentIqConstants/intentIqConstants.js'; +import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, NOT_YET_DEFINED, WITH_IIQ} from '../../../libraries/intentIqConstants/intentIqConstants.js'; const partner = 10; const pai = '11'; @@ -48,6 +48,24 @@ export const testClientHints = { wow64: false }; +const mockGAM = () => { + const targetingObject = {}; + return { + cmd: [], + pubads: () => ({ + setTargeting: (key, value) => { + targetingObject[key] = value; + }, + getTargeting: (key) => { + return [targetingObject[key]]; + }, + getTargetingKeys: () => { + return Object.keys(targetingObject); + } + }) + }; +}; + describe('IntentIQ tests', function () { let logErrorStub; let testLSValue = { @@ -199,6 +217,68 @@ describe('IntentIQ tests', function () { expect(callBackSpy.calledOnce).to.be.true; }); + it('should set GAM targeting to U initially and update to A after server response', function () { + let callBackSpy = sinon.spy(); + let mockGamObject = mockGAM(); + let expectedGamParameterName = 'intent_iq_group'; + + const originalPubads = mockGamObject.pubads; + let setTargetingSpy = sinon.spy(); + mockGamObject.pubads = function () { + const obj = { ...originalPubads.apply(this, arguments) }; + const originalSetTargeting = obj.setTargeting; + obj.setTargeting = function (...args) { + setTargetingSpy(...args); + return originalSetTargeting.apply(this, args); + }; + return obj; + }; + + defaultConfigParams.params.gamObjectReference = mockGamObject; + + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + + submoduleCallback(callBackSpy); + let request = server.requests[0]; + + mockGamObject.cmd.forEach(cb => cb()); + mockGamObject.cmd = [] + + let groupBeforeResponse = mockGamObject.pubads().getTargeting(expectedGamParameterName); + + request.respond( + 200, + responseHeader, + JSON.stringify({ group: 'A', tc: 20 }) + ); + + mockGamObject.cmd.forEach(item => item()); + + let groupAfterResponse = mockGamObject.pubads().getTargeting(expectedGamParameterName); + + expect(request.url).to.contain('https://api.intentiq.com/profiles_engine/ProfilesEngineServlet?at=39'); + expect(groupBeforeResponse).to.deep.equal([NOT_YET_DEFINED]); + expect(groupAfterResponse).to.deep.equal([WITH_IIQ]); + + expect(setTargetingSpy.calledTwice).to.be.true; + }); + + it('should use the provided gamParameterName from configParams', function () { + let callBackSpy = sinon.spy(); + let mockGamObject = mockGAM(); + let customParamName = 'custom_gam_param'; + + defaultConfigParams.params.gamObjectReference = mockGamObject; + defaultConfigParams.params.gamParameterName = customParamName; + + let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; + submoduleCallback(callBackSpy); + mockGamObject.cmd.forEach(cb => cb()); + let targetingKeys = mockGamObject.pubads().getTargetingKeys(); + + expect(targetingKeys).to.include(customParamName); + }); + it('should not throw Uncaught TypeError when IntentIQ endpoint returns empty response', function () { let callBackSpy = sinon.spy(); let submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback;