From 33762d1ee5abd0363e2b35fbbb07cb2d38287972 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Thu, 12 Sep 2019 16:38:12 -0700 Subject: [PATCH 01/30] added support for pubcommon, digitrust, id5id --- modules/pubmaticBidAdapter.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index facecdaa578..2ec353a2f4e 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -624,10 +624,31 @@ function _handleTTDId(eids, validBidRequests) { } } +/** + * Produces external userid object in ortb 3.0 model. + */ +function _addExternalUserId(eids, value, source, atype) { + if (value) { + eids.push({ + source, + uids: [{ + id: value, + atype + }] + }); + } +} + function _handleEids(payload, validBidRequests) { let eids = []; _handleDigitrustId(eids); _handleTTDId(eids, validBidRequests); + const bidRequest = validBidRequests[0]; + if(bidRequest && bidRequest.userId){ + _addExternalUserId(eids, bidRequest.userId.pubcid, 'pubcommon', 1); + _addExternalUserId(eids, utils.deepAccess(bidRequest.userId.digitrustid, 'data.id'), 'digitru.st', 1); + _addExternalUserId(eids, bidRequest.userId.id5id, 'id5id', 1); + } if (eids.length > 0) { payload.user.eids = eids; } From 0722354d7e62dc2237e16f8b7f469557ef388a20 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Thu, 12 Sep 2019 20:11:24 -0700 Subject: [PATCH 02/30] added support for IdentityLink --- modules/pubmaticBidAdapter.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 2ec353a2f4e..8c72367ba6e 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -644,10 +644,12 @@ function _handleEids(payload, validBidRequests) { _handleDigitrustId(eids); _handleTTDId(eids, validBidRequests); const bidRequest = validBidRequests[0]; - if(bidRequest && bidRequest.userId){ - _addExternalUserId(eids, bidRequest.userId.pubcid, 'pubcommon', 1); - _addExternalUserId(eids, utils.deepAccess(bidRequest.userId.digitrustid, 'data.id'), 'digitru.st', 1); - _addExternalUserId(eids, bidRequest.userId.id5id, 'id5id', 1); + if (bidRequest && bidRequest.userId) { + _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.pubcid`), 'pubcommon', 1); + _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.digitrustid.data.id`), 'digitru.st', 1); + _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.id5id`), 'id5id', 1); + _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.criteortus.${BIDDER_CODE}.userid`), 'criteortus', 1); + _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.idl_env`), 'identity_link_envelope', 1); } if (eids.length > 0) { payload.user.eids = eids; From f2c32c07b4a4996e1e7d59510974c07d2eec7d5e Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Fri, 13 Sep 2019 10:02:20 -0700 Subject: [PATCH 03/30] changed the source for id5 --- modules/pubmaticBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 8c72367ba6e..c768583b96c 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -647,7 +647,7 @@ function _handleEids(payload, validBidRequests) { if (bidRequest && bidRequest.userId) { _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.pubcid`), 'pubcommon', 1); _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.digitrustid.data.id`), 'digitru.st', 1); - _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.id5id`), 'id5id', 1); + _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.id5id`), 'id5-sync.com', 1); _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.criteortus.${BIDDER_CODE}.userid`), 'criteortus', 1); _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.idl_env`), 'identity_link_envelope', 1); } From eaed9874c1fc91d79118b148bbd740569e562023 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Fri, 13 Sep 2019 14:19:36 -0700 Subject: [PATCH 04/30] added unit test cases --- modules/pubmaticBidAdapter.js | 2 +- test/spec/modules/pubmaticBidAdapter_spec.js | 186 +++++++++++++++++++ 2 files changed, 187 insertions(+), 1 deletion(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index c768583b96c..a85a2c8e19c 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -628,7 +628,7 @@ function _handleTTDId(eids, validBidRequests) { * Produces external userid object in ortb 3.0 model. */ function _addExternalUserId(eids, value, source, atype) { - if (value) { + if (utils.isStr(value)) { eids.push({ source, uids: [{ diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 3de83c56213..d0d0ce8c08b 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -1476,6 +1476,192 @@ describe('PubMatic adapter', function () { }); }); + describe('UserIds from request', function() { + describe('pubcommon Id', function() { + it('send the pubcommon id if it is present', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.pubcid = 'pub_common_user_id'; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.deep.equal([{ + 'source': 'pubcommon', + 'uids': [{ + 'id': 'pub_common_user_id', + 'atype': 1 + }] + }]); + }); + + it('do not pass if not string', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.pubcid = 1; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.pubcid = []; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.pubcid = null; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.pubcid = {}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + }); + }); + + describe('Digitrust Id', function() { + it('send the digitrust id if it is present', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.digitrustid = {data: {id: 'digitrust_user_id'}}; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.deep.equal([{ + 'source': 'digitru.st', + 'uids': [{ + 'id': 'digitrust_user_id', + 'atype': 1 + }] + }]); + }); + + it('do not pass if not string', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.digitrustid = {data: {id: 1}}; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.digitrustid = {data: {id: []}}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.digitrustid = {data: {id: null}}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.digitrustid = {data: {id: {}}}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + }); + }); + + describe('ID5 Id', function() { + it('send the id5 id if it is present', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.id5id = 'id5-user-id'; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.deep.equal([{ + 'source': 'id5-sync.com', + 'uids': [{ + 'id': 'id5-user-id', + 'atype': 1 + }] + }]); + }); + + it('do not pass if not string', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.id5id = 1; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.id5id = []; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.id5id = null; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.id5id = {}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + }); + }); + + describe('CriteoRTUS Id', function() { + it('send the criteo id if it is present', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.criteortus = {pubmatic: {userid: 'criteo-rtus-user-id'}}; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.deep.equal([{ + 'source': 'criteortus', + 'uids': [{ + 'id': 'criteo-rtus-user-id', + 'atype': 1 + }] + }]); + }); + + it('do not pass if not string', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.criteortus = {appnexus: {userid: 'criteo-rtus-user-id'}}; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.criteortus = {pubmatic: {userid: 1}}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.criteortus = {pubmatic: {userid: []}}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.criteortus = {pubmatic: {userid: null}}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.criteortus = {pubmatic: {userid: {}}}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + }); + }); + + describe('IdentityLink Id', function() { + it('send the identity-link id if it is present', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.idl_env = 'identity-link-user-id'; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.deep.equal([{ + 'source': 'identity_link_envelope', + 'uids': [{ + 'id': 'identity-link-user-id', + 'atype': 1 + }] + }]); + }); + + it('do not pass if not string', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.idl_env = 1; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.idl_env = []; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.idl_env = null; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.idl_env = {}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + }); + }); + }) + it('Request params check for video ad', function () { let request = spec.buildRequests(videoBidRequests); let data = JSON.parse(request.data); From 602ee7ea83c07cf1ff83f6c000473512a9525b4c Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 16 Sep 2019 14:24:27 -0700 Subject: [PATCH 05/30] changed source param for identityLink --- modules/pubmaticBidAdapter.js | 2 +- test/spec/modules/pubmaticBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index a85a2c8e19c..d1e1d072673 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -649,7 +649,7 @@ function _handleEids(payload, validBidRequests) { _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.digitrustid.data.id`), 'digitru.st', 1); _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.id5id`), 'id5-sync.com', 1); _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.criteortus.${BIDDER_CODE}.userid`), 'criteortus', 1); - _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.idl_env`), 'identity_link_envelope', 1); + _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.idl_env`), 'liveramp.com', 1); } if (eids.length > 0) { payload.user.eids = eids; diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index d0d0ce8c08b..ababf2dcf5f 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -1632,7 +1632,7 @@ describe('PubMatic adapter', function () { let request = spec.buildRequests(bidRequests, {}); let data = JSON.parse(request.data); expect(data.user.eids).to.deep.equal([{ - 'source': 'identity_link_envelope', + 'source': 'liveramp.com', 'uids': [{ 'id': 'identity-link-user-id', 'atype': 1 From 4229ba16a35e89676c7cafa632455a8fb3f4bbd8 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Wed, 20 Jan 2021 17:51:26 -0800 Subject: [PATCH 06/30] introducing a new event, bidViewable --- src/constants.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/constants.json b/src/constants.json index 4025e084682..e6b9687f911 100644 --- a/src/constants.json +++ b/src/constants.json @@ -38,7 +38,8 @@ "ADD_AD_UNITS": "addAdUnits", "AD_RENDER_FAILED": "adRenderFailed", "TCF2_ENFORCEMENT": "tcf2Enforcement", - "AUCTION_DEBUG": "auctionDebug" + "AUCTION_DEBUG": "auctionDebug", + "BID_VIEWABLE": "bidViewable" }, "AD_RENDER_FAILED_REASON" : { "PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocument", From 144915d3928519e87be7c1f0a4aa586feb464f34 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Wed, 20 Jan 2021 18:11:27 -0800 Subject: [PATCH 07/30] new module: bidViewability details in bidViewability.md --- modules/bidViewability.js | 52 +++++++++++++++++++++++++++++++++++++++ modules/bidviewability.md | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 modules/bidViewability.js create mode 100644 modules/bidviewability.md diff --git a/modules/bidViewability.js b/modules/bidViewability.js new file mode 100644 index 00000000000..c6a3a4af9e1 --- /dev/null +++ b/modules/bidViewability.js @@ -0,0 +1,52 @@ +// This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Bidders and Analytics adapters +// GPT API is used to find when a bid is viewable, https://developers.google.com/publisher-tag/reference#googletag.events.impressionviewableevent +// Does not work with other than GPT integration + +import { config } from '../src/config.js'; +import * as events from '../src/events.js'; +import { EVENTS } from '../src/constants.json'; +import { logWarn, isFn } from '../src/utils.js'; + +const MODULE_NAME = 'bidViewability'; +const CONFIG_FIRE_PIXELS = 'firePixels'; +const CONFIG_CUSTOM_MATCH = 'customMatchFunction'; +const BID_VURL_ARRAY = 'vurls'; + +export function init() { + + let isBidAdUnitCodeMatchingSlot = (bid, slot) => (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode); + + events.on(EVENTS.AUCTION_INIT, function(){ + // add the GPT event listener + window.googletag = window.googletag || {}; + googletag.cmd = googletag.cmd || []; + googletag.cmd.push(function(){ + googletag.pubads().addEventListener('impressionViewable', function(event) { + let slot = event.slot; + + // read the config for the module + const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; + + // supports custom match function from config + let respectiveBid = pbjs.getAllWinningBids().find( + bid => isFn(globalModuleConfig[CONFIG_CUSTOM_MATCH]) ? globalModuleConfig[CONFIG_CUSTOM_MATCH](bid, slot) : isBidAdUnitCodeMatchingSlot(bid, slot) + ); + + if(respectiveBid === undefined){ + logWarn(`bid details could not be found for ${slot.getSlotElementId()}, probable reasons: a non-prebid bid is served OR check the prebid.AdUnit.code to GPT.AdSlot relation.`); + return; + } + + // if config is enabled AND VURL array is present then execute each pixel + if(globalModuleConfig[CONFIG_FIRE_PIXELS] === true && respectiveBid.hasOwnProperty(BID_VURL_ARRAY)){ + respectiveBid[BID_VURL_ARRAY].forEach(url => (new Image()).src = url ); + } + + // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels + events.emit(EVENTS.BID_VIEWABLE, respectiveBid); + }); + }); + }); +} + +init() \ No newline at end of file diff --git a/modules/bidviewability.md b/modules/bidviewability.md new file mode 100644 index 00000000000..783652fac8f --- /dev/null +++ b/modules/bidviewability.md @@ -0,0 +1,43 @@ +# Overview + +Module Name: bidViewability + +Purpose: Track when a bid is viewable + +Maintainer: harshad.mane@pubmatic.com + +# Description +- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Bidders and Analytics adapters +- GPT API is used to find when a bid is viewable, https://developers.google.com/publisher-tag/reference#googletag.events.impressionviewableevent +- The module does not work with adserver 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``` +- When a rendered PBJS bid is viewable the module will trigger BID_VIEWABLE event, which can be consumed by bidders and analytics adapters +- For the viewable bid if ```bid.vurls type array``` param is and module config ``` firePixels: true ``` is set then the URLs mentioned in bid.vurls will be executed + +# Params +- 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 + +As both params are optional, you do not need to set config if you do not want to set value for any param + +# Example of consuming BID_VIEWABLE event +``` + pbjs.onEvent('bidViewable', function(bid){ + console.log('got bid details in bidViewable event', bid); + }); + +``` + +# Example of using config +``` + pbjs.setConfig({ + bidViewability: { + firePixels: true, + customMatchFunction: function(bid, slot){ + console.log('using custom match function....'); + return bid.adUnitCode === slot.getAdUnitPath(); + } + } + }); +``` + From eb872559dffad9cc74970d5cb0ccca815b712460 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Wed, 20 Jan 2021 18:14:14 -0800 Subject: [PATCH 08/30] Rename bidviewability.md to bidViewability.md --- modules/{bidviewability.md => bidViewability.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/{bidviewability.md => bidViewability.md} (100%) diff --git a/modules/bidviewability.md b/modules/bidViewability.md similarity index 100% rename from modules/bidviewability.md rename to modules/bidViewability.md From f07d6ab6d3c1f3a8eb1e06336e1fee3867742fd2 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Wed, 20 Jan 2021 18:33:21 -0800 Subject: [PATCH 09/30] indentation --- modules/bidViewability.js | 70 +++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index c6a3a4af9e1..75d12ee14cd 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -6,6 +6,7 @@ import { config } from '../src/config.js'; import * as events from '../src/events.js'; import { EVENTS } from '../src/constants.json'; import { logWarn, isFn } from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; const MODULE_NAME = 'bidViewability'; const CONFIG_FIRE_PIXELS = 'firePixels'; @@ -13,40 +14,39 @@ const CONFIG_CUSTOM_MATCH = 'customMatchFunction'; const BID_VURL_ARRAY = 'vurls'; export function init() { - - let isBidAdUnitCodeMatchingSlot = (bid, slot) => (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode); - - events.on(EVENTS.AUCTION_INIT, function(){ - // add the GPT event listener - window.googletag = window.googletag || {}; - googletag.cmd = googletag.cmd || []; - googletag.cmd.push(function(){ - googletag.pubads().addEventListener('impressionViewable', function(event) { - let slot = event.slot; - - // read the config for the module - const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; - - // supports custom match function from config - let respectiveBid = pbjs.getAllWinningBids().find( - bid => isFn(globalModuleConfig[CONFIG_CUSTOM_MATCH]) ? globalModuleConfig[CONFIG_CUSTOM_MATCH](bid, slot) : isBidAdUnitCodeMatchingSlot(bid, slot) - ); - - if(respectiveBid === undefined){ - logWarn(`bid details could not be found for ${slot.getSlotElementId()}, probable reasons: a non-prebid bid is served OR check the prebid.AdUnit.code to GPT.AdSlot relation.`); - return; - } - - // if config is enabled AND VURL array is present then execute each pixel - if(globalModuleConfig[CONFIG_FIRE_PIXELS] === true && respectiveBid.hasOwnProperty(BID_VURL_ARRAY)){ - respectiveBid[BID_VURL_ARRAY].forEach(url => (new Image()).src = url ); - } - - // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels - events.emit(EVENTS.BID_VIEWABLE, respectiveBid); - }); - }); - }); + let isBidAdUnitCodeMatchingSlot = (bid, slot) => (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode); + + events.on(EVENTS.AUCTION_INIT, function() { + // add the GPT event listener + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(function() { + window.googletag.pubads().addEventListener('impressionViewable', function(event) { + let slot = event.slot; + + // read the config for the module + const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; + + // supports custom match function from config + let respectiveBid = getGlobal().getAllWinningBids().find( + bid => isFn(globalModuleConfig[CONFIG_CUSTOM_MATCH]) ? globalModuleConfig[CONFIG_CUSTOM_MATCH](bid, slot) : isBidAdUnitCodeMatchingSlot(bid, slot) + ); + + if (respectiveBid === undefined) { + logWarn(`bid details could not be found for ${slot.getSlotElementId()}, probable reasons: a non-prebid bid is served OR check the prebid.AdUnit.code to GPT.AdSlot relation.`); + return; + } + + // if config is enabled AND VURL array is present then execute each pixel + if (globalModuleConfig[CONFIG_FIRE_PIXELS] === true && respectiveBid.hasOwnProperty(BID_VURL_ARRAY)) { + respectiveBid[BID_VURL_ARRAY].forEach(url => (new Image()).src = url); + } + + // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels + events.emit(EVENTS.BID_VIEWABLE, respectiveBid); + }); + }); + }); } -init() \ No newline at end of file +init() From 8b2be41b93e36ed3aa9af7ac6db4b554ca1980e7 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 09:02:22 -0800 Subject: [PATCH 10/30] using utils.triggerPixel to fire pixel --- modules/bidViewability.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 75d12ee14cd..28326ded55e 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -5,7 +5,7 @@ import { config } from '../src/config.js'; import * as events from '../src/events.js'; import { EVENTS } from '../src/constants.json'; -import { logWarn, isFn } from '../src/utils.js'; +import { logWarn, isFn, triggerPixel } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; const MODULE_NAME = 'bidViewability'; @@ -39,7 +39,7 @@ export function init() { // if config is enabled AND VURL array is present then execute each pixel if (globalModuleConfig[CONFIG_FIRE_PIXELS] === true && respectiveBid.hasOwnProperty(BID_VURL_ARRAY)) { - respectiveBid[BID_VURL_ARRAY].forEach(url => (new Image()).src = url); + respectiveBid[BID_VURL_ARRAY].forEach(url => triggerPixel(url)); } // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels From 4fe5a9a97f050a300821b0aec6229cbf343aa53c Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 10:04:09 -0800 Subject: [PATCH 11/30] split the code in smaller functions --- modules/bidViewability.js | 70 +++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 28326ded55e..c2fe3bcff24 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -12,39 +12,53 @@ const MODULE_NAME = 'bidViewability'; const CONFIG_FIRE_PIXELS = 'firePixels'; const CONFIG_CUSTOM_MATCH = 'customMatchFunction'; const BID_VURL_ARRAY = 'vurls'; +const GPT_IMPRESSION_VIEWABLE_EVENT = 'impressionViewable'; -export function init() { - let isBidAdUnitCodeMatchingSlot = (bid, slot) => (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode); +export let isBidAdUnitCodeMatchingSlot = (bid, slot) => { + return (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode); +} + +export let getMatchingWinnigBidForGPTSlot = (globalModuleConfig, slot) => { + return getGlobal().getAllWinningBids().find( + bid => isFn(globalModuleConfig[CONFIG_CUSTOM_MATCH]) + ? globalModuleConfig[CONFIG_CUSTOM_MATCH](bid, slot) + : isBidAdUnitCodeMatchingSlot(bid, slot) + ); +}; + +export let fireViewabilityPixels = (globalModuleConfig, bid) => { + if (globalModuleConfig[CONFIG_FIRE_PIXELS] === true && bid.hasOwnProperty(BID_VURL_ARRAY)) { + bid[BID_VURL_ARRAY].forEach(url => triggerPixel(url)); + } +}; + +export let logWinningBidNotFound = (slot) => { + logWarn(`bid details could not be found for ${slot.getSlotElementId()}, probable reasons: a non-prebid bid is served OR check the prebid.AdUnit.code to GPT.AdSlot relation.`); +}; + +export let gptImpressionViewableListener = (event) => { + let slot = event.slot; + // read the config for the module + const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; + // supports custom match function from config + let respectiveBid = getMatchingWinnigBidForGPTSlot(globalModuleConfig, slot) + if (respectiveBid === undefined) { + logWinningBidNotFound(slot); + return; + } + // if config is enabled AND VURL array is present then execute each pixel + fireViewabilityPixels(globalModuleConfig, respectiveBid); + // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels + events.emit(EVENTS.BID_VIEWABLE, respectiveBid); +}; - events.on(EVENTS.AUCTION_INIT, function() { +export let init = () => { + events.on(EVENTS.AUCTION_INIT, () => { // add the GPT event listener window.googletag = window.googletag || {}; window.googletag.cmd = window.googletag.cmd || []; - window.googletag.cmd.push(function() { - window.googletag.pubads().addEventListener('impressionViewable', function(event) { - let slot = event.slot; - - // read the config for the module - const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; - - // supports custom match function from config - let respectiveBid = getGlobal().getAllWinningBids().find( - bid => isFn(globalModuleConfig[CONFIG_CUSTOM_MATCH]) ? globalModuleConfig[CONFIG_CUSTOM_MATCH](bid, slot) : isBidAdUnitCodeMatchingSlot(bid, slot) - ); - - if (respectiveBid === undefined) { - logWarn(`bid details could not be found for ${slot.getSlotElementId()}, probable reasons: a non-prebid bid is served OR check the prebid.AdUnit.code to GPT.AdSlot relation.`); - return; - } - - // if config is enabled AND VURL array is present then execute each pixel - if (globalModuleConfig[CONFIG_FIRE_PIXELS] === true && respectiveBid.hasOwnProperty(BID_VURL_ARRAY)) { - respectiveBid[BID_VURL_ARRAY].forEach(url => triggerPixel(url)); - } - - // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels - events.emit(EVENTS.BID_VIEWABLE, respectiveBid); - }); + window.googletag.cmd.push(() => { + window.googletag.pubads().addEventListener(GPT_IMPRESSION_VIEWABLE_EVENT, gptImpressionViewableListener); }); }); } From 0782e1563c4ad0547c8241f9f13924ac1de58747 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 10:04:26 -0800 Subject: [PATCH 12/30] adding unit test cases --- test/spec/modules/bidViewability_spec.js | 59 ++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 test/spec/modules/bidViewability_spec.js diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js new file mode 100644 index 00000000000..a3ec64c863c --- /dev/null +++ b/test/spec/modules/bidViewability_spec.js @@ -0,0 +1,59 @@ +import * as bidViewability from 'modules/bidViewability.js'; +import { config } from 'src/config.js'; +import * as events from 'src/events.js'; +import * as utils from 'src/utils.js'; +import * as sinon from 'sinon'; +import {expect} from 'chai'; + +const GPT_SLOT = { + getAdUnitPath() { + return '/harshad/Jan/2021/'; + }, + + getSlotElementId() { + return 'DIV-1'; + } +}; + +const PBJS_WINNING_BID = { + 'adUnitCode': '/harshad/Jan/2021/', + 'bidderCode': 'pubmatic', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': 'id', + 'requestId': 1024, + 'source': 'client', + 'no_bid': false, + 'cpm': '1.1495', + 'ttl': 180, + 'creativeId': 'id', + 'netRevenue': true, + 'currency': 'USD' +}; + +describe('#bidViewability', function() { + let gptSlot; + let pbjsWinningBid; + + beforeEach(function() { + gptSlot = Object.assign({}, GPT_SLOT); + 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); + }); + }); +}); From 18abcb2061816f7ae400a2777db832c3ba2122de Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 10:11:41 -0800 Subject: [PATCH 13/30] avoiding return --- modules/bidViewability.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index c2fe3bcff24..0d67ab9c275 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -23,7 +23,7 @@ export let getMatchingWinnigBidForGPTSlot = (globalModuleConfig, slot) => { bid => isFn(globalModuleConfig[CONFIG_CUSTOM_MATCH]) ? globalModuleConfig[CONFIG_CUSTOM_MATCH](bid, slot) : isBidAdUnitCodeMatchingSlot(bid, slot) - ); + ) || null; }; export let fireViewabilityPixels = (globalModuleConfig, bid) => { @@ -42,14 +42,14 @@ export let gptImpressionViewableListener = (event) => { const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; // supports custom match function from config let respectiveBid = getMatchingWinnigBidForGPTSlot(globalModuleConfig, slot) - if (respectiveBid === undefined) { + if (respectiveBid === null) { logWinningBidNotFound(slot); - return; + } else { + // if config is enabled AND VURL array is present then execute each pixel + fireViewabilityPixels(globalModuleConfig, respectiveBid); + // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels + events.emit(EVENTS.BID_VIEWABLE, respectiveBid); } - // if config is enabled AND VURL array is present then execute each pixel - fireViewabilityPixels(globalModuleConfig, respectiveBid); - // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels - events.emit(EVENTS.BID_VIEWABLE, respectiveBid); }; export let init = () => { From 18d216625f1e60689fdc0d26001d8f0af6ec0d5c Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 11:30:24 -0800 Subject: [PATCH 14/30] a mremoved white space --- modules/bidViewability.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 0d67ab9c275..3a4d5d72af1 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -48,7 +48,7 @@ export let gptImpressionViewableListener = (event) => { // if config is enabled AND VURL array is present then execute each pixel fireViewabilityPixels(globalModuleConfig, respectiveBid); // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels - events.emit(EVENTS.BID_VIEWABLE, respectiveBid); + events.emit(EVENTS.BID_VIEWABLE, respectiveBid); } }; From f46a002881ec41d1be637b56db5ea8cec1f0d7f1 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 13:59:03 -0800 Subject: [PATCH 15/30] a minor chnage in function name and args --- modules/bidViewability.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 3a4d5d72af1..1fad0612e9c 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -20,6 +20,7 @@ export let isBidAdUnitCodeMatchingSlot = (bid, slot) => { export let getMatchingWinnigBidForGPTSlot = (globalModuleConfig, 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) @@ -36,12 +37,8 @@ export let logWinningBidNotFound = (slot) => { logWarn(`bid details could not be found for ${slot.getSlotElementId()}, probable reasons: a non-prebid bid is served OR check the prebid.AdUnit.code to GPT.AdSlot relation.`); }; -export let gptImpressionViewableListener = (event) => { - let slot = event.slot; - // read the config for the module - const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; - // supports custom match function from config - let respectiveBid = getMatchingWinnigBidForGPTSlot(globalModuleConfig, slot) +export let impressionViewableHandler = (globalModuleConfig, slot, event) => { + let respectiveBid = getMatchingWinnigBidForGPTSlot(globalModuleConfig, slot); if (respectiveBid === null) { logWinningBidNotFound(slot); } else { @@ -54,11 +51,15 @@ export let gptImpressionViewableListener = (event) => { export let init = () => { events.on(EVENTS.AUCTION_INIT, () => { + // read the config for the module + const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; // add the GPT event listener window.googletag = window.googletag || {}; window.googletag.cmd = window.googletag.cmd || []; window.googletag.cmd.push(() => { - window.googletag.pubads().addEventListener(GPT_IMPRESSION_VIEWABLE_EVENT, gptImpressionViewableListener); + window.googletag.pubads().addEventListener(GPT_IMPRESSION_VIEWABLE_EVENT, function(event) { + impressionViewableHandler(globalModuleConfig, event.slot, event); + }); }); }); } From f71742d33d70498457744f01f222693c3de22cb9 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 13:59:26 -0800 Subject: [PATCH 16/30] lots of unit test cases --- test/spec/modules/bidViewability_spec.js | 148 ++++++++++++++++++++++- 1 file changed, 146 insertions(+), 2 deletions(-) diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index a3ec64c863c..095c7a10a7c 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -3,7 +3,9 @@ import { config } from 'src/config.js'; import * as events from 'src/events.js'; import * as utils from 'src/utils.js'; import * as sinon from 'sinon'; -import {expect} from 'chai'; +import {expect, spy} from 'chai'; +import * as prebidGlobal from 'src/prebidGlobal.js'; +import { EVENTS } from 'src/constants.json'; const GPT_SLOT = { getAdUnitPath() { @@ -29,7 +31,8 @@ const PBJS_WINNING_BID = { 'ttl': 180, 'creativeId': 'id', 'netRevenue': true, - 'currency': 'USD' + 'currency': 'USD', + 'vurls': ['URL-1', 'URL-2', 'URL-3'] }; describe('#bidViewability', function() { @@ -56,4 +59,145 @@ describe('#bidViewability', function() { expect(bidViewability.isBidAdUnitCodeMatchingSlot(pbjsWinningBid, gptSlot)).to.equal(false); }); }); + + describe('getMatchingWinnigBidForGPTSlot', function() { + let winningBidsArray; + let sandbox + beforeEach(function() { + sandbox = sinon.sandbox.create(); + // mocking winningBidsArray + winningBidsArray = []; + sandbox.stub(prebidGlobal, 'getGlobal').returns({ + getAllWinningBids: function (number) { + return winningBidsArray; + } + }); + }); + + afterEach(function() { + sandbox.restore(); + }) + + it('should find a match by using customMatchFunction provided in config', function() { + // Needs config to be passed with customMatchFunction + let bidViewabilityConfig = { + customMatchFunction(bid, slot) { + return ('AD-' + slot.getAdUnitPath()) === bid.adUnitCode; + } + }; + let newWinningBid = Object.assign({}, PBJS_WINNING_BID, {adUnitCode: 'AD-' + PBJS_WINNING_BID.adUnitCode}); + // Needs pbjs.getWinningBids to be implemented with match + winningBidsArray.push(newWinningBid); + let wb = bidViewability.getMatchingWinnigBidForGPTSlot(bidViewabilityConfig, gptSlot); + expect(wb).to.deep.equal(newWinningBid); + }); + + it('should NOT find a match by using customMatchFunction provided in config', function() { + // Needs config to be passed with customMatchFunction + let 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 + let wb = bidViewability.getMatchingWinnigBidForGPTSlot(bidViewabilityConfig, gptSlot); + expect(wb).to.equal(null); + }); + + 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 + winningBidsArray.push(PBJS_WINNING_BID); + let wb = bidViewability.getMatchingWinnigBidForGPTSlot({}, 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 + let wb = bidViewability.getMatchingWinnigBidForGPTSlot({}, gptSlot); + expect(wb).to.equal(null); + }); + }); + + describe('fireViewabilityPixels', function() { + let sandbox; + let triggerPixelSpy; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('fire pixels if mentioned in module config', function() { + let moduleConfig = {firePixels: true}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0]).to.equal(url); + }); + }); + + it('DO NOT fire pixels if NOT mentioned in module config', function() { + let moduleConfig = {}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + expect(triggerPixelSpy.callCount).to.equal(0); + }); + }); + + describe('impressionViewableHandler', function() { + let sandbox; + let triggerPixelSpy; + let eventsEmitSpy; + let logWinningBidNotFoundSpy; + let winningBidsArray; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); + eventsEmitSpy = sandbox.spy(events, ['emit']); + bidViewability.logWinningBidNotFound(GPT_SLOT); + /* eslint-disable no-console */ + console.log(bidViewability); + // mocking winningBidsArray + winningBidsArray = []; + sandbox.stub(prebidGlobal, 'getGlobal').returns({ + getAllWinningBids: function (number) { + return winningBidsArray; + } + }); + }); + + afterEach(function() { + sandbox.restore(); + }) + + it('matching winning bid is found', function() { + let moduleConfig = { + firePixels: true + }; + winningBidsArray.push(PBJS_WINNING_BID); + bidViewability.impressionViewableHandler(moduleConfig, GPT_SLOT, null); + // fire pixels should be called + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0]).to.equal(url); + }); + // EVENTS.BID_VIEWABLE is triggered + let call = eventsEmitSpy.getCall(0); + expect(call.args[0]).to.equal(EVENTS.BID_VIEWABLE); + expect(call.args[1]).to.deep.equal(PBJS_WINNING_BID); + }); + + it('matching winning bid is NOT found', function() { + // fire pixels should NOT be called + expect(triggerPixelSpy.callCount).to.equal(0); + // EVENTS.BID_VIEWABLE is NOT triggered + expect(eventsEmitSpy.callCount).to.equal(0); + }); + }); }); From e5bff7dffa5ed08f8a71f7e3318edec845f378f4 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 14:56:10 -0800 Subject: [PATCH 17/30] added support to pass GDPR and USP params in vurls --- modules/bidViewability.js | 20 ++++- test/spec/modules/bidViewability_spec.js | 98 ++++++++++++++++++++++-- 2 files changed, 112 insertions(+), 6 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 1fad0612e9c..19d735d8ddf 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -7,6 +7,7 @@ import * as events from '../src/events.js'; import { EVENTS } from '../src/constants.json'; import { logWarn, isFn, triggerPixel } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; +import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; const MODULE_NAME = 'bidViewability'; const CONFIG_FIRE_PIXELS = 'firePixels'; @@ -29,7 +30,24 @@ export let getMatchingWinnigBidForGPTSlot = (globalModuleConfig, slot) => { export let fireViewabilityPixels = (globalModuleConfig, bid) => { if (globalModuleConfig[CONFIG_FIRE_PIXELS] === true && bid.hasOwnProperty(BID_VURL_ARRAY)) { - bid[BID_VURL_ARRAY].forEach(url => triggerPixel(url)); + let queryParams = {}; + + const gdprConsent = gdprDataHandler.getConsentData(); + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); } + if (gdprConsent.consentString) { queryParams.gdpr_consent = gdprConsent.consentString; } + if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; } + } + + const uspConsent = uspDataHandler.getConsentData(); + if (uspConsent) { queryParams.us_privacy = uspConsent; } + + bid[BID_VURL_ARRAY].forEach(url => { + // we are assuming that "?" will be already present in the url + // append all query params, `&key=urlEncoded(value)` + url += Object.keys(queryParams).reduce((prev, key) => prev += `&${key}=${encodeURIComponent(queryParams[key])}`, ''); + triggerPixel(url) + }); } }; diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index 095c7a10a7c..97d288b8573 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -6,6 +6,8 @@ import * as sinon from 'sinon'; import {expect, spy} from 'chai'; import * as prebidGlobal from 'src/prebidGlobal.js'; import { EVENTS } from 'src/constants.json'; +import { gdprDataHandler, uspDataHandler } from 'src/adapterManager.js'; +import parse from 'url-parse'; const GPT_SLOT = { getAdUnitPath() { @@ -32,7 +34,11 @@ const PBJS_WINNING_BID = { 'creativeId': 'id', 'netRevenue': true, 'currency': 'USD', - 'vurls': ['URL-1', 'URL-2', 'URL-3'] + 'vurls': [ + 'https://domain-1.com/end-point?a=1', + 'https://domain-1.com/end-point?a=1', + 'https://domain-1.com/end-point?a=1' + ] }; describe('#bidViewability', function() { @@ -133,6 +139,12 @@ describe('#bidViewability', function() { sandbox.restore(); }); + it('DO NOT fire pixels if NOT mentioned in module config', function() { + let moduleConfig = {}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + expect(triggerPixelSpy.callCount).to.equal(0); + }); + it('fire pixels if mentioned in module config', function() { let moduleConfig = {firePixels: true}; bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); @@ -142,11 +154,87 @@ describe('#bidViewability', function() { }); }); - it('DO NOT fire pixels if NOT mentioned in module config', function() { - let moduleConfig = {}; + it('USP: should include the us_privacy key when USP Consent is available', function () { + let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspDataHandlerStub.returns('1YYY'); + let moduleConfig = {firePixels: true}; bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); - expect(triggerPixelSpy.callCount).to.equal(0); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0].indexOf(url)).to.equal(0); + const testurl = parse(call.args[0]); + const queryObject = utils.parseQS(testurl.query); + expect(queryObject.us_privacy).to.equal('1YYY'); + }); + uspDataHandlerStub.restore(); }); + + it('USP: should not include the us_privacy key when USP Consent is not available', function () { + let moduleConfig = {firePixels: true}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0].indexOf(url)).to.equal(0); + const testurl = parse(call.args[0]); + const queryObject = utils.parseQS(testurl.query); + expect(queryObject.us_privacy).to.equal(undefined); + }); + }); + + it('GDPR: should include the GDPR keys when GDPR Consent is available', function() { + let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gdprDataHandlerStub.returns({ + gdprApplies: true, + consentString: 'consent', + addtlConsent: 'moreConsent' + }); + let moduleConfig = {firePixels: true}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0].indexOf(url)).to.equal(0); + const testurl = parse(call.args[0]); + const queryObject = utils.parseQS(testurl.query); + expect(queryObject.gdpr).to.equal('1'); + expect(queryObject.gdpr_consent).to.equal('consent'); + expect(queryObject.addtl_consent).to.equal('moreConsent'); + }); + gdprDataHandlerStub.restore(); + }); + + it('GDPR: should not include the GDPR keys when GDPR Consent is not available', function () { + let moduleConfig = {firePixels: true}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0].indexOf(url)).to.equal(0); + const testurl = parse(call.args[0]); + const queryObject = utils.parseQS(testurl.query); + expect(queryObject.gdpr).to.equal(undefined); + expect(queryObject.gdpr_consent).to.equal(undefined); + expect(queryObject.addtl_consent).to.equal(undefined); + }); + }); + + it('GDPR: should only include the GDPR keys for GDPR Consent fields with values', function () { + let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gdprDataHandlerStub.returns({ + gdprApplies: true, + consentString: 'consent' + }); + let moduleConfig = {firePixels: true}; + bidViewability.fireViewabilityPixels(moduleConfig, PBJS_WINNING_BID); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0].indexOf(url)).to.equal(0); + const testurl = parse(call.args[0]); + const queryObject = utils.parseQS(testurl.query); + expect(queryObject.gdpr).to.equal('1'); + expect(queryObject.gdpr_consent).to.equal('consent'); + expect(queryObject.addtl_consent).to.equal(undefined); + }); + gdprDataHandlerStub.restore(); + }) }); describe('impressionViewableHandler', function() { @@ -197,7 +285,7 @@ describe('#bidViewability', function() { // fire pixels should NOT be called expect(triggerPixelSpy.callCount).to.equal(0); // EVENTS.BID_VIEWABLE is NOT triggered - expect(eventsEmitSpy.callCount).to.equal(0); + expect(eventsEmitSpy.callCount).to.equal(0); }); }); }); From f6d55efc32a2799cdc95534606e8705e05c53a26 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 15:19:08 -0800 Subject: [PATCH 18/30] removed log --- test/spec/modules/bidViewability_spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index 97d288b8573..2afbeea7182 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -248,9 +248,6 @@ describe('#bidViewability', function() { sandbox = sinon.sandbox.create(); triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); eventsEmitSpy = sandbox.spy(events, ['emit']); - bidViewability.logWinningBidNotFound(GPT_SLOT); - /* eslint-disable no-console */ - console.log(bidViewability); // mocking winningBidsArray winningBidsArray = []; sandbox.stub(prebidGlobal, 'getGlobal').returns({ From a41b9cb8e28fe7e5669ee5285f404ffc4441573a Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 15:31:58 -0800 Subject: [PATCH 19/30] IE find fix --- modules/bidViewability.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 19d735d8ddf..a9ac74ff7cc 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -8,6 +8,7 @@ import { EVENTS } from '../src/constants.json'; import { logWarn, isFn, triggerPixel } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import find from 'core-js-pure/features/array/find.js'; const MODULE_NAME = 'bidViewability'; const CONFIG_FIRE_PIXELS = 'firePixels'; @@ -20,7 +21,7 @@ export let isBidAdUnitCodeMatchingSlot = (bid, slot) => { } export let getMatchingWinnigBidForGPTSlot = (globalModuleConfig, slot) => { - return getGlobal().getAllWinningBids().find( + return find(getGlobal().getAllWinningBids(), // supports custom match function from config bid => isFn(globalModuleConfig[CONFIG_CUSTOM_MATCH]) ? globalModuleConfig[CONFIG_CUSTOM_MATCH](bid, slot) From 7e77c1052a6cb4b30ccac1d3d3ece76a3bafc21e Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 16:24:50 -0800 Subject: [PATCH 20/30] Updated readme --- modules/bidViewability.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/bidViewability.md b/modules/bidViewability.md index 783652fac8f..d94a528a0af 100644 --- a/modules/bidViewability.md +++ b/modules/bidViewability.md @@ -8,11 +8,13 @@ Maintainer: harshad.mane@pubmatic.com # Description - This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Bidders and Analytics adapters -- GPT API is used to find when a bid is viewable, https://developers.google.com/publisher-tag/reference#googletag.events.impressionviewableevent +- 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 + - The module does not work with adserver 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``` - When a rendered PBJS bid is viewable the module will trigger BID_VIEWABLE event, which can be consumed by bidders and analytics adapters -- For the viewable bid if ```bid.vurls type array``` param is and module config ``` firePixels: true ``` is set then the URLs mentioned in bid.vurls will be executed +- For the viewable bid if ```bid.vurls type array``` param is and module config ``` firePixels: true ``` is set then the URLs mentioned in bid.vurls will be executed. Please note that GDPR and USP related parameters will be added to the given URLs, here we have assumed that URLs will always have "?" symbol included. # Params - firePixels [optional] [type: boolean], when set to true, will fire the urls mentioned in bid.vurls which should be array of urls @@ -41,3 +43,7 @@ As both params are optional, you do not need to set config if you do not want to }); ``` +# Please Note: +- Doesn't seems to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html , GPT's impressionViewable event is not triggered for instream-video-creative +- Works with Banner, Outsteam, Native creatives + From 9b0ce1bd158ab824c5fdf32927c61119b294ac08 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 25 Jan 2021 16:26:10 -0800 Subject: [PATCH 21/30] correction --- modules/bidViewability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bidViewability.md b/modules/bidViewability.md index d94a528a0af..963951cae15 100644 --- a/modules/bidViewability.md +++ b/modules/bidViewability.md @@ -44,6 +44,6 @@ As both params are optional, you do not need to set config if you do not want to ``` # Please Note: -- Doesn't seems to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html , GPT's impressionViewable event is not triggered for instream-video-creative +- Doesn't seems to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html as GPT's impressionViewable event is not triggered for instream-video-creative - Works with Banner, Outsteam, Native creatives From 36bc67da8c101b41715c00245aeafa209bbb7d9a Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Thu, 28 Jan 2021 15:25:48 -0800 Subject: [PATCH 22/30] added some details in md and fixed typo in function name --- modules/bidViewability.js | 4 ++-- modules/bidViewability.md | 3 +-- test/spec/modules/bidViewability_spec.js | 10 +++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index a9ac74ff7cc..618df2b18ab 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -20,7 +20,7 @@ export let isBidAdUnitCodeMatchingSlot = (bid, slot) => { return (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode); } -export let getMatchingWinnigBidForGPTSlot = (globalModuleConfig, slot) => { +export let getMatchingWinningBidForGPTSlot = (globalModuleConfig, slot) => { return find(getGlobal().getAllWinningBids(), // supports custom match function from config bid => isFn(globalModuleConfig[CONFIG_CUSTOM_MATCH]) @@ -57,7 +57,7 @@ export let logWinningBidNotFound = (slot) => { }; export let impressionViewableHandler = (globalModuleConfig, slot, event) => { - let respectiveBid = getMatchingWinnigBidForGPTSlot(globalModuleConfig, slot); + let respectiveBid = getMatchingWinningBidForGPTSlot(globalModuleConfig, slot); if (respectiveBid === null) { logWinningBidNotFound(slot); } else { diff --git a/modules/bidViewability.md b/modules/bidViewability.md index 963951cae15..80d0b13e1b6 100644 --- a/modules/bidViewability.md +++ b/modules/bidViewability.md @@ -10,7 +10,6 @@ Maintainer: harshad.mane@pubmatic.com - This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Bidders and Analytics adapters - 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 - - The module does not work with adserver 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``` - When a rendered PBJS bid is viewable the module will trigger BID_VIEWABLE event, which can be consumed by bidders and analytics adapters @@ -18,7 +17,7 @@ Refer: https://support.google.com/admanager/answer/4524488 # Params - 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 +- 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) ` As both params are optional, you do not need to set config if you do not want to set value for any param diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index 2afbeea7182..6581f013780 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -66,7 +66,7 @@ describe('#bidViewability', function() { }); }); - describe('getMatchingWinnigBidForGPTSlot', function() { + describe('getMatchingWinningBidForGPTSlot', function() { let winningBidsArray; let sandbox beforeEach(function() { @@ -94,7 +94,7 @@ describe('#bidViewability', function() { let newWinningBid = Object.assign({}, PBJS_WINNING_BID, {adUnitCode: 'AD-' + PBJS_WINNING_BID.adUnitCode}); // Needs pbjs.getWinningBids to be implemented with match winningBidsArray.push(newWinningBid); - let wb = bidViewability.getMatchingWinnigBidForGPTSlot(bidViewabilityConfig, gptSlot); + let wb = bidViewability.getMatchingWinningBidForGPTSlot(bidViewabilityConfig, gptSlot); expect(wb).to.deep.equal(newWinningBid); }); @@ -106,7 +106,7 @@ describe('#bidViewability', function() { } }; // Needs pbjs.getWinningBids to be implemented without match; winningBidsArray is set to empty in beforeEach - let wb = bidViewability.getMatchingWinnigBidForGPTSlot(bidViewabilityConfig, gptSlot); + let wb = bidViewability.getMatchingWinningBidForGPTSlot(bidViewabilityConfig, gptSlot); expect(wb).to.equal(null); }); @@ -114,14 +114,14 @@ describe('#bidViewability', function() { // Needs config to be passed without customMatchFunction // Needs pbjs.getWinningBids to be implemented with match winningBidsArray.push(PBJS_WINNING_BID); - let wb = bidViewability.getMatchingWinnigBidForGPTSlot({}, gptSlot); + let 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 - let wb = bidViewability.getMatchingWinnigBidForGPTSlot({}, gptSlot); + let wb = bidViewability.getMatchingWinningBidForGPTSlot({}, gptSlot); expect(wb).to.equal(null); }); }); From c0e7273ed90dd4a15aba9434cd7997ed8c8339bd Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Thu, 28 Jan 2021 16:48:33 -0800 Subject: [PATCH 23/30] added adapterManager.callBidViewableBidder method --- src/adapterManager.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/adapterManager.js b/src/adapterManager.js index aa67fd333c2..0ab33499dd9 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -551,4 +551,8 @@ adapterManager.callSetTargetingBidder = function(bidder, bid) { tryCallBidderMethod(bidder, 'onSetTargeting', bid); }; +adapterManager.callBidViewableBidder = function(bidder, bid) { + tryCallBidderMethod(bidder, 'onBidViewable', bid); +}; + export default adapterManager; From 20ae05609b5a8e70fd0d26e12265c87faae152d8 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Thu, 28 Jan 2021 16:49:47 -0800 Subject: [PATCH 24/30] added unit test cases for bidder.onBidViewable trigger --- modules/bidViewability.js | 4 +++- test/spec/modules/bidViewability_spec.js | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 618df2b18ab..4383db02d75 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -7,7 +7,7 @@ import * as events from '../src/events.js'; import { EVENTS } from '../src/constants.json'; import { logWarn, isFn, triggerPixel } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; -import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import adapterManager, { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; import find from 'core-js-pure/features/array/find.js'; const MODULE_NAME = 'bidViewability'; @@ -63,6 +63,8 @@ export let impressionViewableHandler = (globalModuleConfig, slot, event) => { } else { // if config is enabled AND VURL array is present then execute each pixel fireViewabilityPixels(globalModuleConfig, respectiveBid); + // trigger respective bidder's onBidViewable handler + adapterManager.callBidViewableBidder(respectiveBid.bidderCode, respectiveBid); // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels events.emit(EVENTS.BID_VIEWABLE, respectiveBid); } diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index 6581f013780..77d286705f4 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -6,7 +6,7 @@ import * as sinon from 'sinon'; import {expect, spy} from 'chai'; import * as prebidGlobal from 'src/prebidGlobal.js'; import { EVENTS } from 'src/constants.json'; -import { gdprDataHandler, uspDataHandler } from 'src/adapterManager.js'; +import adapterManager, { gdprDataHandler, uspDataHandler } from 'src/adapterManager.js'; import parse from 'url-parse'; const GPT_SLOT = { @@ -242,12 +242,14 @@ describe('#bidViewability', function() { let triggerPixelSpy; let eventsEmitSpy; let logWinningBidNotFoundSpy; + let callBidViewableBidderSpy; let winningBidsArray; beforeEach(function() { sandbox = sinon.sandbox.create(); triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); eventsEmitSpy = sandbox.spy(events, ['emit']); + callBidViewableBidderSpy = sandbox.spy(adapterManager, ['callBidViewableBidder']); // mocking winningBidsArray winningBidsArray = []; sandbox.stub(prebidGlobal, 'getGlobal').returns({ @@ -272,8 +274,12 @@ describe('#bidViewability', function() { let call = triggerPixelSpy.getCall(i); expect(call.args[0]).to.equal(url); }); + // adapterManager.callBidViewableBidder is called with required args + let call = callBidViewableBidderSpy.getCall(0); + expect(call.args[0]).to.equal(PBJS_WINNING_BID.bidderCode); + expect(call.args[1]).to.deep.equal(PBJS_WINNING_BID); // EVENTS.BID_VIEWABLE is triggered - let call = eventsEmitSpy.getCall(0); + call = eventsEmitSpy.getCall(0); expect(call.args[0]).to.equal(EVENTS.BID_VIEWABLE); expect(call.args[1]).to.deep.equal(PBJS_WINNING_BID); }); @@ -281,6 +287,8 @@ describe('#bidViewability', function() { it('matching winning bid is NOT found', function() { // fire pixels should NOT be called expect(triggerPixelSpy.callCount).to.equal(0); + // adapterManager.callBidViewableBidder is NOT called + expect(callBidViewableBidderSpy.callCount).to.equal(0); // EVENTS.BID_VIEWABLE is NOT triggered expect(eventsEmitSpy.callCount).to.equal(0); }); From 25fcf904fbf924e911a7efea124c3e6134eb7909 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Thu, 28 Jan 2021 16:51:53 -0800 Subject: [PATCH 25/30] updated md file for bidder.onBidViewable --- modules/bidViewability.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bidViewability.md b/modules/bidViewability.md index 80d0b13e1b6..58b5e0d3787 100644 --- a/modules/bidViewability.md +++ b/modules/bidViewability.md @@ -7,7 +7,7 @@ Purpose: Track when a bid is viewable Maintainer: harshad.mane@pubmatic.com # Description -- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Bidders and Analytics adapters +- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Analytics adapters, bidders will need to implement `onBidViewable` method to capture this event - 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 - The module does not work with adserver other than GAM with GPT integration From 886423af8c980261e4876ab69fc0c53ffaa6a2b1 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Thu, 28 Jan 2021 17:08:10 -0800 Subject: [PATCH 26/30] spec for callBidViewableBidder / bidder.onBidViewable --- test/spec/unit/core/adapterManager_spec.js | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 9ccdd6aef59..596787f3f9a 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -387,6 +387,38 @@ describe('adapterManager tests', function () { }); }); // end onSetTargeting + describe('onBidViewable', function () { + var criteoSpec = { onBidViewable: sinon.stub() } + var criteoAdapter = { + bidder: 'criteo', + getSpec: function() { return criteoSpec; } + } + before(function () { + config.setConfig({s2sConfig: { enabled: false }}); + }); + + beforeEach(function () { + adapterManager.bidderRegistry['criteo'] = criteoAdapter; + }); + + afterEach(function () { + delete adapterManager.bidderRegistry['criteo']; + }); + + it('should call spec\'s onBidViewable callback when callBidViewableBidder is called', function () { + const bids = [ + {bidder: 'criteo', params: {placementId: 'id'}}, + ]; + const adUnits = [{ + code: 'adUnit-code', + sizes: [[728, 90]], + bids + }]; + adapterManager.callBidViewableBidder(bids[0].bidder, bids[0]); + sinon.assert.called(criteoSpec.onBidViewable); + }); + }); // end onBidViewable + describe('S2S tests', function () { beforeEach(function () { config.setConfig({s2sConfig: CONFIG}); From 3a20584c0773068a1ec1521c7a9416cb9cbaffc4 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Thu, 28 Jan 2021 17:11:45 -0800 Subject: [PATCH 27/30] using bidder instead of bidderCode --- modules/bidViewability.js | 2 +- test/spec/modules/bidViewability_spec.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 4383db02d75..58e3cdb0073 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -64,7 +64,7 @@ export let impressionViewableHandler = (globalModuleConfig, slot, event) => { // if config is enabled AND VURL array is present then execute each pixel fireViewabilityPixels(globalModuleConfig, respectiveBid); // trigger respective bidder's onBidViewable handler - adapterManager.callBidViewableBidder(respectiveBid.bidderCode, respectiveBid); + adapterManager.callBidViewableBidder(respectiveBid.bidder, respectiveBid); // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels events.emit(EVENTS.BID_VIEWABLE, respectiveBid); } diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index 77d286705f4..b4cddeb4d1d 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -22,6 +22,7 @@ const GPT_SLOT = { const PBJS_WINNING_BID = { 'adUnitCode': '/harshad/Jan/2021/', 'bidderCode': 'pubmatic', + 'bidder': 'pubmatic', 'width': 300, 'height': 250, 'statusMessage': 'Bid available', @@ -276,7 +277,7 @@ describe('#bidViewability', function() { }); // adapterManager.callBidViewableBidder is called with required args let call = callBidViewableBidderSpy.getCall(0); - expect(call.args[0]).to.equal(PBJS_WINNING_BID.bidderCode); + expect(call.args[0]).to.equal(PBJS_WINNING_BID.bidder); expect(call.args[1]).to.deep.equal(PBJS_WINNING_BID); // EVENTS.BID_VIEWABLE is triggered call = eventsEmitSpy.getCall(0); From 5ed61e69789438cd89410e9b56aec86e05963885 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Wed, 3 Feb 2021 13:41:50 -0800 Subject: [PATCH 28/30] add ? in URLs if not present --- modules/bidViewability.js | 5 ++++- modules/bidViewability.md | 2 +- test/spec/modules/bidViewability_spec.js | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 58e3cdb0073..c262d3bd796 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -44,7 +44,10 @@ export let fireViewabilityPixels = (globalModuleConfig, bid) => { if (uspConsent) { queryParams.us_privacy = uspConsent; } bid[BID_VURL_ARRAY].forEach(url => { - // we are assuming that "?" will be already present in the url + // add '?' if not present in URL + if (Object.keys(queryParams).length > 0 && url.indexOf('?') === -1) { + url += '?'; + } // append all query params, `&key=urlEncoded(value)` url += Object.keys(queryParams).reduce((prev, key) => prev += `&${key}=${encodeURIComponent(queryParams[key])}`, ''); triggerPixel(url) diff --git a/modules/bidViewability.md b/modules/bidViewability.md index 58b5e0d3787..afae4083ef5 100644 --- a/modules/bidViewability.md +++ b/modules/bidViewability.md @@ -13,7 +13,7 @@ Refer: https://support.google.com/admanager/answer/4524488 - The module does not work with adserver 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``` - When a rendered PBJS bid is viewable the module will trigger BID_VIEWABLE event, which can be consumed by bidders and analytics adapters -- For the viewable bid if ```bid.vurls type array``` param is and module config ``` firePixels: true ``` is set then the URLs mentioned in bid.vurls will be executed. Please note that GDPR and USP related parameters will be added to the given URLs, here we have assumed that URLs will always have "?" symbol included. +- For the viewable bid if ```bid.vurls type array``` param is and module config ``` firePixels: true ``` is set then the URLs mentioned in bid.vurls will be executed. Please note that GDPR and USP related parameters will be added to the given URLs # Params - firePixels [optional] [type: boolean], when set to true, will fire the urls mentioned in bid.vurls which should be array of urls diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index b4cddeb4d1d..211dec090a5 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -37,8 +37,8 @@ const PBJS_WINNING_BID = { 'currency': 'USD', 'vurls': [ 'https://domain-1.com/end-point?a=1', - 'https://domain-1.com/end-point?a=1', - 'https://domain-1.com/end-point?a=1' + 'https://domain-2.com/end-point/', + 'https://domain-3.com/end-point?a=1' ] }; From ad2e8f837251c4cd8bfe12bbe00cdaa929e096ab Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Wed, 3 Feb 2021 15:15:27 -0800 Subject: [PATCH 29/30] added module-config.enabled flag the module will not emit BID_VIEWABLE event if module-config.enabled is not set to true --- modules/bidViewability.js | 6 ++++++ modules/bidViewability.md | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index c262d3bd796..906980b98ce 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -11,6 +11,7 @@ import adapterManager, { gdprDataHandler, uspDataHandler } from '../src/adapterM import find from 'core-js-pure/features/array/find.js'; const MODULE_NAME = 'bidViewability'; +const CONFIG_ENABLED = 'enabled'; const CONFIG_FIRE_PIXELS = 'firePixels'; const CONFIG_CUSTOM_MATCH = 'customMatchFunction'; const BID_VURL_ARRAY = 'vurls'; @@ -77,6 +78,11 @@ export let init = () => { events.on(EVENTS.AUCTION_INIT, () => { // read the config for the module const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; + // do nothing if module-config.enabled is not set to true + // this way we are adding a way for bidders to know (using pbjs.getConfig('bidViewability').enabled === true) whether this module is added in build and is enabled + if(globalModuleConfig[CONFIG_ENABLED] !== true){ + return; + } // add the GPT event listener window.googletag = window.googletag || {}; window.googletag.cmd = window.googletag.cmd || []; diff --git a/modules/bidViewability.md b/modules/bidViewability.md index afae4083ef5..78a1539fb1a 100644 --- a/modules/bidViewability.md +++ b/modules/bidViewability.md @@ -8,6 +8,7 @@ Maintainer: harshad.mane@pubmatic.com # Description - This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Analytics adapters, bidders will need to implement `onBidViewable` method to capture this event +- Bidderes can check if this module is part of the final build and whether it is enabled or not by accessing ```pbjs.getConfig('bidViewability')``` - 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 - The module does not work with adserver other than GAM with GPT integration @@ -16,11 +17,10 @@ Refer: https://support.google.com/admanager/answer/4524488 - For the viewable bid if ```bid.vurls type array``` param is and module config ``` firePixels: true ``` is set then the URLs mentioned in bid.vurls will be executed. Please note that GDPR and USP related parameters will be added to the given URLs # 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) ` -As both params are optional, you do not need to set config if you do not want to set value for any param - # Example of consuming BID_VIEWABLE event ``` pbjs.onEvent('bidViewable', function(bid){ @@ -33,6 +33,7 @@ As both params are optional, you do not need to set config if you do not want to ``` pbjs.setConfig({ bidViewability: { + enabled: true, firePixels: true, customMatchFunction: function(bid, slot){ console.log('using custom match function....'); From 3850fd3c5fa6b2db64f09a4cd7365dcd25b47d14 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Wed, 3 Feb 2021 15:24:56 -0800 Subject: [PATCH 30/30] indent --- modules/bidViewability.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 906980b98ce..c3b72cda8d4 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -80,7 +80,7 @@ export let init = () => { const globalModuleConfig = config.getConfig(MODULE_NAME) || {}; // do nothing if module-config.enabled is not set to true // this way we are adding a way for bidders to know (using pbjs.getConfig('bidViewability').enabled === true) whether this module is added in build and is enabled - if(globalModuleConfig[CONFIG_ENABLED] !== true){ + if (globalModuleConfig[CONFIG_ENABLED] !== true) { return; } // add the GPT event listener