diff --git a/libraries/intentIqUtils/gamPredictionReport.js b/libraries/intentIqUtils/gamPredictionReport.js index 2191ade6d35..69a81a8a5e6 100644 --- a/libraries/intentIqUtils/gamPredictionReport.js +++ b/libraries/intentIqUtils/gamPredictionReport.js @@ -1,17 +1,35 @@ import { getEvents } from '../../src/events.js'; -import { logError } from '../../src/utils.js'; +import { isPlainObject, logError } from '../../src/utils.js'; export function gamPredictionReport (gamObjectReference, sendData) { try { - if (!gamObjectReference || !sendData) logError('Failed to get gamPredictionReport, required data is missed'); + if (!gamObjectReference || !sendData) { + logError('Failed to get gamPredictionReport, required data is missed'); + return + } const getSlotTargeting = (slot) => { const kvs = {}; try { - (slot.getTargetingKeys() || []).forEach((k) => { - kvs[k] = slot.getTargeting(k); - }); + if (typeof slot.getConfig === 'function') { + const current = slot.getConfig('targeting'); + const targeting = isPlainObject(current?.targeting) + ? current.targeting + : (isPlainObject(current) ? current : {}); + for (const k in targeting) { + const v = targeting[k]; + if (v == null) continue; + kvs[k] = Array.isArray(v) ? v : [typeof v === 'string' ? v : String(v)]; + } + return kvs; + } + // Fallback in case an older version of Google Publisher Tag is used. + if (typeof slot.getTargetingKeys === 'function' && typeof slot.getTargeting === 'function') { + (slot.getTargetingKeys() || []).forEach((k) => { + kvs[k] = slot.getTargeting(k); + }); + } } catch (e) { - logError('Failed to get targeting keys: ' + e); + logError('Failed to get slot targeting: ' + e); } return kvs; }; diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 054afe82371..c064719ae88 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -208,9 +208,16 @@ export function setGamReporting(gamObjectReference, gamParameterName, userGroup, if (isBlacklisted) return; if (isPlainObject(gamObjectReference) && gamObjectReference.cmd) { gamObjectReference.cmd.push(() => { - gamObjectReference - .pubads() - .setTargeting(gamParameterName, userGroup); + if (typeof gamObjectReference.setConfig === 'function') { + gamObjectReference.setConfig({ + targeting: { + [gamParameterName]: userGroup + } + }); + return; + } + // Fallback in case an older version of Google Publisher Tag is used. + gamObjectReference?.pubads?.()?.setTargeting?.(gamParameterName, userGroup); }); } } diff --git a/test/spec/libraries/gamPredictionReport_spec.js b/test/spec/libraries/gamPredictionReport_spec.js new file mode 100644 index 00000000000..65690f9ccd3 --- /dev/null +++ b/test/spec/libraries/gamPredictionReport_spec.js @@ -0,0 +1,114 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import * as events from 'src/events.js'; +import * as utils from 'src/utils.js'; +import { gamPredictionReport } from '../../../libraries/intentIqUtils/gamPredictionReport.js'; + +describe('gamPredictionReport', function () { + let getEventsStub; + let logErrorStub; + + beforeEach(() => { + getEventsStub = sinon.stub(events, 'getEvents').returns([]); + logErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(() => { + getEventsStub.restore(); + logErrorStub.restore(); + }); + + function runWithSlot(slot, sendData) { + let handler; + const gamObjectReference = { + cmd: [], + pubads: () => ({ + addEventListener: (eventName, callback) => { + handler = callback; + } + }) + }; + + gamPredictionReport(gamObjectReference, sendData); + gamObjectReference.cmd.forEach((fn) => fn()); + handler({ isEmpty: false, slot }); + } + + it('reads targeting from slot.getConfig targeting wrapper', () => { + const sendData = sinon.spy(); + const slot = { + getConfig: sinon.stub().withArgs('targeting').returns({ targeting: { hb_bidder: ['test'] } }), + getTargetingKeys: sinon.stub().throws(new Error('deprecated')), + getTargeting: sinon.stub().throws(new Error('deprecated')), + getSlotElementId: () => 'div-1', + getAdUnitPath: () => '/123' + }; + + runWithSlot(slot, sendData); + + expect(sendData.calledOnce).to.equal(true); + expect(sendData.firstCall.args[0].bidderCode).to.equal('test'); + }); + + it('reads targeting from slot.getConfig flat object', () => { + const sendData = sinon.spy(); + const slot = { + getConfig: sinon.stub().withArgs('targeting').returns({ hb_bidder: ['flat'] }), + getSlotElementId: () => 'div-2', + getAdUnitPath: () => '/456' + }; + + runWithSlot(slot, sendData); + + expect(sendData.calledOnce).to.equal(true); + expect(sendData.firstCall.args[0].bidderCode).to.equal('flat'); + }); + + it('reads targeting from legacy slot.getTargeting APIs when getConfig is missing', () => { + const sendData = sinon.spy(); + const slot = { + getTargetingKeys: sinon.stub().returns(['hb_bidder']), + getTargeting: sinon.stub().withArgs('hb_bidder').returns(['legacy']), + getSlotElementId: () => 'div-3', + getAdUnitPath: () => '/789' + }; + + runWithSlot(slot, sendData); + + expect(sendData.calledOnce).to.equal(true); + expect(sendData.firstCall.args[0].bidderCode).to.equal('legacy'); + expect(slot.getTargetingKeys.calledOnce).to.equal(true); + expect(slot.getTargeting.calledOnce).to.equal(true); + }); + + it('coerces non-array targeting values to string arrays', () => { + const sendData = sinon.spy(); + const slot = { + getConfig: sinon.stub().withArgs('targeting').returns({ targeting: { hb_bidder: 42 } }), + getSlotElementId: () => 'div-4', + getAdUnitPath: () => '/101' + }; + + runWithSlot(slot, sendData); + + expect(sendData.calledOnce).to.equal(true); + expect(sendData.firstCall.args[0].bidderCode).to.equal('42'); + }); + + it('logs and recovers when legacy targeting APIs throw', () => { + const sendData = sinon.spy(); + const slot = { + getTargetingKeys: sinon.stub().throws(new Error('legacy broken')), + getTargeting: sinon.stub(), + getSlotElementId: () => 'div-5', + getAdUnitPath: () => '/202' + }; + + runWithSlot(slot, sendData); + + expect(sendData.calledOnce).to.equal(true); + expect(sendData.firstCall.args[0].bidderCode).to.equal(null); + expect(logErrorStub.called).to.equal(true); + expect(logErrorStub.firstCall.args[0]).to.match(/Failed to get slot targeting/); + }); +}); diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index 18dd0452943..b3d3b083cb6 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -7,7 +7,8 @@ import { handleClientHints, firstPartyData as moduleFPD, isCMPStringTheSame, createPixelUrl, translateMetadata, - initializeGlobalIIQ + initializeGlobalIIQ, + setGamReporting } from '../../../modules/intentIqIdSystem.js'; import { storage, readData, storeData } from '../../../libraries/intentIqUtils/storageUtils.js'; import { gppDataHandler, uspDataHandler, gdprDataHandler } from '../../../src/consentHandler.js'; @@ -220,6 +221,45 @@ describe('IntentIQ tests', function () { expect(submodule).to.be.undefined; }); + it('should use setConfig when available in setGamReporting', function () { + const setConfigSpy = sinon.spy(); + const pubadsSetTargetingSpy = sinon.spy(); + const mockGAM = { + cmd: [], + setConfig: setConfigSpy, + pubads: () => ({ + setTargeting: pubadsSetTargetingSpy + }) + }; + + setGamReporting(mockGAM, 'intent_iq_group', 'A'); + mockGAM.cmd.forEach((fn) => fn()); + + expect(setConfigSpy.calledOnce).to.equal(true); + expect(setConfigSpy.firstCall.args[0]).to.deep.equal({ + targeting: { + intent_iq_group: 'A' + } + }); + expect(pubadsSetTargetingSpy.called).to.equal(false); + }); + + it('should fall back to pubads.setTargeting when setConfig is missing', function () { + const pubadsSetTargetingSpy = sinon.spy(); + const mockGAM = { + cmd: [], + pubads: () => ({ + setTargeting: pubadsSetTargetingSpy + }) + }; + + setGamReporting(mockGAM, 'intent_iq_group', 'B'); + mockGAM.cmd.forEach((fn) => fn()); + + expect(pubadsSetTargetingSpy.calledOnce).to.equal(true); + expect(pubadsSetTargetingSpy.firstCall.args).to.deep.equal(['intent_iq_group', 'B']); + }); + it('should not save data in cookie if relevant type not set', async function () { const callBackSpy = sinon.spy(); const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback;