Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions libraries/intentIqUtils/gamPredictionReport.js
Original file line number Diff line number Diff line change
@@ -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;
};
Expand Down
13 changes: 10 additions & 3 deletions modules/intentIqIdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
}
Expand Down
114 changes: 114 additions & 0 deletions test/spec/libraries/gamPredictionReport_spec.js
Original file line number Diff line number Diff line change
@@ -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/);
});
});
42 changes: 41 additions & 1 deletion test/spec/modules/intentIqIdSystem_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
Loading