From e47b9081ef3f6283053c46dd432a6d593d9b3558 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 12 May 2022 11:40:18 -0700 Subject: [PATCH] Multiple modules: automatically fill in PPID for DFP video URLs (#8365) https://github.com/prebid/Prebid.js/issues/8151 --- modules/dfpAdServerVideo.js | 8 ++++ modules/userId/index.js | 40 +++++++++++-------- src/adserver.js | 7 ++++ test/spec/modules/dfpAdServerVideo_spec.js | 45 ++++++++++++++++++++++ test/spec/modules/userId_spec.js | 18 +++++++++ 5 files changed, 102 insertions(+), 16 deletions(-) diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 7f8ad3351fa..37f038d2a67 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -11,6 +11,7 @@ import { auctionManager } from '../src/auctionManager.js'; import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; +import {getPPID} from '../src/adserver.js'; /** * @typedef {Object} DfpVideoParams @@ -118,6 +119,13 @@ export function buildDfpVideoUrl(options) { const uspConsent = uspDataHandler.getConsentData(); if (uspConsent) { queryParams.us_privacy = uspConsent; } + if (!queryParams.ppid) { + const ppid = getPPID(); + if (ppid != null) { + queryParams.ppid = ppid; + } + } + return buildUrl(Object.assign({ protocol: 'https', host: 'securepubads.g.doubleclick.net', diff --git a/modules/userId/index.js b/modules/userId/index.js index 5aed9b808f6..5b426169a19 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -151,6 +151,7 @@ import { timestamp, isEmpty } from '../../src/utils.js'; +import {getPPID as coreGetPPID} from '../../src/adserver.js'; const MODULE_NAME = 'User ID'; const COOKIE = 'cookie'; @@ -650,6 +651,19 @@ function idSystemInitializer({delay = delayFor} = {}) { let initIdSystem; +function getPPID() { + // userSync.ppid should be one of the 'source' values in getUserIdsAsEids() eg pubcid.org or id5-sync.com + const matchingUserId = ppidSource && (getUserIdsAsEids() || []).find(userID => userID.source === ppidSource); + if (matchingUserId && typeof deepAccess(matchingUserId, 'uids.0.id') === 'string') { + const ppidValue = matchingUserId.uids[0].id.replace(/[\W_]/g, ''); + if (ppidValue.length >= 32 && ppidValue.length <= 150) { + return ppidValue; + } else { + logWarn(`User ID - Googletag Publisher Provided ID for ${ppidSource} is not between 32 and 150 characters - ${ppidValue}`); + } + } +} + /** * Hook is executed before adapters, but after consentManagement. Consent data is requied because * this module requires GDPR consent with Purpose #1 to save data locally. @@ -666,23 +680,16 @@ export function requestBidsHook(fn, reqBidsConfigObj, {delay = delayFor} = {}) { ]).then(() => { // pass available user id data to bid adapters addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, initializedSubmodules); - - // userSync.ppid should be one of the 'source' values in getUserIdsAsEids() eg pubcid.org or id5-sync.com - const matchingUserId = ppidSource && (getUserIdsAsEids() || []).find(userID => userID.source === ppidSource); - if (matchingUserId && typeof deepAccess(matchingUserId, 'uids.0.id') === 'string') { - const ppidValue = matchingUserId.uids[0].id.replace(/[\W_]/g, ''); - if (ppidValue.length >= 32 && ppidValue.length <= 150) { - if (isGptPubadsDefined()) { - window.googletag.pubads().setPublisherProvidedId(ppidValue); - } else { - window.googletag = window.googletag || {}; - window.googletag.cmd = window.googletag.cmd || []; - window.googletag.cmd.push(function() { - window.googletag.pubads().setPublisherProvidedId(ppidValue); - }); - } + const ppid = getPPID(); + if (ppid) { + if (isGptPubadsDefined()) { + window.googletag.pubads().setPublisherProvidedId(ppid); } else { - logWarn(`User ID - Googletag Publisher Provided ID for ${ppidSource} is not between 32 and 150 characters - ${ppidValue}`); + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(function() { + window.googletag.pubads().setPublisherProvidedId(ppid); + }); } } @@ -980,6 +987,7 @@ function updateSubmodules() { if (!addedUserIdHook && submodules.length) { // priority value 40 will load after consentManagement with a priority of 50 getGlobal().requestBids.before(requestBidsHook, 40); + coreGetPPID.after((next) => next(getPPID())); logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules: `, submodules.map(a => a.submodule.name)); addedUserIdHook = true; } diff --git a/src/adserver.js b/src/adserver.js index 61af8862972..8d99b29c3ef 100644 --- a/src/adserver.js +++ b/src/adserver.js @@ -1,5 +1,6 @@ import { formatQS } from './utils.js'; import { targeting } from './targeting.js'; +import {hook} from './hook.js'; // Adserver parent class const AdServer = function(attr) { @@ -11,6 +12,7 @@ const AdServer = function(attr) { }; // DFP ad server +// TODO: this seems to be unused? export function dfpAdserver(options, urlComponents) { var adserver = new AdServer(options); adserver.urlComponents = urlComponents; @@ -53,3 +55,8 @@ export function dfpAdserver(options, urlComponents) { return adserver; }; + +/** + * return the GAM PPID, if available (eid for the userID configured with `userSync.ppidSource`) + */ +export const getPPID = hook('sync', () => undefined); diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 300e2104fae..943b7cc0162 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -10,6 +10,9 @@ import { auctionManager } from 'src/auctionManager.js'; import { gdprDataHandler, uspDataHandler } from 'src/adapterManager.js'; import * as adpod from 'modules/adpod.js'; import { server } from 'test/mocks/xhr.js'; +import * as adServer from 'src/adserver.js'; +import {deepClone} from 'src/utils.js'; +import {hook} from '../../../src/hook.js'; const bid = { videoCacheKey: 'abc', @@ -20,6 +23,10 @@ const bid = { }; describe('The DFP video support module', function () { + before(() => { + hook.ready(); + }); + it('should make a legal request URL when given the required params', function () { const url = parse(buildDfpVideoUrl({ adUnit: adUnit, @@ -226,6 +233,44 @@ describe('The DFP video support module', function () { gdprDataHandlerStub.restore(); }); + describe('GAM PPID', () => { + let ppid; + let getPPIDStub; + beforeEach(() => { + getPPIDStub = sinon.stub(adServer, 'getPPID').callsFake(() => ppid); + }); + afterEach(() => { + getPPIDStub.restore(); + }); + + Object.entries({ + 'params': {params: {'iu': 'mock/unit'}}, + 'url': {url: 'https://video.adserver.mock/', params: {'iu': 'mock/unit'}} + }).forEach(([t, opts]) => { + describe(`when using ${t}`, () => { + function buildUrlAndGetParams() { + const url = parse(buildDfpVideoUrl(Object.assign({ + adUnit: adUnit, + bid: deepClone(bid), + }, opts))); + return utils.parseQS(url.query); + } + + it('should be included if available', () => { + ppid = 'mockPPID'; + const q = buildUrlAndGetParams(); + expect(q.ppid).to.equal('mockPPID'); + }); + + it('should not be included if not available', () => { + ppid = undefined; + const q = buildUrlAndGetParams(); + expect(q.hasOwnProperty('ppid')).to.be.false; + }) + }) + }) + }) + describe('special targeting unit test', function () { const allTargetingData = { 'hb_format': 'video', diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 03924e320c0..a90056a7538 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -52,6 +52,7 @@ import * as mockGpt from '../integration/faker/googletag.js'; import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; +import {getPPID} from '../../../src/adserver.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -445,6 +446,23 @@ describe('User ID', function () { }); }); + it('should make PPID available to core', () => { + init(config); + setSubmoduleRegistry([sharedIdSystemSubmodule]); + const id = 'thishastobelongerthan32characters'; + config.setConfig({ + userSync: { + ppid: 'pubcid.org', + userIds: [ + { name: 'pubCommonId', value: {'pubcid': id} }, + ] + } + }); + return getGlobal().refreshUserIds().then(() => { + expect(getPPID()).to.eql(id); + }) + }); + describe('refreshing before init is complete', () => { const MOCK_ID = {'MOCKID': '1111'}; let mockIdCallback;