Skip to content
Open
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
12 changes: 9 additions & 3 deletions libraries/gptUtils/gptUtils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CLIENT_SECTIONS } from '../../src/fpd/oneClient.js';
import {compareCodeAndSlot, deepAccess, isGptPubadsDefined, uniques, isEmpty} from '../../src/utils.js';
import {deepAccess, isGptPubadsDefined, uniques, isEmpty, isAdUnitCodeMatchingSlot} from '../../src/utils.js';

const slotInfoCache = new Map();

Expand All @@ -13,7 +13,10 @@ export function clearSlotInfoCache() {
* @return {function} filter function
*/
export function isSlotMatchingAdUnitCode(adUnitCode) {
return (slot) => compareCodeAndSlot(slot, adUnitCode);
return (slot) => {
const match = isAdUnitCodeMatchingSlot(slot);
return match(adUnitCode);
}
}

/**
Expand All @@ -35,7 +38,10 @@ export function getGptSlotForAdUnitCode(adUnitCode) {
let matchingSlot;
if (isGptPubadsDefined()) {
// find the first matching gpt slot on the page
matchingSlot = window.googletag.pubads().getSlots().find(isSlotMatchingAdUnitCode(adUnitCode));
matchingSlot = window.googletag.pubads().getSlots().find(slot => {
const match = isAdUnitCodeMatchingSlot(slot);
return match(adUnitCode);
});
}
return matchingSlot;
}
Expand Down
16 changes: 5 additions & 11 deletions modules/bidViewability.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,22 @@
import {config} from '../src/config.js';
import * as events from '../src/events.js';
import {EVENTS} from '../src/constants.js';
import {isFn, logWarn, triggerPixel} from '../src/utils.js';
import {isAdUnitCodeMatchingSlot, logWarn, triggerPixel} from '../src/utils.js';
import {getGlobal} from '../src/prebidGlobal.js';
import adapterManager, {gppDataHandler, uspDataHandler} from '../src/adapterManager.js';
import {gdprParams} from '../libraries/dfpUtils/dfpUtils.js';

const MODULE_NAME = 'bidViewability';
const CONFIG_ENABLED = 'enabled';
const CONFIG_FIRE_PIXELS = 'firePixels';
const CONFIG_CUSTOM_MATCH = 'customMatchFunction';
const BID_VURL_ARRAY = 'vurls';
const GPT_IMPRESSION_VIEWABLE_EVENT = 'impressionViewable';

export const isBidAdUnitCodeMatchingSlot = (bid, slot) => {
return (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode);
}

export const getMatchingWinningBidForGPTSlot = (globalModuleConfig, slot) => {
export const getMatchingWinningBidForGPTSlot = (slot) => {
const match = isAdUnitCodeMatchingSlot(slot);
return getGlobal().getAllWinningBids().find(
// supports custom match function from config
bid => isFn(globalModuleConfig[CONFIG_CUSTOM_MATCH])
? globalModuleConfig[CONFIG_CUSTOM_MATCH](bid, slot)
: isBidAdUnitCodeMatchingSlot(bid, slot)
({ adUnitCode }) => match(adUnitCode)
Comment on lines +19 to +23

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep bidViewability customMatchFunction compatibility

getMatchingWinningBidForGPTSlot no longer evaluates bidViewability.customMatchFunction, so existing configs that relied on that hook now cannot customize bid↔slot matching. In those deployments, slots that only matched via the custom function will now return null, which suppresses BID_VIEWABLE emission and viewability pixel firing for otherwise valid impressions. Consider preserving the old module-level matcher as a fallback.

Useful? React with 👍 / 👎.

) || null;
};

Expand Down Expand Up @@ -63,7 +57,7 @@ export const logWinningBidNotFound = (slot) => {

export const impressionViewableHandler = (globalModuleConfig, event) => {
const slot = event.slot;
const respectiveBid = getMatchingWinningBidForGPTSlot(globalModuleConfig, slot);
const respectiveBid = getMatchingWinningBidForGPTSlot(slot);

if (respectiveBid === null) {
logWinningBidNotFound(slot);
Expand Down
3 changes: 1 addition & 2 deletions modules/bidViewability.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ Maintainer: harshad.mane@pubmatic.com
- GPT API is used to find when a bid is viewable, https://developers.google.com/publisher-tag/reference#googletag.events.impressionviewableevent . This event is fired when an impression becomes viewable, according to the Active View criteria.
Refer: https://support.google.com/admanager/answer/4524488
- This module does not work with any adserver's other than GAM with GPT integration
- Logic used to find a matching pbjs-bid for a GPT slot is ``` (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode) ``` this logic can be changed by using param ```customMatchFunction```
- Logic used to find a matching pbjs-bid for a GPT slot is ``` (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode) ``` this logic can be changed by using config param ```customGptSlotMatching```
- When a rendered PBJS bid is viewable the module will trigger a BID_VIEWABLE event, which can be consumed by bidders and analytics adapters
- If the viewable bid contains a ```vurls``` param containing URL's and the Bid Viewability module is configured with ``` firePixels: true ``` then the URLs mentioned in bid.vurls will be called. Please note that GDPR and USP related parameters will be added to the given URLs
- This module is also compatible with Prebid core's billing deferral logic, this means that bids linked to an ad unit marked with `deferBilling: true` will trigger a bid adapter's `onBidBillable` function (if present) indicating an ad slot was viewed and also billing ready (if it were deferred).

# Params
- enabled [required] [type: boolean, default: false], when set to true, the module will emit BID_VIEWABLE when applicable
- firePixels [optional] [type: boolean], when set to true, will fire the urls mentioned in bid.vurls which should be array of urls
- customMatchFunction [optional] [type: function(bid, slot)], when passed this function will be used to `find` the matching winning bid for the GPT slot. Default value is ` (bid, slot) => (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode) `

# Example of consuming BID_VIEWABLE event
```
Expand Down
19 changes: 2 additions & 17 deletions modules/gptPreAuction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import {
pick,
uniques
} from '../src/utils.js';
import type {SlotMatchingFn} from '../src/targeting.ts';
import type {AdUnitCode} from '../src/types/common.d.ts';
import type {AdUnit} from '../src/adUnits.ts';

const MODULE_NAME = 'GPT Pre-Auction';
Expand Down Expand Up @@ -66,8 +64,6 @@ export function getAuctionsIdsFromTargeting(targeting, am = auctionManager) {
}

export const appendGptSlots = adUnits => {
const { customGptSlotMatching } = _currentConfig;

if (!isGptPubadsDefined()) {
return;
}
Expand All @@ -81,9 +77,7 @@ export const appendGptSlots = adUnits => {
const adUnitPaths = {};

window.googletag.pubads().getSlots().forEach((slot: googletag.Slot) => {
const matchingAdUnitCode = Object.keys(adUnitMap).find(customGptSlotMatching
? customGptSlotMatching(slot)
: isAdUnitCodeMatchingSlot(slot));
const matchingAdUnitCode = Object.keys(adUnitMap).find(isAdUnitCodeMatchingSlot(slot));

if (matchingAdUnitCode) {
const path = adUnitPaths[matchingAdUnitCode] = slot.getAdUnitPath();
Expand Down Expand Up @@ -174,16 +168,9 @@ type GPTPreAuctionConfig = {
*/
enabled?: boolean;
/**
* If true, use default behavior for determining GPID and PbAdSlot. Defaults to false.
* If true, use default behavior for determining GPID. Defaults to false.
*/
useDefaultPreAuction?: boolean;
customGptSlotMatching?: SlotMatchingFn;
/**
* @param adUnitCode Ad unit code
* @param adServerAdSlot The value of that ad unit's `ortb2Imp.ext.data.adserver.adslot`
* @returns pbadslot for the ad unit
*/
customPbAdSlot?: (adUnitCode: AdUnitCode, adServerAdSlot: string) => string;
/**
* @param adUnit An ad unit object
* @param adServerAdSlot The value of that ad unit's `ortb2Imp.ext.data.adserver.adslot`
Expand All @@ -206,8 +193,6 @@ declare module '../src/config' {
const handleSetGptConfig = moduleConfig => {
_currentConfig = pick(moduleConfig, [
'enabled', enabled => enabled !== false,
'customGptSlotMatching', customGptSlotMatching =>
typeof customGptSlotMatching === 'function' && customGptSlotMatching,
'customPreAuction', customPreAuction => typeof customPreAuction === 'function' && customPreAuction,
'useDefaultPreAuction', useDefaultPreAuction => useDefaultPreAuction ?? true,
]);
Expand Down
7 changes: 3 additions & 4 deletions src/prebid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {listenMessagesFromCreative} from './secureCreatives.js';
import {userSync} from './userSync.js';
import {config} from './config.js';
import {auctionManager} from './auctionManager.js';
import {isBidUsable, type SlotMatchingFn, targeting} from './targeting.js';
import {isBidUsable, targeting} from './targeting.js';
import {hook, wrapHook} from './hook.js';
import {loadSession} from './debugging.js';
import {storageCallbacks} from './storageManager.js';
Expand Down Expand Up @@ -610,14 +610,13 @@ addApiMethod('getBidResponsesForAdUnitCode', getBidResponsesForAdUnitCode);
/**
* Set query string targeting on one or more GPT ad units.
* @param adUnit a single `adUnit.code` or multiple.
* @param customSlotMatching gets a GoogleTag slot and returns a filter function for adUnitCode, so you can decide to match on either eg. return slot => { return adUnitCode => { return slot.getSlotElementId() === 'myFavoriteDivId'; } };
*/
function setTargetingForGPTAsync(adUnit?: AdUnitCode | AdUnitCode[], customSlotMatching?: SlotMatchingFn) {
function setTargetingForGPTAsync(adUnit?: AdUnitCode | AdUnitCode[]) {
if (!isGptPubadsDefined()) {
logError('window.googletag is not defined on the page');
return;
}
targeting.setTargetingForGPT(adUnit, customSlotMatching);
targeting.setTargetingForGPT(adUnit);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve custom slot matcher arg in setTargetingForGPTAsync

This removes the per-call matcher from pbjs.setTargetingForGPTAsync, so integrations that currently call setTargetingForGPTAsync(adUnits, matcher) will silently fall back to default slot/path matching. On pages with duplicate adUnit.code values, that can target the wrong GPT slot and send incorrect key-values to GAM. Please keep backward compatibility by honoring the second argument (or mapping it to the new global matcher path).

Useful? React with 👍 / 👎.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep custom slot matcher support in setTargetingForGPTAsync

This call now always invokes targeting.setTargetingForGPT(adUnit) and no longer forwards a caller-provided matcher, so existing integrations that pass a per-call custom matcher (used when multiple GPT slots share an adUnit code) will silently fall back to default path/id matching and can set targeting on the wrong slot. Please preserve the second-argument behavior (or a compatibility fallback) to avoid breaking current publisher implementations.

Useful? React with 👍 / 👎.

}
addApiMethod('setTargetingForGPTAsync', setTargetingForGPTAsync);

Expand Down
11 changes: 5 additions & 6 deletions src/targeting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,12 @@ export function sortByDealAndPriceBucketOrCpm(useCpm = false) {
* Return a map where each code in `adUnitCodes` maps to a list of GPT slots that match it.
*
* @param adUnitCodes
* @param customSlotMatching
* @param getSlots
*/
export function getGPTSlotsForAdUnits(adUnitCodes: AdUnitCode[], customSlotMatching, getSlots = () => window.googletag.pubads().getSlots()): ByAdUnit<googletag.Slot[]> {
export function getGPTSlotsForAdUnits(adUnitCodes: AdUnitCode[], getSlots = () => window.googletag.pubads().getSlots()): ByAdUnit<googletag.Slot[]> {
return getSlots().reduce((auToSlots, slot) => {
const customMatch = isFn(customSlotMatching) && customSlotMatching(slot);
Object.keys(auToSlots).filter(isFn(customMatch) ? customMatch : isAdUnitCodeMatchingSlot(slot)).forEach(au => auToSlots[au].push(slot));
Object.keys(auToSlots).filter(isAdUnitCodeMatchingSlot(slot))
.forEach(au => auToSlots[au].push(slot));
return auToSlots;
}, Object.fromEntries(adUnitCodes.map(au => [au, []])));
}
Expand Down Expand Up @@ -305,13 +304,13 @@ export function newTargeting(auctionManager) {
return flatTargeting;
},

setTargetingForGPT: hook('sync', function (adUnit?: AdUnitCode | AdUnitCode[], customSlotMatching?: SlotMatchingFn) {
setTargetingForGPT: hook('sync', function (adUnit?: AdUnitCode | AdUnitCode[]) {
// get our ad unit codes
const targetingSet: ByAdUnit<GPTTargetingValues> = targeting.getAllTargeting(adUnit);

const resetMap = Object.fromEntries(pbTargetingKeys.map(key => [key, null]));

Object.entries(getGPTSlotsForAdUnits(Object.keys(targetingSet), customSlotMatching)).forEach(([targetId, slots]) => {
Object.entries(getGPTSlotsForAdUnits(Object.keys(targetingSet))).forEach(([targetId, slots]) => {
slots.forEach(slot => {
// now set new targeting keys
Object.keys(targetingSet[targetId]).forEach(key => {
Expand Down
4 changes: 3 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,9 @@ export const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() ===
* @return filter function
*/
export function isAdUnitCodeMatchingSlot(slot) {
return (adUnitCode) => compareCodeAndSlot(slot, adUnitCode);
const customGptSlotMatching = config.getConfig('customGptSlotMatching');
const match = isFn(customGptSlotMatching) && customGptSlotMatching(slot);
return isFn(match) ? match : (adUnitCode) => compareCodeAndSlot(slot, adUnitCode);
}

/**
Expand Down
23 changes: 23 additions & 0 deletions src/utils/adUnits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type {AdUnitDefinition} from "../adUnits.ts";
import type {BidRequest} from "../adapterManager.ts";
import type {Bid} from "../bidfactory.ts";

// TODO: for 11, this should be merged with the GPT/AST logic in secureCreatives
// (after customGptSlotMatching becomes config - https://github.com/prebid/Prebid.js/issues/14408)
export function getAdUnitElement(bidRequest: BidRequest<any>): HTMLElement
export function getAdUnitElement(bidResponse: Bid): HTMLElement
export function getAdUnitElement(adUnit: AdUnitDefinition): HTMLElement
export function getAdUnitElement(target: {
code?: string,
adUnitCode?: string,
element?: HTMLElement
}): HTMLElement {
if (target.element != null) {
return target.element;
}
const id = target.adUnitCode ?? target.code;
if (id) {
return document.getElementById(id);
}
return null;
}
56 changes: 18 additions & 38 deletions test/spec/modules/bidViewability_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,6 @@ describe('#bidViewability', function() {
pbjsWinningBid = Object.assign({}, PBJS_WINNING_BID);
});

describe('isBidAdUnitCodeMatchingSlot', function() {
it('match found by GPT Slot getAdUnitPath', function() {
expect(bidViewability.isBidAdUnitCodeMatchingSlot(pbjsWinningBid, gptSlot)).to.equal(true);
});

it('match found by GPT Slot getSlotElementId', function() {
pbjsWinningBid.adUnitCode = 'DIV-1';
expect(bidViewability.isBidAdUnitCodeMatchingSlot(pbjsWinningBid, gptSlot)).to.equal(true);
});

it('match not found', function() {
pbjsWinningBid.adUnitCode = 'DIV-10';
expect(bidViewability.isBidAdUnitCodeMatchingSlot(pbjsWinningBid, gptSlot)).to.equal(false);
});
});

describe('getMatchingWinningBidForGPTSlot', function() {
let winningBidsArray;
let sandbox
Expand All @@ -89,44 +73,40 @@ describe('#bidViewability', function() {
sandbox.restore();
})

it('should find a match by using customMatchFunction provided in config', function() {
// Needs config to be passed with customMatchFunction
const bidViewabilityConfig = {
customMatchFunction(bid, slot) {
return ('AD-' + slot.getAdUnitPath()) === bid.adUnitCode;
it('should find a match by using customGptSlotMatching provided in config', function() {
config.setConfig({
customGptSlotMatching: slot => {
return (adUnitCode) => ('AD-' + slot.getAdUnitPath()) === adUnitCode;
}
};
});
const newWinningBid = Object.assign({}, PBJS_WINNING_BID, {adUnitCode: 'AD-' + PBJS_WINNING_BID.adUnitCode});
// Needs pbjs.getWinningBids to be implemented with match
winningBidsArray.push(newWinningBid);
const wb = bidViewability.getMatchingWinningBidForGPTSlot(bidViewabilityConfig, gptSlot);
const wb = bidViewability.getMatchingWinningBidForGPTSlot(gptSlot);
expect(wb).to.deep.equal(newWinningBid);
config.resetConfig();
});

it('should NOT find a match by using customMatchFunction provided in config', function() {
// Needs config to be passed with customMatchFunction
const bidViewabilityConfig = {
customMatchFunction(bid, slot) {
return ('AD-' + slot.getAdUnitPath()) === bid.adUnitCode;
}
};
// Needs pbjs.getWinningBids to be implemented without match; winningBidsArray is set to empty in beforeEach
const wb = bidViewability.getMatchingWinningBidForGPTSlot(bidViewabilityConfig, gptSlot);
it('should NOT find a match when customGptSlotMatching is set and no winning bid matches', function() {
config.setConfig({
customGptSlotMatching: slot => (adUnitCode) => ('AD-' + slot.getAdUnitPath()) === adUnitCode
});
// winningBidsArray is empty in beforeEach, so no bid matches
const wb = bidViewability.getMatchingWinningBidForGPTSlot(gptSlot);
expect(wb).to.equal(null);
config.resetConfig();
});

it('should find a match by using default matching function', function() {
// Needs config to be passed without customMatchFunction
// Needs pbjs.getWinningBids to be implemented with match
// No customGptSlotMatching in config; pbjs.getWinningBids returns matching bid
winningBidsArray.push(PBJS_WINNING_BID);
const wb = bidViewability.getMatchingWinningBidForGPTSlot({}, gptSlot);
const wb = bidViewability.getMatchingWinningBidForGPTSlot(gptSlot);
expect(wb).to.deep.equal(PBJS_WINNING_BID);
});

it('should NOT find a match by using default matching function', function() {
// Needs config to be passed without customMatchFunction
// Needs pbjs.getWinningBids to be implemented without match; winningBidsArray is set to empty in beforeEach
const wb = bidViewability.getMatchingWinningBidForGPTSlot({}, gptSlot);
// No customGptSlotMatching; winningBidsArray is empty in beforeEach
const wb = bidViewability.getMatchingWinningBidForGPTSlot(gptSlot);
expect(wb).to.equal(null);
});
});
Expand Down
21 changes: 4 additions & 17 deletions test/spec/modules/gptPreAuction_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,8 @@ describe('GPT pre-auction module', () => {

it('should use the customGptSlotMatching function if one is given', () => {
config.setConfig({
gptPreAuction: {
customGptSlotMatching: slot =>
adUnitCode => adUnitCode.toUpperCase() === slot.getAdUnitPath().toUpperCase()
customGptSlotMatching: slot => {
return (adUnitCode) => adUnitCode.toUpperCase() === slot.getAdUnitPath().toUpperCase();
}
});

Expand All @@ -194,6 +193,7 @@ describe('GPT pre-auction module', () => {
appendGptSlots([adUnit]);
expect(adUnit.ortb2Imp.ext.data.adserver).to.be.an('object');
expect(adUnit.ortb2Imp.ext.data.adserver).to.deep.equal({ name: 'gam', adslot: 'slotCode1' });
config.resetConfig();
});
});

Expand All @@ -208,27 +208,14 @@ describe('GPT pre-auction module', () => {
expect(_currentConfig).to.be.an('object').that.is.empty;
});

it('should accept custom functions in config', () => {
config.setConfig({
gptPreAuction: {
customGptSlotMatching: () => 'customGptSlot',
}
});

expect(_currentConfig.enabled).to.equal(true);
expect(_currentConfig.customGptSlotMatching).to.a('function');
expect(_currentConfig.customGptSlotMatching()).to.equal('customGptSlot');
});

it('should check that custom functions in config are type function', () => {
config.setConfig({
gptPreAuction: {
customGptSlotMatching: 12345,
customPreAuction: 12345,
}
});
expect(_currentConfig).to.deep.equal({
enabled: true,
customGptSlotMatching: false,
customPreAuction: false,
useDefaultPreAuction: true
});
Expand Down
Loading
Loading