From 8ea3f3b2727439464858bcfcbbc077d863e45c58 Mon Sep 17 00:00:00 2001 From: susyt Date: Fri, 4 Dec 2020 08:45:09 -0800 Subject: [PATCH 001/181] GumGum: sets mediaType of bidRequest depending on product id (#6066) * adds support for zone and pubId params * adds support for iriscat field * sets mediatype depending on product id * Update doc for mediaType needed for video products --- modules/gumgumBidAdapter.js | 5 ++-- modules/gumgumBidAdapter.md | 27 ++++++++++++++++- test/spec/modules/gumgumBidAdapter_spec.js | 34 +++++++++++++++------- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 3206b7e1727..2cb5ce61064 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -403,6 +403,7 @@ function interpretResponse (serverResponse, bidRequest) { } = Object.assign(defaultResponse, serverResponseBody) let data = bidRequest.data || {} let product = data.pi + let mediaType = (product === 6 || product === 7) ? VIDEO : BANNER let isTestUnit = (product === 3 && data.si === 9) let sizes = utils.parseSizesInput(bidRequest.sizes) let [width, height] = sizes[0].split('x') @@ -424,9 +425,9 @@ function interpretResponse (serverResponse, bidRequest) { bidResponses.push({ // dealId: DEAL_ID, // referrer: REFERER, - ...(product === 7 && { vastXml: markup, mediaType: VIDEO }), ad: wrapper ? getWrapperCode(wrapper, Object.assign({}, serverResponseBody, { bidRequest })) : markup, - ...(product === 6 && {ad: markup}), + ...(mediaType === VIDEO && {ad: markup, vastXml: markup}), + mediaType, cpm: isTestUnit ? 0.1 : cpm, creativeId, currency: cur || 'USD', diff --git a/modules/gumgumBidAdapter.md b/modules/gumgumBidAdapter.md index f47666e9628..7b4f0c98ea7 100644 --- a/modules/gumgumBidAdapter.md +++ b/modules/gumgumBidAdapter.md @@ -8,7 +8,10 @@ Maintainer: engineering@gumgum.com # Description -GumGum adapter for Prebid.js 1.0 +GumGum adapter for Prebid.js +Please note that both video and in-video products require a mediaType of video. +All other products (in-screen, slot, native) should have a mediaType of banner. + # Test Parameters ``` @@ -16,6 +19,11 @@ var adUnits = [ { code: 'test-div', sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, bids: [ { bidder: 'gumgum', @@ -28,6 +36,11 @@ var adUnits = [ },{ code: 'test-div', sizes: [[300, 50]], + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + }, bids: [ { bidder: 'gumgum', @@ -40,6 +53,18 @@ var adUnits = [ },{ code: 'test-div', sizes: [[300, 50]], + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + minduration: 1, + maxduration: 2, + linearity: 2, + startdelay: 1, + placement: 1, + protocols: [1, 2] + } + } bids: [ { bidder: 'gumgum', diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index 701ce9a7e81..52a3a21db4e 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { spec } from 'modules/gumgumBidAdapter.js'; +import { BANNER, VIDEO } from 'src/mediaTypes.js'; const ENDPOINT = 'https://g2.gumgum.com/hbid/imp'; const JCSI = { t: 0, rq: 8, pbv: '$prebid.version$' } @@ -462,16 +463,15 @@ describe('gumgumAdapter', function () { pi: 3 } let expectedResponse = { - 'ad': '

I am an ad

', - 'cpm': 0, - 'creativeId': 29593, - 'currency': 'USD', - 'height': '250', - 'netRevenue': true, - 'requestId': 12345, - 'width': '300', - // dealId: DEAL_ID, - // referrer: REFERER, + ad: '

I am an ad

', + cpm: 0, + creativeId: 29593, + currency: 'USD', + height: '250', + netRevenue: true, + requestId: 12345, + width: '300', + mediaType: BANNER, ttl: 60 }; @@ -552,6 +552,20 @@ describe('gumgumAdapter', function () { const decodedResponse = JSON.parse(atob(bidResponse)); expect(decodedResponse.jcsi).to.eql(JCSI); }); + + it('sets the correct mediaType depending on product', function () { + const bannerBidResponse = spec.interpretResponse({ body: serverResponse }, bidRequest)[0]; + const invideoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 6 } })[0]; + const videoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 7 } })[0]; + expect(bannerBidResponse.mediaType).to.equal(BANNER); + expect(invideoBidResponse.mediaType).to.equal(VIDEO); + expect(videoBidResponse.mediaType).to.equal(VIDEO); + }); + + it('sets a vastXml property if mediaType is video', function () { + const videoBidResponse = spec.interpretResponse({ body: serverResponse }, { ...bidRequest, data: { pi: 7 } })[0]; + expect(videoBidResponse.vastXml).to.exist; + }); }) describe('getUserSyncs', function () { const syncOptions = { From 93536f0e5aa56ec8b3284feafeee698eaa974a74 Mon Sep 17 00:00:00 2001 From: Meng <5110935+edmonl@users.noreply.github.com> Date: Fri, 4 Dec 2020 13:29:42 -0500 Subject: [PATCH 002/181] pubgenius: remove video cache key to be future-proof (#6081) --- modules/pubgeniusBidAdapter.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/pubgeniusBidAdapter.js b/modules/pubgeniusBidAdapter.js index 88e85a4fd7a..5c750e66c25 100644 --- a/modules/pubgeniusBidAdapter.js +++ b/modules/pubgeniusBidAdapter.js @@ -253,10 +253,6 @@ function interpretBid(bid) { bidResponse.vastXml = bid.adm; } - if (pbadapter.cacheKey) { - bidResponse.videoCacheKey = pbadapter.cacheKey; - } - bidResponse.mediaType = VIDEO; break; default: // banner by default From b9a4cc6eb780e79e4f5775d3519af8ad38f23d0a Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Mon, 7 Dec 2020 10:36:18 -0500 Subject: [PATCH 003/181] fix pubgenius unit test (#6090) --- test/spec/modules/pubgeniusBidAdapter_spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/spec/modules/pubgeniusBidAdapter_spec.js b/test/spec/modules/pubgeniusBidAdapter_spec.js index c0b05510707..382199dcffc 100644 --- a/test/spec/modules/pubgeniusBidAdapter_spec.js +++ b/test/spec/modules/pubgeniusBidAdapter_spec.js @@ -472,7 +472,6 @@ describe('pubGENIUS adapter', () => { ...expectedBidResponse, vastUrl: 'http://vasturl/cache?id=x', vastXml: 'fake_creative', - videoCacheKey: 'x', mediaType: VIDEO, }; From 5f6dab3008d4cca306b6d27a1d27937723fc2b89 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Mon, 7 Dec 2020 07:39:25 -0800 Subject: [PATCH 004/181] 6012: Fix for passing US Privacy string in buildVideoUrl and buildAdpodVideoUrl (#6075) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * pass USP consent string in dfpVideoUrl and dfpAdpodVideoUrl * added test cases --- modules/dfpAdServerVideo.js | 7 ++++ test/spec/modules/dfpAdServerVideo_spec.js | 43 ++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 554c44aa708..13677c90bae 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -8,6 +8,7 @@ import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, bui import { config } from '../src/config.js'; import { getHook, submodule } from '../src/hook.js'; import { auctionManager } from '../src/auctionManager.js'; +import { uspDataHandler } from '../src/adapterManager.js'; import events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; @@ -100,6 +101,9 @@ export function buildDfpVideoUrl(options) { const descriptionUrl = getDescriptionUrl(bid, options, 'params'); if (descriptionUrl) { queryParams.description_url = descriptionUrl; } + const uspConsent = uspDataHandler.getConsentData(); + if (uspConsent) { queryParams.us_privacy = uspConsent; } + return buildUrl({ protocol: 'https', host: 'securepubads.g.doubleclick.net', @@ -183,6 +187,9 @@ export function buildAdpodVideoUrl({code, params, callback} = {}) { { cust_params: encodedCustomParams } ); + const uspConsent = uspDataHandler.getConsentData(); + if (uspConsent) { queryParams.us_privacy = uspConsent; } + const masterTag = buildUrl({ protocol: 'https', host: 'securepubads.g.doubleclick.net', diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index c0ecb9cad5e..ed9c968cfa2 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -7,6 +7,7 @@ import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { targeting } from 'src/targeting.js'; import { auctionManager } from 'src/auctionManager.js'; +import { uspDataHandler } from 'src/adapterManager.js'; import * as adpod from 'modules/adpod.js'; import { server } from 'test/mocks/xhr.js'; @@ -115,6 +116,44 @@ describe('The DFP video support module', function () { expect(customParams).to.have.property('hb_cache_id', bid.videoCacheKey); }); + it('should include the us_privacy key when USP Consent is available', function () { + let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspDataHandlerStub.returns('1YYY'); + + const bidCopy = utils.deepClone(bid); + bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + hb_adid: 'ad_id', + }); + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + 'iu': 'my/adUnit' + } + })); + const queryObject = utils.parseQS(url.query); + expect(queryObject.us_privacy).to.equal('1YYY'); + uspDataHandlerStub.restore(); + }); + + it('should not include the us_privacy key when USP Consent is not available', function () { + const bidCopy = utils.deepClone(bid); + bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + hb_adid: 'ad_id', + }); + + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bidCopy, + params: { + 'iu': 'my/adUnit' + } + })); + const queryObject = utils.parseQS(url.query); + expect(queryObject.us_privacy).to.equal(undefined); + }); + describe('special targeting unit test', function () { const allTargetingData = { 'hb_format': 'video', @@ -350,6 +389,8 @@ describe('The DFP video support module', function () { it('should return masterTag url', function() { amStub.returns(getBidsReceived()); + let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + uspDataHandlerStub.returns('1YYY'); let url; parse(buildAdpodVideoUrl({ code: 'adUnitCode-1', @@ -380,10 +421,12 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('unviewed_position_start', '1'); expect(queryParams).to.have.property('url'); expect(queryParams).to.have.property('cust_params'); + expect(queryParams).to.have.property('us_privacy', '1YYY'); const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); expect(custParams).to.have.property('hb_cache_id', '123'); expect(custParams).to.have.property('hb_pb_cat_dur', '15.00_395_15s,15.00_406_30s,10.00_395_15s'); + uspDataHandlerStub.restore(); } }); From dbd0d84e30beb1547f35663e0c30d6efc45b60b0 Mon Sep 17 00:00:00 2001 From: Reinout Stevens Date: Mon, 7 Dec 2020 16:40:06 +0100 Subject: [PATCH 005/181] fix prebid server playerwidth and height (#6073) Co-authored-by: Reinout Stevens --- modules/prebidServerBidAdapter/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 7c7962781d2..d90572d1093 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -750,8 +750,8 @@ const OPEN_RTB_PROTOCOL = { if (utils.deepAccess(bid, 'ext.prebid.type') === VIDEO) { bidObject.mediaType = VIDEO; let sizes = bidRequest.sizes && bidRequest.sizes[0]; - bidObject.playerHeight = sizes[0]; - bidObject.playerWidth = sizes[1]; + bidObject.playerWidth = sizes[0]; + bidObject.playerHeight = sizes[1]; // try to get cache values from 'response.ext.prebid.cache.js' // else try 'bid.ext.prebid.targeting' as fallback From 93f393abf070ff83ce2cc9cefb376864816a1feb Mon Sep 17 00:00:00 2001 From: Paul de Rosanbo Date: Mon, 7 Dec 2020 16:40:51 +0100 Subject: [PATCH 006/181] Fix iframe __tcfapi arguments (#6058) * Fix __tcfapi declaration in iframe * Add tests for cmp declaration in iframe --- modules/consentManagement.js | 58 ++++++++++++++------- test/spec/modules/consentManagement_spec.js | 18 +++++++ 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 1060fdb5cc5..67af2baf959 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -199,29 +199,51 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) { let apiName = (cmpVersion === 2) ? '__tcfapi' : '__cmp'; + let callId = Math.random() + ''; + let callName = `${apiName}Call`; + /* Setup up a __cmp function to do the postMessage and stash the callback. This function behaves (from the caller's perspective identicially to the in-frame __cmp call */ - window[apiName] = function (cmd, arg, callback) { - let callId = Math.random() + ''; - let callName = `${apiName}Call`; - let msg = { - [callName]: { - command: cmd, - parameter: arg, - callId: callId - } - }; - if (cmpVersion !== 1) msg[callName].version = cmpVersion; + if (cmpVersion === 2) { + window[apiName] = function (cmd, cmpVersion, callback, arg) { + let msg = { + [callName]: { + command: cmd, + version: cmpVersion, + parameter: arg, + callId: callId + } + }; - cmpCallbacks[callId] = callback; - cmpFrame.postMessage(msg, '*'); - } + cmpCallbacks[callId] = callback; + cmpFrame.postMessage(msg, '*'); + } - /** when we get the return message, call the stashed callback */ - window.addEventListener('message', readPostMessageResponse, false); + /** when we get the return message, call the stashed callback */ + window.addEventListener('message', readPostMessageResponse, false); - // call CMP - window[apiName](commandName, undefined, moduleCallback); + // call CMP + window[apiName](commandName, cmpVersion, moduleCallback); + } else { + window[apiName] = function (cmd, arg, callback) { + let msg = { + [callName]: { + command: cmd, + parameter: arg, + callId: callId + } + }; + + cmpCallbacks[callId] = callback; + cmpFrame.postMessage(msg, '*'); + } + + /** when we get the return message, call the stashed callback */ + window.addEventListener('message', readPostMessageResponse, false); + + // call CMP + window[apiName](commandName, undefined, moduleCallback); + } function readPostMessageResponse(event) { let cmpDataPkgName = `${apiName}Return`; diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index cf5c578502f..5e9b0f07f46 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -537,6 +537,15 @@ describe('consentManagement', function () { // from CMP window postMessage listener. testIFramedPage('with/JSON response', false, 'encoded_consent_data_via_post_message', 1); testIFramedPage('with/String response', true, 'encoded_consent_data_via_post_message', 1); + + it('should contain correct V1 CMP definition', (done) => { + setConsentConfig(goodConfigWithAllowAuction); + requestBidsHook(() => { + const nbArguments = window.__cmp.toString().split('\n')[0].split(', ').length; + expect(nbArguments).to.equal(3); + done(); + }, {}); + }); }); describe('v2 CMP workflow for iframe pages:', function () { @@ -562,6 +571,15 @@ describe('consentManagement', function () { testIFramedPage('with/JSON response', false, 'abc12345234', 2); testIFramedPage('with/String response', true, 'abc12345234', 2); + + it('should contain correct v2 CMP definition', (done) => { + setConsentConfig(goodConfigWithAllowAuction); + requestBidsHook(() => { + const nbArguments = window.__tcfapi.toString().split('\n')[0].split(', ').length; + expect(nbArguments).to.equal(4); + done(); + }, {}); + }); }); }); From 12d55b951decf7f2786f3cae18d2fa34df76d4c2 Mon Sep 17 00:00:00 2001 From: Samuel Adu Date: Tue, 8 Dec 2020 18:50:33 +0000 Subject: [PATCH 007/181] Feature/vmuid connectid rebrand (#6045) * Key name change from vmuid to vmcid * Change the userId key name to vmconnectid * Support new key in response payload + remain backward compatible. Co-authored-by: slimkrazy --- modules/userId/eids.js | 4 +-- modules/verizonMediaIdSystem.js | 11 +++--- modules/verizonMediaSystemId.md | 4 +-- test/spec/modules/aolBidAdapter_spec.js | 2 +- .../spec/modules/verizonMediaIdSystem_spec.js | 34 +++++++++++++++---- 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 2c627416341..d714d09962d 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -158,8 +158,8 @@ const USER_IDS_CONFIG = { atype: 1 }, - // Verizon Media - 'vmuid': { + // Verizon Media ConnectID + 'connectid': { source: 'verizonmedia.com', atype: 1 }, diff --git a/modules/verizonMediaIdSystem.js b/modules/verizonMediaIdSystem.js index 617561765cc..f39909f6666 100644 --- a/modules/verizonMediaIdSystem.js +++ b/modules/verizonMediaIdSystem.js @@ -12,7 +12,7 @@ import * as utils from '../src/utils.js'; const MODULE_NAME = 'verizonMediaId'; const VENDOR_ID = 25; const PLACEHOLDER = '__PIXEL_ID__'; -const VMUID_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`; +const VMCID_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`; function isEUConsentRequired(consentData) { return !!(consentData && consentData.gdpr && consentData.gdpr.gdprApplies); @@ -33,13 +33,14 @@ export const verizonMediaIdSubmodule = { /** * decode the stored id value for passing to bid requests * @function - * @returns {{vmuid: string} | undefined} + * @returns {{connectid: string} | undefined} */ decode(value) { - return (value && typeof value.vmuid === 'string') ? {vmuid: value.vmuid} : undefined; + return (typeof value === 'object' && (value.connectid || value.vmuid)) + ? {connectid: value.connectid || value.vmuid} : undefined; }, /** - * get the VerizonMedia Id + * Gets the Verizon Media Connect ID * @function * @param {SubmoduleConfig} [config] * @param {ConsentData} [consentData] @@ -83,7 +84,7 @@ export const verizonMediaIdSubmodule = { callback(); } }; - const endpoint = VMUID_ENDPOINT.replace(PLACEHOLDER, params.pixelId); + const endpoint = VMCID_ENDPOINT.replace(PLACEHOLDER, params.pixelId); let url = `${params.endpoint || endpoint}?${utils.formatQS(data)}`; verizonMediaIdSubmodule.getAjaxFn()(url, callbacks, null, {method: 'GET', withCredentials: true}); }; diff --git a/modules/verizonMediaSystemId.md b/modules/verizonMediaSystemId.md index 8d0e0bddaa9..c0d315dc754 100644 --- a/modules/verizonMediaSystemId.md +++ b/modules/verizonMediaSystemId.md @@ -10,9 +10,9 @@ pbjs.setConfig({ userIds: [{ name: 'verizonMediaId', storage: { - name: 'vmuid', + name: 'vmcid', type: 'html5', - expires: 30 + expires: 15 }, params: { pixelId: 58776, diff --git a/test/spec/modules/aolBidAdapter_spec.js b/test/spec/modules/aolBidAdapter_spec.js index 11e1a317b70..8e74e19f420 100644 --- a/test/spec/modules/aolBidAdapter_spec.js +++ b/test/spec/modules/aolBidAdapter_spec.js @@ -93,7 +93,7 @@ describe('AolAdapter', function () { const USER_ID_DATA = { criteoId: SUPPORTED_USER_ID_SOURCES['criteo.com'], - vmuid: SUPPORTED_USER_ID_SOURCES['verizonmedia.com'], + connectid: SUPPORTED_USER_ID_SOURCES['verizonmedia.com'], idl_env: SUPPORTED_USER_ID_SOURCES['liveramp.com'], lipb: { lipbid: SUPPORTED_USER_ID_SOURCES['liveintent.com'], diff --git a/test/spec/modules/verizonMediaIdSystem_spec.js b/test/spec/modules/verizonMediaIdSystem_spec.js index a30be5a2569..c5d743235d6 100644 --- a/test/spec/modules/verizonMediaIdSystem_spec.js +++ b/test/spec/modules/verizonMediaIdSystem_spec.js @@ -165,12 +165,34 @@ describe('Verizon Media ID Submodule', () => { }); describe('decode()', () => { - const VALID_API_RESPONSE = { - vmuid: '1234' - }; - it('should return a newly constructed object with the vmuid property', () => { - expect(verizonMediaIdSubmodule.decode(VALID_API_RESPONSE)).to.deep.equal(VALID_API_RESPONSE); - expect(verizonMediaIdSubmodule.decode(VALID_API_RESPONSE)).to.not.equal(VALID_API_RESPONSE); + const VALID_API_RESPONSES = [{ + key: 'vmiud', + expected: '1234', + payload: { + vmuid: '1234' + } + }, + { + key: 'connectid', + expected: '4567', + payload: { + connectid: '4567' + } + }, + { + key: 'both', + expected: '4567', + payload: { + vmuid: '1234', + connectid: '4567' + } + }]; + VALID_API_RESPONSES.forEach(responseData => { + it('should return a newly constructed object with the connectid for a payload with ${responseData.key} key(s)', () => { + expect(verizonMediaIdSubmodule.decode(responseData.payload)).to.deep.equal( + {connectid: responseData.expected} + ); + }); }); [{}, '', {foo: 'bar'}].forEach((response) => { From 4c047f91e1c3bbc0b495427630f031753c561a42 Mon Sep 17 00:00:00 2001 From: John Salis Date: Tue, 8 Dec 2020 23:48:34 -0500 Subject: [PATCH 008/181] Beachfront adapter: Add banner tagid param (#6086) * add tagid to banner request * bump version * update test case * run tests Co-authored-by: John Salis --- modules/beachfrontBidAdapter.js | 3 ++- test/spec/modules/beachfrontBidAdapter_spec.js | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 4b30f47e2cf..44755a78864 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -6,7 +6,7 @@ import { VIDEO, BANNER } from '../src/mediaTypes.js'; import find from 'core-js-pure/features/array/find.js'; import includes from 'core-js-pure/features/array/includes.js'; -const ADAPTER_VERSION = '1.14'; +const ADAPTER_VERSION = '1.15'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; @@ -387,6 +387,7 @@ function createBannerRequestData(bids, bidderRequest) { slot: bid.adUnitCode, id: getBannerBidParam(bid, 'appId'), bidfloor: getBannerBidParam(bid, 'bidfloor'), + tagid: getBannerBidParam(bid, 'tagid'), sizes: getBannerSizes(bid) }; }); diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index 661780ffac0..43c71dd6349 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -336,6 +336,7 @@ describe('BeachfrontAdapter', function () { const width = 300; const height = 250; const bidRequest = bidRequests[0]; + bidRequest.params.tagid = '7cd7a7b4-ef3f-4aeb-9565-3627f255fa10'; bidRequest.mediaTypes = { banner: { sizes: [ width, height ] @@ -354,6 +355,7 @@ describe('BeachfrontAdapter', function () { slot: bidRequest.adUnitCode, id: bidRequest.params.appId, bidfloor: bidRequest.params.bidfloor, + tagid: bidRequest.params.tagid, sizes: [{ w: width, h: height }] } ]); From cbd1169204dbf7f0bf0a3ea8fea4a85241f8dc11 Mon Sep 17 00:00:00 2001 From: jxdeveloper1 <71084096+jxdeveloper1@users.noreply.github.com> Date: Wed, 9 Dec 2020 12:50:47 +0800 Subject: [PATCH 009/181] changed events endpoint (#6088) --- modules/jixieBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index d3ae090964c..7c6e0027482 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -9,7 +9,7 @@ import { Renderer } from '../src/Renderer.js'; export const storage = getStorageManager(); const BIDDER_CODE = 'jixie'; -const EVENTS_URL = 'https://jxhbtrackers.azurewebsites.net/sync/evt?'; +const EVENTS_URL = 'https://hbtra.jixie.io/sync/hb?'; const JX_OUTSTREAM_RENDERER_URL = 'https://scripts.jixie.io/jxhboutstream.js'; const REQUESTS_URL = 'https://hb.jixie.io/v2/hbpost'; const sidTTLMins_ = 30; From 478e0455b952e7f67e3b6124a862c8a871ee618b Mon Sep 17 00:00:00 2001 From: Brandon Ling <51931757+blingster7@users.noreply.github.com> Date: Wed, 9 Dec 2020 02:29:33 -0500 Subject: [PATCH 010/181] [Triplelift] Fix FPD key-value pairs logic (#6065) * follow spec to parse fpd * ad unit support stub * adunit method * ad unit etc * typo * fix test * typo * change to const --- modules/tripleliftBidAdapter.js | 31 +++++++++++++++++-- .../spec/modules/tripleliftBidAdapter_spec.js | 31 ++++++++++++++----- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index d54d76efb41..4679c1faf62 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -122,6 +122,9 @@ function _buildPostBody(bidRequests) { } else if (bidRequest.mediaTypes.banner) { imp.banner = { format: _sizes(bidRequest.sizes) }; }; + if (!utils.isEmpty(bidRequest.fpd)) { + imp.fpd = _getAdUnitFpd(bidRequest.fpd); + } return imp; }); @@ -183,12 +186,34 @@ function _getFloor (bid) { } function _getGlobalFpd() { - let fpd = {}; + const fpd = {}; + const context = {} + const user = {}; + const fpdContext = Object.assign({}, config.getConfig('fpd.context')); const fpdUser = Object.assign({}, config.getConfig('fpd.user')); - _addEntries(fpd, fpdContext); - _addEntries(fpd, fpdUser); + _addEntries(context, fpdContext); + _addEntries(user, fpdUser); + + if (!utils.isEmpty(context)) { + fpd.context = context; + } + if (!utils.isEmpty(user)) { + fpd.user = user; + } + return fpd; +} + +function _getAdUnitFpd(adUnitFpd) { + const fpd = {}; + const context = {}; + + _addEntries(context, adUnitFpd.context); + + if (!utils.isEmpty(context)) { + fpd.context = context; + } return fpd; } diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index b417876f276..82578424027 100644 --- a/test/spec/modules/tripleliftBidAdapter_spec.js +++ b/test/spec/modules/tripleliftBidAdapter_spec.js @@ -143,6 +143,14 @@ describe('triplelift adapter', function () { auctionId: '1d1a030790a475', userId: {}, schain, + fpd: { + context: { + pbAdSlot: 'homepage-top-rect', + data: { + adUnitSpecificAttribute: 123 + } + } + } }, { bidder: 'triplelift', @@ -597,17 +605,19 @@ describe('triplelift adapter', function () { const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); expect(request.data.imp[0].floor).to.equal(1.99); }); - it('should send fpd on root level ext if kvps are available', function() { + it('should send global config fpd if kvps are available', function() { const sens = null; const category = ['news', 'weather', 'hurricane']; const pmp_elig = 'true'; const fpd = { context: { - pmp_elig, - category, + pmp_elig: pmp_elig, + data: { + category: category + } }, user: { - sens, + sens: sens, } } sandbox.stub(config, 'getConfig').callsFake(key => { @@ -618,9 +628,16 @@ describe('triplelift adapter', function () { }); const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); const { data: payload } = request; - expect(payload.ext.fpd).to.not.haveOwnProperty('sens'); - expect(payload.ext.fpd).to.haveOwnProperty('category'); - expect(payload.ext.fpd).to.haveOwnProperty('pmp_elig'); + expect(payload.ext.fpd.user).to.not.exist; + expect(payload.ext.fpd.context.data).to.haveOwnProperty('category'); + expect(payload.ext.fpd.context).to.haveOwnProperty('pmp_elig'); + }); + it('should send ad unit fpd if kvps are available', function() { + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[0].fpd.context).to.haveOwnProperty('pbAdSlot'); + expect(request.data.imp[0].fpd.context).to.haveOwnProperty('data'); + expect(request.data.imp[0].fpd.context.data).to.haveOwnProperty('adUnitSpecificAttribute'); + expect(request.data.imp[1].fpd).to.not.exist; }); }); From dd51f245abe600801703c70ca688fb55181a8d78 Mon Sep 17 00:00:00 2001 From: nyakove <43004249+nyakove@users.noreply.github.com> Date: Wed, 9 Dec 2020 15:40:23 +0200 Subject: [PATCH 011/181] Add new adapter - adWMGBidAdapter (#6070) * Add adWMG Bid Adapter * Fix unit tests Co-authored-by: Mikhail Dykun --- modules/adWMGBidAdapter.js | 297 ++++++++++++++++++++++ modules/adWMGBidAdapter.md | 34 +++ test/spec/modules/adWMGBidAdapter_spec.js | 292 +++++++++++++++++++++ 3 files changed, 623 insertions(+) create mode 100644 modules/adWMGBidAdapter.js create mode 100644 modules/adWMGBidAdapter.md create mode 100644 test/spec/modules/adWMGBidAdapter_spec.js diff --git a/modules/adWMGBidAdapter.js b/modules/adWMGBidAdapter.js new file mode 100644 index 00000000000..3a0a8a22274 --- /dev/null +++ b/modules/adWMGBidAdapter.js @@ -0,0 +1,297 @@ +'use strict'; + +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adWMG'; +const ENDPOINT = 'https://rtb.adwmg.com/prebid'; +let SYNC_ENDPOINT = 'https://rtb.adwmg.com/cphb.html?'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['wmg'], + supportedMediaTypes: [BANNER], + isBidRequestValid: (bid) => { + if (bid.bidder !== BIDDER_CODE) { + return false; + } + + if (!(bid.params.publisherId)) { + return false; + } + + return true; + }, + buildRequests: (validBidRequests, bidderRequest) => { + const timeout = bidderRequest.timeout || 0; + const debug = config.getConfig('debug') || false; + const referrer = bidderRequest.refererInfo.referer; + const locale = window.navigator.language && window.navigator.language.length > 0 ? window.navigator.language.substr(0, 2) : ''; + const domain = config.getConfig('publisherDomain') || (window.location && window.location.host ? window.location.host : ''); + const ua = window.navigator.userAgent.toLowerCase(); + const additional = spec.parseUserAgent(ua); + + return validBidRequests.map(bidRequest => { + const adUnit = { + code: bidRequest.adUnitCode, + bids: { + bidder: bidRequest.bidder, + params: bidRequest.params + }, + mediaTypes: bidRequest.mediaTypes + }; + + if (bidRequest.hasOwnProperty('sizes') && bidRequest.sizes.length > 0) { + adUnit.sizes = bidRequest.sizes; + } + + const request = { + auctionId: bidRequest.auctionId, + requestId: bidRequest.bidId, + bidRequestsCount: bidRequest.bidRequestsCount, + bidderRequestId: bidRequest.bidderRequestId, + transactionId: bidRequest.transactionId, + referrer: referrer, + timeout: timeout, + adUnit: adUnit, + locale: locale, + domain: domain, + os: additional.os, + osv: additional.osv, + devicetype: additional.devicetype + }; + + if (bidderRequest.gdprConsent) { + request.gdpr = { + applies: bidderRequest.gdprConsent.gdprApplies, + consentString: bidderRequest.gdprConsent.consentString + }; + } + + /* if (bidderRequest.uspConsent) { + request.uspConsent = bidderRequest.uspConsent; + } + */ + if (bidRequest.userId && bidRequest.userId.pubcid) { + request.userId = { + pubcid: bidRequest.userId.pubcid + }; + } + + if (debug) { + request.debug = debug; + } + + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(request) + } + }); + }, + interpretResponse: (serverResponse) => { + const bidResponses = []; + + if (serverResponse.body) { + const response = serverResponse.body; + const bidResponse = { + requestId: response.requestId, + cpm: response.cpm, + width: response.width, + height: response.height, + creativeId: response.creativeId, + currency: response.currency, + netRevenue: response.netRevenue, + ttl: response.ttl, + ad: response.ad, + }; + bidResponses.push(bidResponse); + } + + return bidResponses; + }, + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + if (gdprConsent) { + SYNC_ENDPOINT = utils.tryAppendQueryString(SYNC_ENDPOINT, 'gdpr', (gdprConsent.gdprApplies ? 1 : 0)); + } + + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + SYNC_ENDPOINT = utils.tryAppendQueryString(SYNC_ENDPOINT, 'gdpr_consent', gdprConsent.consentString); + } + + /* if (uspConsent) { + SYNC_ENDPOINT = utils.tryAppendQueryString(SYNC_ENDPOINT, 'us_privacy', uspConsent); + } */ + let syncs = []; + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: SYNC_ENDPOINT + }); + } + return syncs; + }, + parseUserAgent: (ua) => { + function detectDevice() { + if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i + .test(ua.toLowerCase())) { + return 5; + } + if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i + .test(ua.toLowerCase())) { + return 4; + } + if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i + .test(ua.toLowerCase())) { + return 3; + } + return 2; + } + + function detectOs() { + const module = { + options: [], + header: [navigator.platform, ua, navigator.appVersion, navigator.vendor, window.opera], + dataos: [{ + name: 'Windows Phone', + value: 'Windows Phone', + version: 'OS' + }, + { + name: 'Windows', + value: 'Win', + version: 'NT' + }, + { + name: 'iOS', + value: 'iPhone', + version: 'OS' + }, + { + name: 'iOS', + value: 'iPad', + version: 'OS' + }, + { + name: 'Kindle', + value: 'Silk', + version: 'Silk' + }, + { + name: 'Android', + value: 'Android', + version: 'Android' + }, + { + name: 'PlayBook', + value: 'PlayBook', + version: 'OS' + }, + { + name: 'BlackBerry', + value: 'BlackBerry', + version: '/' + }, + { + name: 'Macintosh', + value: 'Mac', + version: 'OS X' + }, + { + name: 'Linux', + value: 'Linux', + version: 'rv' + }, + { + name: 'Palm', + value: 'Palm', + version: 'PalmOS' + } + ], + init: function () { + var agent = this.header.join(' '); + var os = this.matchItem(agent, this.dataos); + return { + os + }; + }, + + getVersion: function (name, version) { + if (name === 'Windows') { + switch (parseFloat(version).toFixed(1)) { + case '5.0': + return '2000'; + case '5.1': + return 'XP'; + case '5.2': + return 'Server 2003'; + case '6.0': + return 'Vista'; + case '6.1': + return '7'; + case '6.2': + return '8'; + case '6.3': + return '8.1'; + default: + return version || 'other'; + } + } else return version || 'other'; + }, + + matchItem: function (string, data) { + var i = 0; + var j = 0; + var regex, regexv, match, matches, version; + + for (i = 0; i < data.length; i += 1) { + regex = new RegExp(data[i].value, 'i'); + match = regex.test(string); + if (match) { + regexv = new RegExp(data[i].version + '[- /:;]([\\d._]+)', 'i'); + matches = string.match(regexv); + version = ''; + if (matches) { + if (matches[1]) { + matches = matches[1]; + } + } + if (matches) { + matches = matches.split(/[._]+/); + for (j = 0; j < matches.length; j += 1) { + if (j === 0) { + version += matches[j] + '.'; + } else { + version += matches[j]; + } + } + } else { + version = 'other'; + } + return { + name: data[i].name, + version: this.getVersion(data[i].name, version) + }; + } + } + return { + name: 'unknown', + version: 'other' + }; + } + }; + + var e = module.init(); + + return { + os: e.os.name || '', + osv: e.os.version || '' + } + } + + return {devicetype: detectDevice(), os: detectOs().os, osv: detectOs().osv} + } +} +registerBidder(spec); diff --git a/modules/adWMGBidAdapter.md b/modules/adWMGBidAdapter.md new file mode 100644 index 00000000000..8c277b803db --- /dev/null +++ b/modules/adWMGBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: adWMG Adapter +Module Type: Bidder Adapter +Maintainer: wbid@adwmg.com +``` + +# Description + +Module that connects to adWMG demand sources to fetch bids. Supports 'banner' ad format. + +# Bid Parameters + +| Key | Required | Example | Description | +| --- | -------- | ------- | ----------- | +| `publisherId` | yes | `'5cebea3c9eea646c7b623d5e'` | publisher ID from WMG Dashboard | +| `IABCategories` | no | `['IAB1', 'IAB5']` | IAB ad categories for adUnit | + + +# Test Parameters + +```javascript +var adUnits = [{ + code: 'wmg-test-div', + sizes: [[300, 250]], + bids: [{ + bidder: 'adWMG', + params: { + publisherId: '5cebea3c9eea646c7b623d5e', + IABCategories: ['IAB1', 'IAB5'] + }, + }] +}] \ No newline at end of file diff --git a/test/spec/modules/adWMGBidAdapter_spec.js b/test/spec/modules/adWMGBidAdapter_spec.js new file mode 100644 index 00000000000..5c2364d454c --- /dev/null +++ b/test/spec/modules/adWMGBidAdapter_spec.js @@ -0,0 +1,292 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adWMGBidAdapter.js'; +import { config } from 'src/config.js'; + +describe('adWMGBidAdapter', function () { + describe('isBidRequestValid', function () { + let bid; + beforeEach(function() { + bid = { + bidder: 'adWMG', + params: { + publisherId: '5cebea3c9eea646c7b623d5e' + }, + mediaTypes: { + banner: { + size: [[300, 250]] + } + } + }; + }); + + it('should return true when valid bid request is set', function() { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when bidder is not set to "adWMG"', function() { + bid.bidder = 'bidder'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when \'publisherId\' param are not set', function() { + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('parseUserAgent', function() { + let ua_desktop, ua_mobile, ua_tv, ua_tablet; + beforeEach(function() { + ua_desktop = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'; + ua_tv = 'Mozilla/5.0 (Linux; NetCast; U) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.31 SmartTV/7.0'; + ua_mobile = 'Mozilla/5.0 (Linux; Android 7.0; SAMSUNG SM-G610M Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/7.2 Chrome/59.0.3071.125 Mobile Safari/537.36'; + ua_tablet = 'Mozilla/5.0 (iPad; CPU OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53'; + }); + + it('should return correct device type: desktop', function() { + let userDeviceInfo = spec.parseUserAgent(ua_desktop); + expect(userDeviceInfo.devicetype).to.equal(2); + }); + + it('should return correct device type: TV', function() { + let userDeviceInfo = spec.parseUserAgent(ua_tv); + expect(userDeviceInfo.devicetype).to.equal(3); + }); + + it('should return correct device type: mobile', function() { + let userDeviceInfo = spec.parseUserAgent(ua_mobile); + expect(userDeviceInfo.devicetype).to.equal(4); + }); + + it('should return correct device type: tablet', function() { + let userDeviceInfo = spec.parseUserAgent(ua_tablet); + expect(userDeviceInfo.devicetype).to.equal(5); + }); + + it('should return correct OS name', function() { + let userDeviceInfo = spec.parseUserAgent(ua_desktop); + expect(userDeviceInfo.os).to.equal('Windows'); + }); + + it('should return correct OS version', function() { + let userDeviceInfo = spec.parseUserAgent(ua_desktop); + expect(userDeviceInfo.osv).to.equal('10.0'); + }); + }); + + describe('buildRequests', function () { + let bidRequests; + beforeEach(function() { + bidRequests = [ + { + bidder: 'adWMG', + adUnitCode: 'adwmg-test-ad', + auctionId: 'test-auction-id', + bidId: 'test-bid-id', + bidRequestsCount: 1, + bidderRequestId: 'bidderrequestid123', + transactionId: 'transaction-id-123', + sizes: [[300, 250]], + requestId: 'requestid123', + params: { + floorPrice: 100, + currency: 'USD' + }, + mediaTypes: { + banner: { + size: [[300, 250]] + } + }, + userId: { + pubcid: 'pubc-id-123' + } + }, { + bidder: 'adWMG', + adUnitCode: 'adwmg-test-ad-2', + auctionId: 'test-auction-id-2', + bidId: 'test-bid-id-2', + bidRequestsCount: 1, + bidderRequestId: 'bidderrequestid456', + transactionId: 'transaction-id-456', + sizes: [[320, 50]], + requestId: 'requestid456', + params: { + floorPrice: 100, + currency: 'USD' + }, + mediaTypes: { + banner: { + size: [[320, 50]] + } + }, + userId: { + pubcid: 'pubc-id-456' + } + } + ]; + }); + + let bidderRequest = { + refererInfo: { + referer: 'https://test.com' + }, + gdprConsent: { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + } + }; + + it('should not contain a sizes when sizes is not set', function() { + delete bidRequests[0].sizes; + delete bidRequests[1].sizes; + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).sizes).to.be.an('undefined'); + expect(JSON.parse(requests[1].data).sizes).to.be.an('undefined'); + }); + + it('should not contain a userId when userId is not set', function() { + delete bidRequests[0].userId; + delete bidRequests[1].userId; + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).userId).to.be.an('undefined'); + expect(JSON.parse(requests[1].data).userId).to.be.an('undefined'); + }); + + it('should have a post method', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests[0].method).to.equal('POST'); + expect(requests[1].method).to.equal('POST'); + }); + + it('should contain a request id equals to the bid id', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).requestId).to.equal(bidRequests[0].bidId); + expect(JSON.parse(requests[1].data).requestId).to.equal(bidRequests[1].bidId); + }); + + it('should have an url that match the default endpoint', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests[0].url).to.equal('https://rtb.adwmg.com/prebid'); + expect(requests[1].url).to.equal('https://rtb.adwmg.com/prebid'); + }); + + it('should contain GDPR consent data if GDPR set', function() { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).gdpr.applies).to.be.true; + expect(JSON.parse(requests[0].data).gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); + expect(JSON.parse(requests[1].data).gdpr.applies).to.be.true; + expect(JSON.parse(requests[1].data).gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); + }) + + it('should not contain GDPR consent data if GDPR not set', function() { + delete bidderRequest.gdprConsent; + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).gdpr).to.be.an('undefined'); + expect(JSON.parse(requests[1].data).gdpr).to.be.an('undefined'); + }) + + it('should set debug mode in requests if enabled', function() { + sinon.stub(config, 'getConfig').withArgs('debug').returns(true); + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(JSON.parse(requests[0].data).debug).to.be.true; + expect(JSON.parse(requests[1].data).debug).to.be.true; + config.getConfig.restore(); + }) + }); + + describe('interpretResponse', function () { + let serverResponse; + beforeEach(function() { + serverResponse = { + body: { + 'requestId': 'request-id', + 'cpm': 100, + 'width': 300, + 'height': 250, + 'ad': '
ad
', + 'ttl': 300, + 'creativeId': 'creative-id', + 'netRevenue': true, + 'currency': 'USD' + } + }; + }); + + it('should return a valid response', () => { + var responses = spec.interpretResponse(serverResponse); + expect(responses).to.be.an('array').that.is.not.empty; + + let response = responses[0]; + expect(response).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency'); + expect(response.requestId).to.equal('request-id'); + expect(response.cpm).to.equal(100); + expect(response.width).to.equal(300); + expect(response.height).to.equal(250); + expect(response.ad).to.equal('
ad
'); + expect(response.ttl).to.equal(300); + expect(response.creativeId).to.equal('creative-id'); + expect(response.netRevenue).to.be.true; + expect(response.currency).to.equal('USD'); + }); + + it('should return an empty array when serverResponse is empty', () => { + serverResponse = {}; + var responses = spec.interpretResponse(serverResponse); + expect(responses).to.deep.equal([]); + }); + }); + + describe('getUserSyncs', function () { + it('should return nothing when sync is disabled', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': false + }; + + let syncs = spec.getUserSyncs(syncOptions); + expect(syncs).to.deep.equal([]); + }); + + it('should register iframe sync when only iframe is enabled', function () { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': false + }; + + let syncs = spec.getUserSyncs(syncOptions); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).includes('https://rtb.adwmg.com/cphb.html?'); + }); + + it('should register iframe sync when iframe and image are enabled', function () { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': true + }; + + let syncs = spec.getUserSyncs(syncOptions); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).includes('https://rtb.adwmg.com/cphb.html?'); + }); + + it('should send GDPR consent if enabled', function() { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': true + }; + const gdprConsent = { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }; + const serverResponse = {}; + let syncs = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent); + expect(syncs[0].url).includes('gdpr=1'); + expect(syncs[0].url).includes(`gdpr_consent=${gdprConsent.consentString}`); + }); + }); +}); From 9143962e09d3fcb00f7b9b23cff7d74be4502332 Mon Sep 17 00:00:00 2001 From: Stephen Johnston Date: Wed, 9 Dec 2020 09:45:14 -0500 Subject: [PATCH 012/181] Add PubWise Bid Adapter (#6044) * updates for first cut at bidder * fix up height and width * adds test spec * remove hello_world from commit' * updates to support native and fix issues found in initial review * fix handling of new node in response for native vs banner * updates to handle OpenRTB base * updates to support RTB style calls * updates to get back to parity * updates to testing * updates to test media type handling * updates to handling testing * updates to testing * remove report file * updates to fix up unit/spec tests * updates to fix up unit/spec tests * updates to fix up unit/spec tests * updates to handling of gdpr * Delete hello_world.html * remove hellow-world-sample * Pubwise 481 (#7) * updates to support PubWise bid adapter, test cases and documentation * updates to fix param tes * Pubwise 481 (#8) * fixes for unit testing * remove unused variables and params * Updates to Remove Unused Vars (#9) * remove unused vars * updates to fix up used and unsused params * updates to fix up used and unsused params (#10) * updates to fix up used and unsused params * updates to remove usersync and add gvlid * Pubwise 481 (#11) * updates to remove usersync, add https, and add gvlid * Update pubwiseBidAdapter.js * updates to remove json, to remove options hit --- modules/pubwiseBidAdapter.js | 777 ++++++++++++++++++++ modules/pubwiseBidAdapter.md | 78 ++ test/spec/modules/pubwiseBidAdapter_spec.js | 575 +++++++++++++++ 3 files changed, 1430 insertions(+) create mode 100644 modules/pubwiseBidAdapter.js create mode 100644 modules/pubwiseBidAdapter.md create mode 100644 test/spec/modules/pubwiseBidAdapter_spec.js diff --git a/modules/pubwiseBidAdapter.js b/modules/pubwiseBidAdapter.js new file mode 100644 index 00000000000..f450a8bede8 --- /dev/null +++ b/modules/pubwiseBidAdapter.js @@ -0,0 +1,777 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; +const VERSION = '0.1.0'; +const GVLID = 842; +const NET_REVENUE = true; +const UNDEFINED = undefined; +const DEFAULT_CURRENCY = 'USD'; +const AUCTION_TYPE = 1; +const BIDDER_CODE = 'pwbid'; +const ENDPOINT_URL = 'https://bid.pubwise.io/prebid'; +const DEFAULT_WIDTH = 0; +const DEFAULT_HEIGHT = 0; +const PREBID_NATIVE_HELP_LINK = 'https://prebid.org/dev-docs/show-native-ads.html'; +// const USERSYNC_URL = '//127.0.0.1:8080/usersync' + +const CUSTOM_PARAMS = { + 'gender': '', // User gender + 'yob': '', // User year of birth + 'lat': '', // User location - Latitude + 'lon': '', // User Location - Longitude +}; + +// rtb native types are meant to be dynamic and extendable +// the extendable data asset types are nicely aligned +// in practice we set an ID that is distinct for each real type of return +const NATIVE_ASSETS = { + 'TITLE': { ID: 1, KEY: 'title', TYPE: 0 }, + 'IMAGE': { ID: 2, KEY: 'image', TYPE: 0 }, + 'ICON': { ID: 3, KEY: 'icon', TYPE: 0 }, + 'SPONSOREDBY': { ID: 4, KEY: 'sponsoredBy', TYPE: 1 }, + 'BODY': { ID: 5, KEY: 'body', TYPE: 2 }, + 'CLICKURL': { ID: 6, KEY: 'clickUrl', TYPE: 0 }, + 'VIDEO': { ID: 7, KEY: 'video', TYPE: 0 }, + 'EXT': { ID: 8, KEY: 'ext', TYPE: 0 }, + 'DATA': { ID: 9, KEY: 'data', TYPE: 0 }, + 'LOGO': { ID: 10, KEY: 'logo', TYPE: 0 }, + 'SPONSORED': { ID: 11, KEY: 'sponsored', TYPE: 1 }, + 'DESC': { ID: 12, KEY: 'data', TYPE: 2 }, + 'RATING': { ID: 13, KEY: 'rating', TYPE: 3 }, + 'LIKES': { ID: 14, KEY: 'likes', TYPE: 4 }, + 'DOWNLOADS': { ID: 15, KEY: 'downloads', TYPE: 5 }, + 'PRICE': { ID: 16, KEY: 'price', TYPE: 6 }, + 'SALEPRICE': { ID: 17, KEY: 'saleprice', TYPE: 7 }, + 'PHONE': { ID: 18, KEY: 'phone', TYPE: 8 }, + 'ADDRESS': { ID: 19, KEY: 'address', TYPE: 9 }, + 'DESC2': { ID: 20, KEY: 'desc2', TYPE: 10 }, + 'DISPLAYURL': { ID: 21, KEY: 'displayurl', TYPE: 11 }, + 'CTA': { ID: 22, KEY: 'cta', TYPE: 12 } +}; + +const NATIVE_ASSET_IMAGE_TYPE = { + 'ICON': 1, + 'LOGO': 2, + 'IMAGE': 3 +} + +// to render any native unit we have to have a few items +const NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS = [ + { + id: NATIVE_ASSETS.SPONSOREDBY.ID, + required: true, + data: { + type: 1 + } + }, + { + id: NATIVE_ASSETS.TITLE.ID, + required: true, + }, + { + id: NATIVE_ASSETS.IMAGE.ID, + required: true, + } +] + +let isInvalidNativeRequest = false +let NATIVE_ASSET_ID_TO_KEY_MAP = {}; +let NATIVE_ASSET_KEY_TO_ASSET_MAP = {}; + +// together allows traversal of NATIVE_ASSETS_LIST in any direction +// id -> key +utils._each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_ID_TO_KEY_MAP[anAsset.ID] = anAsset.KEY }); +// key -> asset +utils._each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_KEY_TO_ASSET_MAP[anAsset.KEY] = anAsset }); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, NATIVE], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + // siteId is required + if (bid.params && bid.params.siteId) { + // it must be a string + if (!utils.isStr(bid.params.siteId)) { + _logWarn('siteId is required for bid', bid); + return false; + } + } else { + return false; + } + + return true; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + var refererInfo; + if (bidderRequest && bidderRequest.refererInfo) { + refererInfo = bidderRequest.refererInfo; + } + var conf = _initConf(refererInfo); + var payload = _createOrtbTemplate(conf); + var bidCurrency = ''; + var bid; + var blockedIabCategories = []; + + validBidRequests.forEach(originalBid => { + bid = utils.deepClone(originalBid); + bid.params.adSlot = bid.params.adSlot || ''; + _parseAdSlot(bid); + + conf = _handleCustomParams(bid.params, conf); + conf.transactionId = bid.transactionId; + bidCurrency = bid.params.currency || UNDEFINED; + bid.params.currency = bidCurrency; + + if (bid.params.hasOwnProperty('bcat') && utils.isArray(bid.params.bcat)) { + blockedIabCategories = blockedIabCategories.concat(bid.params.bcat); + } + + var impObj = _createImpressionObject(bid, conf); + if (impObj) { + payload.imp.push(impObj); + } + }); + + // no payload imps, no rason to continue + if (payload.imp.length == 0) { + return; + } + + // test bids can also be turned on here + if (window.location.href.indexOf('pubwiseTestBid=true') !== -1) { + payload.test = 1; + } + + if (bid.params.isTest) { + payload.test = Number(bid.params.isTest) // should be 1 or 0 + } + payload.site.publisher.id = bid.params.siteId.trim(); + payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED); + payload.user.geo = {}; + payload.user.geo.lat = _parseSlotParam('lat', conf.lat); + payload.user.geo.lon = _parseSlotParam('lon', conf.lon); + payload.user.yob = _parseSlotParam('yob', conf.yob); + payload.device.geo = payload.user.geo; + payload.site.page = payload.site.page.trim(); + payload.site.domain = _getDomainFromURL(payload.site.page); + + // add the content object from config in request + if (typeof config.getConfig('content') === 'object') { + payload.site.content = config.getConfig('content'); + } + + // merge the device from config.getConfig('device') + if (typeof config.getConfig('device') === 'object') { + payload.device = Object.assign(payload.device, config.getConfig('device')); + } + + // passing transactionId in source.tid + utils.deepSetValue(payload, 'source.tid', conf.transactionId); + + // schain + if (validBidRequests[0].schain) { + utils.deepSetValue(payload, 'source.ext.schain', validBidRequests[0].schain); + } + + // gdpr consent + if (bidderRequest && bidderRequest.gdprConsent) { + utils.deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + utils.deepSetValue(payload, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // ccpa on the root object + if (bidderRequest && bidderRequest.uspConsent) { + utils.deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + // if coppa is in effect then note it + if (config.getConfig('coppa') === true) { + utils.deepSetValue(payload, 'regs.coppa', 1); + } + + var options = {contentType: 'text/plain'} + + _logInfo('buildRequests payload', payload); + _logInfo('buildRequests bidderRequest', bidderRequest); + + return { + method: 'POST', + url: ENDPOINT_URL, + data: payload, + options: options, + bidderRequest: bidderRequest, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (response, request) { + const bidResponses = []; + var respCur = DEFAULT_CURRENCY; + _logInfo('interpretResponse request', request); + let parsedRequest = request.data; // not currently stringified + // let parsedReferrer = parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : ''; + + // try { + if (response.body && response.body.seatbid && utils.isArray(response.body.seatbid)) { + // Supporting multiple bid responses for same adSize + respCur = response.body.cur || respCur; + response.body.seatbid.forEach(seatbidder => { + seatbidder.bid && + utils.isArray(seatbidder.bid) && + seatbidder.bid.forEach(bid => { + let newBid = { + requestId: bid.impid, + cpm: (parseFloat(bid.price) || 0).toFixed(2), + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.id, + currency: respCur, + netRevenue: NET_REVENUE, + ttl: 300, + ad: bid.adm, + pw_seat: seatbidder.seat || null, + pw_dspid: bid.ext && bid.ext.dspid ? bid.ext.dspid : null, + partnerImpId: bid.id || '' // partner impression Id + }; + if (parsedRequest.imp && parsedRequest.imp.length > 0) { + parsedRequest.imp.forEach(req => { + if (bid.impid === req.id) { + _checkMediaType(bid.adm, newBid); + switch (newBid.mediaType) { + case BANNER: + break; + case NATIVE: + _parseNativeResponse(bid, newBid); + break; + } + } + }); + } + + newBid.meta = {}; + if (bid.ext && bid.ext.dspid) { + newBid.meta.networkId = bid.ext.dspid; + } + if (bid.ext && bid.ext.advid) { + newBid.meta.buyerId = bid.ext.advid; + } + if (bid.adomain && bid.adomain.length > 0) { + newBid.meta.advertiserDomains = bid.adomain; + newBid.meta.clickUrl = bid.adomain[0]; + } + + bidResponses.push(newBid); + }); + }); + } + // } catch (error) { + // _logError(error); + // } + return bidResponses; + } +} + +function _checkMediaType(adm, newBid) { + // Create a regex here to check the strings + var admJSON = ''; + if (adm.indexOf('"ver":') >= 0) { + try { + admJSON = JSON.parse(adm.replace(/\\/g, '')); + if (admJSON && admJSON.assets) { + newBid.mediaType = NATIVE; + } + } catch (e) { + _logWarn('Error: Cannot parse native reponse for ad response: ' + adm); + } + } else { + newBid.mediaType = BANNER; + } +} + +function _parseNativeResponse(bid, newBid) { + newBid.native = {}; + if (bid.hasOwnProperty('adm')) { + var adm = ''; + try { + adm = JSON.parse(bid.adm.replace(/\\/g, '')); + } catch (ex) { + _logWarn('Error: Cannot parse native reponse for ad response: ' + newBid.adm); + return; + } + if (adm && adm.assets && adm.assets.length > 0) { + newBid.mediaType = NATIVE; + for (let i = 0, len = adm.assets.length; i < len; i++) { + switch (adm.assets[i].id) { + case NATIVE_ASSETS.TITLE.ID: + newBid.native.title = adm.assets[i].title && adm.assets[i].title.text; + break; + case NATIVE_ASSETS.IMAGE.ID: + newBid.native.image = { + url: adm.assets[i].img && adm.assets[i].img.url, + height: adm.assets[i].img && adm.assets[i].img.h, + width: adm.assets[i].img && adm.assets[i].img.w, + }; + break; + case NATIVE_ASSETS.ICON.ID: + newBid.native.icon = { + url: adm.assets[i].img && adm.assets[i].img.url, + height: adm.assets[i].img && adm.assets[i].img.h, + width: adm.assets[i].img && adm.assets[i].img.w, + }; + break; + case NATIVE_ASSETS.SPONSOREDBY.ID: + case NATIVE_ASSETS.BODY.ID: + case NATIVE_ASSETS.LIKES.ID: + case NATIVE_ASSETS.DOWNLOADS.ID: + case NATIVE_ASSETS.PRICE: + case NATIVE_ASSETS.SALEPRICE.ID: + case NATIVE_ASSETS.PHONE.ID: + case NATIVE_ASSETS.ADDRESS.ID: + case NATIVE_ASSETS.DESC2.ID: + case NATIVE_ASSETS.CTA.ID: + case NATIVE_ASSETS.RATING.ID: + case NATIVE_ASSETS.DISPLAYURL.ID: + newBid.native[NATIVE_ASSET_ID_TO_KEY_MAP[adm.assets[i].id]] = adm.assets[i].data && adm.assets[i].data.value; + break; + } + } + newBid.clickUrl = adm.link && adm.link.url; + newBid.clickTrackers = (adm.link && adm.link.clicktrackers) || []; + newBid.impressionTrackers = adm.imptrackers || []; + newBid.jstracker = adm.jstracker || []; + if (!newBid.width) { + newBid.width = DEFAULT_WIDTH; + } + if (!newBid.height) { + newBid.height = DEFAULT_HEIGHT; + } + } + } +} + +function _getDomainFromURL(url) { + let anchor = document.createElement('a'); + anchor.href = url; + return anchor.hostname; +} + +function _handleCustomParams(params, conf) { + var key, value, entry; + for (key in CUSTOM_PARAMS) { + if (CUSTOM_PARAMS.hasOwnProperty(key)) { + value = params[key]; + if (value) { + entry = CUSTOM_PARAMS[key]; + + if (typeof entry === 'object') { + // will be used in future when we want to + // process a custom param before using + // 'keyname': {f: function() {}} + value = entry.f(value, conf); + } + + if (utils.isStr(value)) { + conf[key] = value; + } else { + _logWarn('Ignoring param : ' + key + ' with value : ' + CUSTOM_PARAMS[key] + ', expects string-value, found ' + typeof value); + } + } + } + } + return conf; +} + +function _createOrtbTemplate(conf) { + return { + id: '' + new Date().getTime(), + at: AUCTION_TYPE, + cur: [DEFAULT_CURRENCY], + imp: [], + site: { + page: conf.pageURL, + ref: conf.refURL, + publisher: {} + }, + device: { + ua: navigator.userAgent, + js: 1, + dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, + h: screen.height, + w: screen.width, + language: navigator.language + }, + user: {}, + ext: { + version: VERSION + } + }; +} + +function _createImpressionObject(bid, conf) { + var impObj = {}; + var bannerObj; + var nativeObj = {}; + var mediaTypes = ''; + + impObj = { + id: bid.bidId, + tagid: bid.params.adUnit || undefined, + bidfloor: _parseSlotParam('bidFloor', bid.params.bidFloor), // capitalization dicated by 3.2.4 spec + secure: 1, + bidfloorcur: bid.params.currency ? _parseSlotParam('currency', bid.params.currency) : DEFAULT_CURRENCY // capitalization dicated by 3.2.4 spec + }; + + if (bid.hasOwnProperty('mediaTypes')) { + for (mediaTypes in bid.mediaTypes) { + switch (mediaTypes) { + case BANNER: + bannerObj = _createBannerRequest(bid); + if (bannerObj !== UNDEFINED) { + impObj.banner = bannerObj; + } + break; + case NATIVE: + nativeObj['request'] = JSON.stringify(_createNativeRequest(bid.nativeParams)); + if (!isInvalidNativeRequest) { + impObj.native = nativeObj; + } else { + _logWarn('Error: Error in Native adunit ' + bid.params.adUnit + '. Ignoring the adunit. Refer to ' + PREBID_NATIVE_HELP_LINK + ' for more details.'); + } + break; + } + } + } else { + _logWarn('MediaTypes are Required for all Adunit Configs', bid) + } + + _addFloorFromFloorModule(impObj, bid); + + return impObj.hasOwnProperty(BANNER) || + impObj.hasOwnProperty(NATIVE) ? impObj : UNDEFINED; +} + +function _parseSlotParam(paramName, paramValue) { + if (!utils.isStr(paramValue)) { + paramValue && _logWarn('Ignoring param key: ' + paramName + ', expects string-value, found ' + typeof paramValue); + return UNDEFINED; + } + + switch (paramName) { + case 'bidFloor': + return parseFloat(paramValue) || UNDEFINED; + case 'lat': + return parseFloat(paramValue) || UNDEFINED; + case 'lon': + return parseFloat(paramValue) || UNDEFINED; + case 'yob': + return parseInt(paramValue) || UNDEFINED; + default: + return paramValue; + } +} + +function _parseAdSlot(bid) { + _logInfo('parseAdSlot bid', bid) + bid.params.adUnit = ''; + bid.params.width = 0; + bid.params.height = 0; + bid.params.adSlot = _cleanSlotName(bid.params.adSlot); + + if (bid.hasOwnProperty('mediaTypes')) { + if (bid.mediaTypes.hasOwnProperty(BANNER) && + bid.mediaTypes.banner.hasOwnProperty('sizes')) { // if its a banner, has mediaTypes and sizes + var i = 0; + var sizeArray = []; + for (;i < bid.mediaTypes.banner.sizes.length; i++) { + if (bid.mediaTypes.banner.sizes[i].length === 2) { // sizes[i].length will not be 2 in case where size is set as fluid, we want to skip that entry + sizeArray.push(bid.mediaTypes.banner.sizes[i]); + } + } + bid.mediaTypes.banner.sizes = sizeArray; + if (bid.mediaTypes.banner.sizes.length >= 1) { + // if there is more than one size then pop one onto the banner params width + // pop the first into the params, then remove it from mediaTypes + bid.params.width = bid.mediaTypes.banner.sizes[0][0]; + bid.params.height = bid.mediaTypes.banner.sizes[0][1]; + bid.mediaTypes.banner.sizes = bid.mediaTypes.banner.sizes.splice(1, bid.mediaTypes.banner.sizes.length - 1); + } + } + } else { + _logWarn('MediaTypes are Required for all Adunit Configs', bid) + } +} + +function _cleanSlotName(slotName) { + if (utils.isStr(slotName)) { + return slotName.replace(/^\s+/g, '').replace(/\s+$/g, ''); + } + return ''; +} + +function _initConf(refererInfo) { + return { + pageURL: (refererInfo && refererInfo.referer) ? refererInfo.referer : window.location.href, + refURL: window.document.referrer + }; +} + +function _commonNativeRequestObject(nativeAsset, params) { + var key = nativeAsset.KEY; + return { + id: nativeAsset.ID, + required: params[key].required ? 1 : 0, + data: { + type: nativeAsset.TYPE, + len: params[key].len, + ext: params[key].ext + } + }; +} + +function _addFloorFromFloorModule(impObj, bid) { + let bidFloor = -1; // indicates no floor + + // get lowest floor from floorModule + if (typeof bid.getFloor === 'function' && !config.getConfig('pubwise.disableFloors')) { + [BANNER, NATIVE].forEach(mediaType => { + if (impObj.hasOwnProperty(mediaType)) { + let floorInfo = bid.getFloor({ currency: impObj.bidFloorCur, mediaType: mediaType, size: '*' }); + if (typeof floorInfo === 'object' && floorInfo.currency === impObj.bidFloorCur && !isNaN(parseInt(floorInfo.floor))) { + let mediaTypeFloor = parseFloat(floorInfo.floor); + bidFloor = (bidFloor == -1 ? mediaTypeFloor : Math.min(mediaTypeFloor, bidFloor)) + } + } + }); + } + + // get highest, if none then take the default -1 + if (impObj.bidfloor) { + bidFloor = Math.max(bidFloor, impObj.bidfloor) + } + + // assign if it has a valid floor - > 0 + impObj.bidfloor = ((!isNaN(bidFloor) && bidFloor > 0) ? bidFloor : UNDEFINED); +} + +function _createNativeRequest(params) { + var nativeRequestObject = { + assets: [] + }; + for (var key in params) { + if (params.hasOwnProperty(key)) { + var assetObj = {}; + if (!(nativeRequestObject.assets && nativeRequestObject.assets.length > 0 && nativeRequestObject.assets.hasOwnProperty(key))) { + switch (key) { + case NATIVE_ASSETS.TITLE.KEY: + if (params[key].len || params[key].length) { + assetObj = { + id: NATIVE_ASSETS.TITLE.ID, + required: params[key].required ? 1 : 0, + title: { + len: params[key].len || params[key].length, + ext: params[key].ext + } + }; + } else { + _logWarn('Error: Title Length is required for native ad: ' + JSON.stringify(params)); + } + break; + case NATIVE_ASSETS.IMAGE.KEY: + if (params[key].sizes && params[key].sizes.length > 0) { + assetObj = { + id: NATIVE_ASSETS.IMAGE.ID, + required: params[key].required ? 1 : 0, + img: { + type: NATIVE_ASSET_IMAGE_TYPE.IMAGE, + w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), + h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED), + wmin: params[key].wmin || params[key].minimumWidth || (params[key].minsizes ? params[key].minsizes[0] : UNDEFINED), + hmin: params[key].hmin || params[key].minimumHeight || (params[key].minsizes ? params[key].minsizes[1] : UNDEFINED), + mimes: params[key].mimes, + ext: params[key].ext, + } + }; + } else { + _logWarn('Error: Image sizes is required for native ad: ' + JSON.stringify(params)); + } + break; + case NATIVE_ASSETS.ICON.KEY: + if (params[key].sizes && params[key].sizes.length > 0) { + assetObj = { + id: NATIVE_ASSETS.ICON.ID, + required: params[key].required ? 1 : 0, + img: { + type: NATIVE_ASSET_IMAGE_TYPE.ICON, + w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), + h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED), + } + }; + } else { + _logWarn('Error: Icon sizes is required for native ad: ' + JSON.stringify(params)); + }; + break; + case NATIVE_ASSETS.VIDEO.KEY: + assetObj = { + id: NATIVE_ASSETS.VIDEO.ID, + required: params[key].required ? 1 : 0, + video: { + minduration: params[key].minduration, + maxduration: params[key].maxduration, + protocols: params[key].protocols, + mimes: params[key].mimes, + ext: params[key].ext + } + }; + break; + case NATIVE_ASSETS.EXT.KEY: + assetObj = { + id: NATIVE_ASSETS.EXT.ID, + required: params[key].required ? 1 : 0, + }; + break; + case NATIVE_ASSETS.LOGO.KEY: + assetObj = { + id: NATIVE_ASSETS.LOGO.ID, + required: params[key].required ? 1 : 0, + img: { + type: NATIVE_ASSET_IMAGE_TYPE.LOGO, + w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), + h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED) + } + }; + break; + case NATIVE_ASSETS.SPONSOREDBY.KEY: + case NATIVE_ASSETS.BODY.KEY: + case NATIVE_ASSETS.RATING.KEY: + case NATIVE_ASSETS.LIKES.KEY: + case NATIVE_ASSETS.DOWNLOADS.KEY: + case NATIVE_ASSETS.PRICE.KEY: + case NATIVE_ASSETS.SALEPRICE.KEY: + case NATIVE_ASSETS.PHONE.KEY: + case NATIVE_ASSETS.ADDRESS.KEY: + case NATIVE_ASSETS.DESC2.KEY: + case NATIVE_ASSETS.DISPLAYURL.KEY: + case NATIVE_ASSETS.CTA.KEY: + assetObj = _commonNativeRequestObject(NATIVE_ASSET_KEY_TO_ASSET_MAP[key], params); + break; + } + } + } + if (assetObj && assetObj.id) { + nativeRequestObject.assets[nativeRequestObject.assets.length] = assetObj; + } + }; + + // for native image adtype prebid has to have few required assests i.e. title,sponsoredBy, image + // if any of these are missing from the request then request will not be sent + var requiredAssetCount = NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.length; + var presentrequiredAssetCount = 0; + NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.forEach(ele => { + var lengthOfExistingAssets = nativeRequestObject.assets.length; + for (var i = 0; i < lengthOfExistingAssets; i++) { + if (ele.id == nativeRequestObject.assets[i].id) { + presentrequiredAssetCount++; + break; + } + } + }); + if (requiredAssetCount == presentrequiredAssetCount) { + isInvalidNativeRequest = false; + } else { + isInvalidNativeRequest = true; + } + return nativeRequestObject; +} + +function _createBannerRequest(bid) { + var sizes = bid.mediaTypes.banner.sizes; + var format = []; + var bannerObj; + if (sizes !== UNDEFINED && utils.isArray(sizes)) { + bannerObj = {}; + if (!bid.params.width && !bid.params.height) { + if (sizes.length === 0) { + // i.e. since bid.params does not have width or height, and length of sizes is 0, need to ignore this banner imp + bannerObj = UNDEFINED; + _logWarn('Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); + return bannerObj; + } else { + bannerObj.w = parseInt(sizes[0][0], 10); + bannerObj.h = parseInt(sizes[0][1], 10); + sizes = sizes.splice(1, sizes.length - 1); + } + } else { + bannerObj.w = bid.params.width; + bannerObj.h = bid.params.height; + } + if (sizes.length > 0) { + format = []; + sizes.forEach(function (size) { + if (size.length > 1) { + format.push({ w: size[0], h: size[1] }); + } + }); + if (format.length > 0) { + bannerObj.format = format; + } + } + bannerObj.pos = 0; + bannerObj.topframe = utils.inIframe() ? 0 : 1; + } else { + _logWarn('Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); + bannerObj = UNDEFINED; + } + return bannerObj; +} + +// various error levels are not always used +// eslint-disable-next-line no-unused-vars +function _logMessage(textValue, objectValue) { + utils.logMessage('PubWise: ' + textValue, objectValue); +} + +// eslint-disable-next-line no-unused-vars +function _logInfo(textValue, objectValue) { + utils.logInfo('PubWise: ' + textValue, objectValue); +} + +// eslint-disable-next-line no-unused-vars +function _logWarn(textValue, objectValue) { + utils.logWarn('PubWise: ' + textValue, objectValue); +} + +// eslint-disable-next-line no-unused-vars +function _logError(textValue, objectValue) { + utils.logError('PubWise: ' + textValue, objectValue); +} + +// function _decorateLog() { +// arguments[0] = 'PubWise' + arguments[0]; +// return arguments +// } + +// these are exported only for testing so maintaining the JS convention of _ to indicate the intent +export { + _checkMediaType, + _parseAdSlot +} + +registerBidder(spec); diff --git a/modules/pubwiseBidAdapter.md b/modules/pubwiseBidAdapter.md new file mode 100644 index 00000000000..8cf38a63913 --- /dev/null +++ b/modules/pubwiseBidAdapter.md @@ -0,0 +1,78 @@ +# Overview + +``` +Module Name: PubWise Bid Adapter +Module Type: Bidder Adapter +Maintainer: info@pubwise.io +``` + +# Description + +Connects to PubWise exchange for bids. + +# Sample Banner Ad Unit: For Publishers + +With isTest parameter the system will respond in whatever dimensions provided. + +## Params + + + +## Banner +``` +var adUnits = [ + { + code: "div-gpt-ad-1460505748561-0", + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'pubwise', + params: { + siteId: "xxxxxx", + isTest: true + } + }] + } +] +``` +## Native +``` +var adUnits = [ + { + code: 'div-gpt-ad-1460505748561-1', + sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true, + len: 80 + }, + body: { + required: true + }, + image: { + required: true, + sizes: [150, 50] + }, + sponsoredBy: { + required: true + }, + icon: { + required: false + } + } + }, + bids: [{ + bidder: 'pubwise', + params: { + siteId: "xxxxxx", + isTest: true, + }, + }] + } +] +``` + diff --git a/test/spec/modules/pubwiseBidAdapter_spec.js b/test/spec/modules/pubwiseBidAdapter_spec.js new file mode 100644 index 00000000000..450b028f6c7 --- /dev/null +++ b/test/spec/modules/pubwiseBidAdapter_spec.js @@ -0,0 +1,575 @@ +// import or require modules necessary for the test, e.g.: + +import {expect} from 'chai'; +import {spec} from 'modules/pubwiseBidAdapter.js'; +import {_checkMediaType} from 'modules/pubwiseBidAdapter.js'; // this is exported only for testing so maintaining the JS convention of _ to indicate the intent +import {_parseAdSlot} from 'modules/pubwiseBidAdapter.js'; // this is exported only for testing so maintaining the JS convention of _ to indicate the intent +import * as utils from 'src/utils.js'; + +const sampleRequestBanner = { + 'id': '6c148795eb836a', + 'tagid': 'div-gpt-ad-1460505748561-0', + 'bidfloor': 1, + 'secure': 1, + 'bidfloorcur': 'USD', + 'banner': { + 'w': 300, + 'h': 250, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ], + 'pos': 0, + 'topframe': 1 + } +}; + +const sampleRequest = { + 'at': 1, + 'cur': [ + 'USD' + ], + 'imp': [ + sampleRequestBanner, + { + 'id': '7329ddc1d84eb3', + 'tagid': 'div-gpt-ad-1460505748561-1', + 'secure': 1, + 'bidfloorcur': 'USD', + 'native': { + 'request': '{"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":5,"required":1,"data":{"type":2}},{"id":2,"required":1,"img":{"type":{"ID":2,"KEY":"image","TYPE":0},"w":150,"h":50}},{"id":4,"required":1,"data":{"type":1}}]}' + } + } + ], + 'site': { + 'page': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'ref': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'publisher': { + 'id': 'xxxxxx' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/86.0.4240.198 Safari/537.36', + 'js': 1, + 'dnt': 0, + 'h': 600, + 'w': 800, + 'language': 'en-US', + 'geo': { + 'lat': 33.91989876432274, + 'lon': -84.38897708175764 + } + }, + 'user': { + 'gender': 'M', + 'geo': { + 'lat': 33.91989876432274, + 'lon': -84.38897708175764 + }, + 'yob': 2000 + }, + 'test': 0, + 'ext': { + 'version': '0.0.1' + }, + 'source': { + 'tid': '2c8cd034-f068-4419-8c30-f07292c0d17b' + } +}; + +const sampleValidBannerBidRequest = { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx', + 'bidFloor': '1.00', + 'currency': 'USD', + 'gender': 'M', + 'lat': '33.91989876432274', + 'lon': '-84.38897708175764', + 'yob': '2000', + 'bcat': ['IAB25-3', 'IAB26-1', 'IAB26-2', 'IAB26-3', 'IAB26-4'], + }, + 'gdprConsent': { + 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + 'gdprApplies': 1, + }, + 'uspConsent': 1, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '2001a8b2-3bcf-417d-b64f-92641dae21e0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '6c148795eb836a', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 +}; + +const sampleValidBidRequests = [ + sampleValidBannerBidRequest, + { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx' + }, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'nativeParams': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'native': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-1', + 'transactionId': '2c8cd034-f068-4419-8c30-f07292c0d17b', + 'sizes': [], + 'bidId': '30ab7516a51a7c', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } +] + +const sampleBidderBannerRequest = { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx', + 'height': 250, + 'width': 300, + 'gender': 'M', + 'yob': '2000', + 'lat': '33.91989876432274', + 'lon': '-84.38897708175764', + 'bidFloor': '1.00', + 'currency': 'USD', + 'adSlot': '', + 'adUnit': '', + 'bcat': [ + 'IAB25-3', + 'IAB26-1', + 'IAB26-2', + 'IAB26-3', + 'IAB26-4', + ], + }, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 600 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '2001a8b2-3bcf-417d-b64f-92641dae21e0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] + ], + 'bidId': '6c148795eb836a', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'gdprConsent': { + 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + 'gdprApplies': 1, + }, + 'uspConsent': 1, +}; + +const sampleBidderRequest = { + 'bidderCode': 'pubwise', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'bidderRequestId': '18a45bff5ff705', + 'bids': [ + sampleBidderBannerRequest, + { + 'bidder': 'pubwise', + 'params': { + 'siteId': 'xxxxxx' + }, + 'crumbs': { + 'pubcid': '9a62f261-3c0b-4cc8-8db3-a72ae86ec6ba' + }, + 'nativeParams': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + }, + 'fpd': { + 'context': { + 'adServer': { + 'name': 'gam', + 'adSlot': '/19968336/header-bid-tag-0' + }, + 'pbAdSlot': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'native': { + 'title': { + 'required': true, + 'len': 80 + }, + 'body': { + 'required': true + }, + 'image': { + 'required': true, + 'sizes': [ + 150, + 50 + ] + }, + 'sponsoredBy': { + 'required': true + }, + 'icon': { + 'required': false + } + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-1', + 'transactionId': '2c8cd034-f068-4419-8c30-f07292c0d17b', + 'sizes': [], + 'bidId': '30ab7516a51a7c', + 'bidderRequestId': '18a45bff5ff705', + 'auctionId': '9f20663c-4629-4b5c-bff6-ff3aa8319358', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ], + 'auctionStart': 1606269202001, + 'timeout': 1000, + 'gdprConsent': { + 'consentString': 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + 'gdprApplies': 1, + }, + 'uspConsent': 1, + 'refererInfo': { + 'referer': 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true' + ], + 'canonicalUrl': null + }, + 'start': 1606269202004 +}; + +const sampleRTBResponse = { + 'body': { + 'id': '1606251348404', + 'seatbid': [ + { + 'bid': [ + { + 'id': '1606579704052', + 'impid': '6c148795eb836a', + 'price': 1.23, + 'adm': '\u003cdiv style="box-sizing: border-box;width:298px;height:248px;border: 1px solid rgba(0,0,0,.25);border-radius:10px;"\u003e\n\t\u003ch3 style="margin-top:80px;text-align: center;"\u003ePubWise Test Bid\u003c/h3\u003e\n\u003c/div\u003e', + 'crid': 'test', + 'w': 300, + 'h': 250 + }, + { + 'id': '1606579704052', + 'impid': '7329ddc1d84eb3', + 'price': 1.23, + 'adm': '{"ver":"1.2","assets":[{"id":1,"title":{"text":"PubWise Test"}},{"id":2,"img":{"type":3,"url":"http://www.pubwise.io","w":300,"h":250}},{"id":3,"img":{"type":1,"url":"http://www.pubwise.io","w":150,"h":125}},{"id":5,"data":{"type":2,"value":"PubWise Test Desc"}},{"id":4,"data":{"type":1,"value":"PubWise.io"}}],"link":{"url":"http://www.pubwise.io"}}', + 'crid': 'test', + 'w': 300, + 'h': 250 + } + ] + } + ], + 'bidid': 'testtesttest' + } +}; + +const samplePBBidObjects = [ + { + 'requestId': '6c148795eb836a', + 'cpm': '1.23', + 'width': 300, + 'height': 250, + 'creativeId': 'test', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'ad': '
\n\t

PubWise Test Bid

\n
', + 'pw_seat': null, + 'pw_dspid': null, + 'partnerImpId': '1606579704052', + 'meta': {}, + 'mediaType': 'banner', + }, + { + 'requestId': '7329ddc1d84eb3', + 'cpm': '1.23', + 'width': 300, + 'height': 250, + 'creativeId': 'test', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'ad': '{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"title\":{\"text\":\"PubWise Test\"}},{\"id\":2,\"img\":{\"type\":3,\"url\":\"http://www.pubwise.io\",\"w\":300,\"h\":250}},{\"id\":3,\"img\":{\"type\":1,\"url\":\"http://www.pubwise.io\",\"w\":150,\"h\":125}},{\"id\":5,\"data\":{\"type\":2,\"value\":\"PubWise Test Desc\"}},{\"id\":4,\"data\":{\"type\":1,\"value\":\"PubWise.io\"}}],\"link\":{\"url\":\"http://www.pubwise.io\"}}', + 'pw_seat': null, + 'pw_dspid': null, + 'partnerImpId': '1606579704052', + 'mediaType': 'native', + 'native': { + 'body': 'PubWise Test Desc', + 'icon': { + 'height': 125, + 'url': 'http://www.pubwise.io', + 'width': 150, + }, + 'image': { + 'height': 250, + 'url': 'http://www.pubwise.io', + 'width': 300, + }, + 'sponsoredBy': 'PubWise.io', + 'title': 'PubWise Test' + }, + 'meta': {}, + 'impressionTrackers': [], + 'jstracker': [], + 'clickTrackers': [], + 'clickUrl': 'http://www.pubwise.io' + } +]; + +describe('PubWiseAdapter', function () { + describe('Properly Validates Bids', function () { + it('valid bid', function () { + let validBid = { + bidder: 'pubwise', + params: { + siteId: 'xxxxxx' + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + }); + + it('valid bid: extra fields are ok', function () { + let validBid = { + bidder: 'pubwise', + params: { + siteId: 'xxxxxx', + gender: 'M', + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + }); + + it('invalid bid: no siteId', function () { + let inValidBid = { + bidder: 'pubwise', + params: { + gender: 'M', + } + }, + isValid = spec.isBidRequestValid(inValidBid); + expect(isValid).to.equal(false); + }); + + it('invalid bid: siteId should be a string', function () { + let validBid = { + bidder: 'pubwise', + params: { + siteId: 123456 + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }); + }); + + describe('Handling Request Construction', function () { + it('bid requests are not mutable', function() { + let sourceBidRequest = utils.deepClone(sampleValidBidRequests) + spec.buildRequests(sampleValidBidRequests, {auctinId: 'placeholder'}); + expect(sampleValidBidRequests).to.deep.equal(sourceBidRequest, 'Should be unedited as they are used elsewhere'); + }); + it('should handle complex bidRequest', function() { + let request = spec.buildRequests(sampleValidBidRequests, sampleBidderRequest); + expect(request.bidderRequest).to.equal(sampleBidderRequest); + }); + it('must conform to API for buildRequests', function() { + let request = spec.buildRequests(sampleValidBidRequests); + expect(request.bidderRequest).to.be.undefined; + }); + }); + + describe('Identifies Media Types', function () { + it('identifies native adm type', function() { + let adm = '{"ver":"1.2","assets":[{"title":{"text":"PubWise Test"}},{"img":{"type":3,"url":"http://www.pubwise.io"}},{"img":{"type":1,"url":"http://www.pubwise.io"}},{"data":{"type":2,"value":"PubWise Test Desc"}},{"data":{"type":1,"value":"PubWise.io"}}],"link":{"url":""}}'; + let newBid = {mediaType: 'unknown'}; + _checkMediaType(adm, newBid); + expect(newBid.mediaType).to.equal('native', adm + ' Is a Native adm'); + }); + + it('identifies banner adm type', function() { + let adm = '

PubWise Test Bid

'; + let newBid = {mediaType: 'unknown'}; + _checkMediaType(adm, newBid); + expect(newBid.mediaType).to.equal('banner', adm + ' Is a Banner adm'); + }); + }); + + describe('Properly Parses AdSlot Data', function () { + it('parses banner', function() { + let testBid = utils.deepClone(sampleValidBannerBidRequest) + _parseAdSlot(testBid) + expect(testBid).to.deep.equal(sampleBidderBannerRequest); + }); + }); + + describe('Properly Handles Response', function () { + it('handles response with muiltiple responses', function() { + // the request when it comes back is on the data object + let pbResponse = spec.interpretResponse(sampleRTBResponse, {'data': sampleRequest}) + expect(pbResponse).to.deep.equal(samplePBBidObjects); + }); + }); +}); From a95f1db0e11b416a4885fc59aae1970053c9a30d Mon Sep 17 00:00:00 2001 From: cpuBird <54024689+cpuBird@users.noreply.github.com> Date: Wed, 9 Dec 2020 21:24:30 +0530 Subject: [PATCH 013/181] Vdoai adapter update - Added video mediaType support (#5970) * added video mediatype support to vdoai adapter * added unit test --- modules/vdoaiBidAdapter.js | 16 +++++++--- modules/vdoaiBidAdapter.md | 23 +++++++++++++++ test/spec/modules/vdoaiBidAdapter_spec.js | 36 +++++++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/modules/vdoaiBidAdapter.js b/modules/vdoaiBidAdapter.js index 395953fb737..8cfcd67bd00 100644 --- a/modules/vdoaiBidAdapter.js +++ b/modules/vdoaiBidAdapter.js @@ -1,14 +1,14 @@ import * as utils from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = 'vdo.ai'; const ENDPOINT_URL = 'https://prebid.vdo.ai/auction'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. * @@ -40,7 +40,8 @@ export const spec = { height: height, bidId: bidRequest.bidId, referer: bidderRequest.refererInfo.referer, - id: bidRequest.auctionId + id: bidRequest.auctionId, + mediaType: bidRequest.mediaTypes.video ? 'video' : 'banner' }; bidRequest.params.bidFloor && (payload['bidFloor'] = bidRequest.params.bidFloor); return { @@ -90,8 +91,15 @@ export const spec = { ttl: config.getConfig('_bidderTimeout'), // referrer: referrer, // ad: response.adm - ad: adCreative + // ad: adCreative, + mediaType: response.mediaType }; + + if (response.mediaType == 'video') { + bidResponse.vastXml = adCreative; + } else { + bidResponse.ad = adCreative; + } bidResponses.push(bidResponse); } return bidResponses; diff --git a/modules/vdoaiBidAdapter.md b/modules/vdoaiBidAdapter.md index 81bd8e69c1d..712adc0ec76 100644 --- a/modules/vdoaiBidAdapter.md +++ b/modules/vdoaiBidAdapter.md @@ -31,4 +31,27 @@ Module that connects to VDO.AI's demand sources ] } ]; +``` + + +# Video Test Parameters +``` +var videoAdUnit = { + code: 'test-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + }, + }, + bids: [ + { + bidder: "vdo.ai", + params: { + placement: 'newsdv77' + } + } + ] +}; ``` \ No newline at end of file diff --git a/test/spec/modules/vdoaiBidAdapter_spec.js b/test/spec/modules/vdoaiBidAdapter_spec.js index c9d826d8dc8..5318bb43eca 100644 --- a/test/spec/modules/vdoaiBidAdapter_spec.js +++ b/test/spec/modules/vdoaiBidAdapter_spec.js @@ -37,6 +37,7 @@ describe('vdoaiBidAdapter', function () { 'bidId': '23beaa6af6cdde', 'bidderRequestId': '19c0c1efdf37e7', 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + 'mediaTypes': 'banner' } ]; @@ -101,5 +102,40 @@ describe('vdoaiBidAdapter', function () { let result = spec.interpretResponse(serverResponse, bidRequest[0]); expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); }); + + it('handles instream video responses', function () { + let serverResponse = { + body: { + 'vdoCreative': '', + 'price': 4.2, + 'adid': '12345asdfg', + 'currency': 'EUR', + 'statusMessage': 'Bid available', + 'requestId': 'bidId123', + 'width': 300, + 'height': 250, + 'netRevenue': true, + 'mediaType': 'video' + } + }; + let bidRequest = [ + { + 'method': 'POST', + 'url': ENDPOINT_URL, + 'data': { + 'placementId': 'testPlacementId', + 'width': '300', + 'height': '200', + 'bidId': 'bidId123', + 'referer': 'www.example.com', + 'mediaType': 'video' + } + } + ]; + + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(result[0]).to.have.property('vastXml'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); }); }); From c0af43297cc4ba8e89ca2d238e6469565850ff42 Mon Sep 17 00:00:00 2001 From: mmoschovas <63253416+mmoschovas@users.noreply.github.com> Date: Wed, 9 Dec 2020 13:54:26 -0500 Subject: [PATCH 014/181] Price Floors update to include modelTimestamp displaying when the floors file was produced (#6061) Rubicon Anaytics Update to pass modelTimestamp if exists --- modules/priceFloors.js | 1 + modules/rubiconAnalyticsAdapter.js | 1 + test/spec/modules/priceFloors_spec.js | 23 +++++++++++++++++++ .../modules/rubiconAnalyticsAdapter_spec.js | 3 +++ 4 files changed, 28 insertions(+) diff --git a/modules/priceFloors.js b/modules/priceFloors.js index fd8a46b172f..c0797f710de 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -291,6 +291,7 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { skipRate: floorData.skipRate, floorMin: floorData.floorMin, modelVersion: utils.deepAccess(floorData, 'data.modelVersion'), + modelTimestamp: utils.deepAccess(floorData, 'data.modelTimestamp'), location: utils.deepAccess(floorData, 'data.location', 'noData'), floorProvider: floorData.floorProvider, fetchStatus: _floorsConfig.fetchStatus diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index ff8cb7895b9..ad78c601ab6 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -237,6 +237,7 @@ function sendMessage(auctionId, bidWonId) { auction.floors = utils.pick(auctionCache.floorData, [ 'location', 'modelVersion as modelName', + 'modelTimestamp', 'skipped', 'enforcement', () => utils.deepAccess(auctionCache.floorData, 'enforcements.enforceJS'), 'dealsEnforced', () => utils.deepAccess(auctionCache.floorData, 'enforcements.floorDeals'), diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index 8c673d29701..1b3ce021068 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -23,6 +23,7 @@ describe('the price floors module', function () { let clock; const basicFloorData = { modelVersion: 'basic model', + modelTimestamp: 1606772895, currency: 'USD', schema: { delimiter: '|', @@ -184,6 +185,7 @@ describe('the price floors module', function () { let resultingData = getFloorsDataForAuction(inputFloorData, 'test_div_1'); expect(resultingData).to.deep.equal({ modelVersion: 'basic model', + modelTimestamp: 1606772895, currency: 'USD', schema: { delimiter: '|', @@ -201,6 +203,7 @@ describe('the price floors module', function () { resultingData = getFloorsDataForAuction(inputFloorData, 'this_is_a_div'); expect(resultingData).to.deep.equal({ modelVersion: 'basic model', + modelTimestamp: 1606772895, currency: 'USD', schema: { delimiter: '^', @@ -429,6 +432,7 @@ describe('the price floors module', function () { skipped: true, floorMin: undefined, modelVersion: undefined, + modelTimestamp: undefined, location: 'noData', skipRate: 0, fetchStatus: undefined, @@ -463,6 +467,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'adUnit Model Version', + modelTimestamp: 1606772895, location: 'adUnit', skipRate: 0, fetchStatus: undefined, @@ -496,6 +501,7 @@ describe('the price floors module', function () { validateBidRequests(true, { skipped: false, modelVersion: 'adUnit Model Version', + modelTimestamp: 1606772895, location: 'adUnit', skipRate: 0, floorMin: 7, @@ -510,6 +516,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -531,6 +538,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -545,6 +553,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -559,6 +568,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -582,6 +592,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 50, fetchStatus: undefined, @@ -596,6 +607,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 10, fetchStatus: undefined, @@ -610,6 +622,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -674,6 +687,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'model-1', + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -687,6 +701,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'model-2', + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -700,6 +715,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'model-3', + modelTimestamp: undefined, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -729,6 +745,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: undefined, @@ -808,6 +825,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'timeout', @@ -846,6 +864,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'fetch model name', + modelTimestamp: 1606772895, location: 'fetch', skipRate: 0, fetchStatus: 'success', @@ -883,6 +902,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'fetch model name', + modelTimestamp: 1606772895, location: 'fetch', skipRate: 0, fetchStatus: 'success', @@ -923,6 +943,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'fetch model name', + modelTimestamp: 1606772895, location: 'fetch', skipRate: 95, fetchStatus: 'success', @@ -945,6 +966,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'error', @@ -969,6 +991,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, fetchStatus: 'success', diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 4891b8d3282..0d6cf331e52 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -852,6 +852,7 @@ describe('rubicon analytics adapter', function () { auctionInit.bidderRequests[0].bids[0].floorData = { skipped: false, modelVersion: 'someModelName', + modelTimestamp: 1606772895, location: 'setConfig', skipRate: 15, fetchStatus: 'error', @@ -953,6 +954,7 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', + modelTimestamp: 1606772895, skipped: false, enforcement: true, dealsEnforced: false, @@ -998,6 +1000,7 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', + modelTimestamp: 1606772895, skipped: false, enforcement: true, dealsEnforced: false, From a17c2346c233c301b948c5b5b69ad1d30a26c53a Mon Sep 17 00:00:00 2001 From: Olivier Date: Wed, 9 Dec 2020 20:26:28 +0100 Subject: [PATCH 015/181] Adagio Bid Adapter: hotfix - detect support for intersectionObserver (#6095) * Detect support for intersectionObserver * Fix IE11 * Add special params for non standard integration * Stronger IntersectionObserver detection * Fix test and add new params to .md file --- modules/adagioBidAdapter.js | 19 +++++++++-- modules/adagioBidAdapter.md | 4 +++ test/spec/modules/adagioBidAdapter_spec.js | 37 ++++++++++++++++++++-- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index cab233d5387..6f7feec59c9 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -495,6 +495,12 @@ function autoDetectEnvironment() { return environment; }; +function supportIObs() { + const currentWindow = internal.getCurrentWindow(); + return !!(currentWindow && currentWindow.IntersectionObserver && currentWindow.IntersectionObserverEntry && + currentWindow.IntersectionObserverEntry.prototype && 'intersectionRatio' in currentWindow.IntersectionObserverEntry.prototype); +} + function getFeatures(bidRequest, bidderRequest) { const { adUnitCode, params } = bidRequest; const { adUnitElementId } = params; @@ -569,6 +575,7 @@ export const internal = { getRefererInfo, adagioScriptFromLocalStorageCb, getCurrentWindow, + supportIObs, canAccessTopWindow, isRendererPreferredFromPublisher }; @@ -692,15 +699,21 @@ export const spec = { return false; } - const { organizationId, site, placement } = params; - const adUnitElementId = params.adUnitElementId || internal.autoDetectAdUnitElementId(adUnitCode); + const { organizationId, site } = params; + const adUnitElementId = (params.useAdUnitCodeAsAdUnitElementId === true) + ? adUnitCode + : params.adUnitElementId || internal.autoDetectAdUnitElementId(adUnitCode); + const placement = (params.useAdUnitCodeAsPlacement === true) ? adUnitCode : params.placement; const environment = params.environment || internal.autoDetectEnvironment(); + const supportIObs = internal.supportIObs(); // insure auto-detected params are kept in `bid` object. bid.params = { ...params, adUnitElementId, - environment + environment, + placement, + supportIObs }; const debugData = () => ({ diff --git a/modules/adagioBidAdapter.md b/modules/adagioBidAdapter.md index c55a24f1115..aa79338d79e 100644 --- a/modules/adagioBidAdapter.md +++ b/modules/adagioBidAdapter.md @@ -38,6 +38,8 @@ Connects to Adagio demand source to fetch bids. category: 'sport', // Recommended. Category of the content displayed in the page. subcategory: 'handball', // Optional. Subcategory of the content displayed in the page. postBid: false, // Optional. Use it in case of Post-bid integration only. + useAdUnitCodeAsAdUnitElementId: false // Optional. Use it by-pass adUnitElementId and use the adUnit code as value + useAdUnitCodeAsPlacement: false // Optional. Use it to by-pass placement and use the adUnit code as value // Optional debug mode, used to get a bid response with expected cpm. debug: { enabled: true, @@ -76,6 +78,8 @@ Connects to Adagio demand source to fetch bids. category: 'sport', // Recommended. Category of the content displayed in the page. subcategory: 'handball', // Optional. Subcategory of the content displayed in the page. postBid: false, // Optional. Use it in case of Post-bid integration only. + useAdUnitCodeAsAdUnitElementId: false // Optional. Use it by-pass adUnitElementId and use the adUnit code as value + useAdUnitCodeAsPlacement: false // Optional. Use it to by-pass placement and use the adUnit code as value video: { skip: 0 // OpenRTB 2.5 video options defined here override ones defined in mediaTypes. diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 2cf97a1129b..0a585caaa1a 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -144,6 +144,19 @@ describe('Adagio bid adapter', () => { sinon.assert.callCount(utils.logWarn, 1); }); + it('should use adUnit code for adUnitElementId and placement params', function() { + const bid01 = new BidRequestBuilder({ params: { + organizationId: '1000', + site: 'site-name', + useAdUnitCodeAsPlacement: true, + useAdUnitCodeAsAdUnitElementId: true + }}).build(); + + expect(spec.isBidRequestValid(bid01)).to.equal(true); + expect(bid01.params.adUnitElementId).to.equal('adunit-code'); + expect(bid01.params.placement).to.equal('adunit-code'); + }) + it('should return false when a required param is missing', function() { const bid01 = new BidRequestBuilder({ params: { organizationId: '1000', @@ -229,7 +242,8 @@ describe('Adagio bid adapter', () => { placement: 'PAVE_ATF', site: 'SITE-NAME', adUnitElementId: 'gpt-adunit-code', - environment: 'desktop' + environment: 'desktop', + supportIObs: true } }], auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', @@ -249,7 +263,8 @@ describe('Adagio bid adapter', () => { placement: 'PAVE_ATF', site: 'SITE-NAME', adUnitElementId: 'gpt-adunit-code', - environment: 'desktop' + environment: 'desktop', + supportIObs: true } }], auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', @@ -260,6 +275,7 @@ describe('Adagio bid adapter', () => { it('should store bids config once by bid in window.top if it accessible', function() { sandbox.stub(adagio, 'getCurrentWindow').returns(window.top); + sandbox.stub(adagio, 'supportIObs').returns(true); // replace by the values defined in beforeEach window.top.ADAGIO = { @@ -274,8 +290,22 @@ describe('Adagio bid adapter', () => { expect(find(window.top.ADAGIO.pbjsAdUnits, aU => aU.code === 'adunit-code-02')).to.deep.eql(expected[1]); }); + it('should detect IntersectionObserver support', function() { + sandbox.stub(adagio, 'getCurrentWindow').returns(window.top); + sandbox.stub(adagio, 'supportIObs').returns(false); + + window.top.ADAGIO = { + ...window.ADAGIO + }; + + spec.isBidRequestValid(bid01); + const validBidReq = find(window.top.ADAGIO.pbjsAdUnits, aU => aU.code === 'adunit-code-01'); + expect(validBidReq.bids[0].params.supportIObs).to.equal(false); + }); + it('should store bids config once by bid in current window', function() { sandbox.stub(adagio, 'getCurrentWindow').returns(window.self); + sandbox.stub(adagio, 'supportIObs').returns(true); spec.isBidRequestValid(bid01); spec.isBidRequestValid(bid02); @@ -740,7 +770,8 @@ describe('Adagio bid adapter', () => { pagetype: 'ARTICLE', category: 'NEWS', subcategory: 'SPORT', - environment: 'desktop' + environment: 'desktop', + supportIObs: true }, adUnitCode: 'adunit-code', mediaTypes: { From 83bafc0475a68033f6e0d9d1fe9e7014cea5ead8 Mon Sep 17 00:00:00 2001 From: Gena Date: Wed, 9 Dec 2020 22:11:58 +0200 Subject: [PATCH 016/181] Adt new alias (#6004) * Add new alias * Add new aliases and fix endpoints * lint * Fix tests --- modules/adtelligentBidAdapter.js | 16 ++++++++++++---- test/spec/modules/adtelligentBidAdapter_spec.js | 17 +++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index 51138a2cac7..b2e37767a1e 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -12,15 +12,23 @@ const HOST_GETTERS = { default: (function () { let num = 0; return function () { - return 'ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com' + return 'ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com'; } }()), appaloosa: function () { - return 'hb.appaloosa.media' + return 'ghb.hb.appaloosa.media'; + }, + onefiftytwomedia: function() { + return 'ghb.ads.152media.com'; + }, + mediafuse: function() { + return 'ghb.hbmp.mediafuse.com'; } + } const getUri = function (bidderCode) { - let getter = HOST_GETTERS[bidderCode] || HOST_GETTERS['default']; + let bidderWithoutSuffix = bidderCode.split('_')[0]; + let getter = HOST_GETTERS[bidderWithoutSuffix] || HOST_GETTERS['default']; return PROTOCOL + getter() + AUCTION_PATH } const OUTSTREAM_SRC = 'https://player.adtelligent.com/outstream-unit/2.01/outstream.min.js'; @@ -32,7 +40,7 @@ const syncsCache = {}; export const spec = { code: BIDDER_CODE, gvlid: 410, - aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa'], + aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa', 'mediafuse'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { return !!utils.deepAccess(bid, 'params.aid'); diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index 62449771416..5c1e4a38d03 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -11,8 +11,13 @@ const EXPECTED_ENDPOINTS = [ 'https://ghb.adtelligent.com/v2/auction/' ]; const aliasEP = { - appaloosa: 'https://hb.appaloosa.media/v2/auction/' + appaloosa: 'https://ghb.hb.appaloosa.media/v2/auction/', + appaloosa_publisherSuffix: 'https://ghb.hb.appaloosa.media/v2/auction/', + onefiftytwomedia: 'https://ghb.ads.152media.com/v2/auction/', + mediafuse: 'https://ghb.hbmp.mediafuse.com/v2/auction/' }; + +const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; const DISPLAY_REQUEST = { 'bidder': 'adtelligent', 'params': { @@ -244,10 +249,10 @@ describe('adtelligentBidAdapter', () => { let videoBidRequests = [VIDEO_REQUEST]; let displayBidRequests = [DISPLAY_REQUEST]; let videoAndDisplayBidRequests = [DISPLAY_REQUEST, VIDEO_REQUEST]; - const displayRequest = spec.buildRequests(displayBidRequests, {}); - const videoRequest = spec.buildRequests(videoBidRequests, {}); - const videoAndDisplayRequests = spec.buildRequests(videoAndDisplayBidRequests, {}); - const rotatingRequest = spec.buildRequests(displayBidRequests, {}); + const displayRequest = spec.buildRequests(displayBidRequests, DEFAULT_ADATPER_REQ); + const videoRequest = spec.buildRequests(videoBidRequests, DEFAULT_ADATPER_REQ); + const videoAndDisplayRequests = spec.buildRequests(videoAndDisplayBidRequests, DEFAULT_ADATPER_REQ); + const rotatingRequest = spec.buildRequests(displayBidRequests, DEFAULT_ADATPER_REQ); it('rotates endpoints', () => { const bidReqUrls = [displayRequest[0], videoRequest[0], videoAndDisplayRequests[0], rotatingRequest[0]].map(br => br.url); expect(bidReqUrls).to.deep.equal(EXPECTED_ENDPOINTS); @@ -276,7 +281,7 @@ describe('adtelligentBidAdapter', () => { expect(videoAndDisplayRequests.every(comparator)).to.be.true; }); it('forms correct ADPOD request', () => { - const pbBidReqData = spec.buildRequests([ADPOD_REQUEST], {})[0].data; + const pbBidReqData = spec.buildRequests([ADPOD_REQUEST], DEFAULT_ADATPER_REQ)[0].data; const impRequest = pbBidReqData.BidRequests[0] expect(impRequest.AdType).to.be.equal('video'); expect(impRequest.Adpod).to.be.a('object'); From 09601b65a6a82aca419afcc618bd798cf429aa22 Mon Sep 17 00:00:00 2001 From: Mike Chowla Date: Wed, 9 Dec 2020 12:49:22 -0800 Subject: [PATCH 017/181] Prebid 4.9.0 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ed4514cee42..5c82e724453 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.19.0-pre", + "version": "4.19.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 3496b3f627202fedca7a4a42d97407b819d6c014 Mon Sep 17 00:00:00 2001 From: Mike Chowla Date: Wed, 9 Dec 2020 13:14:51 -0800 Subject: [PATCH 018/181] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c82e724453..04ec495c93d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.19.0", + "version": "4.20.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From f7a91ca2e00c2530d7a4a8fbaa959aab8e455b26 Mon Sep 17 00:00:00 2001 From: Stephen Johnston Date: Wed, 9 Dec 2020 17:54:33 -0500 Subject: [PATCH 019/181] Add Release Drafter Instructions to PR_REVIEW.md (#6085) * Add Release Drafter Instructions to PR_REVIEW.md * Add Prettier Formatting Makes keywords more distinct. Co-authored-by: Scott Menzer Co-authored-by: Scott Menzer --- PR_REVIEW.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/PR_REVIEW.md b/PR_REVIEW.md index f991a0254f5..662a1a871c8 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -18,7 +18,10 @@ For modules and core platform updates, the initial reviewer should request an ad - If the change results in needing updates to docs (such as public API change, module interface etc), add a label for "needs docs" and inform the submitter they must submit a docs PR to update the appropriate area of Prebid.org **before the PR can merge**. Help them with finding where the docs are located on prebid.org if needed. - If all above is good, add a `LGTM` comment and, if the change is in PBS-core or is an important module like the prebidServerBidAdapter, request 1 additional core member to review. - Once there are 2 `LGTM` on the PR, merge to master -- Add a line into the [draft release](https://github.com/prebid/Prebid.js/releases) notes for this submission. If no draft release is available, create one using [this template]( https://gist.github.com/mkendall07/c3af6f4691bed8a46738b3675cb5a479) +- The [draft release](https://github.com/prebid/Prebid.js/releases) notes are managed by [release drafter](https://github.com/release-drafter/release-drafter). To get the PR added to the release notes do the steps below. A github action will use that information to build the release notes. + - Adjust the PR Title to be appropriate for release notes + - Add a label for `feature`, `maintenance`, `fix`, `bugfix` or `bug` to categorize the PR + - Add a semver label of `major`, `minor` or `patch` to indicate the scope of change ### Reviewing a New or Updated Bid Adapter Documentation they're supposed to be following is https://docs.prebid.org/dev-docs/bidder-adaptor.html From 03b8213de8bede7883323fff6f3ccf00bad08410 Mon Sep 17 00:00:00 2001 From: olafbuitelaar Date: Thu, 10 Dec 2020 10:36:45 +0100 Subject: [PATCH 020/181] fix typo in PREVENT_WRITING_ON_MAIN_DOCUMENT (#6102) --- src/constants.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.json b/src/constants.json index 7c0af445cdb..7e82946b65c 100644 --- a/src/constants.json +++ b/src/constants.json @@ -41,7 +41,7 @@ "AUCTION_DEBUG": "auctionDebug" }, "AD_RENDER_FAILED_REASON" : { - "PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocuemnt", + "PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocument", "NO_AD": "noAd", "EXCEPTION": "exception", "CANNOT_FIND_AD": "cannotFindAd", From 754ce678b47b8e13148e0a02a9db0d477ce3eaa6 Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Thu, 10 Dec 2020 06:10:25 -0500 Subject: [PATCH 021/181] appnexusBidAdapter - update segment param logic (#6103) --- modules/appnexusBidAdapter.js | 14 +++++++++++++- test/spec/modules/appnexusBidAdapter_spec.js | 2 ++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 203835db611..c102bee4e58 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -107,7 +107,19 @@ export const spec = { .filter(param => includes(USER_PARAMS, param)) .forEach((param) => { let uparam = utils.convertCamelToUnderscore(param); - userObj[uparam] = userObjBid.params.user[param] + if (param === 'segments' && utils.isArray(userObjBid.params.user[param])) { + let segs = []; + userObjBid.params.user[param].forEach(val => { + if (utils.isNumber(val)) { + segs.push({'id': val}); + } else if (utils.isPlainObject(val)) { + segs.push(val); + } + }); + userObj[uparam] = segs; + } else if (param !== 'segments') { + userObj[uparam] = userObjBid.params.user[param]; + } }); } diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 426259639e8..9b12d892440 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -265,6 +265,7 @@ describe('AppNexusAdapter', function () { placementId: '10433394', user: { externalUid: '123', + segments: [123, { id: 987, value: 876 }], foobar: 'invalid' } } @@ -277,6 +278,7 @@ describe('AppNexusAdapter', function () { expect(payload.user).to.exist; expect(payload.user).to.deep.equal({ external_uid: '123', + segments: [{id: 123}, {id: 987, value: 876}] }); }); From c8353d39bc769e4fe3ee3b9a7dd3467dbd7bdf2c Mon Sep 17 00:00:00 2001 From: Hiroaki Kubota Date: Fri, 11 Dec 2020 08:56:10 +0900 Subject: [PATCH 022/181] Change craftBidAdapter request URL (#6096) --- modules/craftBidAdapter.js | 5 +++-- test/spec/modules/craftBidAdapter_spec.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 3838f5dee59..0124f96a107 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -7,7 +7,7 @@ import includes from 'core-js-pure/features/array/includes.js'; import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'craft'; -const URL = 'https://gacraft.jp/prebid-v3'; +const URL_BASE = 'https://gacraft.jp/prebid-v3'; const TTL = 360; const storage = getStorageManager(); @@ -143,10 +143,11 @@ function formatRequest(payload, bidderRequest) { withCredentials: false }; } + const payloadString = JSON.stringify(payload); return { method: 'POST', - url: URL, + url: `${URL_BASE}/${payload.tags[0].sitekey}`, data: payloadString, bidderRequest, options diff --git a/test/spec/modules/craftBidAdapter_spec.js b/test/spec/modules/craftBidAdapter_spec.js index ef7dd7c3232..3f4bc977016 100644 --- a/test/spec/modules/craftBidAdapter_spec.js +++ b/test/spec/modules/craftBidAdapter_spec.js @@ -81,7 +81,7 @@ describe('craftAdapter', function () { it('sends bid request to ENDPOINT via POST', function () { let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.method).to.equal('POST'); - expect(request.url).to.equal('https://gacraft.jp/prebid-v3'); + expect(request.url).to.equal('https://gacraft.jp/prebid-v3/craft-prebid-example'); let data = JSON.parse(request.data); expect(data.tags).to.deep.equals([{ sitekey: 'craft-prebid-example', From 0a5f9db693d54278e8198fb00be018089d892db4 Mon Sep 17 00:00:00 2001 From: Oleg Naydenov Date: Fri, 11 Dec 2020 20:59:20 +0200 Subject: [PATCH 023/181] Add Kubient bid adapter, Remove alias from Fidelity bid adapter. (#6084) * Add New Kubient Bid Adapter * Add New Kubient Bid Adapter * Fidelity Bid Adapter Update. Less 'Kubient' Alias * New Kubient Bid Adapter. Errors fix. --- modules/fidelityBidAdapter.js | 1 - modules/kubientBidAdapter.js | 111 +++++++++ modules/kubientBidAdapter.md | 26 ++ test/spec/modules/kubientBidAdapter_spec.js | 259 ++++++++++++++++++++ 4 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 modules/kubientBidAdapter.js create mode 100644 modules/kubientBidAdapter.md create mode 100644 test/spec/modules/kubientBidAdapter_spec.js diff --git a/modules/fidelityBidAdapter.js b/modules/fidelityBidAdapter.js index baf5384fbfe..fac273721ff 100644 --- a/modules/fidelityBidAdapter.js +++ b/modules/fidelityBidAdapter.js @@ -6,7 +6,6 @@ const BIDDER_SERVER = 'x.fidelity-media.com'; const FIDELITY_VENDOR_ID = 408; export const spec = { code: BIDDER_CODE, - aliases: ['kubient'], gvlid: 408, isBidRequestValid: function isBidRequestValid(bid) { return !!(bid && bid.params && bid.params.zoneid); diff --git a/modules/kubientBidAdapter.js b/modules/kubientBidAdapter.js new file mode 100644 index 00000000000..8f6ea53ecce --- /dev/null +++ b/modules/kubientBidAdapter.js @@ -0,0 +1,111 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const BIDDER_CODE = 'kubient'; +const END_POINT = 'https://kssp.kbntx.ch/pbjs'; +const VERSION = '1.0'; +const VENDOR_ID = 794; +export const spec = { + code: BIDDER_CODE, + gvlid: VENDOR_ID, + supportedMediaTypes: [BANNER], + isBidRequestValid: function (bid) { + return !!(bid && bid.params); + }, + buildRequests: function (validBidRequests, bidderRequest) { + if (!validBidRequests || !bidderRequest) { + return; + } + const result = validBidRequests.map(function (bid) { + let data = { + v: VERSION, + requestId: bid.bidderRequestId, + adSlots: [{ + bidId: bid.bidId, + zoneId: bid.params.zoneid || '', + floor: bid.params.floor || 0.0, + sizes: bid.sizes || [], + schain: bid.schain || {}, + mediaTypes: bid.mediaTypes + }], + referer: (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) ? bidderRequest.refererInfo.referer : null, + tmax: bidderRequest.timeout, + gdpr: (bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) ? 1 : 0, + consent: (bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString) ? bidderRequest.gdprConsent.consentString : null, + consentGiven: kubientGetConsentGiven(bidderRequest.gdprConsent), + uspConsent: bidderRequest.uspConsent + }; + return { + method: 'POST', + url: END_POINT, + data: JSON.stringify(data) + }; + }); + return result; + }, + interpretResponse: function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body || !serverResponse.body.seatbid) { + return []; + } + let bidResponses = []; + serverResponse.body.seatbid.forEach(seatbid => { + let bids = seatbid.bid || []; + bids.forEach(bid => { + bidResponses.push({ + requestId: bid.bidId, + cpm: bid.price, + currency: bid.cur, + width: bid.w, + height: bid.h, + creativeId: bid.creativeId, + netRevenue: bid.netRevenue, + ttl: bid.ttl, + ad: bid.adm + }); + }); + }); + return bidResponses; + }, + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + let gdprParams = ''; + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + gdprParams = `?consent_str=${gdprConsent.consentString}`; + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = gdprParams + `&gdpr=${Number(gdprConsent.gdprApplies)}`; + } + gdprParams = gdprParams + `&consent_given=` + kubientGetConsentGiven(gdprConsent); + } + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: 'https://kdmp.kbntx.ch/init.html' + gdprParams + }); + } + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: 'https://kdmp.kbntx.ch/init.png' + gdprParams + }); + } + return syncs; + } +}; + +function kubientGetConsentGiven(gdprConsent) { + let consentGiven = 0; + if (typeof gdprConsent !== 'undefined') { + let apiVersion = utils.deepAccess(gdprConsent, `apiVersion`); + switch (apiVersion) { + case 1: + consentGiven = utils.deepAccess(gdprConsent, `vendorData.vendorConsents.${VENDOR_ID}`) ? 1 : 0; + break; + case 2: + consentGiven = utils.deepAccess(gdprConsent, `vendorData.vendor.consents.${VENDOR_ID}`) ? 1 : 0; + break; + } + } + return consentGiven; +} +registerBidder(spec); diff --git a/modules/kubientBidAdapter.md b/modules/kubientBidAdapter.md new file mode 100644 index 00000000000..9f3e1d5f52e --- /dev/null +++ b/modules/kubientBidAdapter.md @@ -0,0 +1,26 @@ +# Overview +​ +**Module Name**: Kubient Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: artem.aleksashkin@kubient.com +​ +# Description +​ +Connects to Kubient KSSP demand source to fetch bids. +​ +# Test Parameters +``` + var adUnits = [{ + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250],[728, 90]], + } + }, + bids: [{ + "bidder": "kubient", + "params": { + "zoneid": "5fbb948f1e22b", + } + }] + }]; diff --git a/test/spec/modules/kubientBidAdapter_spec.js b/test/spec/modules/kubientBidAdapter_spec.js new file mode 100644 index 00000000000..1df4370b2ba --- /dev/null +++ b/test/spec/modules/kubientBidAdapter_spec.js @@ -0,0 +1,259 @@ +import { expect, assert } from 'chai'; +import { spec } from 'modules/kubientBidAdapter.js'; + +describe('KubientAdapter', function () { + let bid = { + bidId: '2dd581a2b6281d', + bidder: 'kubient', + bidderRequestId: '145e1d6a7837c9', + params: { + zoneid: '5678', + floor: 0.05, + }, + auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + transactionId: '3bb2f6da-87a6-4029-aeb0-bfe951372e62', + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'example.com', + sid: '0', + hp: 1, + rid: 'bidrequestid', + domain: 'example.com' + } + ] + } + }; + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let uspConsentData = '1YCC'; + let bidderRequest = { + bidderCode: 'kubient', + auctionId: 'fffffff-ffff-ffff-ffff-ffffffffffff', + bidderRequestId: 'ffffffffffffff', + start: 1472239426002, + auctionStart: 1472239426000, + timeout: 5000, + refererInfo: { + referer: 'http://www.example.com', + reachedTop: true, + }, + gdprConsent: { + consentString: consentString, + gdprApplies: true + }, + uspConsent: uspConsentData, + bids: [bid] + }; + describe('buildRequests', function () { + let serverRequests = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequests).to.be.an('array'); + }); + for (let i = 0; i < serverRequests.length; i++) { + let serverRequest = serverRequests[i]; + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest.method).to.be.a('string'); + expect(serverRequest.url).to.be.a('string'); + expect(serverRequest.data).to.be.a('string'); + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://kssp.kbntx.ch/pbjs'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = JSON.parse(serverRequest.data); + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('v', 'requestId', 'adSlots', 'gdpr', 'referer', 'tmax', 'consent', 'consentGiven', 'uspConsent'); + expect(data.v).to.exist.and.to.be.a('string'); + expect(data.requestId).to.exist.and.to.be.a('string'); + expect(data.referer).to.be.a('string'); + expect(data.tmax).to.exist.and.to.be.a('number'); + expect(data.gdpr).to.exist.and.to.be.within(0, 1); + expect(data.consent).to.equal(consentString); + expect(data.uspConsent).to.exist.and.to.equal(uspConsentData); + for (let j = 0; j < data['adSlots'].length; j++) { + let adSlot = data['adSlots'][i]; + expect(adSlot).to.have.all.keys('bidId', 'zoneId', 'floor', 'sizes', 'schain', 'mediaTypes'); + expect(adSlot.bidId).to.be.a('string'); + expect(adSlot.zoneId).to.be.a('string'); + expect(adSlot.floor).to.be.a('number'); + expect(adSlot.sizes).to.be.an('array'); + expect(adSlot.schain).to.be.an('object'); + expect(adSlot.mediaTypes).to.be.an('object'); + } + }); + } + }); + + describe('isBidRequestValid', function () { + it('Should return true when required params are found', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false when required params are not found', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false when params are not found', function () { + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret response', function () { + const serverResponse = { + body: + { + seatbid: [ + { + bid: [ + { + bidId: '000', + price: 1.5, + adm: '
test
', + creativeId: 'creativeId', + w: 300, + h: 250, + cur: 'USD', + netRevenue: false, + ttl: 360 + } + ] + } + ] + } + }; + let bannerResponses = spec.interpretResponse(serverResponse); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'ad', 'creativeId', 'width', 'height', 'currency', 'netRevenue', 'ttl'); + expect(dataItem.requestId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].bidId); + expect(dataItem.cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(dataItem.ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(dataItem.creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].creativeId); + expect(dataItem.width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(dataItem.height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(dataItem.currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].cur); + expect(dataItem.netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(serverResponse.body.seatbid[0].bid[0].netRevenue); + expect(dataItem.ttl).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].ttl); + }); + + it('Should return no ad when not given a server response', function () { + const ads = spec.interpretResponse(null); + expect(ads).to.be.an('array').and.to.have.length(0); + }); + }); + + describe('getUserSyncs', function () { + it('should register the sync iframe without gdpr', function () { + let syncOptions = { + iframeEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&consent_given=0'); + }); + it('should register the sync iframe with gdpr', function () { + let syncOptions = { + iframeEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&gdpr=1&consent_given=0'); + }); + it('should register the sync iframe with gdpr vendor', function () { + let syncOptions = { + iframeEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString, + apiVersion: 1, + vendorData: { + vendorConsents: { + 794: 1 + } + } + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&gdpr=1&consent_given=1'); + }); + it('should register the sync image without gdpr', function () { + let syncOptions = { + pixelEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&consent_given=0'); + }); + it('should register the sync image with gdpr', function () { + let syncOptions = { + pixelEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&gdpr=1&consent_given=0'); + }); + it('should register the sync image with gdpr vendor', function () { + let syncOptions = { + pixelEnabled: true + }; + let serverResponses = null; + let gdprConsent = { + gdprApplies: true, + consentString: consentString, + apiVersion: 2, + vendorData: { + vendor: { + consents: { + 794: 1 + } + } + } + }; + let uspConsent = null; + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&gdpr=1&consent_given=1'); + }); + }) +}); From 9f38423d98a9572163541957a50c70f1dedae037 Mon Sep 17 00:00:00 2001 From: Mathieu Pheulpin Date: Fri, 11 Dec 2020 13:30:24 -0800 Subject: [PATCH 024/181] [Sharethrough] Add Support for badv/bcat and Identity Link User ID (#6100) * Add support for badv and bcat [#175358412] Co-authored-by: Michael Duran * Add support for Identity Link [#175688307] Co-authored-by: Michael Duran Co-authored-by: Michael Duran --- modules/sharethroughBidAdapter.js | 16 +++++- modules/sharethroughBidAdapter.md | 8 ++- .../modules/sharethroughBidAdapter_spec.js | 54 ++++++++++++++++--- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 7df161db713..89484b1c68b 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,7 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; -const VERSION = '3.2.1'; +const VERSION = '3.3.0'; const BIDDER_CODE = 'sharethrough'; const STR_ENDPOINT = 'https://btlr.sharethrough.com/WYu2BXv1/v1'; const DEFAULT_SIZE = [1, 1]; @@ -56,6 +56,10 @@ export const sharethroughAdapterSpec = { query.pubcid = bidRequest.crumbs.pubcid; } + if (bidRequest.userId && bidRequest.userId.idl_env) { + query.idluid = bidRequest.userId.idl_env; + } + if (bidRequest.schain) { query.schain = JSON.stringify(bidRequest.schain); } @@ -64,6 +68,14 @@ export const sharethroughAdapterSpec = { query.bidfloor = parseFloat(bidRequest.bidfloor); } + if (bidRequest.params.badv) { + query.badv = bidRequest.params.badv; + } + + if (bidRequest.params.bcat) { + query.bcat = bidRequest.params.bcat; + } + // Data that does not need to go to the server, // but we need as part of interpretResponse() const strData = { @@ -73,7 +85,7 @@ export const sharethroughAdapterSpec = { }; return { - method: 'GET', + method: 'POST', url: STR_ENDPOINT, data: query, strData: strData diff --git a/modules/sharethroughBidAdapter.md b/modules/sharethroughBidAdapter.md index 2290e370cae..396b8164577 100644 --- a/modules/sharethroughBidAdapter.md +++ b/modules/sharethroughBidAdapter.md @@ -29,7 +29,13 @@ Module that connects to Sharethrough's demand sources // OPTIONAL - If iframeSize is provided, we'll use this size for the iframe // otherwise we'll grab the largest size from the sizes array // This is ignored if iframe: false - iframeSize: [250, 250] + iframeSize: [250, 250], + + // OPTIONAL - Blocked Advertiser Domains + badv: ['domain1.com', 'domain2.com'], + + // OPTIONAL - Blocked Categories (IAB codes) + bcat: ['IAB1-1', 'IAB1-2'], } } ] diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index d45d1e977e6..cd9071a6098 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -14,7 +14,8 @@ const bidRequests = [ }, userId: { tdid: 'fake-tdid', - pubcid: 'fake-pubcid' + pubcid: 'fake-pubcid', + idl_env: 'fake-identity-link' }, crumbs: { pubcid: 'fake-pubcid-in-crumbs-obj' @@ -40,12 +41,32 @@ const bidRequests = [ iframe: true, iframeSize: [500, 500] } - } + }, + { + bidder: 'sharethrough', + bidId: 'bidId4', + sizes: [[700, 400]], + placementCode: 'bar', + params: { + pkey: 'dddd4444', + badv: ['domain1.com', 'domain2.com'] + } + }, + { + bidder: 'sharethrough', + bidId: 'bidId5', + sizes: [[700, 400]], + placementCode: 'bar', + params: { + pkey: 'eeee5555', + bcat: ['IAB1-1', 'IAB1-2'] + } + }, ]; const prebidRequests = [ { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -57,7 +78,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -69,7 +90,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -82,7 +103,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -94,7 +115,7 @@ const prebidRequests = [ } }, { - method: 'GET', + method: 'POST', url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', data: { bidId: 'bidId', @@ -225,7 +246,7 @@ describe('sharethrough adapter spec', function() { expect(builtBidRequests[0].url).to.eq('https://btlr.sharethrough.com/WYu2BXv1/v1'); expect(builtBidRequests[1].url).to.eq('https://btlr.sharethrough.com/WYu2BXv1/v1'); - expect(builtBidRequests[0].method).to.eq('GET'); + expect(builtBidRequests[0].method).to.eq('POST'); }); it('should set the instant_play_capable parameter correctly based on browser userAgent string', function() { @@ -315,6 +336,11 @@ describe('sharethrough adapter spec', function() { expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); }); + it('should add the idluid parameter if a bid request contains a value for Identity Link from Live Ramp', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.idluid).to.eq('fake-identity-link'); + }); + it('should add Sharethrough specific parameters', function() { const builtBidRequests = spec.buildRequests(bidRequests); expect(builtBidRequests[0]).to.deep.include({ @@ -346,6 +372,18 @@ describe('sharethrough adapter spec', function() { expect(builtBidRequest.data.schain).to.eq(JSON.stringify(bidRequest.schain)); }); + it('should add badv if provided', () => { + const builtBidRequest = spec.buildRequests([bidRequests[3]])[0]; + + expect(builtBidRequest.data.badv).to.have.members(['domain1.com', 'domain2.com']) + }); + + it('should add bcat if provided', () => { + const builtBidRequest = spec.buildRequests([bidRequests[4]])[0]; + + expect(builtBidRequest.data.bcat).to.have.members(['IAB1-1', 'IAB1-2']) + }); + it('should not add a supply chain parameter if schain is missing', function() { const bidRequest = spec.buildRequests(bidRequests)[0]; expect(bidRequest.data).to.not.include.any.keys('schain'); From 25270510561bf2b8d3f303d25b0a6c80277fd03c Mon Sep 17 00:00:00 2001 From: Egor Gordeev <48566506+egsgordeev@users.noreply.github.com> Date: Mon, 14 Dec 2020 10:51:48 +0400 Subject: [PATCH 025/181] Sovrn: Pass the imp.ext.deals field (#6098) * EX-2549 Pass segments parameter as imp.ext.dealids array * EX-2549 Address Jon's feedback * EX-2549 Reworked the solution * EX-2549 Blackbird compatibility * EX-2549 Address Jon's comments * EX-2549 Addressed upstream PR comments --- modules/sovrnBidAdapter.js | 14 ++++++++++-- test/spec/modules/sovrnBidAdapter_spec.js | 26 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 62f5e85779e..8f8158fd0c9 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -53,7 +53,7 @@ export const spec = { bidSizes = ((utils.isArray(bidSizes) && utils.isArray(bidSizes[0])) ? bidSizes : [bidSizes]) bidSizes = bidSizes.filter(size => utils.isArray(size)) const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})) - sovrnImps.push({ + const imp = { adunitcode: bid.adUnitCode, id: bid.bidId, banner: { @@ -63,7 +63,17 @@ export const spec = { }, tagid: String(utils.getBidIdParameter('tagid', bid.params)), bidfloor: utils.getBidIdParameter('bidfloor', bid.params) - }); + } + + const segmentsString = utils.getBidIdParameter('segments', bid.params) + + if (segmentsString) { + imp.ext = { + deals: segmentsString.split(',').map(deal => deal.trim()) + } + } + + sovrnImps.push(imp); }); const page = bidderRequest.refererInfo.referer diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 983ade4dd14..769be73a272 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -276,6 +276,32 @@ describe('sovrnBidAdapter', function() { expect(data.user.ext.tpid[0].uid).to.equal('A_CRITEO_ID') expect(data.user.ext.prebid_criteoid).to.equal('A_CRITEO_ID') }); + + it('should ignore empty segments', function() { + const payload = JSON.parse(request.data) + expect(payload.imp[0].ext).to.be.undefined + }) + + it('should pass the segments param value as trimmed deal ids array', function() { + const segmentsRequests = [{ + 'bidder': 'sovrn', + 'params': { + 'segments': ' test1,test2 ' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250], + [300, 600] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + }]; + const request = spec.buildRequests(segmentsRequests, bidderRequest) + const payload = JSON.parse(request.data) + expect(payload.imp[0].ext.deals[0]).to.equal('test1') + expect(payload.imp[0].ext.deals[1]).to.equal('test2') + }) }); describe('interpretResponse', function () { From 42be508d35e10ab60d9421873d2b60ae97b66cb0 Mon Sep 17 00:00:00 2001 From: Adam Browning <19834421+adam-browning@users.noreply.github.com> Date: Mon, 14 Dec 2020 12:07:32 +0200 Subject: [PATCH 026/181] oneVideo Adapter - Dynamic TTL support (SAPR-15473) (#6108) * ttl incoming value or default 300 * ttl validation 0-3600 * Fix to oneVideo tests * added ttl unit tests * updated md file * update minor version 3.0.5 * Updated unit test for minor version check * remove x from liveIntentIdSystem_spec.js * md conflict fix * fix missing comma in md file * update minor version 3.0.5 * remove x from liveIntentIdSystem_spec.js * update md file * added ttl to md file * ttl incoming value or default 300 * ttl validation 0-3600 * Fix to oneVideo tests * added ttl unit tests * updated md file * update minor version 3.0.5 * Updated unit test for minor version check * remove x from liveIntentIdSystem_spec.js * md conflict fix * update minor version 3.0.5 * remove x from liveIntentIdSystem_spec.js * update md file * added ttl to md file * cleanup --- modules/oneVideoBidAdapter.js | 5 ++-- modules/oneVideoBidAdapter.md | 2 ++ test/spec/modules/oneVideoBidAdapter_spec.js | 28 ++++++++++++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/modules/oneVideoBidAdapter.js b/modules/oneVideoBidAdapter.js index c287bc2f3b7..8a71910e8fc 100644 --- a/modules/oneVideoBidAdapter.js +++ b/modules/oneVideoBidAdapter.js @@ -4,7 +4,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'oneVideo'; export const spec = { code: 'oneVideo', - VERSION: '3.0.4', + VERSION: '3.0.5', ENDPOINT: 'https://ads.adaptv.advertising.com/rtb/openrtb?ext_id=', E2ETESTENDPOINT: 'https://ads-wc.v.ssp.yahoo.com/rtb/openrtb?ext_id=', SYNC_ENDPOINT1: 'https://pixel.advertising.com/ups/57304/sync?gdpr=&gdpr_consent=&_origin=0&redir=true', @@ -99,7 +99,7 @@ export const spec = { width: size.width, height: size.height, currency: response.cur, - ttl: 100, + ttl: (bidRequest.params.video.ttl > 0 && bidRequest.params.video.ttl <= 3600) ? bidRequest.params.video.ttl : 300, netRevenue: true, adUnitCode: bidRequest.adUnitCode }; @@ -113,7 +113,6 @@ export const spec = { } else if (bid.adm) { bidResponse.vastXml = bid.adm; } - if (bidRequest.mediaTypes.video) { bidResponse.renderer = (bidRequest.mediaTypes.video.context === 'outstream') ? newRenderer(bidRequest, bidResponse) : undefined; } diff --git a/modules/oneVideoBidAdapter.md b/modules/oneVideoBidAdapter.md index 92958af9e83..d413c9d64e5 100644 --- a/modules/oneVideoBidAdapter.md +++ b/modules/oneVideoBidAdapter.md @@ -40,6 +40,7 @@ Connects to Verizon Media's Video SSP (AKA ONE Video / Adap.tv) demand source to inventoryid: 123, minduration: 10, maxduration: 30, + ttl: 300, custom: { key1: "value1", key2: 123345 @@ -89,6 +90,7 @@ Connects to Verizon Media's Video SSP (AKA ONE Video / Adap.tv) demand source to inventoryid: 123, minduration: 10, maxduration: 30, + ttl: 250 }, site: { id: 1, diff --git a/test/spec/modules/oneVideoBidAdapter_spec.js b/test/spec/modules/oneVideoBidAdapter_spec.js index 331ac8976e6..83adf8a4b11 100644 --- a/test/spec/modules/oneVideoBidAdapter_spec.js +++ b/test/spec/modules/oneVideoBidAdapter_spec.js @@ -217,7 +217,7 @@ describe('OneVideoBidAdapter', function () { const placement = bidRequest.params.video.placement; const rewarded = bidRequest.params.video.rewarded; const inventoryid = bidRequest.params.video.inventoryid; - const VERSION = '3.0.4'; + const VERSION = '3.0.5'; expect(data.imp[0].video.w).to.equal(width); expect(data.imp[0].video.h).to.equal(height); expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); @@ -407,7 +407,7 @@ describe('OneVideoBidAdapter', function () { height: 480, mediaType: 'video', currency: 'USD', - ttl: 100, + ttl: 300, netRevenue: true, adUnitCode: bidRequest.adUnitCode, renderer: (bidRequest.mediaTypes.video.context === 'outstream') ? newRenderer(bidRequest, bidResponse) : undefined, @@ -434,6 +434,30 @@ describe('OneVideoBidAdapter', function () { expect(bidResponse.mediaType).to.equal('banner'); expect(bidResponse.renderer).to.be.undefined; }); + + it('should default ttl to 300', function () { + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(300); + }); + it('should not allow ttl above 3601, default to 300', function () { + bidRequest.params.video.ttl = 3601; + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(300); + }); + it('should not allow ttl below 1, default to 300', function () { + bidRequest.params.video.ttl = 0; + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(300); + }); + it('should use custom ttl if under 3600', function () { + bidRequest.params.video.ttl = 1000; + const serverResponse = {seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], cur: 'USD'}; + const bidResponse = spec.interpretResponse({ body: serverResponse }, { bidRequest }); + expect(bidResponse.ttl).to.equal(1000); + }); }); describe('when GDPR and uspConsent applies', function () { From a2454a5e9f18ca72e34c4eedbd144f88f5e7872e Mon Sep 17 00:00:00 2001 From: Stephen Johnston Date: Mon, 14 Dec 2020 07:04:32 -0500 Subject: [PATCH 027/181] Add Gulp Review-Start Task (#6067) * add review-start gulp command * remove watch, unecessary for reviews * add instructions to reviewer file for new command * Updates to add back watch * updates to had reviewer hub page * Update PR_REVIEW.md --- PR_REVIEW.md | 11 ++++++ gulpfile.js | 20 +++++++++- integrationExamples/reviewerTools/index.html | 40 ++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100755 integrationExamples/reviewerTools/index.html diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 662a1a871c8..0519cbb7b6e 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -5,6 +5,17 @@ If the PR is for a standard bid adapter or a standard analytics adapter, just th For modules and core platform updates, the initial reviewer should request an additional team member to review as a sanity check. Merge should only happen when the PR has 2 `LGTM` from the core team and a documentation PR if required. +### Running Tests and Verifying Integrations + +General gulp commands include separate commands for serving the codebase on a built in webserver, creating code coverage reports and allowing serving integration examples. The `review-start` gulp command combinese those into one command. + +- Run `gulp review-start`, adding the host parameter `gulp review-start --host=0.0.0.0` will bind to all IPs on the machine + - A page will open which provides a hub for common reviewer tools. + - If you need to manually acceess the tools: + - Navigate to build/coverage/lcov-report/index.html to view coverage + - Navigate to integrationExamples/gpt/hellow_world.html for basic integration testing + - The hello_world.html and other exampls can be edited and used as needed to verify functionality + ### General PR review Process - All required global and bidder-adapter rules defined in the [Module Rules](https://docs.prebid.org/dev-docs/module-rules.html) must be followed. Please review these rules often - we depend on reviewers to enforce them. - Checkout the branch (these instructions are available on the github PR page as well). diff --git a/gulpfile.js b/gulpfile.js index 879e34ae588..b7a9e442a8c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -86,7 +86,8 @@ function viewCoverage(done) { connect.server({ port: coveragePort, root: 'build/coverage/lcov-report', - livereload: false + livereload: false, + debug: true }); opens('http://' + mylocalhost + ':' + coveragePort); done(); @@ -94,6 +95,19 @@ function viewCoverage(done) { viewCoverage.displayName = 'view-coverage'; +// View the reviewer tools page +function viewReview(done) { + var mylocalhost = (argv.host) ? argv.host : 'localhost'; + var reviewUrl = 'http://' + mylocalhost + ':' + port + '/integrationExamples/reviewerTools/index.html'; // reuse the main port from 9999 + + // console.log(`stdout: opening` + reviewUrl); + + opens(reviewUrl); + done(); +}; + +viewReview.displayName = 'view-review'; + // Watch Task with Live Reload function watch(done) { var mainWatcher = gulp.watch([ @@ -383,4 +397,8 @@ gulp.task('e2e-test', gulp.series(clean, setupE2e, gulp.parallel('build-bundle-p gulp.task(bundleToStdout); gulp.task('bundle', gulpBundle.bind(null, false)); // used for just concatenating pre-built files with no build step +// build task for reviewers, runs test-coverage, serves, without watching +gulp.task(viewReview); +gulp.task('review-start', gulp.series(clean, lint, gulp.parallel('build-bundle-dev', watch, testCoverage), viewReview)); + module.exports = nodeBundle; diff --git a/integrationExamples/reviewerTools/index.html b/integrationExamples/reviewerTools/index.html new file mode 100755 index 00000000000..2732cb4fd88 --- /dev/null +++ b/integrationExamples/reviewerTools/index.html @@ -0,0 +1,40 @@ + + + + + + + Prebid Reviewer Tools + + + + +
+
+
+

Reviewer Tools

+

Below are links to the most common tool used by Prebid reviewers. For more info on PR review processes check out the General PR Review Process page on Github.

+

Common

+ +

Other Tools

+ +

Documentation & Training Material

+ +
+
+
+ + \ No newline at end of file From 94c9dcc5d64b3ec2ec10636bdc68182b9d5be462 Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Mon, 14 Dec 2020 09:20:34 -0500 Subject: [PATCH 028/181] appnexusBidAdapter - add support for test flag (#6119) --- modules/appnexusBidAdapter.js | 6 ++++++ test/spec/modules/appnexusBidAdapter_spec.js | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index c102bee4e58..3a9f37f4ca8 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -498,6 +498,12 @@ function formatRequest(payload, bidderRequest) { } } + if (utils.getParameterByName('apn_test').toUpperCase() === 'TRUE' || config.getConfig('apn_test') === true) { + options.customHeaders = { + 'X-Is-Test': 1 + } + } + if (payload.tags.length > MAX_IMPS_PER_REQUEST) { const clonedPayload = utils.deepClone(payload); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 9b12d892440..a36eab03fe1 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -784,6 +784,18 @@ describe('AppNexusAdapter', function () { config.getConfig.restore(); }); + it('should set the X-Is-Test customHeader if test flag is enabled', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('apn_test') + .returns(true); + + const request = spec.buildRequests([bidRequest]); + expect(request.options.customHeaders).to.deep.equal({'X-Is-Test': 1}); + + config.getConfig.restore(); + }); + it('should set withCredentials to false if purpose 1 consent is not given', function () { let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; let bidderRequest = { From 2102f4a1c1548bacb775cf6c011c6eba621c81d1 Mon Sep 17 00:00:00 2001 From: fgcloutier Date: Mon, 14 Dec 2020 15:55:29 +0100 Subject: [PATCH 029/181] feat(sublimeBidAdapter): updating sublimeBidAdapter module (#6113) - using a simple-request for the POST bid request - renaming our internal ver pixel param to pbav --- modules/sublimeBidAdapter.js | 11 ++--- test/spec/modules/sublimeBidAdapter_spec.js | 47 ++++++++++++++++++--- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/modules/sublimeBidAdapter.js b/modules/sublimeBidAdapter.js index e9f7cf19033..7a573ca4c4b 100644 --- a/modules/sublimeBidAdapter.js +++ b/modules/sublimeBidAdapter.js @@ -9,7 +9,7 @@ const DEFAULT_CURRENCY = 'EUR'; const DEFAULT_PROTOCOL = 'https'; const DEFAULT_TTL = 600; const SUBLIME_ANTENNA = 'antenna.ayads.co'; -const SUBLIME_VERSION = '0.6.0'; +const SUBLIME_VERSION = '0.7.0'; /** * Debug log message @@ -50,7 +50,7 @@ export function sendEvent(eventName) { src: 'pa', puid: state.transactionId || state.notifyId, trId: state.transactionId || state.notifyId, - ver: SUBLIME_VERSION, + pbav: SUBLIME_VERSION, }; log('Sending pixel for event: ' + eventName, eventObject); @@ -128,10 +128,10 @@ function buildRequests(validBidRequests, bidderRequest) { return { method: 'POST', url: protocol + '://' + bidHost + '/bid', - data: payload, + data: JSON.stringify(payload), options: { - contentType: 'application/json', - withCredentials: true + contentType: 'text/plain', + withCredentials: false }, } }); @@ -210,6 +210,7 @@ export const spec = { code: BIDDER_CODE, gvlid: BIDDER_GVLID, aliases: [], + sendEvent: sendEvent, isBidRequestValid: isBidRequestValid, buildRequests: buildRequests, interpretResponse: interpretResponse, diff --git a/test/spec/modules/sublimeBidAdapter_spec.js b/test/spec/modules/sublimeBidAdapter_spec.js index 008f24730bc..a0765a0d396 100644 --- a/test/spec/modules/sublimeBidAdapter_spec.js +++ b/test/spec/modules/sublimeBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec, sendEvent, log, setState, state } from 'modules/sublimeBidAdapter.js'; +import { spec } from 'modules/sublimeBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; let utils = require('src/utils'); @@ -9,6 +9,16 @@ describe('Sublime Adapter', function() { describe('sendEvent', function() { let sandbox; + const triggeredPixelProperties = [ + 't', + 'tse', + 'z', + 'e', + 'src', + 'puid', + 'trId', + 'pbav', + ]; beforeEach(function () { sandbox = sinon.sandbox.create(); @@ -16,8 +26,10 @@ describe('Sublime Adapter', function() { it('should trigger pixel', function () { sandbox.spy(utils, 'triggerPixel'); - sendEvent('test', true); + spec.sendEvent('test'); expect(utils.triggerPixel.called).to.equal(true); + const params = utils.parseUrl(utils.triggerPixel.args[0][0]).search; + expect(Object.keys(params)).to.have.members(triggeredPixelProperties); }); afterEach(function () { @@ -94,8 +106,9 @@ describe('Sublime Adapter', function() { }); it('should contains a request id equals to the bid id', function() { - expect(request[0].data.requestId).to.equal(bidRequests[0].bidId); - expect(request[1].data.requestId).to.equal(bidRequests[1].bidId); + for (let i = 0; i < request.length; i = i + 1) { + expect(JSON.parse(request[i].data).requestId).to.equal(bidRequests[i].bidId); + } }); it('should have an url that contains bid keyword', function() { @@ -149,7 +162,7 @@ describe('Sublime Adapter', function() { currency: 'USD', netRevenue: true, ttl: 600, - pbav: '0.6.0', + pbav: '0.7.0', ad: '', }, ]; @@ -191,7 +204,7 @@ describe('Sublime Adapter', function() { netRevenue: true, ttl: 600, ad: '', - pbav: '0.6.0', + pbav: '0.7.0', }; expect(result[0]).to.deep.equal(expectedResponse); @@ -241,7 +254,7 @@ describe('Sublime Adapter', function() { netRevenue: true, ttl: 600, ad: '', - pbav: '0.6.0', + pbav: '0.7.0', }; expect(result[0]).to.deep.equal(expectedResponse); @@ -279,4 +292,24 @@ describe('Sublime Adapter', function() { }); }); }); + + describe('onBidWon', function() { + let sandbox; + let bid = { foo: 'bar' }; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + it('should trigger "bidwon" pixel', function () { + sandbox.spy(utils, 'triggerPixel'); + spec.onBidWon(bid); + const params = utils.parseUrl(utils.triggerPixel.args[0][0]).search; + expect(params.e).to.equal('bidwon'); + }); + + afterEach(function () { + sandbox.restore(); + }); + }) }); From 0fec20d2914a291abdfa052a59b988f3b300b656 Mon Sep 17 00:00:00 2001 From: OneTagDevOps <38786435+OneTagDevOps@users.noreply.github.com> Date: Mon, 14 Dec 2020 16:36:39 +0100 Subject: [PATCH 030/181] oneTag Bid Adapter: bidRequest object adjustments (#6105) * Propagates server data directly without members picking, extracts userIds from the the validBidRequest object * Picks netRevenue value from received bid, defaulting to false Co-authored-by: francesco --- modules/onetagBidAdapter.js | 87 ++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 50 deletions(-) diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index 1a2df023b81..16b8096646f 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -6,6 +6,7 @@ import { Renderer } from '../src/Renderer.js'; import find from 'core-js-pure/features/array/find.js'; import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { createEidsArray } from './userId/eids.js'; const ENDPOINT = 'https://onetag-sys.com/prebid-request'; const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; @@ -63,8 +64,8 @@ function buildRequests(validBidRequests, bidderRequest) { if (bidderRequest && bidderRequest.uspConsent) { payload.usPrivacy = bidderRequest.uspConsent; } - if (bidderRequest && bidderRequest.userId) { - payload.userId = bidderRequest.userId; + if (validBidRequests && validBidRequests.length !== 0 && validBidRequests[0].userId) { + payload.userId = createEidsArray(validBidRequests[0].userId); } try { if (storage.hasLocalStorage()) { @@ -88,47 +89,34 @@ function interpretResponse(serverResponse, bidderRequest) { if (!body.bids || !Array.isArray(body.bids) || body.bids.length === 0) { return bids; } - body.bids.forEach(({ - requestId, - cpm, - width, - height, - creativeId, - dealId, - currency, - mediaType, - ttl, - rendererUrl, - ad, - vastUrl, - videoCacheKey - }) => { + body.bids.forEach(bid => { const responseBid = { - requestId, - cpm, - width, - height, - creativeId, - dealId: dealId == null ? dealId : '', - currency, - netRevenue: false, + requestId: bid.requestId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.creativeId, + dealId: bid.dealId == null ? bid.dealId : '', + currency: bid.currency, + netRevenue: bid.netRevenue || false, + mediaType: bid.mediaType, meta: { - mediaType + mediaType: bid.mediaType }, - ttl: ttl || 300 + ttl: bid.ttl || 300 }; - if (mediaType === BANNER) { - responseBid.ad = ad; - } else if (mediaType === VIDEO) { - const {context, adUnitCode} = find(requestData.bids, (item) => item.bidId === requestId); + if (bid.mediaType === BANNER) { + responseBid.ad = bid.ad; + } else if (bid.mediaType === VIDEO) { + const {context, adUnitCode} = find(requestData.bids, (item) => item.bidId === bid.requestId); if (context === INSTREAM) { - responseBid.vastUrl = vastUrl; - responseBid.videoCacheKey = videoCacheKey; + responseBid.vastUrl = bid.vastUrl; + responseBid.videoCacheKey = bid.videoCacheKey; } else if (context === OUTSTREAM) { - responseBid.vastXml = ad; - responseBid.vastUrl = vastUrl; - if (rendererUrl) { - responseBid.renderer = createRenderer({requestId, rendererUrl, adUnitCode}); + responseBid.vastXml = bid.ad; + responseBid.vastUrl = bid.vastUrl; + if (bid.rendererUrl) { + responseBid.renderer = createRenderer({ ...bid, adUnitCode }); } } } @@ -146,25 +134,24 @@ function createRenderer(bid, rendererOptions = {}) { loaded: false }); try { - renderer.setRender(onetagRenderer); + renderer.setRender(({renderer, width, height, vastXml, adUnitCode}) => { + renderer.push(() => { + window.onetag.Player.init({ + ...bid, + width, + height, + vastXml, + nodeId: adUnitCode, + config: renderer.getConfig() + }); + }); + }); } catch (e) { } return renderer; } -function onetagRenderer({renderer, width, height, vastXml, adUnitCode}) { - renderer.push(() => { - window.onetag.Player.init({ - width, - height, - vastXml, - nodeId: adUnitCode, - config: renderer.getConfig() - }); - }); -} - function getFrameNesting() { let topmostFrame = window; let parent = window.parent; From 30711be00243e653a25785b2d7b46b9df8f5e120 Mon Sep 17 00:00:00 2001 From: Scott Menzer Date: Mon, 14 Dec 2020 21:41:42 +0100 Subject: [PATCH 031/181] a/b testing framework baked in to the ID5 user id module (#6076) --- modules/id5IdSystem.js | 47 ++++++-- modules/id5IdSystem.md | 21 +++- modules/userId/eids.md | 3 +- test/spec/modules/id5IdSystem_spec.js | 157 +++++++++++++++++++++++++- 4 files changed, 209 insertions(+), 19 deletions(-) diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 7033a71d015..17a808badaa 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -42,27 +42,58 @@ export const id5IdSubmodule = { * decode the stored id value for passing to bid requests * @function decode * @param {(Object|string)} value + * @param {SubmoduleConfig|undefined} config * @returns {(Object|undefined)} */ - decode(value) { - let uid; + decode(value, config) { + let universalUid; let linkType = 0; if (value && typeof value.universal_uid === 'string') { - uid = value.universal_uid; + universalUid = value.universal_uid; linkType = value.link_type || linkType; } else { return undefined; } - return { - 'id5id': { - 'uid': uid, - 'ext': { - 'linkType': linkType + // check for A/B testing configuration and hide ID if in Control Group + let abConfig = (config && config.params && config.params.abTesting) || { enabled: false }; + let controlGroup = false; + if ( + abConfig.enabled === true && + (!utils.isNumber(abConfig.controlGroupPct) || + abConfig.controlGroupPct < 0 || + abConfig.controlGroupPct > 1) + ) { + // A/B Testing is enabled, but configured improperly, so skip A/B testing + utils.logError('User ID - ID5 submodule: A/B Testing controlGroupPct must be a number >= 0 and <= 1! Skipping A/B Testing'); + } else if ( + abConfig.enabled === true && + Math.random() < abConfig.controlGroupPct + ) { + // A/B Testing is enabled and user is in the Control Group, so do not share the ID5 ID + utils.logInfo('User ID - ID5 submodule: A/B Testing Enabled - request is in the Control Group, so the ID5 ID is NOT exposed'); + universalUid = linkType = 0; + controlGroup = true; + } else if (abConfig.enabled === true) { + // A/B Testing is enabled but user is not in the Control Group, so ID5 ID is shared + utils.logInfo('User ID - ID5 submodule: A/B Testing Enabled - request is NOT in the Control Group, so the ID5 ID is exposed'); + } + + let responseObj = { + id5id: { + uid: universalUid, + ext: { + linkType: linkType } } }; + + if (abConfig.enabled === true) { + utils.deepSetValue(responseObj, 'id5id.ext.abTestingControlGroup', controlGroup); + } + + return responseObj; }, /** diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md index e5e3969c19c..6b2192834fa 100644 --- a/modules/id5IdSystem.md +++ b/modules/id5IdSystem.md @@ -22,14 +22,18 @@ The following configuration parameters are available: pbjs.setConfig({ userSync: { userIds: [{ - name: "id5Id", + name: 'id5Id', params: { partner: 173, // change to the Partner Number you received from ID5 - pd: "MT1iNTBjY..." // optional, see table below for a link to how to generate this + pd: 'MT1iNTBjY...', // optional, see table below for a link to how to generate this + abTesting: { // optional + enabled: true, // false by default + controlGroupPct: 0.1 // valid values are 0.0 - 1.0 (inclusive) + } }, storage: { - type: "html5", // "html5" is the required storage type - name: "id5id", // "id5id" is the required storage name + type: 'html5', // "html5" is the required storage type + name: 'id5id', // "id5id" is the required storage name expires: 90, // storage lasts for 90 days refreshInSeconds: 8*3600 // refresh ID every 8 hours to ensure it's fresh } @@ -46,6 +50,9 @@ pbjs.setConfig({ | params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | | params.pd | Optional | String | Publisher-supplied data used for linking ID5 IDs across domains. See [our documentation](https://wiki.id5.io/x/BIAZ) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | | params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | +| params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default | +| params.abTesting.enabled | Optional | Boolean | Set this to `true` to turn on this feature | `true` or `false` | +| params.abTesting.controlGroupPct | Optional | Number | Must be a number between `0.0` and `1.0` (inclusive) and is used to determine the percentage of requests that fall into the control group (and thus not exposing the ID5 ID). For example, a value of `0.20` will result in 20% of requests without an ID5 ID and 80% with an ID. | `0.1` | | storage | Required | Object | Storage settings for how the User ID module will cache the ID5 ID locally | | | storage.type | Required | String | This is where the results of the user ID will be stored. ID5 **requires** `"html5"`. | `"html5"` | | storage.name | Required | String | The name of the local storage where the user ID will be stored. ID5 **requires** `"id5id"`. | `"id5id"` | @@ -53,3 +60,9 @@ pbjs.setConfig({ | storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 8 hours between refreshes | `8*3600` | **ATTENTION:** As of Prebid.js v4.14.0, ID5 requires `storage.type` to be `"html5"` and `storage.name` to be `"id5id"`. Using other values will display a warning today, but in an upcoming release, it will prevent the ID5 module from loading. This change is to ensure the ID5 module in Prebid.js interoperates properly with the [ID5 API](https://github.com/id5io/id5-api.js) and to reduce the size of publishers' first-party cookies that are sent to their web servers. If you have any questions, please reach out to us at [prebid@id5.io](mailto:prebid@id5.io). + +### A Note on A/B Testing + +Publishers may want to test the value of the ID5 ID with their downstream partners. While there are various ways to do this, A/B testing is a standard approach. Instead of publishers manually enabling or disabling the ID5 User ID Module based on their control group settings (which leads to fewer calls to ID5, reducing our ability to recognize the user), we have baked this in to our module directly. + +To turn on A/B Testing, simply edit the configuration (see above table) to enable it and set what percentage of requests you would like to set for the control group. The control group is the set of requests where an ID5 ID will not be exposed in to bid adapters or in the various user id functions available on the `pbjs` global. An additional value of `ext.abTestingControlGroup` will be set to `true` or `false` that can be used to inform reporting systems that the request was in the control group or not. It's important to note that the control group is request based, and not user based. In other words, from one page view to another, a user may be in or out of the control group. diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 0cf9b6d2d22..fecf7e888bf 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -36,7 +36,8 @@ userIdAsEids = [ atype: 1 }, ext: { - linkType: 2 + linkType: 2, + abTestingControlGroup: false }] }, diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index 845cf7fa010..afde1696766 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -26,16 +26,19 @@ describe('ID5 ID System', function() { const ID5_NB_STORAGE_NAME = nbCacheName(ID5_TEST_PARTNER_ID); const ID5_STORED_ID = 'storedid5id'; const ID5_STORED_SIGNATURE = '123456'; + const ID5_STORED_LINK_TYPE = 1; const ID5_STORED_OBJ = { 'universal_uid': ID5_STORED_ID, - 'signature': ID5_STORED_SIGNATURE + 'signature': ID5_STORED_SIGNATURE, + 'link_type': ID5_STORED_LINK_TYPE }; const ID5_RESPONSE_ID = 'newid5id'; const ID5_RESPONSE_SIGNATURE = 'abcdef'; + const ID5_RESPONSE_LINK_TYPE = 2; const ID5_JSON_RESPONSE = { 'universal_uid': ID5_RESPONSE_ID, 'signature': ID5_RESPONSE_SIGNATURE, - 'link_type': 0 + 'link_type': ID5_RESPONSE_LINK_TYPE }; function getId5FetchConfig(storageName = ID5_STORAGE_NAME, storageType = 'html5') { @@ -268,7 +271,7 @@ describe('ID5 ID System', function() { source: ID5_SOURCE, uids: [{ id: ID5_STORED_ID, atype: 1 }], ext: { - linkType: 0 + linkType: ID5_STORED_LINK_TYPE } }); }); @@ -360,13 +363,155 @@ describe('ID5 ID System', function() { }); describe('Decode stored object', function() { - const expectedDecodedObject = { id5id: { uid: ID5_STORED_ID, ext: { linkType: 0 } } }; + const expectedDecodedObject = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } }; it('should properly decode from a stored object', function() { - expect(id5IdSubmodule.decode(ID5_STORED_OBJ)).to.deep.equal(expectedDecodedObject); + expect(id5IdSubmodule.decode(ID5_STORED_OBJ, getId5FetchConfig())).to.deep.equal(expectedDecodedObject); }); it('should return undefined if passed a string', function() { - expect(id5IdSubmodule.decode('somestring')).to.eq(undefined); + expect(id5IdSubmodule.decode('somestring', getId5FetchConfig())).to.eq(undefined); + }); + }); + + describe('A/B Testing', function() { + const expectedDecodedObjectWithIdAbOff = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE } } }; + const expectedDecodedObjectWithIdAbOn = { id5id: { uid: ID5_STORED_ID, ext: { linkType: ID5_STORED_LINK_TYPE, abTestingControlGroup: false } } }; + const expectedDecodedObjectWithoutIdAbOn = { id5id: { uid: 0, ext: { linkType: 0, abTestingControlGroup: true } } }; + let testConfig; + + beforeEach(function() { + testConfig = getId5FetchConfig(); + }); + + describe('Configuration Validation', function() { + let logErrorSpy; + let logInfoSpy; + + beforeEach(function() { + logErrorSpy = sinon.spy(utils, 'logError'); + logInfoSpy = sinon.spy(utils, 'logInfo'); + }); + afterEach(function() { + logErrorSpy.restore(); + logInfoSpy.restore(); + }); + + // A/B Testing ON, but invalid config + let testInvalidAbTestingConfigsWithError = [ + { enabled: true }, + { enabled: true, controlGroupPct: 2 }, + { enabled: true, controlGroupPct: -1 }, + { enabled: true, controlGroupPct: 'a' }, + { enabled: true, controlGroupPct: true } + ]; + testInvalidAbTestingConfigsWithError.forEach((testAbTestingConfig) => { + it('should error if config is invalid, and always return an ID', function () { + testConfig.params.abTesting = testAbTestingConfig; + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOn); + sinon.assert.calledOnce(logErrorSpy); + }); + }); + + // A/B Testing OFF, with invalid config (ignore) + let testInvalidAbTestingConfigsWithoutError = [ + { enabled: false, controlGroupPct: -1 }, + { enabled: false, controlGroupPct: 2 }, + { enabled: false, controlGroupPct: 'a' }, + { enabled: false, controlGroupPct: true } + ]; + testInvalidAbTestingConfigsWithoutError.forEach((testAbTestingConfig) => { + it('should not error if config is invalid but A/B testing is off, and always return an ID', function () { + testConfig.params.abTesting = testAbTestingConfig; + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + sinon.assert.notCalled(logErrorSpy); + sinon.assert.notCalled(logInfoSpy); + }); + }); + + // A/B Testing ON, with valid config + let testValidConfigs = [ + { enabled: true, controlGroupPct: 0 }, + { enabled: true, controlGroupPct: 0.5 }, + { enabled: true, controlGroupPct: 1 } + ]; + testValidConfigs.forEach((testAbTestingConfig) => { + it('should not error if config is valid', function () { + testConfig.params.abTesting = testAbTestingConfig; + id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + sinon.assert.notCalled(logErrorSpy); + sinon.assert.calledOnce(logInfoSpy); + }); + }); + }); + + describe('A/B Testing Config is not Set', function() { + let randStub; + + beforeEach(function() { + randStub = sinon.stub(Math, 'random').callsFake(function() { + return 0; + }); + }); + afterEach(function () { + randStub.restore(); + }); + + it('should expose ID when A/B config is not set', function () { + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + }); + + it('should expose ID when A/B config is empty', function () { + testConfig.params.abTesting = { }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + }); + }); + + describe('A/B Testing Config is Set', function() { + let randStub; + + beforeEach(function() { + randStub = sinon.stub(Math, 'random').callsFake(function() { + return 0.25; + }); + }); + afterEach(function () { + randStub.restore(); + }); + + it('should expose ID when A/B testing is off', function () { + testConfig.params.abTesting = { + enabled: false, + controlGroupPct: 0.5 + }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + }); + + it('should expose ID when not in control group', function () { + testConfig.params.abTesting = { + enabled: true, + controlGroupPct: 0.1 + }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOn); + }); + + it('should not expose ID when in control group', function () { + testConfig.params.abTesting = { + enabled: true, + controlGroupPct: 0.5 + }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithoutIdAbOn); + }); }); }); }); From f6f27dd06a8fcbc06cf47dfacf61347a7d892da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rok=20Su=C5=A1nik?= Date: Mon, 14 Dec 2020 22:47:11 +0100 Subject: [PATCH 032/181] Add Zemanta adapter (#6039) --- modules/zemantaBidAdapter.js | 256 ++++++++++ modules/zemantaBidAdapter.md | 107 +++++ test/spec/modules/zemantaBidAdapter_spec.js | 487 ++++++++++++++++++++ 3 files changed, 850 insertions(+) create mode 100644 modules/zemantaBidAdapter.js create mode 100644 modules/zemantaBidAdapter.md create mode 100644 test/spec/modules/zemantaBidAdapter_spec.js diff --git a/modules/zemantaBidAdapter.js b/modules/zemantaBidAdapter.js new file mode 100644 index 00000000000..aa7a24985e0 --- /dev/null +++ b/modules/zemantaBidAdapter.js @@ -0,0 +1,256 @@ +// jshint esversion: 6, es3: false, node: true +'use strict'; + +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { NATIVE, BANNER } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'zemanta'; +const CURRENCY = 'USD'; +const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; +const NATIVE_PARAMS = { + title: { id: 0, name: 'title' }, + icon: { id: 2, type: 1, name: 'img' }, + image: { id: 3, type: 3, name: 'img' }, + sponsoredBy: { id: 5, name: 'data', type: 1 }, + body: { id: 4, name: 'data', type: 2 }, + cta: { id: 1, type: 12, name: 'data' } +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [ NATIVE, BANNER ], + isBidRequestValid: (bid) => { + return ( + !!config.getConfig('zemanta.bidderUrl') && + !!utils.deepAccess(bid, 'params.publisher.id') && + !!(bid.nativeParams || bid.sizes) + ); + }, + buildRequests: (validBidRequests, bidderRequest) => { + const page = bidderRequest.refererInfo.referer; + const ua = navigator.userAgent; + const test = setOnAny(validBidRequests, 'params.test'); + const publisher = setOnAny(validBidRequests, 'params.publisher'); + const cur = CURRENCY; + const endpointUrl = config.getConfig('zemanta.bidderUrl'); + const timeout = bidderRequest.timeout; + + const imps = validBidRequests.map((bid, id) => { + bid.netRevenue = 'net'; + const imp = { + id: id + 1 + '' + } + + if (bid.params.tagid) { + imp.tagid = bid.params.tagid + } + + if (bid.nativeParams) { + imp.native = { + request: JSON.stringify({ + assets: getNativeAssets(bid) + }) + } + } else { + imp.banner = { + format: transformSizes(bid.sizes) + } + } + + return imp; + }); + + const request = { + id: bidderRequest.auctionId, + site: { page, publisher }, + device: { ua }, + source: { fd: 1 }, + cur: [cur], + tmax: timeout, + imp: imps + }; + + if (test) { + request.is_debug = !!test; + request.test = 1; + } + + if (utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies')) { + utils.deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString) + utils.deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1) + } + if (bidderRequest.uspConsent) { + utils.deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent) + } + if (config.getConfig('coppa') === true) { + utils.deepSetValue(request, 'regs.coppa', config.getConfig('coppa') & 1) + } + + return { + method: 'POST', + url: endpointUrl, + data: JSON.stringify(request), + bids: validBidRequests + }; + }, + interpretResponse: (serverResponse, { bids }) => { + if (!serverResponse.body) { + return []; + } + const { seatbid, cur } = serverResponse.body; + + const bidResponses = flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { + result[bid.impid - 1] = bid; + return result; + }, []); + + return bids.map((bid, id) => { + const bidResponse = bidResponses[id]; + if (bidResponse) { + const type = bid.nativeParams ? NATIVE : BANNER; + const bidObject = { + requestId: bid.bidId, + cpm: bidResponse.price, + creativeId: bidResponse.crid, + ttl: 360, + netRevenue: bid.netRevenue === 'net', + currency: cur, + mediaType: type, + nurl: bidResponse.nurl, + }; + if (type === NATIVE) { + bidObject.native = parseNative(bidResponse); + } else { + bidObject.ad = bidResponse.adm; + bidObject.width = bidResponse.w; + bidObject.height = bidResponse.h; + } + return bidObject; + } + }).filter(Boolean); + }, + getUserSyncs: (syncOptions) => { + const syncs = []; + const syncUrl = config.getConfig('zemanta.usersyncUrl'); + if (syncOptions.pixelEnabled && syncUrl) { + syncs.push({ + type: 'image', + url: syncUrl + }); + } + return syncs; + }, + onBidWon: (bid) => { + ajax(utils.replaceAuctionPrice(bid.nurl, bid.originalCpm)) + } +}; + +registerBidder(spec); + +function parseNative(bid) { + const { assets, link, eventtrackers } = JSON.parse(bid.adm); + const result = { + clickUrl: link.url, + clickTrackers: link.clicktrackers || undefined + }; + assets.forEach(asset => { + const kind = NATIVE_ASSET_IDS[asset.id]; + const content = kind && asset[NATIVE_PARAMS[kind].name]; + if (content) { + result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; + } + }); + if (eventtrackers) { + result.impressionTrackers = []; + eventtrackers.forEach(tracker => { + if (tracker.event !== 1) return; + switch (tracker.method) { + case 1: // img + result.impressionTrackers.push(tracker.url); + break; + case 2: // js + result.javascriptTrackers = ``; + break; + } + }); + } + return result; +} + +function setOnAny(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = utils.deepAccess(collection[i], key); + if (result) { + return result; + } + } +} + +function flatten(arr) { + return [].concat(...arr); +} + +function getNativeAssets(bid) { + return utils._map(bid.nativeParams, (bidParams, key) => { + const props = NATIVE_PARAMS[key]; + const asset = { + required: bidParams.required & 1, + }; + if (props) { + asset.id = props.id; + let wmin, hmin, w, h; + let aRatios = bidParams.aspect_ratios; + + if (aRatios && aRatios[0]) { + aRatios = aRatios[0]; + wmin = aRatios.min_width || 0; + hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; + } + + if (bidParams.sizes) { + const sizes = flatten(bidParams.sizes); + w = sizes[0]; + h = sizes[1]; + } + + asset[props.name] = { + len: bidParams.len, + type: props.type, + wmin, + hmin, + w, + h + }; + + return asset; + } + }).filter(Boolean); +} + +/* Turn bid request sizes into ut-compatible format */ +function transformSizes(requestSizes) { + if (!utils.isArray(requestSizes)) { + return []; + } + + if (requestSizes.length === 2 && !utils.isArray(requestSizes[0])) { + return [{ + w: parseInt(requestSizes[0], 10), + h: parseInt(requestSizes[1], 10) + }]; + } else if (utils.isArray(requestSizes[0])) { + return requestSizes.map(item => + ({ + w: parseInt(item[0], 10), + h: parseInt(item[1], 10) + }) + ); + } + + return []; +} diff --git a/modules/zemantaBidAdapter.md b/modules/zemantaBidAdapter.md new file mode 100644 index 00000000000..d991b67d429 --- /dev/null +++ b/modules/zemantaBidAdapter.md @@ -0,0 +1,107 @@ +# Overview + +``` +Module Name: Zemanta Adapter +Module Type: Bidder Adapter +Maintainer: prog-ops-team@outbrain.com +``` + +# Description + +Module that connects to zemanta bidder to fetch bids. +Both native and display formats are supported but not at the same time. Using OpenRTB standard. + +# Configuration + +## Bidder and usersync URLs + +The Zemanta adapter does not work without setting the correct bidder and usersync URLs. +You will receive the URLs when contacting us. + +``` +pbjs.setConfig({ + zemanta: { + bidderUrl: 'https://bidder-url.com', + usersyncUrl: 'https://usersync-url.com' + } +}); +``` + + +# Test Native Parameters +``` + var adUnits = [ + code: '/19968336/prebid_native_example_1', + mediaTypes: { + native: { + image: { + required: false, + sizes: [100, 50] + }, + title: { + required: false, + len: 140 + }, + sponsoredBy: { + required: false + }, + clickUrl: { + required: false + }, + body: { + required: false + }, + icon: { + required: false, + sizes: [50, 50] + } + } + }, + bids: [{ + bidder: 'zemanta', + params: { + publisher: { + id: '2706', // required + name: 'Publishers Name', + domain: 'publisher.com' + }, + tagid: 'tag-id' + } + }] + ]; + + pbjs.setConfig({ + zemanta: { + bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + } + }); +``` + +# Test Display Parameters +``` + var adUnits = [ + code: '/19968336/prebid_display_example_1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'zemanta', + params: { + publisher: { + id: '2706', // required + name: 'Publishers Name', + domain: 'publisher.com' + }, + tagid: 'tag-id' + }, + }] + ]; + + pbjs.setConfig({ + zemanta: { + bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + } + }); +``` diff --git a/test/spec/modules/zemantaBidAdapter_spec.js b/test/spec/modules/zemantaBidAdapter_spec.js new file mode 100644 index 00000000000..ab685890620 --- /dev/null +++ b/test/spec/modules/zemantaBidAdapter_spec.js @@ -0,0 +1,487 @@ +import {expect} from 'chai'; +import {spec} from 'modules/zemantaBidAdapter.js'; +import {config} from 'src/config.js'; +import {server} from 'test/mocks/xhr'; + +describe('Zemanta Adapter', function () { + describe('Bid request and response', function () { + const commonBidRequest = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id' + }, + }, + bidId: '2d6815a92ba1ba', + auctionId: '12043683-3254-4f74-8934-f941b085579e', + } + const nativeBidRequestParams = { + nativeParams: { + image: { + required: true, + sizes: [ + 120, + 100 + ], + sendId: true + }, + title: { + required: true, + sendId: true + }, + sponsoredBy: { + required: false + } + }, + } + + const displayBidRequestParams = { + sizes: [ + [ + 300, + 250 + ] + ] + } + + describe('isBidRequestValid', function () { + before(() => { + config.setConfig({ + zemanta: { + bidderUrl: 'https://bidder-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + it('should fail when bid is invalid', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should succeed when bid contains native params', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + it('should succeed when bid contains sizes', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...displayBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + it('should fail if publisher id is not set', function () { + const bid = { + bidder: 'zemanta', + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should fail if bidder url is not set', function () { + const bid = { + bidder: 'zemanta', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...nativeBidRequestParams, + } + config.resetConfig() + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + }) + + describe('buildRequests', function () { + before(() => { + config.setConfig({ + zemanta: { + bidderUrl: 'https://bidder-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + const commonBidderRequest = { + refererInfo: { + referer: 'https://example.com/' + } + } + + it('should build native request', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const expectedNativeAssets = { + assets: [ + { + required: 1, + id: 3, + img: { + type: 3, + w: 120, + h: 100 + } + }, + { + required: 1, + id: 0, + title: {} + }, + { + required: 0, + id: 5, + data: { + type: 1 + } + } + ] + } + const expectedData = { + site: { + page: 'https://example.com/', + publisher: { + id: 'publisher-id' + } + }, + device: { + ua: navigator.userAgent + }, + source: { + fd: 1 + }, + cur: [ + 'USD' + ], + imp: [ + { + id: '1', + native: { + request: JSON.stringify(expectedNativeAssets) + } + } + ] + } + const res = spec.buildRequests([bidRequest], commonBidderRequest) + expect(res.url).to.equal('https://bidder-url.com') + expect(res.data).to.deep.equal(JSON.stringify(expectedData)) + }); + + it('should build display request', function () { + const bidRequest = { + ...commonBidRequest, + ...displayBidRequestParams, + } + const expectedData = { + site: { + page: 'https://example.com/', + publisher: { + id: 'publisher-id' + } + }, + device: { + ua: navigator.userAgent + }, + source: { + fd: 1 + }, + cur: [ + 'USD' + ], + imp: [ + { + id: '1', + banner: { + format: [ + { + w: 300, + h: 250 + } + ] + } + } + ] + } + const res = spec.buildRequests([bidRequest], commonBidderRequest) + expect(res.url).to.equal('https://bidder-url.com') + expect(res.data).to.deep.equal(JSON.stringify(expectedData)) + }) + + it('should pass optional tagid in request', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + bidRequest.params.tagid = 'test-tag' + + const res = spec.buildRequests([bidRequest], commonBidderRequest) + const resData = JSON.parse(res.data) + expect(resData.imp[0].tagid).to.equal('test-tag') + }); + + it('should pass bidder timeout', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + timeout: 500 + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.tmax).to.equal(500) + }); + + it('should pass GDPR consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + } + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.user.ext.consent).to.equal('consentString') + expect(resData.regs.ext.gdpr).to.equal(1) + }); + + it('should pass us privacy consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + const bidderRequest = { + ...commonBidderRequest, + uspConsent: 'consentString' + } + const res = spec.buildRequests([bidRequest], bidderRequest) + const resData = JSON.parse(res.data) + expect(resData.regs.ext.us_privacy).to.equal('consentString') + }); + + it('should pass coppa consent', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + config.setConfig({coppa: true}) + + const res = spec.buildRequests([bidRequest], commonBidderRequest) + const resData = JSON.parse(res.data) + expect(resData.regs.coppa).to.equal(1) + + config.resetConfig() + }); + }) + + describe('interpretResponse', function () { + it('should return empty array if no valid bids', function () { + const res = spec.interpretResponse({}, []) + expect(res).to.be.an('array').that.is.empty + }); + + it('should interpret native response', function () { + const serverResponse = { + body: { + id: '0a73e68c-9967-4391-b01b-dda2d9fc54e4', + seatbid: [ + { + bid: [ + { + id: '82822cf5-259c-11eb-8a52-f29e5275aa57', + impid: '1', + price: 1.1, + nurl: 'http://example.com/win/${AUCTION_PRICE}', + adm: '{"ver":"1.2","assets":[{"id":3,"required":1,"img":{"url":"http://example.com/img/url","w":120,"h":100}},{"id":0,"required":1,"title":{"text":"Test title"}},{"id":5,"data":{"value":"Test sponsor"}}],"link":{"url":"http://example.com/click/url"},"eventtrackers":[{"event":1,"method":1,"url":"http://example.com/impression"}]}', + adomain: [ + 'example.co' + ], + cid: '3487171', + crid: '28023739', + cat: [ + 'IAB10-2' + ] + } + ], + seat: 'acc-5537' + } + ], + bidid: '82822cf5-259c-11eb-8a52-b48e7518c657', + cur: 'USD' + }, + } + const request = { + bids: [ + { + ...commonBidRequest, + ...nativeBidRequestParams, + } + ] + } + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: 1.1, + creativeId: '28023739', + ttl: 360, + netRevenue: false, + currency: 'USD', + mediaType: 'native', + nurl: 'http://example.com/win/${AUCTION_PRICE}', + native: { + clickTrackers: undefined, + clickUrl: 'http://example.com/click/url', + image: { + url: 'http://example.com/img/url', + width: 120, + height: 100 + }, + title: 'Test title', + sponsoredBy: 'Test sponsor', + impressionTrackers: [ + 'http://example.com/impression', + ] + } + } + ] + + const res = spec.interpretResponse(serverResponse, request) + expect(res).to.deep.equal(expectedRes) + }); + + it('should interpret display response', function () { + const serverResponse = { + body: { + id: '6b2eedc8-8ff5-46ef-adcf-e701b508943e', + seatbid: [ + { + bid: [ + { + id: 'd90fe7fa-28d7-11eb-8ce4-462a842a7cf9', + impid: '1', + price: 1.1, + nurl: 'http://example.com/win/${AUCTION_PRICE}', + adm: '
ad
', + adomain: [ + 'example.com' + ], + cid: '3865084', + crid: '29998660', + cat: [ + 'IAB10-2' + ], + w: 300, + h: 250 + } + ], + seat: 'acc-6536' + } + ], + bidid: 'd90fe7fa-28d7-11eb-8ce4-13d94bfa26f9', + cur: 'USD' + } + } + const request = { + bids: [ + { + ...commonBidRequest, + ...displayBidRequestParams + } + ] + } + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: 1.1, + creativeId: '29998660', + ttl: 360, + netRevenue: false, + currency: 'USD', + mediaType: 'banner', + nurl: 'http://example.com/win/${AUCTION_PRICE}', + ad: '
ad
', + width: 300, + height: 250 + } + ] + + const res = spec.interpretResponse(serverResponse, request) + expect(res).to.deep.equal(expectedRes) + }); + }) + }) + + describe('getUserSyncs', function () { + before(() => { + config.setConfig({ + zemanta: { + usersyncUrl: 'https://usersync-url.com', + } + } + ) + }) + after(() => { + config.resetConfig() + }) + + it('should return user sync if pixel enabled', function () { + const ret = spec.getUserSyncs({pixelEnabled: true}) + expect(ret).to.deep.equal([{type: 'image', url: 'https://usersync-url.com'}]) + }) + + it('should not return user sync if pixel disabled', function () { + const ret = spec.getUserSyncs({pixelEnabled: false}) + expect(ret).to.be.an('array').that.is.empty + }) + + it('should not return user sync if url is not set', function () { + config.resetConfig() + const ret = spec.getUserSyncs({pixelEnabled: true}) + expect(ret).to.be.an('array').that.is.empty + }) + }) + + describe('onBidWon', function () { + it('should make an ajax call with the original cpm', function () { + const bid = { + nurl: 'http://example.com/win/${AUCTION_PRICE}', + cpm: 2.1, + originalCpm: 1.1, + } + spec.onBidWon(bid) + expect(server.requests[0].url).to.equals('http://example.com/win/1.1') + }); + }) +}) From 6b4494ccae3e928f8aa1bbd1cdbb17c49c079424 Mon Sep 17 00:00:00 2001 From: cpuBird <54024689+cpuBird@users.noreply.github.com> Date: Tue, 15 Dec 2020 14:57:25 +0530 Subject: [PATCH 033/181] vdoai Bid Adapter: added multisize array in bid requests (#6101) * added multisize array in vdoai bid requests * fixing a bug dimentions --- modules/vdoaiBidAdapter.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/vdoaiBidAdapter.js b/modules/vdoaiBidAdapter.js index 8cfcd67bd00..d4fda36b1fc 100644 --- a/modules/vdoaiBidAdapter.js +++ b/modules/vdoaiBidAdapter.js @@ -30,14 +30,12 @@ export const spec = { if (validBidRequests.length === 0) { return []; } + return validBidRequests.map(bidRequest => { - const sizes = utils.parseSizesInput(bidRequest.params.size || bidRequest.sizes)[0]; - const width = sizes.split('x')[0]; - const height = sizes.split('x')[1]; + const sizes = utils.getAdUnitSizes(bidRequest); const payload = { placementId: bidRequest.params.placementId, - width: width, - height: height, + sizes: sizes, bidId: bidRequest.bidId, referer: bidderRequest.refererInfo.referer, id: bidRequest.auctionId, @@ -64,9 +62,9 @@ export const spec = { const response = serverResponse.body; const creativeId = response.adid || 0; // const width = response.w || 0; - const width = bidRequest.data.width; + const width = response.width; // const height = response.h || 0; - const height = bidRequest.data.height; + const height = response.height; const cpm = response.price || 0; response.rWidth = width; From 53e629145010fbc97925e5dd2207324e3f2891d7 Mon Sep 17 00:00:00 2001 From: Vadim Gush Date: Wed, 16 Dec 2020 06:49:25 +0400 Subject: [PATCH 034/181] Sovrn Bid Adapter: Change TTL field (#6083) * Change TTL field for SovrnBidAdapter * Fix unit tests * Fix codestyle in unit tests * Fix tests * Fix tests * Removed ext field from some tests * Trying to make ext field optional * Codestyle changes --- modules/sovrnBidAdapter.js | 2 +- test/spec/modules/sovrnBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 8f8158fd0c9..176b090fbe5 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -153,7 +153,7 @@ export const spec = { netRevenue: true, mediaType: BANNER, ad: decodeURIComponent(`${sovrnBid.adm}`), - ttl: sovrnBid.ttl || 90 + ttl: sovrnBid.ext ? (sovrnBid.ext.ttl || 90) : 90 }); }); } diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 769be73a272..2bb5cdbdf3c 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -387,7 +387,7 @@ describe('sovrnBidAdapter', function() { }); it('should get correct bid response when ttl is set', function () { - response.body.seatbid[0].bid[0].ttl = 480; + response.body.seatbid[0].bid[0].ext = { 'ttl': 480 }; let expectedResponse = [{ 'requestId': '263c448586f5a1', From a9f1795f6de579d84f03a542458b6c4228a7257f Mon Sep 17 00:00:00 2001 From: pro-nsk <32703851+pro-nsk@users.noreply.github.com> Date: Wed, 16 Dec 2020 16:32:39 +0700 Subject: [PATCH 035/181] Change bidder url for Qwarry adapter (#6128) * qwarry bid adapter * formatting fixes * fix tests for qwarry * qwarry bid adapter * add header for qwarry bid adapter * bid requests fix * fix tests * response fix * fix tests for Qwarry bid adapter * add pos parameter to qwarry bid adapter * qwarryBidAdapter onBidWon hotfix * Change bidder endpoint url for Qwarry adapter Co-authored-by: Artem Kostritsa Co-authored-by: Alexander Kascheev --- modules/qwarryBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/qwarryBidAdapter.js b/modules/qwarryBidAdapter.js index 7c2ec0f085b..7cb83520979 100644 --- a/modules/qwarryBidAdapter.js +++ b/modules/qwarryBidAdapter.js @@ -4,7 +4,7 @@ import { ajax } from '../src/ajax.js'; import { VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'qwarry'; -export const ENDPOINT = 'https://ui-bidder.kantics.co/bid/adtag?prebid=true' +export const ENDPOINT = 'https://bidder.qwarry.co/bid/adtag?prebid=true' export const spec = { code: BIDDER_CODE, From 1410a738cf7802227e3dc353b62440e9545f9947 Mon Sep 17 00:00:00 2001 From: Stephen Johnston Date: Wed, 16 Dec 2020 05:33:36 -0500 Subject: [PATCH 036/181] Add Automatic Release Drafter Functionality to Prebid Repository (#5954) * Create release-drafter.yml * Create release-drafter.yml * Update release-drafter.yml * Update release-drafter.yml --- .github/release-drafter.yml | 28 +++++++++++++++++++++++++++ .github/workflows/release-drafter.yml | 18 +++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/release-drafter.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000000..8984252f4c3 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,28 @@ + +name-template: 'Prebid $RESOLVED_VERSION Release' +tag-template: '$RESOLVED_VERSION' +categories: + - title: '🚀 New Features' + label: 'feature' + - title: '🛠 Maintenance' + label: 'maintenance' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' +change-template: '- $TITLE (#$NUMBER)' +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ## In This Release + $CHANGES diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000000..8152b61275d --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,18 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + with: + config-name: release-drafter.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 54df547a22551cba288348768ed2d61cd2181667 Mon Sep 17 00:00:00 2001 From: mmoschovas <63253416+mmoschovas@users.noreply.github.com> Date: Wed, 16 Dec 2020 12:43:41 -0500 Subject: [PATCH 037/181] Price Floors update to include modelWeight in the bid request to give additional context for Analytics adapters that the Floors Module is schema mode 2, and the floors module picks a model prior to an auction based on the modelWeight supplied in the floors module definition (#6126) Rubicon Analytics Update to pass modelWeight if exists --- modules/priceFloors.js | 1 + modules/rubiconAnalyticsAdapter.js | 1 + test/spec/modules/priceFloors_spec.js | 25 +++++++++++++++++++ .../modules/rubiconAnalyticsAdapter_spec.js | 3 +++ 4 files changed, 30 insertions(+) diff --git a/modules/priceFloors.js b/modules/priceFloors.js index c0797f710de..7c8834c2ae2 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -291,6 +291,7 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { skipRate: floorData.skipRate, floorMin: floorData.floorMin, modelVersion: utils.deepAccess(floorData, 'data.modelVersion'), + modelWeight: utils.deepAccess(floorData, 'data.modelWeight'), modelTimestamp: utils.deepAccess(floorData, 'data.modelTimestamp'), location: utils.deepAccess(floorData, 'data.location', 'noData'), floorProvider: floorData.floorProvider, diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index ad78c601ab6..38d83a40bc7 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -237,6 +237,7 @@ function sendMessage(auctionId, bidWonId) { auction.floors = utils.pick(auctionCache.floorData, [ 'location', 'modelVersion as modelName', + 'modelWeight', 'modelTimestamp', 'skipped', 'enforcement', () => utils.deepAccess(auctionCache.floorData, 'enforcements.enforceJS'), diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index 1b3ce021068..984d6da1cb9 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -23,6 +23,7 @@ describe('the price floors module', function () { let clock; const basicFloorData = { modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, currency: 'USD', schema: { @@ -38,6 +39,7 @@ describe('the price floors module', function () { const basicFloorDataHigh = { floorMin: 7.0, modelVersion: 'basic model', + modelWeight: 10, currency: 'USD', schema: { delimiter: '|', @@ -52,6 +54,7 @@ describe('the price floors module', function () { const basicFloorDataLow = { floorMin: 2.3, modelVersion: 'basic model', + modelWeight: 10, currency: 'USD', schema: { delimiter: '|', @@ -185,6 +188,7 @@ describe('the price floors module', function () { let resultingData = getFloorsDataForAuction(inputFloorData, 'test_div_1'); expect(resultingData).to.deep.equal({ modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, currency: 'USD', schema: { @@ -203,6 +207,7 @@ describe('the price floors module', function () { resultingData = getFloorsDataForAuction(inputFloorData, 'this_is_a_div'); expect(resultingData).to.deep.equal({ modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, currency: 'USD', schema: { @@ -432,6 +437,7 @@ describe('the price floors module', function () { skipped: true, floorMin: undefined, modelVersion: undefined, + modelWeight: undefined, modelTimestamp: undefined, location: 'noData', skipRate: 0, @@ -467,6 +473,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'adUnit Model Version', + modelWeight: 10, modelTimestamp: 1606772895, location: 'adUnit', skipRate: 0, @@ -501,6 +508,7 @@ describe('the price floors module', function () { validateBidRequests(true, { skipped: false, modelVersion: 'adUnit Model Version', + modelWeight: 10, modelTimestamp: 1606772895, location: 'adUnit', skipRate: 0, @@ -516,6 +524,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, @@ -538,6 +547,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, @@ -553,6 +563,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, @@ -568,6 +579,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, @@ -592,6 +604,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 50, @@ -607,6 +620,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 10, @@ -622,6 +636,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, @@ -687,6 +702,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'model-1', + modelWeight: 10, modelTimestamp: undefined, location: 'setConfig', skipRate: 0, @@ -701,6 +717,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'model-2', + modelWeight: 40, modelTimestamp: undefined, location: 'setConfig', skipRate: 0, @@ -715,6 +732,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'model-3', + modelWeight: 50, modelTimestamp: undefined, location: 'setConfig', skipRate: 0, @@ -745,6 +763,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, @@ -825,6 +844,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, @@ -864,6 +884,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, modelTimestamp: 1606772895, location: 'fetch', skipRate: 0, @@ -902,6 +923,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, modelTimestamp: 1606772895, location: 'fetch', skipRate: 0, @@ -943,6 +965,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'fetch model name', + modelWeight: 10, modelTimestamp: 1606772895, location: 'fetch', skipRate: 95, @@ -966,6 +989,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, @@ -991,6 +1015,7 @@ describe('the price floors module', function () { skipped: false, floorMin: undefined, modelVersion: 'basic model', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 0, diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 0d6cf331e52..1dd524e1838 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -852,6 +852,7 @@ describe('rubicon analytics adapter', function () { auctionInit.bidderRequests[0].bids[0].floorData = { skipped: false, modelVersion: 'someModelName', + modelWeight: 10, modelTimestamp: 1606772895, location: 'setConfig', skipRate: 15, @@ -954,6 +955,7 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', + modelWeight: 10, modelTimestamp: 1606772895, skipped: false, enforcement: true, @@ -1000,6 +1002,7 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].floors).to.deep.equal({ location: 'setConfig', modelName: 'someModelName', + modelWeight: 10, modelTimestamp: 1606772895, skipped: false, enforcement: true, From eebdee25e946792e21cf64eff5f97f32d4e299a0 Mon Sep 17 00:00:00 2001 From: Scott Menzer Date: Wed, 16 Dec 2020 19:39:22 +0100 Subject: [PATCH 038/181] Prebid 4.20.0 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04ec495c93d..30ba2494182 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.20.0-pre", + "version": "4.20.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 282866ae79e92873d02620beac56d15b03dc7635 Mon Sep 17 00:00:00 2001 From: Scott Menzer Date: Wed, 16 Dec 2020 20:12:28 +0100 Subject: [PATCH 039/181] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 30ba2494182..f65b996a393 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.20.0", + "version": "4.21.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 47e96ad7ae62040ede8c9b84a192e0b718d9947a Mon Sep 17 00:00:00 2001 From: susyt Date: Wed, 16 Dec 2020 12:00:48 -0800 Subject: [PATCH 040/181] GumGum: makes slot and invideo products avail with pubId (#6107) --- modules/gumgumBidAdapter.js | 10 ++-- modules/gumgumBidAdapter.md | 69 +++++++++++++++++++++- test/spec/modules/gumgumBidAdapter_spec.js | 58 ++++++++---------- 3 files changed, 97 insertions(+), 40 deletions(-) diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 2cb5ce61064..c9bf77494cf 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -273,8 +273,9 @@ function buildRequests (validBidRequests, bidderRequest) { data.iriscat = params.iriscat; } - if (params.zone) { - data.t = params.zone; + if (params.zone || params.pubId) { + params.zone ? (data.t = params.zone) : (data.pubId = params.pubId); + data.pi = 2; // inscreen // override pi if the following is found if (params.slot) { @@ -285,11 +286,8 @@ function buildRequests (validBidRequests, bidderRequest) { data.ni = parseInt(params.native, 10); data.pi = 5; } else if (mediaTypes.video) { - data.pi = mediaTypes.video.linearity === 1 ? 7 : 6; // video : invideo + data.pi = mediaTypes.video.linearity === 2 ? 6 : 7; // invideo : video } - } else if (params.pubId) { - data.pubId = params.pubId - data.pi = mediaTypes.video ? 7 : 2; // video : inscreen } else { // legacy params data = { ...data, ...handleLegacyParams(params, sizes) } } diff --git a/modules/gumgumBidAdapter.md b/modules/gumgumBidAdapter.md index 7b4f0c98ea7..57d56235d1c 100644 --- a/modules/gumgumBidAdapter.md +++ b/modules/gumgumBidAdapter.md @@ -10,11 +10,76 @@ Maintainer: engineering@gumgum.com GumGum adapter for Prebid.js Please note that both video and in-video products require a mediaType of video. -All other products (in-screen, slot, native) should have a mediaType of banner. - +In-screen and slot products should have a mediaType of banner. # Test Parameters ``` +var adUnits = [ + { + code: 'slot-placement', + sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'gumgum', + params: { + zone: 'dc9d6be1', // GumGum Zone ID given to the client + slot: '15901', // GumGum Slot ID given to the client, + bidfloor: 0.03 // CPM bid floor + } + } + ] + },{ + code: 'inscreen-placement', + sizes: [[300, 50]], + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + }, + bids: [ + { + bidder: 'gumgum', + params: { + zone: 'dc9d6be1', // GumGum Zone ID given to the client + bidfloor: 0.03 // CPM bid floor + } + } + ] + },{ + code: 'video-placement', + sizes: [[300, 50]], + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + minduration: 1, + maxduration: 2, + linearity: 1, + startdelay: 1, + placement: 1, + protocols: [1, 2] + } + }, + bids: [ + { + bidder: 'gumgum', + params: { + zone: 'ggumtest', // GumGum Zone ID given to the client + bidfloor: 0.03 // CPM bid floor + } + } + ] + } +]; +``` + +# Legacy Test Parameters +``` var adUnits = [ { code: 'test-div', diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index 52a3a21db4e..a2a1b733d3c 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -35,7 +35,7 @@ describe('gumgumAdapter', function () { it('should return true when required params found', function () { const zoneBid = { ...bid, params: { 'zone': '123' } }; - const pubIdBid = { ...bid, params: { 'pubId': '123' } }; + const pubIdBid = { ...bid, params: { 'pubId': 123 } }; expect(spec.isBidRequestValid(bid)).to.equal(true); expect(spec.isBidRequestValid(zoneBid)).to.equal(true); expect(spec.isBidRequestValid(pubIdBid)).to.equal(true); @@ -143,23 +143,24 @@ describe('gumgumAdapter', function () { protocols: [1, 2] } }; + const zoneParam = { 'zone': '123a' }; + const pubIdParam = { 'pubId': 123 }; - describe('zone param', function () { - const zoneParam = { 'zone': '123a' }; + it('should set pubId param if found', function () { + const request = { ...bidRequests[0], params: pubIdParam }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.pubId).to.equal(pubIdParam.pubId); + }); - it('should set t and pi param', function () { - const request = { ...bidRequests[0], params: zoneParam }; - const bidRequest = spec.buildRequests([request])[0]; - expect(bidRequest.data.t).to.equal(zoneParam.zone); - expect(bidRequest.data.pi).to.equal(2); - }); - it('should set the correct pi param if slot param is found', function () { - const request = { ...bidRequests[0], params: { ...zoneParam, 'slot': 1 } }; - const bidRequest = spec.buildRequests([request])[0]; - expect(bidRequest.data.pi).to.equal(3); - }); + it('should set t param when zone param is found', function () { + const request = { ...bidRequests[0], params: zoneParam }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.t).to.equal(zoneParam.zone); + }); + + describe('product id', function () { it('should set the correct pi param if native param is found', function () { - const request = { ...bidRequests[0], params: { ...zoneParam, 'native': 2 } }; + const request = { ...bidRequests[0], params: { ...zoneParam, native: 2 } }; const bidRequest = spec.buildRequests([request])[0]; expect(bidRequest.data.pi).to.equal(5); }); @@ -174,25 +175,18 @@ describe('gumgumAdapter', function () { const bidRequest = spec.buildRequests([request])[0]; expect(bidRequest.data.pi).to.equal(6); }); - }); - - describe('pubId zone', function () { - const pubIdParam = { 'pubId': 'abc' }; - - it('should set t param', function () { - const request = { ...bidRequests[0], params: pubIdParam }; + it('should set the correct pi param if slot param is found', function () { + const request = { ...bidRequests[0], params: { ...zoneParam, slot: '123s' } }; const bidRequest = spec.buildRequests([request])[0]; - expect(bidRequest.data.pubId).to.equal(pubIdParam.pubId); + expect(bidRequest.data.pi).to.equal(3); }); - - it('should set the correct pi depending on what is found in mediaTypes', function () { - const request = { ...bidRequests[0], params: pubIdParam }; - const bidRequest = spec.buildRequests([request])[0]; - const vidRequest = { ...bidRequests[0], mediaTypes: vidMediaTypes, params: { 'videoPubID': 123 } }; - const vidBidRequest = spec.buildRequests([vidRequest])[0]; - - expect(bidRequest.data.pi).to.equal(2); - expect(vidBidRequest.data.pi).to.equal(7); + it('should default the pi param to 2 if only zone or pubId param is found', function () { + const zoneRequest = { ...bidRequests[0], params: zoneParam }; + const pubIdRequest = { ...bidRequests[0], params: pubIdParam }; + const zoneBidRequest = spec.buildRequests([zoneRequest])[0]; + const pubIdBidRequest = spec.buildRequests([pubIdRequest])[0]; + expect(zoneBidRequest.data.pi).to.equal(2); + expect(pubIdBidRequest.data.pi).to.equal(2); }); }); From 453e080e678570a394d8c5a9fbed8650b2a04be5 Mon Sep 17 00:00:00 2001 From: readpeaktuomo <66239046+readpeaktuomo@users.noreply.github.com> Date: Fri, 18 Dec 2020 18:37:43 +0200 Subject: [PATCH 041/181] Add support for tagId parameter (#6133) * Add support for tagId parameter * Update maintainer email to an alias --- modules/readpeakBidAdapter.js | 3 ++- modules/readpeakBidAdapter.md | 5 +++-- test/spec/modules/readpeakBidAdapter_spec.js | 7 +++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index c72bbdd658f..2f4173f240b 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -98,7 +98,8 @@ function impression(slot) { id: slot.bidId, native: nativeImpression(slot), bidfloor: slot.params.bidfloor || 0, - bidfloorcur: slot.params.bidfloorcur || 'USD' + bidfloorcur: slot.params.bidfloorcur || 'USD', + tagId: slot.params.tagId || '0' }; } diff --git a/modules/readpeakBidAdapter.md b/modules/readpeakBidAdapter.md index a15767f29a7..da250e7f77a 100644 --- a/modules/readpeakBidAdapter.md +++ b/modules/readpeakBidAdapter.md @@ -4,7 +4,7 @@ Module Name: ReadPeak Bid Adapter Module Type: Bidder Adapter -Maintainer: kurre.stahlberg@readpeak.com +Maintainer: devteam@readpeak.com # Description @@ -23,7 +23,8 @@ Please reach out to your account team or hello@readpeak.com for more information params: { bidfloor: 5.00, publisherId: 'test', - siteId: 'test' + siteId: 'test', + tagId: 'test-tag-1' }, }] }]; diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js index 0c6f942e724..d5a877f6221 100644 --- a/test/spec/modules/readpeakBidAdapter_spec.js +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -28,7 +28,8 @@ describe('ReadPeakAdapter', function() { params: { bidfloor: 5.0, publisherId: '11bc5dd5-7421-4dd8-c926-40fa653bec76', - siteId: '11bc5dd5-7421-4dd8-c926-40fa653bec77' + siteId: '11bc5dd5-7421-4dd8-c926-40fa653bec77', + tagId: 'test-tag-1' }, bidId: '2ffb201a808da7', bidderRequestId: '178e34bad3658f', @@ -104,7 +105,8 @@ describe('ReadPeakAdapter', function() { ver: '1.1' }, bidfloor: 5, - bidfloorcur: 'USD' + bidfloorcur: 'USD', + tagId: 'test-tag-1' } ], site: { @@ -177,6 +179,7 @@ describe('ReadPeakAdapter', function() { expect(data.id).to.equal(bidRequest.bidderRequestId); expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); expect(data.imp[0].bidfloorcur).to.equal('USD'); + expect(data.imp[0].tagId).to.equal('test-tag-1'); expect(data.site.publisher.id).to.equal(bidRequest.params.publisherId); expect(data.site.id).to.equal(bidRequest.params.siteId); expect(data.site.page).to.equal(bidderRequest.refererInfo.referer); From 78917f5a5128cde43e063facfaa61c81c9b14e6a Mon Sep 17 00:00:00 2001 From: Zak Andree Date: Fri, 18 Dec 2020 11:47:21 -0800 Subject: [PATCH 042/181] Inmar bidder adapter: Make adNetId an optional paramater (#6136) * Update Inmar bidder adapter * Set withCredentials to true * Remove inmarId from Inmar bidder adapter * Remove withCredentials because it defaults to true --- modules/inmarBidAdapter.js | 5 +---- modules/inmarBidAdapter.md | 2 -- test/spec/modules/inmarBidAdapter_spec.js | 11 ----------- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/modules/inmarBidAdapter.js b/modules/inmarBidAdapter.js index b5ab72266fc..7583985b23c 100755 --- a/modules/inmarBidAdapter.js +++ b/modules/inmarBidAdapter.js @@ -17,7 +17,7 @@ export const spec = { * @returns {boolean} True if this is a valid bid, and false otherwise */ isBidRequestValid: function(bid) { - return !!(bid.params && bid.params.partnerId && bid.params.adnetId); + return !!(bid.params && bid.params.partnerId); }, /** @@ -49,9 +49,6 @@ export const spec = { return { method: 'POST', url: 'https://prebid.owneriq.net:8443/bidder/pb/bid', - options: { - withCredentials: false - }, data: payloadString, }; }, diff --git a/modules/inmarBidAdapter.md b/modules/inmarBidAdapter.md index 1bacb30f2dd..8ed6b998602 100644 --- a/modules/inmarBidAdapter.md +++ b/modules/inmarBidAdapter.md @@ -25,7 +25,6 @@ Please reach out to your account manager for more information. bidder: 'inmar', params: { partnerId: 12345, - adnetId: 'ADb1f40rmi', position: 1 } }] @@ -37,7 +36,6 @@ Please reach out to your account manager for more information. bidder: 'inmar', params: { partnerId: 12345, - adnetId: 'ADb1f40rmo', position: 0 } }] diff --git a/test/spec/modules/inmarBidAdapter_spec.js b/test/spec/modules/inmarBidAdapter_spec.js index 86b7ab3a8af..998fe20d369 100644 --- a/test/spec/modules/inmarBidAdapter_spec.js +++ b/test/spec/modules/inmarBidAdapter_spec.js @@ -17,7 +17,6 @@ describe('Inmar adapter tests', function () { }, bidder: 'inmar', params: { - adnetId: 'ADb1f40rmi', partnerId: 12345 }, auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', @@ -39,7 +38,6 @@ describe('Inmar adapter tests', function () { }, bidder: 'inmar', params: { - adnetId: 'ADb1f40rmi', partnerId: 12345 }, auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', @@ -60,7 +58,6 @@ describe('Inmar adapter tests', function () { ], bidder: 'inmar', params: { - adnetId: 'ADb1f40rmi', partnerId: 12345, }, auctionId: '851adee7-d843-48f9-a7e9-9ff00573fcbf', @@ -118,7 +115,6 @@ describe('Inmar adapter tests', function () { expect(request).to.have.property('method').and.to.equal('POST'); const requestContent = JSON.parse(request.data); - expect(requestContent.bidRequests[0].params).to.have.property('adnetId').and.to.equal('ADb1f40rmi'); expect(requestContent.bidRequests[0].params).to.have.property('partnerId').and.to.equal(12345); expect(requestContent.bidRequests[0]).to.have.property('auctionId').and.to.equal('0cb3144c-d084-4686-b0d6-f5dbe917c563'); expect(requestContent.bidRequests[0]).to.have.property('bidId').and.to.equal('2c7c8e9c900244'); @@ -198,19 +194,12 @@ describe('Inmar adapter tests', function () { })).to.equal(false); expect(spec.isBidRequestValid({ params: { - adnetId: 'ADb1f40rmi' } })).to.equal(false); expect(spec.isBidRequestValid({ params: { partnerId: 12345 } - })).to.equal(false); - expect(spec.isBidRequestValid({ - params: { - adnetId: 'ADb1f40rmi', - partnerId: 12345 - } })).to.equal(true); }); From 2c1e5352efafc8fb5eae02eff4ac59bee82fdc74 Mon Sep 17 00:00:00 2001 From: hybrid-ai <58724131+hybrid-ai@users.noreply.github.com> Date: Sat, 19 Dec 2020 18:46:55 +0300 Subject: [PATCH 043/181] Added VOX Bidder Adapter (#6030) * Added voxBidAdapter.js to get a bid from partners.hybrid.ai * Added placements ids for testing Co-authored-by: s-shevtsov --- modules/voxBidAdapter.js | 247 ++++++++++++++++++ modules/voxBidAdapter.md | 237 ++++++++++++++++++ test/spec/modules/voxBidAdapter_spec.js | 320 ++++++++++++++++++++++++ 3 files changed, 804 insertions(+) create mode 100644 modules/voxBidAdapter.js create mode 100644 modules/voxBidAdapter.md create mode 100644 test/spec/modules/voxBidAdapter_spec.js diff --git a/modules/voxBidAdapter.js b/modules/voxBidAdapter.js new file mode 100644 index 00000000000..450f270db31 --- /dev/null +++ b/modules/voxBidAdapter.js @@ -0,0 +1,247 @@ +import * as utils from '../src/utils.js' +import { registerBidder } from '../src/adapters/bidderFactory.js' +import {BANNER, VIDEO} from '../src/mediaTypes.js' +import find from 'core-js-pure/features/array/find.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {Renderer} from '../src/Renderer.js'; + +const BIDDER_CODE = 'vox'; +const SSP_ENDPOINT = 'https://ssp.hybrid.ai/auction/prebid'; +const VIDEO_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; +const TTL = 60; + +function buildBidRequests(validBidRequests) { + return utils._map(validBidRequests, function(validBidRequest) { + const params = validBidRequest.params; + const bidRequest = { + bidId: validBidRequest.bidId, + transactionId: validBidRequest.transactionId, + sizes: validBidRequest.sizes, + placement: params.placement, + placeId: params.placementId, + imageUrl: params.imageUrl + }; + + return bidRequest; + }) +} + +const outstreamRender = bid => { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [bid.width, bid.height], + targetId: bid.adUnitCode, + rendererOptions: { + showBigPlayButton: false, + showProgressBar: 'bar', + showVolume: false, + allowFullscreen: true, + skippable: false, + content: bid.vastXml + } + }); + }); +} + +const createRenderer = (bid) => { + const renderer = Renderer.install({ + targetId: bid.adUnitCode, + url: VIDEO_RENDERER_URL, + loaded: false + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; +} + +function buildBid(bidData) { + const bid = { + requestId: bidData.bidId, + cpm: bidData.price, + width: bidData.content.width, + height: bidData.content.height, + creativeId: bidData.content.seanceId || bidData.bidId, + currency: bidData.currency, + netRevenue: true, + mediaType: BANNER, + ttl: TTL, + content: bidData.content + }; + + if (bidData.placement === 'video') { + bid.vastXml = bidData.content; + bid.mediaType = VIDEO; + + let adUnit = find(auctionManager.getAdUnits(), function (unit) { + return unit.transactionId === bidData.transactionId; + }); + + if (adUnit) { + bid.width = adUnit.mediaTypes.video.playerSize[0][0]; + bid.height = adUnit.mediaTypes.video.playerSize[0][1]; + + if (adUnit.mediaTypes.video.context === 'outstream') { + bid.renderer = createRenderer(bid); + } + } + } else if (bidData.placement === 'inImage') { + bid.mediaType = BANNER; + bid.ad = wrapInImageBanner(bid, bidData); + } else { + bid.mediaType = BANNER; + bid.ad = wrapBanner(bid, bidData); + } + + return bid; +} + +function getMediaTypeFromBid(bid) { + return bid.mediaTypes && Object.keys(bid.mediaTypes)[0]; +} + +function hasVideoMandatoryParams(mediaTypes) { + const isHasVideoContext = !!mediaTypes.video && (mediaTypes.video.context === 'instream' || mediaTypes.video.context === 'outstream'); + + const isPlayerSize = + !!utils.deepAccess(mediaTypes, 'video.playerSize') && + utils.isArray(utils.deepAccess(mediaTypes, 'video.playerSize')); + + return isHasVideoContext && isPlayerSize; +} + +function wrapInImageBanner(bid, bidData) { + return ` + + + + + + + + +
+ + + `; +} + +function wrapBanner(bid, bidData) { + return ` + + + + + + + + +
+ + + `; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid(bid) { + return ( + !!bid.params.placementId && + !!bid.params.placement && + ( + (getMediaTypeFromBid(bid) === BANNER && bid.params.placement === 'banner') || + (getMediaTypeFromBid(bid) === BANNER && bid.params.placement === 'inImage' && !!bid.params.imageUrl) || + (getMediaTypeFromBid(bid) === VIDEO && bid.params.placement === 'video' && hasVideoMandatoryParams(bid.mediaTypes)) + ) + ); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests(validBidRequests, bidderRequest) { + const payload = { + url: bidderRequest.refererInfo.referer, + cmp: !!bidderRequest.gdprConsent, + bidRequests: buildBidRequests(validBidRequests) + }; + + if (payload.cmp) { + const gdprApplies = bidderRequest.gdprConsent.gdprApplies; + if (gdprApplies !== undefined) payload['ga'] = gdprApplies; + payload['cs'] = bidderRequest.gdprConsent.consentString; + } + + const payloadString = JSON.stringify(payload); + + return { + method: 'POST', + url: SSP_ENDPOINT, + data: payloadString, + options: { + contentType: 'application/json' + } + } + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + let bidRequests = JSON.parse(bidRequest.data).bidRequests; + const serverBody = serverResponse.body; + + if (serverBody && serverBody.bids && utils.isArray(serverBody.bids)) { + return utils._map(serverBody.bids, function(bid) { + let rawBid = find(bidRequests, function (item) { + return item.bidId === bid.bidId; + }); + bid.placement = rawBid.placement; + bid.transactionId = rawBid.transactionId; + bid.placeId = rawBid.placeId; + return buildBid(bid); + }); + } else { + return []; + } + } + +} +registerBidder(spec); diff --git a/modules/voxBidAdapter.md b/modules/voxBidAdapter.md new file mode 100644 index 00000000000..3fc0383e6f8 --- /dev/null +++ b/modules/voxBidAdapter.md @@ -0,0 +1,237 @@ +# Overview + + +**Module Name**: VOX Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: prebid@hybrid.ai + +# Description + +You can use this adapter to get a bid from partners.hybrid.ai + + +## Sample Banner Ad Unit + +```js +var adUnits = [{ + code: 'banner_ad_unit', + mediaTypes: { + banner: { + sizes: [[160, 600]] + } + }, + bids: [{ + bidder: "vox", + params: { + placement: "banner", // required + placementId: "5fc77bc5a757531e24c89a4c" // required + } + }] +}]; +``` + +## Sample Video Ad Unit + +```js +var adUnits = [{ + code: 'video_ad_unit', + mediaTypes: { + video: { + context: 'outstream', // required + playerSize: [[640, 480]] // required + } + }, + bids: [{ + bidder: 'vox', + params: { + placement: "video", // required + placementId: "5fc77a94a757531e24c89a3d" // required + } + }] +}]; +``` + +# Sample In-Image Ad Unit + +```js +var adUnits = [{ + code: 'test-div', + mediaTypes: { + banner: { + sizes: [0, 0] + } + }, + bids: [{ + bidder: "vox", + params: { + placement: "inImage", + placementId: "5fc77b40a757531e24c89a42", + imageUrl: "https://gallery.voxexchange.io/vox-main.png" + } + }] +}]; +``` + +# Example page with In-Image + +```html + + + + + Prebid.js Banner Example + + + + + +

Prebid.js InImage Banner Test

+
+ + +
+ + +``` + +# Example page with In-Image and GPT + +```html + + + + + Prebid.js Banner Example + + + + + + +

Prebid.js Banner Ad Unit Test

+
+ + +
+ + +``` diff --git a/test/spec/modules/voxBidAdapter_spec.js b/test/spec/modules/voxBidAdapter_spec.js new file mode 100644 index 00000000000..c6221cba9e5 --- /dev/null +++ b/test/spec/modules/voxBidAdapter_spec.js @@ -0,0 +1,320 @@ +import { expect } from 'chai' +import { spec } from 'modules/voxBidAdapter.js' + +function getSlotConfigs(mediaTypes, params) { + return { + params: params, + sizes: [], + bidId: '2df8c0733f284e', + bidder: 'vox', + mediaTypes: mediaTypes, + transactionId: '31a58515-3634-4e90-9c96-f86196db1459' + } +} + +describe('VOX Adapter', function() { + const PLACE_ID = '5af45ad34d506ee7acad0c26'; + const bidderRequest = { + refererInfo: { referer: 'referer' } + } + const bannerMandatoryParams = { + placementId: PLACE_ID, + placement: 'banner' + } + const videoMandatoryParams = { + placementId: PLACE_ID, + placement: 'video' + } + const inImageMandatoryParams = { + placementId: PLACE_ID, + placement: 'inImage', + imageUrl: 'https://hybrid.ai/images/image.jpg' + } + const validBidRequests = [ + getSlotConfigs({ banner: {} }, bannerMandatoryParams), + getSlotConfigs({ video: {playerSize: [[640, 480]], context: 'outstream'} }, videoMandatoryParams), + getSlotConfigs({ banner: {sizes: [0, 0]} }, inImageMandatoryParams) + ] + describe('isBidRequestValid method', function() { + describe('returns true', function() { + describe('when banner slot config has all mandatory params', () => { + describe('and banner placement has the correct value', function() { + const slotConfig = getSlotConfigs( + {banner: {}}, + { + placementId: PLACE_ID, + placement: 'banner' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) + describe('and In-Image placement has the correct value', function() { + const slotConfig = getSlotConfigs( + { + banner: { + sizes: [[0, 0]] + } + }, + { + placementId: PLACE_ID, + placement: 'inImage', + imageUrl: 'imageUrl' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) + describe('when video slot has all mandatory params.', function() { + it('should return true, when video mediatype object are correct.', function() { + const slotConfig = getSlotConfigs( + { + video: { + context: 'instream', + playerSize: [[640, 480]] + } + }, + { + placementId: PLACE_ID, + placement: 'video' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) + }) + }) + }) + describe('returns false', function() { + describe('when params are not correct', function() { + function createSlotconfig(params) { + return getSlotConfigs({ banner: {} }, params) + } + it('does not have the placementId.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placement: 'banner' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have the placement.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placementId: PLACE_ID + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have the imageUrl.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placementId: PLACE_ID, + placement: 'inImage' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have a the correct placement.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placementId: PLACE_ID, + placement: 'something' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + }) + describe('when video mediaType object is not correct.', function() { + function createVideoSlotconfig(mediaType) { + return getSlotConfigs(mediaType, { + placementId: PLACE_ID, + placement: 'video' + }) + } + it('is a void object', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ video: {} }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have playerSize.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ video: { context: 'instream' } }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have context', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ + video: { + playerSize: [[640, 480]] + } + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + }) + }) + }) + it('Url params should be correct ', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + expect(request.method).to.equal('POST') + expect(request.url).to.equal('https://ssp.hybrid.ai/auction/prebid') + }) + + describe('buildRequests method', function() { + it('Common data request should be correct', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(Array.isArray(data.bidRequests)).to.equal(true) + expect(data.url).to.equal('referer') + data.bidRequests.forEach(bid => { + expect(bid.bidId).to.equal('2df8c0733f284e') + expect(bid.placeId).to.equal(PLACE_ID) + expect(bid.transactionId).to.equal('31a58515-3634-4e90-9c96-f86196db1459') + }) + }) + + describe('GDPR params', function() { + describe('when there are not consent management platform', function() { + it('cmp should be false', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(false) + }) + }) + describe('when there are consent management platform', function() { + it('cmps should be true and ga should not sended, when gdprApplies is undefined', function() { + bidderRequest['gdprConsent'] = { + gdprApplies: undefined, + consentString: 'consentString' + } + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(true) + expect(Object.keys(data).indexOf('data')).to.equal(-1) + expect(data.cs).to.equal('consentString') + }) + it('cmps should be true and all gdpr parameters should be sended, when there are gdprApplies', function() { + bidderRequest['gdprConsent'] = { + gdprApplies: true, + consentString: 'consentString' + } + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(true) + expect(data.ga).to.equal(true) + expect(data.cs).to.equal('consentString') + }) + }) + }) + }) + + describe('interpret response method', function() { + it('should return a void array, when the server response are not correct.', function() { + const request = { data: JSON.stringify({}) } + const serverResponse = { + body: {} + } + const bids = spec.interpretResponse(serverResponse, request) + expect(typeof bids).to.equal('object') + expect(bids.length).to.equal(0) + }) + it('should return a void array, when the server response have not got bids.', function() { + const request = { data: JSON.stringify({}) } + const serverResponse = { body: { bids: [] } } + const bids = spec.interpretResponse(serverResponse, request) + expect(typeof bids).to.equal('object') + expect(bids.length).to.equal(0) + }) + describe('when the server response return a bid', function() { + describe('the bid is a banner', function() { + it('should return a banner bid', function() { + const request = spec.buildRequests([validBidRequests[0]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: { + content: 'html', + width: 100, + height: 100 + } + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].mediaType).to.equal(spec.supportedMediaTypes[0]) + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].width).to.equal(100) + expect(bids[0].height).to.equal(100) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].ad).to.equal('string') + }) + it('should return a In-Image bid', function() { + const request = spec.buildRequests([validBidRequests[2]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: { + content: 'html', + width: 100, + height: 100 + }, + ttl: 360 + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].width).to.equal(100) + expect(bids[0].height).to.equal(100) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].ad).to.equal('string') + }) + }) + describe('the bid is a video', function() { + it('should return a video bid', function() { + const request = spec.buildRequests([validBidRequests[1]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: 'html', + transactionId: '31a58515-3634-4e90-9c96-f86196db1459' + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].mediaType).to.equal(spec.supportedMediaTypes[1]) + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].vastXml).to.equal('string') + }) + }) + }) + }) +}) From 5dd60a1078ebe39602ce5722e32fb3fd3a3f9d13 Mon Sep 17 00:00:00 2001 From: reemeng <29702905+reemeng@users.noreply.github.com> Date: Mon, 21 Dec 2020 10:12:30 +0200 Subject: [PATCH 044/181] added Engageya bid adapter (#6109) * added Engageya bid adapter * moved test function from adapter to spec * remove function import * PAGE_URL should be String --- modules/engageyaBidAdapter.js | 133 +++++++++++++++ modules/engageyaBidAdapter.md | 68 ++++++++ test/spec/modules/engageyaBidAdapter_spec.js | 161 +++++++++++++++++++ 3 files changed, 362 insertions(+) create mode 100644 modules/engageyaBidAdapter.js create mode 100644 modules/engageyaBidAdapter.md create mode 100644 test/spec/modules/engageyaBidAdapter_spec.js diff --git a/modules/engageyaBidAdapter.js b/modules/engageyaBidAdapter.js new file mode 100644 index 00000000000..321b3287c2b --- /dev/null +++ b/modules/engageyaBidAdapter.js @@ -0,0 +1,133 @@ +import { + BANNER, + NATIVE +} from '../src/mediaTypes.js'; + +const { + registerBidder +} = require('../src/adapters/bidderFactory.js'); +const BIDDER_CODE = 'engageya'; +const ENDPOINT_URL = 'https://recs.engageya.com/rec-api/getrecs.json'; +const ENDPOINT_METHOD = 'GET'; + +function getPageUrl() { + var pUrl = window.location.href; + if (isInIframe()) { + pUrl = document.referrer ? document.referrer : pUrl; + } + pUrl = encodeURIComponent(pUrl); + return pUrl; +} + +function isInIframe() { + try { + var isInIframe = (window.self !== window.top); + } catch (e) { + isInIframe = true; + } + return isInIframe; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE], + isBidRequestValid: function(bid) { + return bid && bid.params && bid.params.hasOwnProperty('widgetId') && bid.params.hasOwnProperty('websiteId') && !isNaN(bid.params.widgetId) && !isNaN(bid.params.websiteId); + }, + + buildRequests: function(validBidRequests, bidderRequest) { + var bidRequests = []; + if (validBidRequests && validBidRequests.length > 0) { + validBidRequests.forEach(function(bidRequest) { + if (bidRequest.params) { + var mediaType = bidRequest.hasOwnProperty('nativeParams') ? 1 : 2; + var imageWidth = -1; + var imageHeight = -1; + if (bidRequest.sizes && bidRequest.sizes.length > 0) { + imageWidth = bidRequest.sizes[0][0]; + imageHeight = bidRequest.sizes[0][1]; + } else if (bidRequest.nativeParams && bidRequest.nativeParams.image && bidRequest.nativeParams.image.sizes) { + imageWidth = bidRequest.nativeParams.image.sizes[0]; + imageHeight = bidRequest.nativeParams.image.sizes[1]; + } + + var widgetId = bidRequest.params.widgetId; + var websiteId = bidRequest.params.websiteId; + var pageUrl = (bidRequest.params.pageUrl && bidRequest.params.pageUrl != '[PAGE_URL]') ? bidRequest.params.pageUrl : ''; + if (!pageUrl) { + pageUrl = (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) ? bidderRequest.refererInfo.referer : getPageUrl(); + } + var bidId = bidRequest.bidId; + var finalUrl = ENDPOINT_URL + '?pubid=0&webid=' + websiteId + '&wid=' + widgetId + '&url=' + pageUrl + '&ireqid=' + bidId + '&pbtpid=' + mediaType + '&imw=' + imageWidth + '&imh=' + imageHeight; + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprApplies && bidderRequest.consentString) { + finalUrl += '&is_gdpr=1&gdpr_consent=' + bidderRequest.consentString; + } + bidRequests.push({ + url: finalUrl, + method: ENDPOINT_METHOD, + data: '' + }); + } + }); + } + + return bidRequests; + }, + + interpretResponse: function(serverResponse, bidRequest) { + const bidResponses = []; + if (serverResponse.body && serverResponse.body.recs && serverResponse.body.recs.length > 0) { + var response = serverResponse.body; + var isNative = response.pbtypeId == 1; + response.recs.forEach(function(rec) { + var imageSrc = rec.thumbnail_path.indexOf('http') == -1 ? 'https:' + rec.thumbnail_path : rec.thumbnail_path; + if (isNative) { + bidResponses.push({ + requestId: response.ireqId, + cpm: rec.ecpm, + width: response.imageWidth, + height: response.imageHeight, + creativeId: rec.postId, + currency: 'USD', + netRevenue: false, + ttl: 360, + native: { + title: rec.title, + body: '', + image: { + url: imageSrc, + width: response.imageWidth, + height: response.imageHeight + }, + privacyLink: '', + clickUrl: rec.clickUrl, + displayUrl: rec.url, + cta: '', + sponsoredBy: rec.displayName, + impressionTrackers: [], + }, + }); + } else { + // var htmlTag = ""; + var htmlTag = '
'; + var tag = rec.tag ? rec.tag : htmlTag; + bidResponses.push({ + requestId: response.ireqId, + cpm: rec.ecpm, + width: response.imageWidth, + height: response.imageHeight, + creativeId: rec.postId, + currency: 'USD', + netRevenue: false, + ttl: 360, + ad: tag, + }); + } + }); + } + + return bidResponses; + } +}; + +registerBidder(spec); diff --git a/modules/engageyaBidAdapter.md b/modules/engageyaBidAdapter.md new file mode 100644 index 00000000000..541ba548eeb --- /dev/null +++ b/modules/engageyaBidAdapter.md @@ -0,0 +1,68 @@ +# Overview + +``` +Module Name: Engageya's Bidder Adapter +Module Type: Bidder Adapter +Maintainer: reem@engageya.com +``` + +# Description + +Module that connects to Engageya's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: "engageya", + params: { + widgetId: '', + websiteId: '', + pageUrl:'[PAGE_URL]' + } + } + ] + },{ + code: 'test-div', + mediaTypes: { + native: { + image: { + required: true, + sizes: [236, 202] + }, + title: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + body: { + required: true + } + } + }, + bids: [ + { + bidder: "engageya", + params: { + widgetId: '', + websiteId: '', + pageUrl:'[PAGE_URL]' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/test/spec/modules/engageyaBidAdapter_spec.js b/test/spec/modules/engageyaBidAdapter_spec.js new file mode 100644 index 00000000000..ad411fc9350 --- /dev/null +++ b/test/spec/modules/engageyaBidAdapter_spec.js @@ -0,0 +1,161 @@ +import {expect} from 'chai'; +import {spec} from 'modules/engageyaBidAdapter.js'; +import * as utils from 'src/utils.js'; + +const ENDPOINT_URL = 'https://recs.engageya.com/rec-api/getrecs.json'; + +export const _getUrlVars = function(url) { + var hash; + var myJson = {}; + var hashes = url.slice(url.indexOf('?') + 1).split('&'); + for (var i = 0; i < hashes.length; i++) { + hash = hashes[i].split('='); + myJson[hash[0]] = hash[1]; + } + return myJson; +} + +describe('engageya adapter', function() { + let bidRequests; + let nativeBidRequests; + + beforeEach(function() { + bidRequests = [ + { + bidder: 'engageya', + params: { + widgetId: 85610, + websiteId: 91140, + pageUrl: '[PAGE_URL]' + } + } + ] + + nativeBidRequests = [ + { + bidder: 'engageya', + params: { + widgetId: 85610, + websiteId: 91140, + pageUrl: '[PAGE_URL]' + }, + nativeParams: { + title: { + required: true, + len: 80 + }, + image: { + required: true, + sizes: [150, 50] + }, + sponsoredBy: { + required: true + } + } + } + ] + }) + describe('isBidRequestValid', function () { + it('valid bid case', function () { + let validBid = { + bidder: 'engageya', + params: { + widgetId: 85610, + websiteId: 91140, + pageUrl: '[PAGE_URL]' + } + } + let isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + }); + + it('invalid bid case: widgetId and websiteId is not passed', function() { + let validBid = { + bidder: 'engageya', + params: { + } + } + let isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }) + + it('invalid bid case: widget id must be number', function() { + let invalidBid = { + bidder: 'engageya', + params: { + widgetId: '157746a', + websiteId: 91140, + pageUrl: '[PAGE_URL]' + } + } + let isValid = spec.isBidRequestValid(invalidBid); + expect(isValid).to.equal(false); + }) + }) + + describe('buildRequests', function () { + it('sends bid request to ENDPOINT via GET', function () { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.url).to.include(ENDPOINT_URL); + expect(request.method).to.equal('GET'); + }); + + it('buildRequests function should not modify original bidRequests object', function () { + let originalBidRequests = utils.deepClone(bidRequests); + let request = spec.buildRequests(bidRequests); + expect(bidRequests).to.deep.equal(originalBidRequests); + }); + + it('buildRequests function should not modify original nativeBidRequests object', function () { + let originalBidRequests = utils.deepClone(nativeBidRequests); + let request = spec.buildRequests(nativeBidRequests); + expect(nativeBidRequests).to.deep.equal(originalBidRequests); + }); + + it('Request params check', function() { + let request = spec.buildRequests(bidRequests)[0]; + const data = _getUrlVars(request.url) + expect(parseInt(data.wid)).to.exist.and.to.equal(bidRequests[0].params.widgetId); + expect(parseInt(data.webid)).to.exist.and.to.equal(bidRequests[0].params.websiteId); + }) + }) + + describe('interpretResponse', function () { + let response = {recs: [ + { + 'ecpm': 0.0920, + 'postId': '', + 'ad': '', + 'thumbnail_path': 'https://engageya.live/wp-content/uploads/2019/05/images.png' + } + ], + imageWidth: 300, + imageHeight: 250, + ireqId: '1d236f7890b', + pbtypeId: 2}; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + 'requestId': '1d236f7890b', + 'cpm': 0.0920, + 'width': 300, + 'height': 250, + 'netRevenue': false, + 'currency': 'USD', + 'creativeId': '', + 'ttl': 700, + 'ad': '' + } + ]; + let request = spec.buildRequests(bidRequests)[0]; + let result = spec.interpretResponse({body: response}, request); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(result[0].cpm).to.not.equal(null); + expect(result[0].creativeId).to.not.equal(null); + expect(result[0].ad).to.not.equal(null); + expect(result[0].currency).to.equal('USD'); + expect(result[0].netRevenue).to.equal(false); + }); + }) +}) From d68f2e0c43df4429ea81fc0d8675b0ddf8fddec7 Mon Sep 17 00:00:00 2001 From: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Date: Mon, 21 Dec 2020 12:54:18 +0300 Subject: [PATCH 045/181] Fix userIds format for TheMediaGrid Bid Adapter (#6142) * Added TheMediaGridNM Bid Adapter * Updated required params for TheMediaGridNM Bid Adapter * Update TheMediGridNM Bid Adapter * Fix tests for TheMediaGridNM Bid Adapter * Fixes after review for TheMediaGridNM Bid Adapter * Add support of multi-format in TheMediaGrid Bid Adapter * Update sync url for grid and gridNM Bid Adapters * TheMediaGrid Bid Adapter: added keywords adUnit parameter * Update TheMediaGrid Bid Adapter to support keywords from config * Implement new request format for TheMediaGrid Bid Adapter * Fix jwpseg params for TheMediaGrid Bid Adapter * Update unit tests for The Media Grid Bid Adapter * Fix typo in TheMediaGrid Bid Adapter * Added test for jwTargeting in TheMediaGrid Bid Adapter * The new request format was made by default in TheMediaGrid Bid Adapter * Update userId format in ad request for TheMediaGrid Bid Adapter * Added bidFloor parameter for TheMediaGrid Bid Adapter * Fix for review TheMediaGrid Bid Adapter * Support floorModule in TheMediaGrid Bid Adapter * Fix empty bidfloor for TheMediaGrid Bid Adapter * Some change to restart autotests * Fix userIds format for TheMediaGrid Bid Adapter * Remove digitrust userId from TheMediaGrid Bid Adapter --- modules/gridBidAdapter.js | 67 +++----------------- test/spec/modules/gridBidAdapter_spec.js | 78 +++++++----------------- 2 files changed, 30 insertions(+), 115 deletions(-) diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index 3f9d4fba31d..6e610b67e0e 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -51,6 +51,7 @@ export const spec = { let content = null; let schain = null; let userId = null; + let userIdAsEids = null; let user = null; let userExt = null; let {bidderRequestId, auctionId, gdprConsent, uspConsent, timeout, refererInfo} = bidderRequest || {}; @@ -72,6 +73,9 @@ export const spec = { if (!userId) { userId = bid.userId; } + if (!userIdAsEids) { + userIdAsEids = bid.userIdAsEids; + } const {params: {uid, keywords}, mediaTypes, bidId, adUnitCode, rtd} = bid; bidsMap[bidId] = bid; if (!pageKeywords && !utils.isEmpty(keywords)) { @@ -161,66 +165,9 @@ export const spec = { userExt = {consent: gdprConsent.consentString}; } - if (userId) { - if (userId.tdid) { - userExt = userExt || {}; - userExt.eids = userExt.eids || []; - userExt.eids.push({ - source: 'adserver.org', // Unified ID - uids: [{ - id: userId.tdid, - ext: { - rtiPartner: 'TDID' - } - }] - }); - } - if (userId.id5id && userId.id5id.uid) { - userExt = userExt || {}; - userExt.eids = userExt.eids || []; - userExt.eids.push({ - source: 'id5-sync.com', - uids: [{ - id: userId.id5id.uid - }], - ext: userId.id5id.ext - }); - } - if (userId.lipb && userId.lipb.lipbid) { - userExt = userExt || {}; - userExt.eids = userExt.eids || []; - userExt.eids.push({ - source: 'liveintent.com', - uids: [{ - id: userId.lipb.lipbid - }] - }); - } - if (userId.idl_env) { - userExt = userExt || {}; - userExt.eids = userExt.eids || []; - userExt.eids.push({ - source: 'identityLink', - uids: [{ - id: userId.idl_env - }] - }); - } - if (userId.criteoId) { - userExt = userExt || {}; - userExt.eids = userExt.eids || []; - userExt.eids.push({ - source: 'criteo.com', - uids: [{ - id: userId.criteoId - }] - }); - } - - if (userId.digitrustid && userId.digitrustid.data && userId.digitrustid.data.id) { - userExt = userExt || {}; - userExt.digitrust = Object.assign({}, userId.digitrustid.data); - } + if (userIdAsEids && userIdAsEids.length) { + userExt = userExt || {}; + userExt.eids = [...userIdAsEids]; } if (userExt && Object.keys(userExt).length) { diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index e884df40c5e..084c67562e6 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -311,68 +311,36 @@ describe('TheMediaGrid Adapter', function () { }); it('if userId is present payload must have user.ext param with right keys', function () { - const bidRequestsWithUserIds = bidRequests.map((bid) => { - return Object.assign({ - userId: { - id5id: { uid: 'id5id_1', ext: { linkType: 2 } }, - tdid: 'tdid_1', - digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, - lipb: {lipbid: 'lipb_1'}, - idl_env: 'idl_env_1', - criteoId: 'criteoId_1' - } - }, bid); - }); - const request = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload).to.have.property('user'); - expect(payload.user).to.have.property('ext'); - expect(payload.user.ext.digitrust).to.deep.equal({ - id: 'DTID', - keyv: 4, - privacy: { - optout: false + const eids = [ + { + source: 'pubcid.org', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] }, - producer: 'ABC', - version: 2 - }); - expect(payload.user.ext.eids).to.deep.equal([ { source: 'adserver.org', uids: [{ - id: 'tdid_1', + id: 'some-random-id-value', + atype: 1, ext: { rtiPartner: 'TDID' } }] - }, - { - source: 'id5-sync.com', - uids: [{ - id: 'id5id_1' - }], - ext: { linkType: 2 } - }, - { - source: 'liveintent.com', - uids: [{ - id: 'lipb_1' - }] - }, - { - source: 'identityLink', - uids: [{ - id: 'idl_env_1' - }] - }, - { - source: 'criteo.com', - uids: [{ - id: 'criteoId_1' - }] } - ]); + ]; + const bidRequestsWithUserIds = bidRequests.map((bid) => { + return Object.assign({ + userIdAsEids: eids + }, bid); + }); + const request = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('user'); + expect(payload.user).to.have.property('ext'); + expect(payload.user.ext.eids).to.deep.equal(eids); }); it('if schain is present payload must have source.ext.schain param', function () { @@ -403,7 +371,7 @@ describe('TheMediaGrid Adapter', function () { it('if content and segment is present in jwTargeting, payload must have right params', function () { const jsContent = {id: 'test_jw_content_id'}; const jsSegments = ['test_seg_1', 'test_seg_2']; - const bidRequestsWithUserIds = bidRequests.map((bid) => { + const bidRequestsWithJwTargeting = bidRequests.map((bid) => { return Object.assign({ rtd: { jwplayer: { @@ -415,7 +383,7 @@ describe('TheMediaGrid Adapter', function () { } }, bid); }); - const request = spec.buildRequests(bidRequestsWithUserIds, bidderRequest); + const request = spec.buildRequests(bidRequestsWithJwTargeting, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); expect(payload).to.have.property('user'); From 3e73c11e1285aa40588c21e616602f853e4a4dab Mon Sep 17 00:00:00 2001 From: Newton <5769156+iamnewton@users.noreply.github.com> Date: Mon, 21 Dec 2020 02:00:15 -0800 Subject: [PATCH 046/181] ID Library feat: turn off fullscan by default (#6140) * chore: lint HTML example page * chore: add space to logging statement for clarity * chore: update package lock * feat: turn off fullscan by default * chore: format the HTML for readibility * chore: fix test * Revert "chore: update package lock" This reverts commit 4faf19489677de3c278b625535f4ba01877ccacb. --- integrationExamples/gpt/userId_example.html | 589 ++++++++++---------- modules/idLibrary.js | 4 +- modules/idLibrary.md | 38 +- test/spec/modules/idLibrary_spec.js | 6 +- 4 files changed, 318 insertions(+), 319 deletions(-) diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index 7375293fdf0..e6f9255326a 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -1,334 +1,335 @@ + - - + + + ] + } + ]; - + - var adUnits = [ - { - code: 'test-div', - mediaTypes: { - banner: { - sizes: [[300,250],[300,600],[728,90]] - } - }, - bids: [ - { - bidder: 'rubicon', - params: { - accountId: '1001', - siteId: '113932', - zoneId: '535510' - } - } - ] - } - ]; + - + pbjs.que.push(function() { + pbjs.setConfig({ + debug: true, + consentManagement: { + cmpApi: 'iab', + timeout: 1000, + defaultGdprScope: true + }, + // consentManagement: { + // cmpApi: 'static', + // consentData: { + // consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA' + // vendorData: { + // purposeConsents: { + // '1': true + // } + // } + // } + // }, + userSync: { + userIds: [{ + name: "pubProvidedId", + params: { + eids: [{ + source: "domain.com", + uids:[{ + id: "value read from cookie or local storage", + atype: 1, + ext: { + stype: "ppuid" // allowable options are sha256email, DMP, ppuid for now + } + }] + },{ + source: "3rdpartyprovided.com", + uids:[{ + id: "value read from cookie or local storage", + atype: 3, + ext: { + stype: "sha256email" + } + }] + }], + eidsFunction: getHashedEmail // any user defined function that exists in the page + } + },{ + name: "unifiedId", + params: { + partner: "prebid", + url: "http://match.adsrvr.org/track/rid?ttd_pid=prebid&fmt=json" + }, + storage: { + type: "html5", + name: "unifiedid", + expires: 30 + }, + },{ + name: "intentIqId", + params: { + partner: 0, //Set your real IntentIQ partner ID here for production. + }, + storage: { + type: "cookie", + name: "intentIqId", + expires: 30 + }, + }, + { + name: "id5Id", + params: { + partner: 173 //Set your real ID5 partner ID here for production, please ask for one at http://id5.io/prebid + }, + storage: { + type: "cookie", + name: "id5id", + expires: 90, + refreshInSeconds: 8*3600 // Refresh frequency of cookies, defaulting to 'expires' + }, - + function sendAdserverRequest() { + if (pbjs.adserverRequestSent) return; + pbjs.adserverRequestSent = true; + googletag.cmd.push(function() { + pbjs.que.push(function() { + pbjs.setTargetingForGPTAsync(); + googletag.pubads().refresh(); + }); + }); + } - + setTimeout(function() { + sendAdserverRequest(); + }, FAILSAFE_TIMEOUT); + - - + - -

Rubicon Project Prebid

+ + -
- -
- + +

Rubicon Project Prebid

+
+ +
+ diff --git a/modules/idLibrary.js b/modules/idLibrary.js index ba3cc0b5efb..988f3417b30 100644 --- a/modules/idLibrary.js +++ b/modules/idLibrary.js @@ -8,7 +8,7 @@ let email; let conf; const LOG_PRE_FIX = 'ID-Library: '; const CONF_DEFAULT_OBSERVER_DEBOUNCE_MS = 250; -const CONF_DEFAULT_FULL_BODY_SCAN = true; +const CONF_DEFAULT_FULL_BODY_SCAN = false; const OBSERVER_CONFIG = { subtree: true, attributes: true, @@ -38,7 +38,7 @@ function getEmail(value) { if (!matched) { return null; } - logInfo('Email found' + matched[0]); + logInfo('Email found: ' + matched[0]); return matched[0]; } diff --git a/modules/idLibrary.md b/modules/idLibrary.md index 69b63dc466b..28d40c389f3 100644 --- a/modules/idLibrary.md +++ b/modules/idLibrary.md @@ -1,24 +1,22 @@ -## ID Library Configuration Example +# ID Library +## Configuration Options -|Param |Required |Description | -|----------------|-------------------------------|-----------------------------| -|url |Yes | The url endpoint is used to post the hashed email and user ids. | -|target |No |It should contain the element id from which the email can be read. | -|debounce |No | Time in milliseconds before the email and ids are fetched | -|fullscan |No | Option to enable/disable full body scan to get email. By default the full body scan is enabled. | +| Parameter | Required | Type | Default | Description | +| :--------- | :------- | :------ | :------ | :---------- | +| `target` | Yes | String | N/A | ID attribute of the element from which the email can be read. | +| `url` | Yes | String | N/A | URL endpoint used to post the hashed email and user IDs. | +| `debounce` | No | Number | 250 | Time in milliseconds before the email and IDs are fetched. | +| `fullscan` | No | Boolean | false | Enable/disable a full page body scan to get email. | -### Example -``` - pbjs.setConfig({ - idLibrary:{ - url: , - debounce: 250, - target: 'username', - fullscan: false - }, - }); -``` +## Example - -``` +```javascript +pbjs.setConfig({ + idLibrary: { + target: 'username', + url: 'https://example.com', + debounce: 250, + fullscan: false, + }, +}); diff --git a/test/spec/modules/idLibrary_spec.js b/test/spec/modules/idLibrary_spec.js index da61850f29b..682c2df1e44 100644 --- a/test/spec/modules/idLibrary_spec.js +++ b/test/spec/modules/idLibrary_spec.js @@ -50,12 +50,12 @@ describe('currency', function () { it('results with config default fullscan ', function () { let config = { 'url': 'URL' } idlibrary.setConfig(config); - expect(config.fullscan).to.be.equal(true); + expect(config.fullscan).to.be.equal(false); }); it('results with config fullscan ', function () { - let config = { 'url': 'URL', 'fullscan': false } + let config = { 'url': 'URL', 'fullscan': true } idlibrary.setConfig(config); - expect(config.fullscan).to.be.equal(false); + expect(config.fullscan).to.be.equal(true); }); }); }); From 2f055063ccca4d08c2cb5e8fe13abadd8346ba4c Mon Sep 17 00:00:00 2001 From: Scott Menzer Date: Mon, 21 Dec 2020 11:01:50 +0100 Subject: [PATCH 047/181] update release process for notes on release drafter checks (#6137) * update release process for notes on release drafter checks * update section titles --- RELEASE_SCHEDULE.md | 83 ++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/RELEASE_SCHEDULE.md b/RELEASE_SCHEDULE.md index 7b2c6244bd7..bfbd0772c3e 100644 --- a/RELEASE_SCHEDULE.md +++ b/RELEASE_SCHEDULE.md @@ -1,6 +1,14 @@ **Table of Contents** - [Release Schedule](#release-schedule) - [Release Process](#release-process) + - [1. Make sure that all PRs have been named and labeled properly per the PR Process](#1-make-sure-that-all-prs-have-been-named-and-labeled-properly-per-the-pr-process) + - [2. Make sure all browserstack tests are passing](#2-make-sure-all-browserstack-tests-are-passing) + - [3. Prepare Prebid Code](#3-prepare-prebid-code) + - [4. Verify the Release](#4-verify-the-release) + - [5. Create a GitHub release](#5-create-a-github-release) + - [6. Update coveralls _(skip for legacy)_](#6-update-coveralls-skip-for-legacy) + - [7. Distribute the code](#7-distribute-the-code) + - [8. Increment Version for Next Release](#8-increment-version-for-next-release) - [Beta Releases](#beta-releases) - [FAQs](#faqs) @@ -9,7 +17,7 @@ We aim to push a new release of Prebid.js every week on Tuesday. While the releases will be available immediately for those using direct Git access, -it will be about a week before the Prebid Org [Download Page](http://prebid.org/download.html) will be updated. +it will be about a week before the Prebid Org [Download Page](http://prebid.org/download.html) will be updated. You can determine what is in a given build using the [releases page](https://github.com/prebid/Prebid.js/releases) @@ -19,14 +27,20 @@ Announcements regarding releases will be made to the #headerbidding-dev channel _Note: If `github.com/prebid/Prebid.js` is not configured as the git origin for your repo, all of the following git commands will have to be modified to reference the proper remote (e.g. `upstream`)_ -1. Make Sure all browserstack tests are passing. On PR merge to master CircleCI will run unit tests on browserstack. Checking the last CircleCI build [here](https://circleci.com/gh/prebid/Prebid.js) for master branch will show you detailed results. - - In case of failure do following, +### 1. Make sure that all PRs have been named and labeled properly per the [PR Process](https://github.com/prebid/Prebid.js/blob/master/PR_REVIEW.md#general-pr-review-process) + * Do this by checking the latest draft release from the [releases page](https://github.com/prebid/Prebid.js/releases) and make sure nothing appears in the first section called "In This Release". If they do, please open the PRs and add the appropriate labels. + * Do a quick check that all the titles/descriptions look ok, and if not, adjust the PR title. + +### 2. Make sure all browserstack tests are passing + + On PR merge to master, CircleCI will run unit tests on browserstack. Checking the last CircleCI build [here](https://circleci.com/gh/prebid/Prebid.js) for master branch will show you detailed results.** + + In case of failure do following, - Try to fix the failing tests. - If you are not able to fix tests in time. Skip the test, create issue and tag contributor. - #### How to run tests in browserstack - + **How to run tests in browserstack** + _Note: the following browserstack information is only relevant for debugging purposes, if you will not be debugging then it can be skipped._ Set the environment variables. You may want to add these to your `~/.bashrc` for convenience. @@ -35,40 +49,40 @@ _Note: If `github.com/prebid/Prebid.js` is not configured as the git origin for export BROWSERSTACK_USERNAME="my browserstack username" export BROWSERSTACK_ACCESS_KEY="my browserstack access key" ``` - + ``` gulp test --browserstack >> prebid_test.log - + vim prebid_test.log // Will show the test results ``` -2. Prepare Prebid Code +### 3. Prepare Prebid Code Update the package.json version to become the current release. Then commit your changes. ``` - git commit -m "Prebid 1.x.x Release" + git commit -m "Prebid 4.x.x Release" git push ``` -3. Verify Release +### 4. Verify the Release Make sure your there are no more merges to master branch. Prebid code is clean and up to date. -4. Create a GitHub release +### 5. Create a GitHub release + + Edit the most recent [release notes](https://github.com/prebid/Prebid.js/releases) draft and make sure the correct version is set and the master branch is selected in the dropdown. Click `Publish release`. GitHub will create release tag. - Edit the most recent [release notes](https://github.com/prebid/Prebid.js/releases) draft and make sure the correct tag is in the dropdown. Click `Publish`. GitHub will create release tag. - - Pull these changes locally by running command + Pull these changes locally by running command ``` git pull git fetch --tags - ``` - + ``` + and verify the tag. -5. Update coveralls _(skip for legacy)_ +### 6. Update coveralls _(skip for legacy)_ We use https://coveralls.io/ to show parts of code covered by unit tests. @@ -80,35 +94,23 @@ _Note: If `github.com/prebid/Prebid.js` is not configured as the git origin for Run `gulp coveralls` to update code coverage history. -6. Distribute the code +### 7. Distribute the code - _Note: do not go to step 7 until step 6 has been verified completed._ + _Note: do not go to step 8 until step 7 has been verified completed._ Reach out to any of the Appnexus folks to trigger the jenkins job. - // TODO + // TODO: Jenkins job is moving files to appnexus cdn, pushing prebid.js to npm, purging cache and sending notification to slack. Move all the files from Appnexus CDN to jsDelivr and create bash script to do above tasks. -7. Post Release Version - - Update the version - Manually edit Prebid's package.json to become "1.x.x-pre" (using the values for the next release). Then commit your changes. +### 8. Increment Version for Next Release + + Update the version by manually editing Prebid's `package.json` to become "4.x.x-pre" (using the values for the next release). Then commit your changes. ``` git commit -m "Increment pre version" git push ``` - -8. Create new release draft - - Go to [github releases](https://github.com/prebid/Prebid.js/releases) and add a new draft for the next version of Prebid.js with the following template: -``` -## 🚀New Features - -## 🛠Maintenance - -## 🐛Bug Fixes -``` ## Beta Releases @@ -129,11 +131,8 @@ Characteristics of a `GA` release: ## FAQs **1. Is there flexibility in the schedule?** - -If a major bug is found in the current release, a maintenance patch will be done as soon as possible. - -It is unlikely that we will put out a maintenance patch at the request of a given bid adapter or module owner. +* If a major bug is found in the current release, a maintenance patch will be done as soon as possible. +* It is unlikely that we will put out a maintenance patch at the request of a given bid adapter or module owner. **2. What Pull Requests make it into a release?** - -Every PR that's merged into master will be part of a release. Here are the [PR review guidelines](https://github.com/prebid/Prebid.js/blob/master/PR_REVIEW.md). +* Every PR that's merged into master will be part of a release. Here are the [PR review guidelines](https://github.com/prebid/Prebid.js/blob/master/PR_REVIEW.md). From 5d2ccb2fdd52c7bffa1b75b01afcd2af94036da6 Mon Sep 17 00:00:00 2001 From: wojciech-bialy-wpm <67895844+wojciech-bialy-wpm@users.noreply.github.com> Date: Mon, 21 Dec 2020 22:56:52 +0100 Subject: [PATCH 048/181] Sspbc Bid Adapter: multiple updates (#6118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add sspbc adapter * tests for sspbc adapter * sspBC adapter v4.5: set correct creativeId, add adomain to bid.meta, set test mode in adexchange, read site SN from bid response * sspBC adapter v4.5: set meta.advertiserDomains, update test to expect bid.meta * sspBC Adapter: add ajax tests (test ad with & without gdpr) * sspBC Adapter: remove ajax tests * Update adapter to v4.6 Update adapter to v4.6 - add notification endpoint - send bidWon and onTimeout notifications - send CMP version to user sync endpoint * Remove console logs for notification events * Change payload data in onTimeout event * Update tests for sspBC adapter Update tests for sspBC adapter: - add onBidWon test - add onTimeout test - alter getUserSyncs test * Update sspBC adapter to v4.7; enable oneCodeId mode; change module name to ensure combatibility with prebid.org downloader * sspBc adapter: Bug fixes in v4.7 - change notification format, fix oneCode detection data, convert slot number to int Co-authored-by: Wojciech Biały --- .../{sspBCAdapter.js => sspBCBidAdapter.js} | 140 +++++++++++------- .../{sspBCAdapter.md => sspBCBidAdapter.md} | 16 +- ...dapter_spec.js => sspBCBidAdapter_spec.js} | 109 ++++++++++++-- 3 files changed, 188 insertions(+), 77 deletions(-) rename modules/{sspBCAdapter.js => sspBCBidAdapter.js} (72%) rename modules/{sspBCAdapter.md => sspBCBidAdapter.md} (68%) rename test/spec/modules/{sspBCAdapter_spec.js => sspBCBidAdapter_spec.js} (78%) diff --git a/modules/sspBCAdapter.js b/modules/sspBCBidAdapter.js similarity index 72% rename from modules/sspBCAdapter.js rename to modules/sspBCBidAdapter.js index 4069c722e9d..1f250992fee 100644 --- a/modules/sspBCAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -8,11 +8,58 @@ const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; const NOTIFY_URL = 'https://ssp.wp.pl/bidder/notify'; const TMAX = 450; -const BIDDER_VERSION = '4.6'; +const BIDDER_VERSION = '4.7'; const W = window; const { navigator } = W; +const oneCodeDetection = {}; var consentApiVersion; +/** + * Get bid parameters for notification + * @param {*} bidData - bid (bidWon), or array of bids (timeout) + */ +const getNotificationPayload = bidData => { + if (bidData) { + const bids = utils.isArray(bidData) ? bidData : [bidData]; + if (bids.length > 0) { + const result = { + requestId: undefined, + siteId: [], + adUnit: [], + slotId: [], + } + bids.forEach(bid => { + let params = utils.isArray(bid.params) ? bid.params[0] : bid.params; + params = params || {}; + + // check for stored detection + if (oneCodeDetection[bid.requestId]) { + params.siteId = oneCodeDetection[bid.requestId][0]; + params.id = oneCodeDetection[bid.requestId][1]; + } + + if (params.siteId) { + result.siteId.push(params.siteId); + } + if (params.id) { + result.slotId.push(params.id); + } + if (bid.cpm) { + const meta = bid.meta || {}; + result.cpm = bid.cpm; + result.creativeId = bid.creativeId; + result.adomain = meta.advertiserDomains && meta.advertiserDomains[0]; + result.networkName = meta.networkName; + } + result.adUnit.push(bid.adUnitCode) + result.requestId = bid.auctionId || result.requestId; + result.timeout = bid.timeout || result.timeout; + }) + return result; + } + } +} + const cookieSupport = () => { const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); const useCookies = navigator.cookieEnabled || !!document.cookie.length; @@ -104,13 +151,13 @@ function mapBanner(slot) { function mapImpression(slot) { const imp = { - id: slot.params.id, + id: (slot.params && slot.params.id) ? slot.params.id : 'bidid-' + slot.bidId, banner: mapBanner(slot), /* native: mapNative(slot), */ - tagid: slot.params.id, + tagid: slot.adUnitCode, }; - const bidfloor = parseFloat(slot.params.bidfloor); + const bidfloor = (slot.params && slot.params.bidFloor) ? parseFloat(slot.params.bidFloor) : undefined; if (bidfloor) { imp.bidfloor = bidfloor; @@ -170,6 +217,7 @@ function renderCreative(site, auctionId, bid, seat, request) { ` + ad: `` } if (isVideo(bidRequest, adType)) { @@ -124,8 +124,7 @@ export const spec = { bidResponse.height = playersize[1] } bidResponse.mediaType = VIDEO - bidResponse.vastUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/${customsize[0]}x${customsize[1]}?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}` - + bidResponse.vastUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}` if (isOutstream(bidRequest)) { const renderer = Renderer.install({ id: bidRequest.bidId, diff --git a/modules/yieldlabBidAdapter.md b/modules/yieldlabBidAdapter.md index 37897b83f12..a7a3f2715dc 100644 --- a/modules/yieldlabBidAdapter.md +++ b/modules/yieldlabBidAdapter.md @@ -21,7 +21,6 @@ Module that connects to Yieldlab's demand sources params: { adslotId: "5220336", supplyId: "1381604", - adSize: "728x90", targeting: { key1: "value1", key2: "value2" @@ -41,8 +40,7 @@ Module that connects to Yieldlab's demand sources bidder: "yieldlab", params: { adslotId: "5220339", - supplyId: "1381604", - adSize: "640x480" + supplyId: "1381604" } }] } diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index cd2c46a5664..72f70ca0208 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -7,7 +7,6 @@ const REQUEST = { 'params': { 'adslotId': '1111', 'supplyId': '2222', - 'adSize': '728x90', 'targeting': { 'key1': 'value1', 'key2': 'value2', @@ -57,6 +56,7 @@ const RESPONSE = { id: 1111, price: 1, pid: 2222, + adsize: '728x90', adtype: 'BANNER' } @@ -88,8 +88,7 @@ describe('yieldlabBidAdapter', function () { const request = { 'params': { 'adslotId': '1111', - 'supplyId': '2222', - 'adSize': '728x90' + 'supplyId': '2222' } } expect(spec.isBidRequestValid(request)).to.equal(true) @@ -180,7 +179,7 @@ describe('yieldlabBidAdapter', function () { expect(result[0].netRevenue).to.equal(false) expect(result[0].ttl).to.equal(300) expect(result[0].referrer).to.equal('') - expect(result[0].ad).to.include(' @@ -126,14 +128,9 @@ export const spec = { * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function(serverResponse) { - const serverBody = serverResponse.body; - if (serverBody && utils.isArray(serverBody)) { - return utils._map(serverBody, function(bid) { - return buildBid(bid); - }); - } else { - return []; - } + const bids = serverResponse.body && serverResponse.body.bids; + + return Array.isArray(bids) ? bids.map(bid => buildBid(bid)) : [] } } diff --git a/modules/astraoneBidAdapter.md b/modules/astraoneBidAdapter.md index a7eaeeef5a4..e090cfe1e54 100644 --- a/modules/astraoneBidAdapter.md +++ b/modules/astraoneBidAdapter.md @@ -18,17 +18,17 @@ About us: https://astraone.io var adUnits = [{ code: 'test-div', mediaTypes: { - banner: { - sizes: [1, 1] - } + banner: { + sizes: [1, 1], + } }, bids: [{ - bidder: "astraone", - params: { - placement: "inImage", - placeId: "5af45ad34d506ee7acad0c26", - imageUrl: "https://creative.astraone.io/files/default_image-1-600x400.jpg" - } + bidder: "astraone", + params: { + placement: "inImage", + placeId: "5f477bf94d506ebe2c4240f3", + imageUrl: "https://creative.astraone.io/files/default_image-1-600x400.jpg" + } }] }]; ``` @@ -39,66 +39,69 @@ var adUnits = [{ - - Prebid.js Banner Example - - + + }, + bids: [{ + bidder: "astraone", + params: { + placement: "inImage", + placeId: "5f477bf94d506ebe2c4240f3", + imageUrl: "https://creative.astraone.io/files/default_image-1-600x400.jpg" + } + }] + }]; + + var pbjs = pbjs || {}; + pbjs.que = pbjs.que || []; + + pbjs.que.push(function() { + pbjs.addAdUnits(adUnits); + pbjs.requestBids({ + bidsBackHandler: function (e) { + if (pbjs.adserverRequestSent) return; + pbjs.adserverRequestSent = true; + var params = pbjs.getAdserverTargetingForAdUnitCode("test-div"); + var iframe = document.getElementById('test-div'); + + if (params && params['hb_adid']) { + iframe.parentElement.style.position = "relative"; + iframe.style.display = "block"; + pbjs.renderAd(iframe.contentDocument, params['hb_adid']); + } + } + }); + }); + -

Prebid.js InImage Banner Test

+

Prebid.js InImage Banner Test

-
- - -
+
+ + +
@@ -109,90 +112,91 @@ var adUnits = [{ - - Prebid.js Banner Example - - - - + + Prebid.js Banner gpt Example + + + + -

Prebid.js Banner Ad Unit Test

+

Prebid.js InImage Banner gpt Test

-
- +
+ - -
+ +
``` diff --git a/test/spec/modules/astraoneBidAdapter_spec.js b/test/spec/modules/astraoneBidAdapter_spec.js index e422f64b570..0e545081869 100644 --- a/test/spec/modules/astraoneBidAdapter_spec.js +++ b/test/spec/modules/astraoneBidAdapter_spec.js @@ -14,7 +14,7 @@ function getSlotConfigs(mediaTypes, params) { describe('AstraOne Adapter', function() { describe('isBidRequestValid method', function() { - const PLACE_ID = '5af45ad34d506ee7acad0c26'; + const PLACE_ID = '5f477bf94d506ebe2c4240f3'; const IMAGE_URL = 'https://creative.astraone.io/files/default_image-1-600x400.jpg'; describe('returns true', function() { @@ -176,21 +176,23 @@ describe('AstraOne Adapter', function() { describe('the bid is a banner', function() { it('should return a banner bid', function() { const serverResponse = { - body: [ - { - bidId: '2df8c0733f284e', - price: 0.5, - currency: 'USD', - content: { - content: 'html', - actionUrls: {}, - seanceId: '123123' - }, - width: 100, - height: 100, - ttl: 360 - } - ] + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: { + content: 'html', + actionUrls: {}, + seanceId: '123123' + }, + width: 100, + height: 100, + ttl: 360 + } + ] + } } const bids = spec.interpretResponse(serverResponse) expect(bids.length).to.equal(1) From b7ec359ca242ceab4cd0c7e37307247974126e51 Mon Sep 17 00:00:00 2001 From: Steve Alliance Date: Thu, 28 Jan 2021 03:14:49 -0500 Subject: [PATCH 120/181] update banner ttl (#6228) --- modules/districtmDMXBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/districtmDMXBidAdapter.js b/modules/districtmDMXBidAdapter.js index b5969d4ed1e..f01c3c2ce9f 100644 --- a/modules/districtmDMXBidAdapter.js +++ b/modules/districtmDMXBidAdapter.js @@ -39,7 +39,7 @@ export const spec = { nBid.requestId = nBid.impid; nBid.width = nBid.w || width; nBid.height = nBid.h || height; - nBid.ttl = 360; + nBid.ttl = 300; nBid.mediaType = bid.mediaTypes && bid.mediaTypes.video ? 'video' : 'banner'; if (nBid.mediaType) { nBid.vastXml = cleanVast(nBid.adm, nBid.nurl); From 4f2af660066308f166b10da2c11220205f45ab3b Mon Sep 17 00:00:00 2001 From: bretg Date: Thu, 28 Jan 2021 09:31:36 -0500 Subject: [PATCH 121/181] browsi: updating test parameters (#6048) --- modules/browsiRtdProvider.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/browsiRtdProvider.md b/modules/browsiRtdProvider.md index 0dd8c1d7609..9eed4b2b2d4 100644 --- a/modules/browsiRtdProvider.md +++ b/modules/browsiRtdProvider.md @@ -19,9 +19,9 @@ Configuration example for using RTD module with `browsi` provider "name": "browsi", "waitForIt": "true" "params": { - "url": "testUrl.com", - "siteKey": "testKey", - "pubKey": "testPub", + "url": "yield-manager.browsiprod.com", + "siteKey": "browsidemo", + "pubKey": "browsidemo" "keyName":"bv" } }] From 5f56b1875bb3072fee3645f7c582bfbb3d4b980a Mon Sep 17 00:00:00 2001 From: Amanda Dillon <41923726+agdillon@users.noreply.github.com> Date: Thu, 28 Jan 2021 08:23:51 -0700 Subject: [PATCH 122/181] SpotX Bid Adapter: default to 4/3 aspect ratio when response doesn't contain w or h (#6159) * Default to 4/3 aspect ratio when response doesn't contain w or h * SpotX bid adapter: reorder tests and remove extra assertions --- modules/spotxBidAdapter.js | 46 +++++++++++------------ test/spec/modules/spotxBidAdapter_spec.js | 43 +++++++++++++++++++-- 2 files changed, 61 insertions(+), 28 deletions(-) diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index 5ed3884e977..c85a836435e 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -51,7 +51,7 @@ export const spec = { return false; } if (!utils.getBidIdParameter('slot', bid.params.outstream_options)) { - utils.logError(BIDDER_CODE + ': please define parameters slot outstream_options object in the configuration.'); + utils.logError(BIDDER_CODE + ': please define parameter slot in outstream_options object in the configuration.'); return false; } } @@ -382,7 +382,7 @@ export const spec = { } }); } catch (err) { - utils.logWarn('Prebid Error calling setRender or setEve,tHandlers on renderer', err); + utils.logWarn('Prebid Error calling setRender or setEventHandlers on renderer', err); } bid.renderer = renderer; } @@ -408,7 +408,7 @@ function createOutstreamScript(bid) { dataSpotXParams['data-spotx_content_page_url'] = bid.renderer.config.content_page_url; dataSpotXParams['data-spotx_ad_unit'] = 'incontent'; - utils.logMessage('[SPOTX][renderer] Default beahavior'); + utils.logMessage('[SPOTX][renderer] Default behavior'); if (utils.getBidIdParameter('ad_mute', bid.renderer.config.outstream_options)) { dataSpotXParams['data-spotx_ad_mute'] = '1'; } @@ -419,30 +419,26 @@ function createOutstreamScript(bid) { const playersizeAutoAdapt = utils.getBidIdParameter('playersize_auto_adapt', bid.renderer.config.outstream_options); if (playersizeAutoAdapt && utils.isBoolean(playersizeAutoAdapt) && playersizeAutoAdapt === true) { - if (bid.width && utils.isNumber(bid.width) && bid.height && utils.isNumber(bid.height)) { - const ratio = bid.width / bid.height; - const slotClientWidth = window.document.getElementById(slot).clientWidth; - let playerWidth = bid.renderer.config.player_width; - let playerHeight = bid.renderer.config.player_height; - let contentWidth = 0; - let contentHeight = 0; - if (slotClientWidth < playerWidth) { - playerWidth = slotClientWidth; - playerHeight = playerWidth / ratio; - } - if (ratio <= 1) { - contentWidth = Math.round(playerHeight * ratio); - contentHeight = playerHeight; - } else { - contentWidth = playerWidth; - contentHeight = Math.round(playerWidth / ratio); - } - - dataSpotXParams['data-spotx_content_width'] = '' + contentWidth; - dataSpotXParams['data-spotx_content_height'] = '' + contentHeight; + const ratio = bid.width && utils.isNumber(bid.width) && bid.height && utils.isNumber(bid.height) ? bid.width / bid.height : 4 / 3; + const slotClientWidth = window.document.getElementById(slot).clientWidth; + let playerWidth = bid.renderer.config.player_width; + let playerHeight = bid.renderer.config.player_height; + let contentWidth = 0; + let contentHeight = 0; + if (slotClientWidth < playerWidth) { + playerWidth = slotClientWidth; + playerHeight = playerWidth / ratio; + } + if (ratio <= 1) { + contentWidth = Math.round(playerHeight * ratio); + contentHeight = playerHeight; } else { - utils.logWarn('[SPOTX][renderer] PlayerSize auto adapt: bid.width and bid.height are incorrect'); + contentWidth = playerWidth; + contentHeight = Math.round(playerWidth / ratio); } + + dataSpotXParams['data-spotx_content_width'] = '' + contentWidth; + dataSpotXParams['data-spotx_content_height'] = '' + contentHeight; } const customOverride = utils.getBidIdParameter('custom_override', bid.renderer.config.outstream_options); diff --git a/test/spec/modules/spotxBidAdapter_spec.js b/test/spec/modules/spotxBidAdapter_spec.js index fb5ce63f543..927599ac986 100644 --- a/test/spec/modules/spotxBidAdapter_spec.js +++ b/test/spec/modules/spotxBidAdapter_spec.js @@ -488,7 +488,7 @@ describe('the spotx adapter', function () { }); }); - describe('oustreamRender', function() { + describe('outstreamRender', function() { var serverResponse, bidderRequestObj; beforeEach(function() { @@ -545,7 +545,7 @@ describe('the spotx adapter', function () { it('should attempt to insert the EASI script', function() { var scriptTag; sinon.stub(window.document, 'getElementById').returns({ - appendChild: sinon.stub().callsFake(function(script) { scriptTag = script }) + appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) }); var responses = spec.interpretResponse(serverResponse, bidderRequestObj); @@ -573,7 +573,7 @@ describe('the spotx adapter', function () { nodeName: 'IFRAME', contentDocument: { body: { - appendChild: sinon.stub().callsFake(function(script) { scriptTag = script }) + appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) } } }); @@ -598,5 +598,42 @@ describe('the spotx adapter', function () { expect(scriptTag.getAttribute('data-spotx_content_height')).to.equal('300'); window.document.getElementById.restore(); }); + + it('should adjust width and height to match slot clientWidth if playersize_auto_adapt is used', function() { + var scriptTag; + sinon.stub(window.document, 'getElementById').returns({ + clientWidth: 200, + appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) + }); + var responses = spec.interpretResponse(serverResponse, bidderRequestObj); + + responses[0].renderer.render(responses[0]); + + expect(scriptTag.getAttribute('type')).to.equal('text/javascript'); + expect(scriptTag.getAttribute('src')).to.equal('https://js.spotx.tv/easi/v1/12345.js'); + expect(scriptTag.getAttribute('data-spotx_content_width')).to.equal('200'); + expect(scriptTag.getAttribute('data-spotx_content_height')).to.equal('150'); + window.document.getElementById.restore(); + }); + + it('should use a default 4/3 ratio if playersize_auto_adapt is used and response does not contain width or height', function() { + delete serverResponse.body.seatbid[0].bid[0].w; + delete serverResponse.body.seatbid[0].bid[0].h; + + var scriptTag; + sinon.stub(window.document, 'getElementById').returns({ + clientWidth: 200, + appendChild: sinon.stub().callsFake(function(script) { scriptTag = script; }) + }); + var responses = spec.interpretResponse(serverResponse, bidderRequestObj); + + responses[0].renderer.render(responses[0]); + + expect(scriptTag.getAttribute('type')).to.equal('text/javascript'); + expect(scriptTag.getAttribute('src')).to.equal('https://js.spotx.tv/easi/v1/12345.js'); + expect(scriptTag.getAttribute('data-spotx_content_width')).to.equal('200'); + expect(scriptTag.getAttribute('data-spotx_content_height')).to.equal('150'); + window.document.getElementById.restore(); + }); }); }); From a926dee9e108ca4b8792ba8992a9bca7c2f42781 Mon Sep 17 00:00:00 2001 From: YerkovichM <48519843+YerkovichM@users.noreply.github.com> Date: Thu, 28 Jan 2021 18:22:54 +0200 Subject: [PATCH 123/181] Extended ID permissions supported by bidder (#6112) * User id bidder permission scheme * styling * prebidServer support * - * fix * prebidServerBidAdapter take eidPermissions directly from userId module * - * unit tests * - * - * update * - * - * changed pbjs_bidders to pbjsBidders * changed pbjsBidders to bidders ext.prebid.data.eidPermissions to ext.prebid.data.eidpermissions * rerun circleci * rerun circleci * omitting eidPermission entry if 'bidders' is not configured Co-authored-by: myerkovich Co-authored-by: Marko Yerkovich --- integrationExamples/gpt/userId_example.html | 1 + modules/prebidServerBidAdapter/index.js | 17 ++- modules/userId/eids.js | 22 ++++ modules/userId/index.js | 47 +++++-- plugins/eslint/validateImports.js | 3 +- .../modules/prebidServerBidAdapter_spec.js | 46 ++++--- test/spec/modules/userId_spec.js | 117 +++++++++++++++++- 7 files changed, 212 insertions(+), 41 deletions(-) diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index fa9b1e3b5fd..71299a4a6e1 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -236,6 +236,7 @@ }, { name: "sharedId", + // bidders: ["rubicon", "sampleBidders"], // to allow this ID for specific bidders params: { syncTime: 60 // in seconds, default is 24 hours }, diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 8fb512ff7e2..7274912efb5 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -12,6 +12,7 @@ import includes from 'core-js-pure/features/array/includes.js'; import { S2S_VENDORS } from './config.js'; import { ajax } from '../../src/ajax.js'; import find from 'core-js-pure/features/array/find.js'; +import { getEidPermissions } from '../userId/index.js'; const getConfig = config.getConfig; @@ -455,7 +456,7 @@ export function resetWurlMap() { } const OPEN_RTB_PROTOCOL = { - buildRequest(s2sBidRequest, bidRequests, adUnits, s2sConfig) { + buildRequest(s2sBidRequest, bidRequests, adUnits, s2sConfig, requestedBidders) { let imps = []; let aliases = {}; const firstBidRequest = bidRequests[0]; @@ -700,6 +701,18 @@ const OPEN_RTB_PROTOCOL = { utils.deepSetValue(request, 'user.ext.eids', bidUserIdAsEids); } + const eidPermissions = getEidPermissions(); + if (utils.isArray(eidPermissions) && eidPermissions.length > 0) { + if (requestedBidders && utils.isArray(requestedBidders)) { + eidPermissions.forEach(i => { + if (i.bidders) { + i.bidders = i.bidders.filter(bidder => requestedBidders.includes(bidder)) + } + }); + } + utils.deepSetValue(request, 'ext.prebid.data.eidpermissions', eidPermissions); + } + if (bidRequests) { if (firstBidRequest.gdprConsent) { // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module @@ -958,7 +971,7 @@ export function PrebidServer() { queueSync(syncBidders, gdprConsent, uspConsent, s2sBidRequest.s2sConfig); } - const request = OPEN_RTB_PROTOCOL.buildRequest(s2sBidRequest, bidRequests, validAdUnits, s2sBidRequest.s2sConfig); + const request = OPEN_RTB_PROTOCOL.buildRequest(s2sBidRequest, bidRequests, validAdUnits, s2sBidRequest.s2sConfig, requestedBidders); const requestJson = request && JSON.stringify(request); if (request && requestJson) { ajax( diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 27665de4136..80750ccaae8 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -224,3 +224,25 @@ export function createEidsArray(bidRequestUserId) { } return eids; } + +/** + * @param {SubmoduleContainer[]} submodules + */ +export function buildEidPermissions(submodules) { + let eidPermissions = []; + submodules.filter(i => utils.isPlainObject(i.idObj) && Object.keys(i.idObj).length) + .forEach(i => { + Object.keys(i.idObj).forEach(key => { + if (utils.deepAccess(i, 'config.bidders') && Array.isArray(i.config.bidders) && + utils.deepAccess(USER_IDS_CONFIG, key + '.source')) { + eidPermissions.push( + { + source: USER_IDS_CONFIG[key].source, + bidders: i.config.bidders + } + ); + } + }); + }); + return eidPermissions; +} diff --git a/modules/userId/index.js b/modules/userId/index.js index 9294311de69..8f7d2a36699 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -136,7 +136,7 @@ import { getGlobal } from '../../src/prebidGlobal.js'; import { gdprDataHandler } from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; import { module, hook } from '../../src/hook.js'; -import { createEidsArray } from './eids.js'; +import { createEidsArray, buildEidPermissions } from './eids.js'; import { getCoreStorageManager } from '../../src/storageManager.js'; const MODULE_NAME = 'User ID'; @@ -214,6 +214,10 @@ export function setStoredValue(submodule, value) { } } +export function getEidPermissions() { + return buildEidPermissions(initializedSubmodules); +} + /** * @param {SubmoduleStorage} storage * @param {String|undefined} key optional key of the value @@ -433,6 +437,26 @@ function getCombinedSubmoduleIds(submodules) { return combinedSubmoduleIds; } +/** + * This function will create a combined object for bidder with allowed subModule Ids + * @param {SubmoduleContainer[]} submodules + * @param {string} bidder + */ +function getCombinedSubmoduleIdsForBidder(submodules, bidder) { + if (!Array.isArray(submodules) || !submodules.length || !bidder) { + return {}; + } + return submodules + .filter(i => !i.config.bidders || !utils.isArray(i.config.bidders) || i.config.bidders.includes(bidder)) + .filter(i => utils.isPlainObject(i.idObj) && Object.keys(i.idObj).length) + .reduce((carry, i) => { + Object.keys(i.idObj).forEach(key => { + carry[key] = i.idObj[key]; + }); + return carry; + }, {}); +} + /** * @param {AdUnit[]} adUnits * @param {SubmoduleContainer[]} submodules @@ -441,19 +465,18 @@ function addIdDataToAdUnitBids(adUnits, submodules) { if ([adUnits].some(i => !Array.isArray(i) || !i.length)) { return; } - const combinedSubmoduleIds = getCombinedSubmoduleIds(submodules); - const combinedSubmoduleIdsAsEids = createEidsArray(combinedSubmoduleIds); - if (Object.keys(combinedSubmoduleIds).length) { - adUnits.forEach(adUnit => { - if (adUnit.bids && utils.isArray(adUnit.bids)) { - adUnit.bids.forEach(bid => { + adUnits.forEach(adUnit => { + if (adUnit.bids && utils.isArray(adUnit.bids)) { + adUnit.bids.forEach(bid => { + const combinedSubmoduleIds = getCombinedSubmoduleIdsForBidder(submodules, bid.bidder); + if (Object.keys(combinedSubmoduleIds).length) { // create a User ID object on the bid, bid.userId = combinedSubmoduleIds; - bid.userIdAsEids = combinedSubmoduleIdsAsEids; - }); - } - }); - } + bid.userIdAsEids = createEidsArray(combinedSubmoduleIds); + } + }); + } + }); } /** diff --git a/plugins/eslint/validateImports.js b/plugins/eslint/validateImports.js index a39bf9b26d5..53f4ace8381 100644 --- a/plugins/eslint/validateImports.js +++ b/plugins/eslint/validateImports.js @@ -26,7 +26,8 @@ function flagErrors(context, node, importPath) { if ( path.dirname(absImportPath) === absModulePath || ( absImportPath.startsWith(absModulePath) && - path.basename(absImportPath) === 'index.js' + path.basename(absImportPath) === 'index.js' && + path.basename(absFileDir) !== 'prebidServerBidAdapter' ) ) { context.report(node, `import "${importPath}" cannot require module entry point`); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index f17cd3ab14f..4652bbb63f7 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -953,17 +953,15 @@ describe('S2S Adapter', function () { adapter.callBids(request, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.ext).to.deep.equal({ - prebid: { - aliases: { - brealtime: 'appnexus' - }, - auctiontimestamp: 1510852447530, - targeting: { - includebidderkeys: false, - includewinners: true - } + expect(requestBid.ext).to.haveOwnProperty('prebid'); + expect(requestBid.ext.prebid).to.deep.include({ + aliases: { + brealtime: 'appnexus' + }, + auctiontimestamp: 1510852447530, + targeting: { + includebidderkeys: false, + includewinners: true } }); }); @@ -985,17 +983,15 @@ describe('S2S Adapter', function () { adapter.callBids(request, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.ext).to.deep.equal({ - prebid: { - aliases: { - [alias]: 'appnexus' - }, - auctiontimestamp: 1510852447530, - targeting: { - includebidderkeys: false, - includewinners: true - } + expect(requestBid.ext).to.haveOwnProperty('prebid'); + expect(requestBid.ext.prebid).to.deep.include({ + aliases: { + [alias]: 'appnexus' + }, + auctiontimestamp: 1510852447530, + targeting: { + includebidderkeys: false, + includewinners: true } }); }); @@ -1376,7 +1372,7 @@ describe('S2S Adapter', function () { expect(requestBid).to.haveOwnProperty('ext'); expect(requestBid.ext).to.haveOwnProperty('prebid'); - expect(requestBid.ext.prebid).to.deep.equal({ + expect(requestBid.ext.prebid).to.deep.include({ auctiontimestamp: 1510852447530, foo: 'bar', targeting: { @@ -1410,7 +1406,7 @@ describe('S2S Adapter', function () { expect(requestBid).to.haveOwnProperty('ext'); expect(requestBid.ext).to.haveOwnProperty('prebid'); - expect(requestBid.ext.prebid).to.deep.equal({ + expect(requestBid.ext.prebid).to.deep.include({ auctiontimestamp: 1510852447530, targeting: { includewinners: false, @@ -1446,7 +1442,7 @@ describe('S2S Adapter', function () { expect(requestBid).to.haveOwnProperty('ext'); expect(requestBid.ext).to.haveOwnProperty('prebid'); - expect(requestBid.ext.prebid).to.deep.equal({ + expect(requestBid.ext.prebid).to.deep.include({ auctiontimestamp: 1510852447530, cache: { vastxml: 'vastxml-set-though-extPrebid.cache.vastXml' diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index ed592c0cba5..1ae023ae947 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -2,6 +2,7 @@ import { attachIdSystem, auctionDelay, coreStorage, + getEidPermissions, init, requestBidsHook, setStoredConsentData, @@ -70,7 +71,7 @@ describe('User ID', function () { code, mediaTypes: {banner: {}, native: {}}, sizes: [[300, 200], [300, 600]], - bids: [{bidder: 'sampleBidder', params: {placementId: 'banner-only-bidder'}}] + bids: [{bidder: 'sampleBidder', params: {placementId: 'banner-only-bidder'}}, {bidder: 'anotherSampleBidder', params: {placementId: 'banner-only-bidder'}}] }; } @@ -1196,6 +1197,120 @@ describe('User ID', function () { }, {adUnits}); }); + it('eidPermissions fun with bidders', function (done) { + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': 'test222', + 'ts': 1590525289611 + }), (new Date(Date.now() + 5000).toUTCString())); + + setSubmoduleRegistry([sharedIdSubmodule]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [ + { + name: 'sharedId', + bidders: [ + 'sampleBidder' + ], + storage: { + type: 'cookie', + name: 'sharedid', + expires: 28 + } + } + ] + } + }); + + requestBidsHook(function () { + const eidPermissions = getEidPermissions(); + expect(eidPermissions).to.deep.equal( + [ + {source: 'sharedid.org', bidders: ['sampleBidder']} + ] + ); + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + if (bid.bidder === 'sampleBidder') { + expect(bid).to.have.deep.nested.property('userId.sharedid'); + expect(bid.userId.sharedid.id).to.equal('test222'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'sharedid.org', + uids: [ + { + id: 'test222', + atype: 1, + ext: { + third: 'test222' + } + } + ] + }); + } + if (bid.bidder === 'anotherSampleBidder') { + expect(bid).to.not.have.deep.nested.property('userId.sharedid'); + expect(bid).to.not.have.property('userIdAsEids'); + } + }); + }); + coreStorage.setCookie('sharedid', '', EXPIRED_COOKIE_DATE); + done(); + }, {adUnits}); + }); + + it('eidPermissions fun without bidders', function (done) { + coreStorage.setCookie('sharedid', JSON.stringify({ + 'id': 'test222', + 'ts': 1590525289611 + }), (new Date(Date.now() + 5000).toUTCString())); + + setSubmoduleRegistry([sharedIdSubmodule]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [ + { + name: 'sharedId', + storage: { + type: 'cookie', + name: 'sharedid', + expires: 28 + } + } + ] + } + }); + + requestBidsHook(function () { + const eidPermissions = getEidPermissions(); + expect(eidPermissions).to.deep.equal( + [] + ); + adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid).to.have.deep.nested.property('userId.sharedid'); + expect(bid.userId.sharedid.id).to.equal('test222'); + expect(bid.userIdAsEids[0]).to.deep.equal({ + source: 'sharedid.org', + uids: [ + { + id: 'test222', + atype: 1, + ext: { + third: 'test222' + } + }] + }); + }); + }); + coreStorage.setCookie('sharedid', '', EXPIRED_COOKIE_DATE); + done(); + }, {adUnits}); + }); + it('test hook from pubProvidedId config params', function (done) { setSubmoduleRegistry([pubProvidedIdSubmodule]); init(config); From 524efda0ec579d82ee29848b77bd829aead76107 Mon Sep 17 00:00:00 2001 From: samuel-palmer-relevant-digital <77437973+samuel-palmer-relevant-digital@users.noreply.github.com> Date: Thu, 28 Jan 2021 17:27:43 +0100 Subject: [PATCH 124/181] Relevant Yield analytics adapter (#6195) --- modules/relevantAnalyticsAdapter.js | 33 ++++++++++++++ modules/relevantAnalyticsAdapter.md | 13 ++++++ .../modules/relevantAnalyticsAdapter_spec.js | 43 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 modules/relevantAnalyticsAdapter.js create mode 100644 modules/relevantAnalyticsAdapter.md create mode 100644 test/spec/modules/relevantAnalyticsAdapter_spec.js diff --git a/modules/relevantAnalyticsAdapter.js b/modules/relevantAnalyticsAdapter.js new file mode 100644 index 00000000000..5917262c810 --- /dev/null +++ b/modules/relevantAnalyticsAdapter.js @@ -0,0 +1,33 @@ +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; + +const relevantAnalytics = adapter({ analyticsType: 'bundle', handler: 'on' }); + +const { enableAnalytics: orgEnableAnalytics } = relevantAnalytics; + +Object.assign(relevantAnalytics, { + /** + * Save event in the global array that will be consumed later by the Relevant Yield library + */ + track: ({ eventType: ev, args }) => { + window.relevantDigital.pbEventLog.push({ ev, args, ts: new Date() }); + }, + + /** + * Before forwarding the call to the original enableAnalytics function - + * create (if needed) the global array that is used to pass events to the Relevant Yield library + * by the 'track' function above. + */ + enableAnalytics: function(...args) { + window.relevantDigital = window.relevantDigital || {}; + window.relevantDigital.pbEventLog = window.relevantDigital.pbEventLog || []; + return orgEnableAnalytics.call(this, ...args); + }, +}); + +adapterManager.registerAnalyticsAdapter({ + adapter: relevantAnalytics, + code: 'relevant', +}); + +export default relevantAnalytics; diff --git a/modules/relevantAnalyticsAdapter.md b/modules/relevantAnalyticsAdapter.md new file mode 100644 index 00000000000..e6383fa77e1 --- /dev/null +++ b/modules/relevantAnalyticsAdapter.md @@ -0,0 +1,13 @@ +# Overview + +Module Name: Relevant Yield Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: [support@relevant-digital.com](mailto:support@relevant-digital.com) + +# Description + +Analytics adapter to be used with [Relevant Yield](https://www.relevant-digital.com/relevantyield) + +Contact [sales@relevant-digital.com](mailto:sales@relevant-digital.com) for information. diff --git a/test/spec/modules/relevantAnalyticsAdapter_spec.js b/test/spec/modules/relevantAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..3e31db2d7dc --- /dev/null +++ b/test/spec/modules/relevantAnalyticsAdapter_spec.js @@ -0,0 +1,43 @@ +import relevantAnalytics from '../../../modules/relevantAnalyticsAdapter.js'; +import adapterManager from 'src/adapterManager'; +import events from 'src/events'; +import constants from 'src/constants.json' +import { expect } from 'chai'; + +describe('Relevant Analytics Adapter', () => { + beforeEach(() => { + adapterManager.enableAnalytics({ + provider: 'relevant' + }); + }); + + afterEach(() => { + relevantAnalytics.disableAnalytics(); + }); + + it('should pass all events to the global array', () => { + // Given + const testEvents = [ + { ev: constants.EVENTS.AUCTION_INIT, args: { test: 1 } }, + { ev: constants.EVENTS.BID_REQUESTED, args: { test: 2 } }, + ]; + + // When + testEvents.forEach(({ ev, args }) => ( + events.emit(ev, args) + )); + + // Then + const eventQueue = (window.relevantDigital || {}).pbEventLog; + expect(eventQueue).to.be.an('array'); + expect(eventQueue.length).to.be.at.least(testEvents.length); + + // The last events should be our test events + const myEvents = eventQueue.slice(-testEvents.length); + testEvents.forEach(({ ev, args }, idx) => { + const actualEvent = myEvents[idx]; + expect(actualEvent.ev).to.eql(ev); + expect(actualEvent.args).to.eql(args); + }); + }); +}); From 136ad4cee32bda212ceee67d92afe494ecbd16d8 Mon Sep 17 00:00:00 2001 From: pm-shashank-jain <40654031+pm-shashank-jain@users.noreply.github.com> Date: Thu, 28 Jan 2021 23:34:31 +0530 Subject: [PATCH 125/181] Pubmatic: fix issue where using an adUnit outstream renderer throws an error (#6152) --- modules/pubmaticBidAdapter.js | 4 ++-- modules/pubmaticBidAdapter.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index b5514ab0344..c70ecb4af27 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -861,8 +861,8 @@ export const spec = { utils.logError(`${LOG_WARN_PREFIX}: no context specified in bid. Rejecting bid: `, bid); return false; } - if (bid.mediaTypes[VIDEO].context === 'outstream' && !utils.isStr(bid.params.outstreamAU)) { - utils.logError(`${LOG_WARN_PREFIX}: for "outstream" bids outstreamAU is required. Rejecting bid: `, bid); + if (bid.mediaTypes[VIDEO].context === 'outstream' && !utils.isStr(bid.params.outstreamAU) && !bid.hasOwnProperty('renderer') && !bid.mediaTypes[VIDEO].hasOwnProperty('renderer')) { + utils.logError(`${LOG_WARN_PREFIX}: for "outstream" bids either outstreamAU parameter must be provided or ad unit supplied renderer is required. Rejecting bid: `, bid); return false; } } else { diff --git a/modules/pubmaticBidAdapter.md b/modules/pubmaticBidAdapter.md index 0ef89a22cbd..a34df148630 100644 --- a/modules/pubmaticBidAdapter.md +++ b/modules/pubmaticBidAdapter.md @@ -25,7 +25,7 @@ var adUnits = [ bidder: 'pubmatic', params: { publisherId: '156209', // required, must be wrapped in quotes - oustreamAU: 'renderer_test_pubmatic', // required if mediaTypes-> video-> context is 'outstream'. This value can be get by BlueBillyWig Team. + oustreamAU: 'renderer_test_pubmatic', // required if mediaTypes-> video-> context is 'outstream' and optional if renderer is defined in adUnits or in mediaType video. This value can be get by BlueBillyWig Team. adSlot: 'pubmatic_test2', // optional pmzoneid: 'zone1, zone11', // optional lat: '40.712775', // optional From f61311769823d0431cd90a05c125d88905c0555e Mon Sep 17 00:00:00 2001 From: ardit-baloku <77985953+ardit-baloku@users.noreply.github.com> Date: Fri, 29 Jan 2021 11:51:51 +0100 Subject: [PATCH 126/181] Malltv Bid Adapter : added data object as a param (#6232) * Updated malltv adapter * Updated markdown * Added test for malltvBidAdapter --- modules/malltvBidAdapter.js | 7 ++- modules/malltvBidAdapter.md | 63 +++++++++++++++------- test/spec/modules/malltvBidAdapter_spec.js | 30 +++++++++-- 3 files changed, 74 insertions(+), 26 deletions(-) diff --git a/modules/malltvBidAdapter.js b/modules/malltvBidAdapter.js index 776e3f6458b..7deffe6c07a 100644 --- a/modules/malltvBidAdapter.js +++ b/modules/malltvBidAdapter.js @@ -31,6 +31,7 @@ export const spec = { let bidderRequestId = ''; let url = ''; let contents = []; + let data = {}; let placements = validBidRequests.map(bidRequest => { if (!propertyId) { propertyId = bidRequest.params.propertyId; } @@ -38,7 +39,8 @@ export const spec = { if (!storageId && bidRequest.params) { storageId = bidRequest.params.storageId || ''; } if (!bidderRequestId) { bidderRequestId = bidRequest.bidderRequestId; } if (!url && bidderRequest) { url = bidderRequest.refererInfo.referer; } - if (!contents.length && bidRequest.params.contents && bidRequest.params.contents.length) { contents = bidRequest.params.contents } + if (!contents.length && bidRequest.params.contents && bidRequest.params.contents.length) { contents = bidRequest.params.contents; } + if (Object.keys(data).length === 0 && bidRequest.params.data && Object.keys(bidRequest.params.data).length !== 0) { data = bidRequest.params.data; } let adUnitId = bidRequest.adUnitCode; let placementId = bidRequest.params.placementId; @@ -61,7 +63,8 @@ export const spec = { url: url, requestid: bidderRequestId, placements: placements, - contents: contents + contents: contents, + data: data } return [{ diff --git a/modules/malltvBidAdapter.md b/modules/malltvBidAdapter.md index 3d419fa0916..e32eb54f90f 100644 --- a/modules/malltvBidAdapter.md +++ b/modules/malltvBidAdapter.md @@ -1,45 +1,68 @@ # Overview -Module Name: MallTV Bidder Adapter Module -Type: Bidder Adapter -Maintainer: drilon@gjirafa.com +Module Name: MallTV Bidder Adapter Module + +Type: Bidder Adapter + +Maintainer: arditb@gjirafa.com # Description MallTV Bidder Adapter for Prebid.js. # Test Parameters +```js var adUnits = [ { code: 'test-div', mediaTypes: { banner: { - sizes: [[300, 250], [300, 300]] + sizes: [ + [300, 250], + [300, 300] + ] } }, - bids: [ - { - bidder: 'malltv', - params: { - propertyId: '105134', - placementId: '846832' + bids: [{ + bidder: 'malltv', + params: { + propertyId: '105134', //Required + placementId: '846832', //Required + data: { //Optional + catalogs: [{ + catalogId: 9, + items: ["193", "4", "1"] + }], + inventory: { + category: ["tech"], + query: ["iphone 12"] + } } } - ] + }] }, { code: 'test-div', mediaTypes: { - video: { + video: { context: 'instream' - } + } }, - bids: [ - { - bidder: 'malltv', - params: { - propertyId: '105134', - placementId: '846841' + bids: [{ + bidder: 'malltv', + params: { + propertyId: '105134', //Required + placementId: '846832', //Required + data: { //Optional + catalogs: [{ + catalogId: 9, + items: ["193", "4", "1"] + }], + inventory: { + category: ["tech"], + query: ["iphone 12"] + } } } - ] + }] } ]; +``` diff --git a/test/spec/modules/malltvBidAdapter_spec.js b/test/spec/modules/malltvBidAdapter_spec.js index e1e9ad867e7..ffe08ad1a5e 100644 --- a/test/spec/modules/malltvBidAdapter_spec.js +++ b/test/spec/modules/malltvBidAdapter_spec.js @@ -34,9 +34,7 @@ describe('malltvAdapterTest', () => { it('bidRequest without propertyId or placementId', () => { expect(spec.isBidRequestValid({ bidder: 'malltv', - params: { - propertyId: '{propertyId}', - } + params: {} })).to.equal(false); }); }); @@ -46,7 +44,17 @@ describe('malltvAdapterTest', () => { 'bidder': 'malltv', 'params': { 'propertyId': '{propertyId}', - 'placementId': '{placementId}' + 'placementId': '{placementId}', + 'data': { + 'catalogs': [{ + 'catalogId': 1, + 'items': ['1', '2', '3'] + }], + 'inventory': { + 'category': ['category1', 'category2'], + 'query': ['query'] + } + } }, 'adUnitCode': 'hb-leaderboard', 'transactionId': 'b6b889bb-776c-48fd-bc7b-d11a1cf0425e', @@ -86,6 +94,20 @@ describe('malltvAdapterTest', () => { expect(requestItem.data.placements[0].sizes).to.equal('300x250'); }); }); + + it('bidRequest data param', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach((requestItem) => { + expect(requestItem.data.data).to.exist; + expect(requestItem.data.data.catalogs).to.exist; + expect(requestItem.data.data.inventory).to.exist; + expect(requestItem.data.data.catalogs.length).to.equal(1); + expect(requestItem.data.data.catalogs[0].items.length).to.equal(3); + expect(Object.keys(requestItem.data.data.inventory).length).to.equal(2); + expect(requestItem.data.data.inventory.category.length).to.equal(2); + expect(requestItem.data.data.inventory.query.length).to.equal(1); + }); + }); }); describe('interpretResponse', () => { From 25dd35c99f404390242133b84245bb5cafeb7d7b Mon Sep 17 00:00:00 2001 From: msm0504 <51493331+msm0504@users.noreply.github.com> Date: Mon, 1 Feb 2021 06:27:04 -0500 Subject: [PATCH 127/181] support setting coopSync in s2sConfig (#6213) Co-authored-by: Mark Monday --- modules/prebidServerBidAdapter/index.js | 4 +++ .../modules/prebidServerBidAdapter_spec.js | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 7274912efb5..cb059b809b7 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -201,6 +201,10 @@ function queueSync(bidderCodes, gdprConsent, uspConsent, s2sConfig) { payload.us_privacy = uspConsent; } + if (typeof _s2sConfig.coopSync === 'boolean') { + payload.coopSync = _s2sConfig.coopSync; + } + const jsonPayload = JSON.stringify(payload); ajax(s2sConfig.syncEndpoint, (response) => { diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 4652bbb63f7..7ebb570823a 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -2434,5 +2434,36 @@ describe('S2S Adapter', function () { const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.bidders).to.deep.equal(['appnexus', 'rubicon']); }); + + it('should add cooperative sync flag to cookie_sync request if property is present', function () { + let cookieSyncConfig = utils.deepClone(CONFIG); + cookieSyncConfig.coopSync = false; + cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + + let consentConfig = { s2sConfig: cookieSyncConfig }; + config.setConfig(consentConfig); + + let bidRequest = utils.deepClone(BID_REQUESTS); + + adapter.callBids(REQUEST, bidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(server.requests[0].requestBody); + + expect(requestBid.coopSync).to.equal(false); + }); + + it('should not add cooperative sync flag to cookie_sync request if property is not present', function () { + let cookieSyncConfig = utils.deepClone(CONFIG); + cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; + + let consentConfig = { s2sConfig: cookieSyncConfig }; + config.setConfig(consentConfig); + + let bidRequest = utils.deepClone(BID_REQUESTS); + + adapter.callBids(REQUEST, bidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(server.requests[0].requestBody); + + expect(requestBid.coopSync).to.be.undefined; + }); }); }); From e60d10b89e2380c949c3e1d6b388d8f460107f79 Mon Sep 17 00:00:00 2001 From: bretg Date: Mon, 1 Feb 2021 14:00:12 -0500 Subject: [PATCH 128/181] Revert "support setting coopSync in s2sConfig (#6213)" (#6249) This reverts commit 25dd35c99f404390242133b84245bb5cafeb7d7b. --- modules/prebidServerBidAdapter/index.js | 4 --- .../modules/prebidServerBidAdapter_spec.js | 31 ------------------- 2 files changed, 35 deletions(-) diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index cb059b809b7..7274912efb5 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -201,10 +201,6 @@ function queueSync(bidderCodes, gdprConsent, uspConsent, s2sConfig) { payload.us_privacy = uspConsent; } - if (typeof _s2sConfig.coopSync === 'boolean') { - payload.coopSync = _s2sConfig.coopSync; - } - const jsonPayload = JSON.stringify(payload); ajax(s2sConfig.syncEndpoint, (response) => { diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 7ebb570823a..4652bbb63f7 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -2434,36 +2434,5 @@ describe('S2S Adapter', function () { const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.bidders).to.deep.equal(['appnexus', 'rubicon']); }); - - it('should add cooperative sync flag to cookie_sync request if property is present', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.coopSync = false; - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; - - let consentConfig = { s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - let bidRequest = utils.deepClone(BID_REQUESTS); - - adapter.callBids(REQUEST, bidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.coopSync).to.equal(false); - }); - - it('should not add cooperative sync flag to cookie_sync request if property is not present', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; - - let consentConfig = { s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - let bidRequest = utils.deepClone(BID_REQUESTS); - - adapter.callBids(REQUEST, bidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.coopSync).to.be.undefined; - }); }); }); From 802cfd0b7f221508ebb99682acea04e424dc4460 Mon Sep 17 00:00:00 2001 From: bretg Date: Tue, 2 Feb 2021 14:08:40 -0500 Subject: [PATCH 129/181] pbsBidAdapter: change order of client syncs (#6248) * pbsBidAdapter: change order cookie_syncs Prebid Server places cookie-sync URLs in a specific order. PBJS was pulling them off in reverse order. * moving comment * reverting coopSync --- modules/prebidServerBidAdapter/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 7274912efb5..04aedb63ae6 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -223,10 +223,14 @@ function doAllSyncs(bidders, s2sConfig) { return; } - const thisSync = bidders.pop(); + // pull the syncs off the list in the order that prebid server sends them + const thisSync = bidders.shift(); + + // if PBS reports this bidder doesn't have an ID, then call the sync and recurse to the next sync entry if (thisSync.no_cookie) { doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, utils.bind.call(doAllSyncs, null, bidders, s2sConfig), s2sConfig); } else { + // bidder already has an ID, so just recurse to the next sync entry doAllSyncs(bidders, s2sConfig); } } From 99c1256f53cf95ed75f2d85ac2a249d801836ae9 Mon Sep 17 00:00:00 2001 From: Scott Menzer Date: Tue, 2 Feb 2021 22:05:38 +0100 Subject: [PATCH 130/181] pass a flag back to ID5 servers if abTesting was enabled by the publisher for monitoring usage of the feature (#6170) --- modules/id5IdSystem.js | 16 ++++++++++- test/spec/modules/id5IdSystem_spec.js | 41 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 45ddc85da5f..baa2ff954dc 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -58,7 +58,7 @@ export const id5IdSubmodule = { } // check for A/B testing configuration and hide ID if in Control Group - let abConfig = (config && config.params && config.params.abTesting) || { enabled: false }; + let abConfig = getAbTestingConfig(config); let controlGroup = false; if ( abConfig.enabled === true && @@ -130,6 +130,10 @@ export const id5IdSubmodule = { 'v': '$prebid.version$' }; + if (getAbTestingConfig(config).enabled === true) { + utils.deepSetValue(data, 'features.ab', 1); + } + const resp = function (callback) { const callbacks = { success: response => { @@ -282,4 +286,14 @@ export function storeInLocalStorage(key, value, expDays) { storage.setDataInLocalStorage(`${key}`, value); } +/** + * gets the existing abTesting config or generates a default config with abTesting off + * + * @param {SubmoduleConfig|undefined} config + * @returns {(Object|undefined)} + */ +function getAbTestingConfig(config) { + return (config && config.params && config.params.abTesting) || { enabled: false }; +} + submodule('userId', id5IdSubmodule); diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index 24f50db9980..59fa8977fc8 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -238,6 +238,47 @@ describe('ID5 ID System', function() { expect(getNbFromCache(ID5_TEST_PARTNER_ID)).to.be.eq(0); }); + it('should call the ID5 server with ab feature = 1 when abTesting is turned on', function () { + let id5Config = getId5FetchConfig(); + id5Config.params.abTesting = { enabled: true, controlGroupPct: 10 } + + let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + let requestBody = JSON.parse(request.requestBody); + expect(requestBody.features.ab).to.eq(1); + + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + }); + + it('should call the ID5 server without ab feature when abTesting is turned off', function () { + let id5Config = getId5FetchConfig(); + id5Config.params.abTesting = { enabled: false, controlGroupPct: 10 } + + let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + let requestBody = JSON.parse(request.requestBody); + expect(requestBody.features).to.be.undefined; + + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + }); + + it('should call the ID5 server without ab feature when when abTesting is not set', function () { + let id5Config = getId5FetchConfig(); + + let submoduleCallback = id5IdSubmodule.getId(id5Config, undefined, ID5_STORED_OBJ).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + let requestBody = JSON.parse(request.requestBody); + expect(requestBody.features).to.be.undefined; + + request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + }); + it('should store the privacy object from the ID5 server response', function () { let submoduleCallback = id5IdSubmodule.getId(getId5FetchConfig(), undefined, ID5_STORED_OBJ).callback; submoduleCallback(callbackSpy); From 62e2169ec72c17ee3e7eb70e8a1263c961bf5d1a Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Wed, 3 Feb 2021 00:18:09 -0500 Subject: [PATCH 131/181] appneuxs Bid Adapter - add support for identitylink userId (#6245) --- modules/appnexusBidAdapter.js | 46 +++++++++----------- test/spec/modules/appnexusBidAdapter_spec.js | 8 +++- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index a3cabe62d39..6feec56e919 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -240,34 +240,17 @@ export const spec = { }); } - const criteoId = utils.deepAccess(bidRequests[0], `userId.criteoId`); - let eids = []; - if (criteoId) { - eids.push({ - source: 'criteo.com', - id: criteoId - }); - } + if (bidRequests[0].userId) { + let eids = []; - const netidId = utils.deepAccess(bidRequests[0], `userId.netId`); - if (netidId) { - eids.push({ - source: 'netid.de', - id: netidId - }); - } - - const tdid = utils.deepAccess(bidRequests[0], `userId.tdid`); - if (tdid) { - eids.push({ - source: 'adserver.org', - id: tdid, - rti_partner: 'TDID' - }); - } + addUserId(eids, utils.deepAccess(bidRequests[0], `userId.criteoId`), 'criteo.com', null); + addUserId(eids, utils.deepAccess(bidRequests[0], `userId.netId`), 'netid.de', null); + addUserId(eids, utils.deepAccess(bidRequests[0], `userId.idl_env`), 'liveramp.com', null); + addUserId(eids, utils.deepAccess(bidRequests[0], `userId.tdid`), 'adserver.org', 'TDID'); - if (eids.length) { - payload.eids = eids; + if (eids.length) { + payload.eids = eids; + } } if (tags[0].publisher_id) { @@ -1042,4 +1025,15 @@ function parseMediaType(rtbBid) { } } +function addUserId(eids, id, source, rti) { + if (id) { + if (rti) { + eids.push({ source, id, rti_partner: rti }); + } else { + eids.push({ source, id }); + } + } + return eids; +} + registerBidder(spec); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 81ff5daeaeb..3a3f4effcb8 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -827,7 +827,8 @@ describe('AppNexusAdapter', function () { userId: { tdid: 'sample-userid', criteoId: 'sample-criteo-userid', - netId: 'sample-netId-userid' + netId: 'sample-netId-userid', + idl_env: 'sample-idl-userid' } }); @@ -848,6 +849,11 @@ describe('AppNexusAdapter', function () { source: 'netid.de', id: 'sample-netId-userid', }); + + expect(payload.eids).to.deep.include({ + source: 'liveramp.com', + id: 'sample-idl-userid' + }) }); it('should populate iab_support object at the root level if omid support is detected', function () { From 6a8f95341ec9a3acb29bb3cf0d7433f5006cfa93 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Wed, 3 Feb 2021 00:37:38 -0500 Subject: [PATCH 132/181] Update britepoolIdSystem.md (#6254) Eliot from Britepool says you can set just the api key without any params (eg ssid or hash) --- modules/britepoolIdSystem.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/britepoolIdSystem.md b/modules/britepoolIdSystem.md index a15f601aee3..72edbe2324b 100644 --- a/modules/britepoolIdSystem.md +++ b/modules/britepoolIdSystem.md @@ -4,7 +4,7 @@ BritePool User ID Module. For assistance setting up your module please contact u ### Prebid Params -Individual params may be set for the BritePool User ID Submodule. At least one identifier must be set in the params. +Individual params may be set for the BritePool User ID Submodule. ``` pbjs.setConfig({ userSync: { From c95427605699cdd262280de4697229f7d2a1f43e Mon Sep 17 00:00:00 2001 From: Kotaro Shikata Date: Wed, 3 Feb 2021 14:41:28 +0900 Subject: [PATCH 133/181] UNICORN Adapter - accept multiple formats (#6255) * enable multiple formats add version * add banner w/h * fix w/h & spec --- modules/unicornBidAdapter.js | 18 ++++++-- test/spec/modules/unicornBidAdapter_spec.js | 51 +++++++++++++++------ 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/modules/unicornBidAdapter.js b/modules/unicornBidAdapter.js index 33e60ad2789..2d4b5b53966 100644 --- a/modules/unicornBidAdapter.js +++ b/modules/unicornBidAdapter.js @@ -8,6 +8,7 @@ const BIDDER_CODE = 'unicorn'; const UNICORN_ENDPOINT = 'https://ds.uncn.jp/pb/0/bid.json'; const UNICORN_DEFAULT_CURRENCY = 'JPY'; const UNICORN_PB_COOKIE_KEY = '__pb_unicorn_aud'; +const UNICORN_PB_VERSION = '1.0'; /** * Placement ID and Account ID are required. @@ -47,12 +48,12 @@ function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { bidderRequest ); const imp = validBidRequests.map(br => { - const sizes = utils.parseSizesInput(br.sizes)[0]; return { id: br.bidId, banner: { - w: sizes.split('x')[0], - h: sizes.split('x')[1] + format: makeFormat(br.sizes), + w: br.sizes[0][0], + h: br.sizes[0][1], }, tagid: utils.deepAccess(br, 'params.placementId') || br.adUnitCode, secure: 1, @@ -84,7 +85,8 @@ function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { source: { ext: { stype: 'prebid_uncn', - bidder: BIDDER_CODE + bidder: BIDDER_CODE, + prebid_version: UNICORN_PB_VERSION } }, ext: { @@ -138,6 +140,14 @@ const getUid = () => { } }; +/** + * Make imp.banner.format + * @param {Array} arr + */ +const makeFormat = arr => arr.map((s) => { + return { w: s[0], h: s[1] }; +}); + export const spec = { code: BIDDER_CODE, aliases: ['uncn'], diff --git a/test/spec/modules/unicornBidAdapter_spec.js b/test/spec/modules/unicornBidAdapter_spec.js index 4c56c37700b..dcd446b2bb0 100644 --- a/test/spec/modules/unicornBidAdapter_spec.js +++ b/test/spec/modules/unicornBidAdapter_spec.js @@ -11,12 +11,12 @@ const bidRequests = [ }, mediaTypes: { banner: { - sizes: [[300, 250]] + sizes: [[300, 250], [336, 280]] } }, adUnitCode: '/19968336/header-bid-tag-0', transactionId: 'ea0aa332-a6e1-4474-8180-83720e6b87bc', - sizes: [[300, 250]], + sizes: [[300, 250], [336, 280]], bidId: '226416e6e6bf41', bidderRequestId: '1f41cbdcbe58d5', auctionId: '77987c3a-9be9-4e43-985a-26fc91d84724', @@ -81,12 +81,12 @@ const validBidRequests = [ }, mediaTypes: { banner: { - sizes: [[300, 250]] + sizes: [[300, 250], [336, 280]] } }, adUnitCode: '/19968336/header-bid-tag-0', transactionId: 'fbf94ccf-f377-4201-a662-32c2feb8ab6d', - sizes: [[300, 250]], + sizes: [[300, 250], [336, 280]], bidId: '2fb90842443e24', bidderRequestId: '123ae4cc3eeb7e', auctionId: 'c594a888-6744-46c6-8b0e-d188e40e83ef', @@ -156,12 +156,12 @@ const bidderRequest = { }, mediaTypes: { banner: { - sizes: [[300, 250]] + sizes: [[300, 250], [336, 280]] } }, adUnitCode: '/19968336/header-bid-tag-0', transactionId: 'fbf94ccf-f377-4201-a662-32c2feb8ab6d', - sizes: [[300, 250]], + sizes: [[300, 250], [336, 280]], bidId: '2fb90842443e24', bidderRequestId: '123ae4cc3eeb7e', auctionId: 'c594a888-6744-46c6-8b0e-d188e40e83ef', @@ -234,8 +234,18 @@ const openRTBRequest = { { id: '216255f234b602', banner: { - w: '300', - h: '250' + w: 300, + h: 250, + format: [ + { + w: 300, + h: 250 + }, + { + w: 336, + h: 280 + } + ] }, secure: 1, bidfloor: 0, @@ -244,8 +254,14 @@ const openRTBRequest = { { id: '31e2b28ced2475', banner: { - w: '300', - h: '250' + w: 300, + h: 250, + format: [ + { + w: 300, + h: 250 + } + ] }, secure: 1, bidfloor: 0, @@ -254,8 +270,14 @@ const openRTBRequest = { { id: '40a333e047a9bd', banner: { - w: '300', - h: '250' + w: 300, + h: 250, + format: [ + { + w: 300, + h: 250 + } + ] }, secure: 1, bidfloor: 0, @@ -287,7 +309,8 @@ const openRTBRequest = { source: { ext: { stype: 'prebid_uncn', - bidder: 'unicorn' + bidder: 'unicorn', + prebid_version: '1.0' } } }; @@ -378,7 +401,7 @@ const request = { method: 'POST', url: 'https://ds.uncn.jp/pb/0/bid.json', data: - '{"id":"5ebea288-f13a-4754-be6d-4ade66c68877","at":1,"imp":[{"id":"216255f234b602","banner":{"w":"300","h":"250"},"secure":1,"bidfloor":0,"tagid":"/19968336/header-bid-tag-0"},{"id":"31e2b28ced2475","banner":{"w":"300","h":"250"},"secure":1,"bidfloor":0"tagid":"/19968336/header-bid-tag-1"},{"id":"40a333e047a9bd","banner":{"w":"300","h":"250"},"secure":1,"bidfloor":0,"tagid":"/19968336/header-bid-tag-2"}],"cur":"JPY","site":{"id":"uni-corn.net","publisher":{"id":12345},"domain":"uni-corn.net","page":"https://uni-corn.net/","ref":"https://uni-corn.net/"},"device":{"language":"ja","ua":"Mozilla/5.0 (Linux; Android 8.0.0; ONEPLUS A5000) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.93 Mobile Safari/537.36"},"user":{"id":"69d9e1c2-801e-4901-a665-fad467550fec"},"bcat":[],"source":{"ext":{"stype":"prebid_uncn","bidder":"unicorn"}}}' + '{"id":"5ebea288-f13a-4754-be6d-4ade66c68877","at":1,"imp":[{"id":"216255f234b602","banner":{"w":300,"h":250},"format":[{"w":300,"h":250},{"w":336,"h":280}],"secure":1,"bidfloor":0,"tagid":"/19968336/header-bid-tag-0"},{"id":"31e2b28ced2475","banner":{"w":"300","h":"250"},"format":[{"w":"300","h":"250"}],"secure":1,"bidfloor":0"tagid":"/19968336/header-bid-tag-1"},{"id":"40a333e047a9bd","banner":{"w":300,"h":250},"format":[{"w":300,"h":250}],"secure":1,"bidfloor":0,"tagid":"/19968336/header-bid-tag-2"}],"cur":"JPY","site":{"id":"uni-corn.net","publisher":{"id":12345},"domain":"uni-corn.net","page":"https://uni-corn.net/","ref":"https://uni-corn.net/"},"device":{"language":"ja","ua":"Mozilla/5.0 (Linux; Android 8.0.0; ONEPLUS A5000) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.93 Mobile Safari/537.36"},"user":{"id":"69d9e1c2-801e-4901-a665-fad467550fec"},"bcat":[],"source":{"ext":{"stype":"prebid_uncn","bidder":"unicorn","prebid_version":"1.0"}}}' }; const interpretedBids = [ From 8c0c7ab844c3ffff41fcc8350f9a98cb71d99c92 Mon Sep 17 00:00:00 2001 From: mamatic <52153441+mamatic@users.noreply.github.com> Date: Wed, 3 Feb 2021 06:46:59 +0100 Subject: [PATCH 134/181] ATS-analytics - add comment clarifying ownership of atsAnalytics (#6257) --- modules/atsAnalyticsAdapter.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/atsAnalyticsAdapter.js b/modules/atsAnalyticsAdapter.js index 3e618692725..31e46dead89 100644 --- a/modules/atsAnalyticsAdapter.js +++ b/modules/atsAnalyticsAdapter.js @@ -7,6 +7,11 @@ import {getStorageManager} from '../src/storageManager.js'; export const storage = getStorageManager(); +/** + * Analytics adapter for - https://liveramp.com + * Maintainer - prebid@liveramp.com + */ + const analyticsType = 'endpoint'; // dev endpoints // const preflightUrl = 'https://analytics-check.publishersite.xyz/check/'; From 951f1e459cd5c619ab077dfc7981bb32e611ae6d Mon Sep 17 00:00:00 2001 From: Ian Flournoy Date: Wed, 3 Feb 2021 00:52:59 -0500 Subject: [PATCH 135/181] [ParrableIdSystem] Ensure base64 payload is url-safe (#6258) * Added url safe base64 encoding * Added url safe base64 encoding test Co-authored-by: Victor --- modules/parrableIdSystem.js | 11 ++++++++++- test/spec/modules/parrableIdSystem_spec.js | 13 +++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index 7587962c62b..9aa2b251f2c 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -70,6 +70,15 @@ function isValidConfig(configParams) { return true; } +function encodeBase64UrlSafe(base64) { + const ENC = { + '+': '-', + '/': '_', + '=': '.' + }; + return base64.replace(/[+/=]/g, (m) => ENC[m]); +} + function readCookie() { const parrableIdStr = storage.getCookie(PARRABLE_COOKIE_NAME); if (parrableIdStr) { @@ -182,7 +191,7 @@ function fetchId(configParams) { }; const searchParams = { - data: btoa(JSON.stringify(data)), + data: encodeBase64UrlSafe(btoa(JSON.stringify(data))), _rand: Math.random() }; diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 5e62af9b2fa..6d0748b592d 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -136,6 +136,19 @@ describe('Parrable ID System', function() { expect(server.requests[0].url).to.contain('us_privacy=' + uspString); }); + it('xhr base64 safely encodes url data object', function() { + const urlSafeBase64EncodedData = '-_.'; + const btoaStub = sinon.stub(window, 'btoa').returns('+/='); + let getIdResult = parrableIdSubmodule.getId(P_CONFIG_MOCK); + + getIdResult.callback(callbackSpy); + + let request = server.requests[0]; + let queryParams = utils.parseQS(request.url.split('?')[1]); + expect(queryParams.data).to.equal(urlSafeBase64EncodedData); + btoaStub.restore(); + }); + it('should log an error and continue to callback if ajax request errors', function () { let callBackSpy = sinon.spy(); let submoduleCallback = parrableIdSubmodule.getId({ params: {partner: 'prebid'} }).callback; From 17ce37606611012726576150bd1f185295f0032e Mon Sep 17 00:00:00 2001 From: Rich Audience Date: Wed, 3 Feb 2021 06:57:21 +0100 Subject: [PATCH 136/181] Keywords + Screen resolution + CPU Core (#6259) Co-authored-by: sgimenez --- modules/richaudienceBidAdapter.js | 13 ++++++++++++- modules/richaudienceBidAdapter.md | 1 + test/spec/modules/richaudienceBidAdapter_spec.js | 7 +++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index a6b4202fc91..07de3e40594 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -49,7 +49,10 @@ export const spec = { timeout: config.getConfig('bidderTimeout'), user: raiSetEids(bid), demand: raiGetDemandType(bid), - videoData: raiGetVideoInfo(bid) + videoData: raiGetVideoInfo(bid), + scr_rsl: raiGetResolution(), + cpuc: (typeof window.navigator != 'undefined' ? window.navigator.hardwareConcurrency : null), + kws: (!utils.isEmpty(bid.params.keywords) ? bid.params.keywords : null) }; REFERER = (typeof bidderRequest.refererInfo.referer != 'undefined' ? encodeURIComponent(bidderRequest.refererInfo.referer) : null) @@ -242,3 +245,11 @@ function renderAd(bid) { window.raParams(raPlayerHB, raOutstreamHBPassback, true); } + +function raiGetResolution() { + let resolution = ''; + if (typeof window.screen != 'undefined') { + resolution = window.screen.width + 'x' + window.screen.height; + } + return resolution; +} diff --git a/modules/richaudienceBidAdapter.md b/modules/richaudienceBidAdapter.md index 932cdb8f8de..fbf59a0208a 100644 --- a/modules/richaudienceBidAdapter.md +++ b/modules/richaudienceBidAdapter.md @@ -39,6 +39,7 @@ Please reach out to your account manager for more information. "pid":"ADb1f40rmo", "supplyType":"site", "bidfloor":0.40, + "keywords": "bici=scott;coche=audi;coche=mercedes;" } }] } diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index 90723fb863f..3b9f07b6efc 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -4,7 +4,6 @@ import { spec } from 'modules/richaudienceBidAdapter.js'; import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; describe('Richaudience adapter tests', function () { var DEFAULT_PARAMS_NEW_SIZES = [{ @@ -20,7 +19,8 @@ describe('Richaudience adapter tests', function () { params: { bidfloor: 0.5, pid: 'ADb1f40rmi', - supplyType: 'site' + supplyType: 'site', + keywords: 'coche=mercedes;coche=audi' }, auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', bidRequestsCount: 1, @@ -240,6 +240,9 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('transactionId').and.to.equal('29df2112-348b-4961-8863-1b33684d95e6'); expect(requestContent).to.have.property('timeout').and.to.equal(3000); expect(requestContent).to.have.property('numIframes').and.to.equal(0); + expect(typeof requestContent.scr_rsl === 'string') + expect(typeof requestContent.cpuc === 'number') + expect(requestContent).to.have.property('kws').and.to.equal('coche=mercedes;coche=audi'); }) it('Verify build request to prebid video inestream', function() { From 3e99b0dc100d23d50eba7f10b0c62ad334b1284b Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Wed, 3 Feb 2021 04:17:03 -0800 Subject: [PATCH 137/181] Rubicon Analytics: Fire event once gptSlots render (#6241) * Once all gam slots are back fire event * push to cmd queue --- modules/rubiconAnalyticsAdapter.js | 71 +++++++++---------- .../modules/rubiconAnalyticsAdapter_spec.js | 27 ------- 2 files changed, 33 insertions(+), 65 deletions(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index af5228b8b92..644a7d24e16 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -458,47 +458,35 @@ function updateRpaCookie() { return decodedRpaCookie; } -const gamEventFunctions = { - 'slotOnload': (auctionId, bid) => { - cache.auctions[auctionId].gamHasRendered[bid.adUnit.adUnitCode] = true; - }, - 'slotRenderEnded': (auctionId, bid, event) => { - if (event.isEmpty) { - cache.auctions[auctionId].gamHasRendered[bid.adUnit.adUnitCode] = true; - } - bid.adUnit.gam = utils.pick(event, [ - // these come in as `null` from Gpt, which when stringified does not get removed - // so set explicitly to undefined when not a number - 'advertiserId', advertiserId => utils.isNumber(advertiserId) ? advertiserId : undefined, - 'creativeId', creativeId => utils.isNumber(creativeId) ? creativeId : undefined, - 'lineItemId', lineItemId => utils.isNumber(lineItemId) ? lineItemId : undefined, - 'adSlot', () => event.slot.getAdUnitPath(), - 'isSlotEmpty', () => event.isEmpty || undefined - ]); - } -} - function subscribeToGamSlots() { - ['slotOnload', 'slotRenderEnded'].forEach(eventName => { - window.googletag.pubads().addEventListener(eventName, event => { - const isMatchingAdSlot = utils.isAdUnitCodeMatchingSlot(event.slot); - // loop through auctions and adUnits and mark the info - Object.keys(cache.auctions).forEach(auctionId => { - (Object.keys(cache.auctions[auctionId].bids) || []).forEach(bidId => { - let bid = cache.auctions[auctionId].bids[bidId]; - // if this slot matches this bids adUnit, add the adUnit info - if (isMatchingAdSlot(bid.adUnit.adUnitCode)) { - // mark this adUnit as having been rendered by gam - gamEventFunctions[eventName](auctionId, bid, event); - } - }); - // Now if all adUnits have gam rendered, send the payload - if (rubiConf.waitForGamSlots && !cache.auctions[auctionId].sent && Object.keys(cache.auctions[auctionId].gamHasRendered).every(adUnitCode => cache.auctions[auctionId].gamHasRendered[adUnitCode])) { - clearTimeout(cache.timeouts[auctionId]); - delete cache.timeouts[auctionId]; - sendMessage.call(rubiconAdapter, auctionId); + window.googletag.pubads().addEventListener('slotRenderEnded', event => { + const isMatchingAdSlot = utils.isAdUnitCodeMatchingSlot(event.slot); + // loop through auctions and adUnits and mark the info + Object.keys(cache.auctions).forEach(auctionId => { + (Object.keys(cache.auctions[auctionId].bids) || []).forEach(bidId => { + let bid = cache.auctions[auctionId].bids[bidId]; + // if this slot matches this bids adUnit, add the adUnit info + if (isMatchingAdSlot(bid.adUnit.adUnitCode)) { + // mark this adUnit as having been rendered by gam + cache.auctions[auctionId].gamHasRendered[bid.adUnit.adUnitCode] = true; + + bid.adUnit.gam = utils.pick(event, [ + // these come in as `null` from Gpt, which when stringified does not get removed + // so set explicitly to undefined when not a number + 'advertiserId', advertiserId => utils.isNumber(advertiserId) ? advertiserId : undefined, + 'creativeId', creativeId => utils.isNumber(creativeId) ? creativeId : undefined, + 'lineItemId', lineItemId => utils.isNumber(lineItemId) ? lineItemId : undefined, + 'adSlot', () => event.slot.getAdUnitPath(), + 'isSlotEmpty', () => event.isEmpty || undefined + ]); } }); + // Now if all adUnits have gam rendered, send the payload + if (rubiConf.waitForGamSlots && !cache.auctions[auctionId].sent && Object.keys(cache.auctions[auctionId].gamHasRendered).every(adUnitCode => cache.auctions[auctionId].gamHasRendered[adUnitCode])) { + clearTimeout(cache.timeouts[auctionId]); + delete cache.timeouts[auctionId]; + sendMessage.call(rubiconAdapter, auctionId); + } }); }); } @@ -576,6 +564,13 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { if (!cache.gpt.registered && utils.isGptPubadsDefined()) { subscribeToGamSlots(); cache.gpt.registered = true; + } else if (!cache.gpt.registered) { + cache.gpt.registered = true; + let googletag = window.googletag || {}; + googletag.cmd = googletag.cmd || []; + googletag.cmd.push(function() { + subscribeToGamSlots(); + }); } break; case BID_REQUESTED: diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 4bb2f6e217c..d8eb4fb0bdc 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -1361,7 +1361,6 @@ describe('rubicon analytics adapter', function () { describe('with googletag enabled', function () { let gptSlot0, gptSlot1; let gptSlotRenderEnded0, gptSlotRenderEnded1; - let gptslotOnload0, gptslotOnload1; beforeEach(function () { mockGpt.enable(); gptSlot0 = mockGpt.makeSlot({code: '/19968336/header-bid-tag-0'}); @@ -1375,16 +1374,6 @@ describe('rubicon analytics adapter', function () { lineItemId: 3333 } }; - gptslotOnload0 = { - eventName: 'slotOnload', - params: { - slot: gptSlot0, - isEmpty: false, - advertiserId: 1111, - creativeId: 2222, - lineItemId: 3333 - } - }; gptSlot1 = mockGpt.makeSlot({code: '/19968336/header-bid-tag1'}); gptSlotRenderEnded1 = { @@ -1397,16 +1386,6 @@ describe('rubicon analytics adapter', function () { lineItemId: 6666 } }; - gptslotOnload1 = { - eventName: 'slotOnload', - params: { - slot: gptSlot1, - isEmpty: false, - advertiserId: 1111, - creativeId: 2222, - lineItemId: 3333 - } - }; }); afterEach(function () { @@ -1536,12 +1515,6 @@ describe('rubicon analytics adapter', function () { mockGpt.emitEvent(gptSlotRenderEnded0.eventName, gptSlotRenderEnded0.params); mockGpt.emitEvent(gptSlotRenderEnded1.eventName, gptSlotRenderEnded1.params); - expect(server.requests.length).to.equal(0); - - // now emit slotOnload and it should send - mockGpt.emitEvent(gptslotOnload0.eventName, gptslotOnload0.params); - mockGpt.emitEvent(gptslotOnload1.eventName, gptslotOnload1.params); - expect(server.requests.length).to.equal(1); let request = server.requests[0]; let message = JSON.parse(request.requestBody); From d1900a9bb0070880890e0ab60311a8ad29ce60ab Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Wed, 3 Feb 2021 04:19:04 -0800 Subject: [PATCH 138/181] New dimension for tracking name of the matching adUnit pattern (#6252) --- modules/rubiconAnalyticsAdapter.js | 6 ++-- .../modules/rubiconAnalyticsAdapter_spec.js | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 644a7d24e16..679e86aa0a0 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -185,7 +185,8 @@ function sendMessage(auctionId, bidWonId) { 'dimensions', 'adserverTargeting', () => stringProperties(cache.targeting[bid.adUnit.adUnitCode] || {}), 'gam', - 'pbAdSlot' + 'pbAdSlot', + 'pattern' ]); adUnit.bids = []; adUnit.status = 'no-bid'; // default it to be no bid @@ -651,7 +652,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { return {adSlot: bid.fpd.context.adServer.adSlot} } }, - 'pbAdSlot', () => utils.deepAccess(bid, 'fpd.context.pbAdSlot') + 'pbAdSlot', () => utils.deepAccess(bid, 'fpd.context.pbAdSlot'), + 'pattern', () => utils.deepAccess(bid, 'fpd.context.aupName') ]) ]); return memo; diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index d8eb4fb0bdc..16d25ec400c 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -1669,6 +1669,35 @@ describe('rubicon analytics adapter', function () { expect(timedOutBid).to.not.have.property('bidResponse'); }); + it('should pass aupName as pattern', function () { + let bidRequest = utils.deepClone(MOCK.BID_REQUESTED); + bidRequest.bids[0].fpd = { + context: { + aupName: '1234/mycoolsite/*&gpt_leaderboard&deviceType=mobile' + } + }; + bidRequest.bids[1].fpd = { + context: { + aupName: '1234/mycoolsite/*&gpt_skyscraper&deviceType=mobile' + } + }; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, bidRequest); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + + clock.tick(SEND_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + expect(message.auctions[0].adUnits[0].pattern).to.equal('1234/mycoolsite/*&gpt_leaderboard&deviceType=mobile'); + expect(message.auctions[0].adUnits[1].pattern).to.equal('1234/mycoolsite/*&gpt_skyscraper&deviceType=mobile'); + }); + it('should successfully convert bid price to USD in parseBidResponse', function () { // Set the rates setConfig({ From f704369adf1aece9ad6fa887bd7135e944107f74 Mon Sep 17 00:00:00 2001 From: mefjush Date: Wed, 3 Feb 2021 14:26:47 +0100 Subject: [PATCH 139/181] Adhese Bid Adapter: Per adunit targets (#6256) * adpod category support test * Revert "adpod category support test" --- modules/adheseBidAdapter.js | 13 +++++++++---- test/spec/modules/adheseBidAdapter_spec.js | 14 ++++++++------ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/modules/adheseBidAdapter.js b/modules/adheseBidAdapter.js index 80758668a95..5510db62950 100644 --- a/modules/adheseBidAdapter.js +++ b/modules/adheseBidAdapter.js @@ -22,15 +22,19 @@ export const spec = { } const { gdprConsent, refererInfo } = bidderRequest; - const targets = validBidRequests.map(bid => bid.params.data).reduce(mergeTargets, {}); const gdprParams = (gdprConsent && gdprConsent.consentString) ? { xt: [gdprConsent.consentString] } : {}; const refererParams = (refererInfo && refererInfo.referer) ? { xf: [base64urlEncode(refererInfo.referer)] } : {}; const id5Params = (getId5Id(validBidRequests)) ? { x5: [getId5Id(validBidRequests)] } : {}; - const slots = validBidRequests.map(bid => ({ slotname: bidToSlotName(bid) })); + const commonParams = { ...gdprParams, ...refererParams, ...id5Params }; + + const slots = validBidRequests.map(bid => ({ + slotname: bidToSlotName(bid), + parameters: cleanTargets(bid.params.data) + })); const payload = { slots: slots, - parameters: { ...targets, ...gdprParams, ...refererParams, ...id5Params } + parameters: commonParams } const account = getAccount(validBidRequests); @@ -110,7 +114,8 @@ function adResponse(bid, ad) { return bidResponse; } -function mergeTargets(targets, target) { +function cleanTargets(target) { + const targets = {}; if (target) { Object.keys(target).forEach(function (key) { const val = target[key]; diff --git a/test/spec/modules/adheseBidAdapter_spec.js b/test/spec/modules/adheseBidAdapter_spec.js index 4d888db269d..0089a403749 100644 --- a/test/spec/modules/adheseBidAdapter_spec.js +++ b/test/spec/modules/adheseBidAdapter_spec.js @@ -74,31 +74,33 @@ describe('AdheseAdapter', function () { it('should include requested slots', function () { let req = spec.buildRequests([ minimalBid() ], bidderRequest); - expect(JSON.parse(req.data).slots).to.deep.include({ 'slotname': '_main_page_-leaderboard' }); + expect(JSON.parse(req.data).slots[0].slotname).to.equal('_main_page_-leaderboard'); }); it('should include all extra bid params', function () { let req = spec.buildRequests([ bidWithParams({ 'ag': '25' }) ], bidderRequest); - expect(JSON.parse(req.data).parameters).to.deep.include({ 'ag': [ '25' ] }); + expect(JSON.parse(req.data).slots[0].parameters).to.deep.include({ 'ag': [ '25' ] }); }); - it('should include duplicate bid params once', function () { + it('should assign bid params per slot', function () { let req = spec.buildRequests([ bidWithParams({ 'ag': '25' }), bidWithParams({ 'ag': '25', 'ci': 'gent' }) ], bidderRequest); - expect(JSON.parse(req.data).parameters).to.deep.include({'ag': ['25']}).and.to.deep.include({ 'ci': [ 'gent' ] }); + expect(JSON.parse(req.data).slots[0].parameters).to.deep.include({ 'ag': [ '25' ] }).and.not.to.deep.include({ 'ci': [ 'gent' ] }); + expect(JSON.parse(req.data).slots[1].parameters).to.deep.include({ 'ag': [ '25' ] }).and.to.deep.include({ 'ci': [ 'gent' ] }); }); it('should split multiple target values', function () { let req = spec.buildRequests([ bidWithParams({ 'ci': 'london' }), bidWithParams({ 'ci': 'gent' }) ], bidderRequest); - expect(JSON.parse(req.data).parameters).to.deep.include({ 'ci': [ 'london', 'gent' ] }); + expect(JSON.parse(req.data).slots[0].parameters).to.deep.include({ 'ci': [ 'london' ] }); + expect(JSON.parse(req.data).slots[1].parameters).to.deep.include({ 'ci': [ 'gent' ] }); }); it('should filter out empty params', function () { let req = spec.buildRequests([ bidWithParams({ 'aa': [], 'bb': null, 'cc': '', 'dd': [ '', '' ], 'ee': [ 0, 1, null ], 'ff': 0, 'gg': [ 'x', 'y', '' ] }) ], bidderRequest); - let params = JSON.parse(req.data).parameters; + let params = JSON.parse(req.data).slots[0].parameters; expect(params).to.not.have.any.keys('aa', 'bb', 'cc', 'dd'); expect(params).to.deep.include({ 'ee': [ 0, 1 ], 'ff': [ 0 ], 'gg': [ 'x', 'y' ] }); }); From 97e5351c2ed0900557bc09277cce33a4db1a4678 Mon Sep 17 00:00:00 2001 From: ardit-baloku <77985953+ardit-baloku@users.noreply.github.com> Date: Wed, 3 Feb 2021 15:03:46 +0100 Subject: [PATCH 140/181] Gjirafa Bid Adapter: added data object as a param (#6231) * Added data parameter to gjirafaBidAdapter * Updated gjirafaBidAdapter markdown * Added test for gjirafaBidAdapter --- modules/gjirafaBidAdapter.js | 7 ++- modules/gjirafaBidAdapter.md | 62 ++++++++++++++------- test/spec/modules/gjirafaBidAdapter_spec.js | 30 ++++++++-- 3 files changed, 73 insertions(+), 26 deletions(-) diff --git a/modules/gjirafaBidAdapter.js b/modules/gjirafaBidAdapter.js index fc1232436b4..77589cd9071 100644 --- a/modules/gjirafaBidAdapter.js +++ b/modules/gjirafaBidAdapter.js @@ -31,6 +31,7 @@ export const spec = { let bidderRequestId = ''; let url = ''; let contents = []; + let data = {}; let placements = validBidRequests.map(bidRequest => { if (!propertyId) { propertyId = bidRequest.params.propertyId; } @@ -38,7 +39,8 @@ export const spec = { if (!storageId && bidRequest.params) { storageId = bidRequest.params.storageId || ''; } if (!bidderRequestId) { bidderRequestId = bidRequest.bidderRequestId; } if (!url && bidderRequest) { url = bidderRequest.refererInfo.referer; } - if (!contents.length && bidRequest.params.contents && bidRequest.params.contents.length) { contents = bidRequest.params.contents } + if (!contents.length && bidRequest.params.contents && bidRequest.params.contents.length) { contents = bidRequest.params.contents; } + if (Object.keys(data).length === 0 && bidRequest.params.data && Object.keys(bidRequest.params.data).length !== 0) { data = bidRequest.params.data; } let adUnitId = bidRequest.adUnitCode; let placementId = bidRequest.params.placementId; @@ -61,7 +63,8 @@ export const spec = { url: url, requestid: bidderRequestId, placements: placements, - contents: contents + contents: contents, + data: data } return [{ diff --git a/modules/gjirafaBidAdapter.md b/modules/gjirafaBidAdapter.md index deb06e74a27..fb4960d61f6 100644 --- a/modules/gjirafaBidAdapter.md +++ b/modules/gjirafaBidAdapter.md @@ -1,45 +1,67 @@ # Overview -Module Name: Gjirafa Bidder Adapter Module -Type: Bidder Adapter -Maintainer: drilon@gjirafa.com +Module Name: Gjirafa Bidder Adapter Module + +Type: Bidder Adapter + +Maintainer: arditb@gjirafa.com # Description Gjirafa Bidder Adapter for Prebid.js. # Test Parameters +```js var adUnits = [ { code: 'test-div', mediaTypes: { banner: { - sizes: [[728, 90]] + sizes: [ + [728, 90] + ] } }, - bids: [ - { - bidder: 'gjirafa', - params: { - propertyId: '105227', - placementId: '846841' + bids: [{ + bidder: 'gjirafa', + params: { + propertyId: '105227', //Required + placementId: '846841', //Required + data: { //Optional + catalogs: [{ + catalogId: 9, + items: ["193", "4", "1"] + }], + inventory: { + category: ["tech"], + query: ["iphone 12"] + } } } - ] + }] }, { code: 'test-div', mediaTypes: { - video: { + video: { context: 'instream' - } + } }, - bids: [ - { - bidder: 'gjirafa', - params: { - propertyId: '105227', - placementId: '846836' + bids: [{ + bidder: 'gjirafa', + params: { + propertyId: '105227', //Required + placementId: '846836', //Required + data: { //Optional + catalogs: [{ + catalogId: 9, + items: ["193", "4", "1"] + }], + inventory: { + category: ["tech"], + query: ["iphone 12"] + } } } - ] + }] } ]; +``` diff --git a/test/spec/modules/gjirafaBidAdapter_spec.js b/test/spec/modules/gjirafaBidAdapter_spec.js index db9b82e0a10..f0fb01f4398 100644 --- a/test/spec/modules/gjirafaBidAdapter_spec.js +++ b/test/spec/modules/gjirafaBidAdapter_spec.js @@ -34,9 +34,7 @@ describe('gjirafaAdapterTest', () => { it('bidRequest without propertyId or placementId', () => { expect(spec.isBidRequestValid({ bidder: 'gjirafa', - params: { - propertyId: '{propertyId}', - } + params: {} })).to.equal(false); }); }); @@ -46,7 +44,17 @@ describe('gjirafaAdapterTest', () => { 'bidder': 'gjirafa', 'params': { 'propertyId': '{propertyId}', - 'placementId': '{placementId}' + 'placementId': '{placementId}', + 'data': { + 'catalogs': [{ + 'catalogId': 1, + 'items': ['1', '2', '3'] + }], + 'inventory': { + 'category': ['category1', 'category2'], + 'query': ['query'] + } + } }, 'adUnitCode': 'hb-leaderboard', 'transactionId': 'b6b889bb-776c-48fd-bc7b-d11a1cf0425e', @@ -86,6 +94,20 @@ describe('gjirafaAdapterTest', () => { expect(requestItem.data.placements[0].sizes).to.equal('728x90'); }); }); + + it('bidRequest data param', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach((requestItem) => { + expect(requestItem.data.data).to.exist; + expect(requestItem.data.data.catalogs).to.exist; + expect(requestItem.data.data.inventory).to.exist; + expect(requestItem.data.data.catalogs.length).to.equal(1); + expect(requestItem.data.data.catalogs[0].items.length).to.equal(3); + expect(Object.keys(requestItem.data.data.inventory).length).to.equal(2); + expect(requestItem.data.data.inventory.category.length).to.equal(2); + expect(requestItem.data.data.inventory.query.length).to.equal(1); + }); + }); }); describe('interpretResponse', () => { From f8d42badebf950b7749f46de7ade1ce99a295fbe Mon Sep 17 00:00:00 2001 From: Catalin Ciocov Date: Wed, 3 Feb 2021 17:28:25 +0200 Subject: [PATCH 141/181] Fix a TypeError when message event source is not available (#6224) --- modules/inskinBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/inskinBidAdapter.js b/modules/inskinBidAdapter.js index 2a55b5280db..3951e27c870 100644 --- a/modules/inskinBidAdapter.js +++ b/modules/inskinBidAdapter.js @@ -188,7 +188,7 @@ export const spec = { const id = 'ism_tag_' + Math.floor((Math.random() * 10e16)); window[id] = { - plr_AdSlot: e.source.frameElement, + plr_AdSlot: e.source && e.source.frameElement, bidId: e.data.bidId, bidPrice: bidsMap[e.data.bidId].price, serverResponse From 5504f12a3a29dc8f44a147443dca72fc9c214edc Mon Sep 17 00:00:00 2001 From: harpere Date: Wed, 3 Feb 2021 13:10:41 -0500 Subject: [PATCH 142/181] fix broken tests due to "encoded" base64 logic (#6268) --- test/spec/modules/parrableIdSystem_spec.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 6d0748b592d..046ab3a4005 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -80,6 +80,15 @@ describe('Parrable ID System', function() { let logErrorStub; let callbackSpy = sinon.spy(); + let decodeBase64UrlSafe = function (encBase64) { + const DEC = { + '-': '+', + '_': '/', + '.': '=' + }; + return encBase64.replace(/[-_.]/g, (m) => DEC[m]); + } + beforeEach(function() { logErrorStub = sinon.stub(utils, 'logError'); callbackSpy.resetHistory(); @@ -98,7 +107,7 @@ describe('Parrable ID System', function() { let request = server.requests[0]; let queryParams = utils.parseQS(request.url.split('?')[1]); - let data = JSON.parse(atob(queryParams.data)); + let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data))); expect(getIdResult.callback).to.be.a('function'); expect(request.url).to.contain('h.parrable.com'); From eed6db7cfa79e92ff030ca1154c04ce0b818fa96 Mon Sep 17 00:00:00 2001 From: Denis Logachov Date: Wed, 3 Feb 2021 20:19:45 +0200 Subject: [PATCH 143/181] Adkernel Bid Adapter: stringads alias added (#6262) * Adkernel: stringads alias --- modules/adkernelBidAdapter.js | 2 +- test/spec/modules/adkernelBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 1b238f8d1c4..20ed65fe2e2 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -52,7 +52,7 @@ const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => { export const spec = { code: 'adkernel', - aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon', 'andbeyond', 'adbite', 'houseofpubs', 'torchad'], + aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon', 'andbeyond', 'adbite', 'houseofpubs', 'torchad', 'stringads'], supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 0bc14c877ab..4454aa00a71 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -556,7 +556,7 @@ describe('Adkernel adapter', function () { describe('adapter configuration', () => { it('should have aliases', () => { - expect(spec.aliases).to.have.lengthOf(10); + expect(spec.aliases).to.have.lengthOf(11); }); }); From 4d1e906c6a627fc7f249f8e8b9d2f2dd390963ec Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Wed, 3 Feb 2021 14:33:17 -0500 Subject: [PATCH 144/181] Revert "Extended ID permissions supported by bidder (#6112)" (#6269) This reverts commit a926dee9e108ca4b8792ba8992a9bca7c2f42781. --- integrationExamples/gpt/userId_example.html | 1 - modules/prebidServerBidAdapter/index.js | 17 +-- modules/userId/eids.js | 22 ---- modules/userId/index.js | 47 ++----- plugins/eslint/validateImports.js | 3 +- .../modules/prebidServerBidAdapter_spec.js | 46 +++---- test/spec/modules/userId_spec.js | 117 +----------------- 7 files changed, 41 insertions(+), 212 deletions(-) diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index 71299a4a6e1..fa9b1e3b5fd 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -236,7 +236,6 @@ }, { name: "sharedId", - // bidders: ["rubicon", "sampleBidders"], // to allow this ID for specific bidders params: { syncTime: 60 // in seconds, default is 24 hours }, diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 04aedb63ae6..d878b520c78 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -12,7 +12,6 @@ import includes from 'core-js-pure/features/array/includes.js'; import { S2S_VENDORS } from './config.js'; import { ajax } from '../../src/ajax.js'; import find from 'core-js-pure/features/array/find.js'; -import { getEidPermissions } from '../userId/index.js'; const getConfig = config.getConfig; @@ -460,7 +459,7 @@ export function resetWurlMap() { } const OPEN_RTB_PROTOCOL = { - buildRequest(s2sBidRequest, bidRequests, adUnits, s2sConfig, requestedBidders) { + buildRequest(s2sBidRequest, bidRequests, adUnits, s2sConfig) { let imps = []; let aliases = {}; const firstBidRequest = bidRequests[0]; @@ -705,18 +704,6 @@ const OPEN_RTB_PROTOCOL = { utils.deepSetValue(request, 'user.ext.eids', bidUserIdAsEids); } - const eidPermissions = getEidPermissions(); - if (utils.isArray(eidPermissions) && eidPermissions.length > 0) { - if (requestedBidders && utils.isArray(requestedBidders)) { - eidPermissions.forEach(i => { - if (i.bidders) { - i.bidders = i.bidders.filter(bidder => requestedBidders.includes(bidder)) - } - }); - } - utils.deepSetValue(request, 'ext.prebid.data.eidpermissions', eidPermissions); - } - if (bidRequests) { if (firstBidRequest.gdprConsent) { // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module @@ -975,7 +962,7 @@ export function PrebidServer() { queueSync(syncBidders, gdprConsent, uspConsent, s2sBidRequest.s2sConfig); } - const request = OPEN_RTB_PROTOCOL.buildRequest(s2sBidRequest, bidRequests, validAdUnits, s2sBidRequest.s2sConfig, requestedBidders); + const request = OPEN_RTB_PROTOCOL.buildRequest(s2sBidRequest, bidRequests, validAdUnits, s2sBidRequest.s2sConfig); const requestJson = request && JSON.stringify(request); if (request && requestJson) { ajax( diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 80750ccaae8..27665de4136 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -224,25 +224,3 @@ export function createEidsArray(bidRequestUserId) { } return eids; } - -/** - * @param {SubmoduleContainer[]} submodules - */ -export function buildEidPermissions(submodules) { - let eidPermissions = []; - submodules.filter(i => utils.isPlainObject(i.idObj) && Object.keys(i.idObj).length) - .forEach(i => { - Object.keys(i.idObj).forEach(key => { - if (utils.deepAccess(i, 'config.bidders') && Array.isArray(i.config.bidders) && - utils.deepAccess(USER_IDS_CONFIG, key + '.source')) { - eidPermissions.push( - { - source: USER_IDS_CONFIG[key].source, - bidders: i.config.bidders - } - ); - } - }); - }); - return eidPermissions; -} diff --git a/modules/userId/index.js b/modules/userId/index.js index 8f7d2a36699..9294311de69 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -136,7 +136,7 @@ import { getGlobal } from '../../src/prebidGlobal.js'; import { gdprDataHandler } from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; import { module, hook } from '../../src/hook.js'; -import { createEidsArray, buildEidPermissions } from './eids.js'; +import { createEidsArray } from './eids.js'; import { getCoreStorageManager } from '../../src/storageManager.js'; const MODULE_NAME = 'User ID'; @@ -214,10 +214,6 @@ export function setStoredValue(submodule, value) { } } -export function getEidPermissions() { - return buildEidPermissions(initializedSubmodules); -} - /** * @param {SubmoduleStorage} storage * @param {String|undefined} key optional key of the value @@ -437,26 +433,6 @@ function getCombinedSubmoduleIds(submodules) { return combinedSubmoduleIds; } -/** - * This function will create a combined object for bidder with allowed subModule Ids - * @param {SubmoduleContainer[]} submodules - * @param {string} bidder - */ -function getCombinedSubmoduleIdsForBidder(submodules, bidder) { - if (!Array.isArray(submodules) || !submodules.length || !bidder) { - return {}; - } - return submodules - .filter(i => !i.config.bidders || !utils.isArray(i.config.bidders) || i.config.bidders.includes(bidder)) - .filter(i => utils.isPlainObject(i.idObj) && Object.keys(i.idObj).length) - .reduce((carry, i) => { - Object.keys(i.idObj).forEach(key => { - carry[key] = i.idObj[key]; - }); - return carry; - }, {}); -} - /** * @param {AdUnit[]} adUnits * @param {SubmoduleContainer[]} submodules @@ -465,18 +441,19 @@ function addIdDataToAdUnitBids(adUnits, submodules) { if ([adUnits].some(i => !Array.isArray(i) || !i.length)) { return; } - adUnits.forEach(adUnit => { - if (adUnit.bids && utils.isArray(adUnit.bids)) { - adUnit.bids.forEach(bid => { - const combinedSubmoduleIds = getCombinedSubmoduleIdsForBidder(submodules, bid.bidder); - if (Object.keys(combinedSubmoduleIds).length) { + const combinedSubmoduleIds = getCombinedSubmoduleIds(submodules); + const combinedSubmoduleIdsAsEids = createEidsArray(combinedSubmoduleIds); + if (Object.keys(combinedSubmoduleIds).length) { + adUnits.forEach(adUnit => { + if (adUnit.bids && utils.isArray(adUnit.bids)) { + adUnit.bids.forEach(bid => { // create a User ID object on the bid, bid.userId = combinedSubmoduleIds; - bid.userIdAsEids = createEidsArray(combinedSubmoduleIds); - } - }); - } - }); + bid.userIdAsEids = combinedSubmoduleIdsAsEids; + }); + } + }); + } } /** diff --git a/plugins/eslint/validateImports.js b/plugins/eslint/validateImports.js index 53f4ace8381..a39bf9b26d5 100644 --- a/plugins/eslint/validateImports.js +++ b/plugins/eslint/validateImports.js @@ -26,8 +26,7 @@ function flagErrors(context, node, importPath) { if ( path.dirname(absImportPath) === absModulePath || ( absImportPath.startsWith(absModulePath) && - path.basename(absImportPath) === 'index.js' && - path.basename(absFileDir) !== 'prebidServerBidAdapter' + path.basename(absImportPath) === 'index.js' ) ) { context.report(node, `import "${importPath}" cannot require module entry point`); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 4652bbb63f7..f17cd3ab14f 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -953,15 +953,17 @@ describe('S2S Adapter', function () { adapter.callBids(request, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); - expect(requestBid.ext).to.haveOwnProperty('prebid'); - expect(requestBid.ext.prebid).to.deep.include({ - aliases: { - brealtime: 'appnexus' - }, - auctiontimestamp: 1510852447530, - targeting: { - includebidderkeys: false, - includewinners: true + + expect(requestBid.ext).to.deep.equal({ + prebid: { + aliases: { + brealtime: 'appnexus' + }, + auctiontimestamp: 1510852447530, + targeting: { + includebidderkeys: false, + includewinners: true + } } }); }); @@ -983,15 +985,17 @@ describe('S2S Adapter', function () { adapter.callBids(request, BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); - expect(requestBid.ext).to.haveOwnProperty('prebid'); - expect(requestBid.ext.prebid).to.deep.include({ - aliases: { - [alias]: 'appnexus' - }, - auctiontimestamp: 1510852447530, - targeting: { - includebidderkeys: false, - includewinners: true + + expect(requestBid.ext).to.deep.equal({ + prebid: { + aliases: { + [alias]: 'appnexus' + }, + auctiontimestamp: 1510852447530, + targeting: { + includebidderkeys: false, + includewinners: true + } } }); }); @@ -1372,7 +1376,7 @@ describe('S2S Adapter', function () { expect(requestBid).to.haveOwnProperty('ext'); expect(requestBid.ext).to.haveOwnProperty('prebid'); - expect(requestBid.ext.prebid).to.deep.include({ + expect(requestBid.ext.prebid).to.deep.equal({ auctiontimestamp: 1510852447530, foo: 'bar', targeting: { @@ -1406,7 +1410,7 @@ describe('S2S Adapter', function () { expect(requestBid).to.haveOwnProperty('ext'); expect(requestBid.ext).to.haveOwnProperty('prebid'); - expect(requestBid.ext.prebid).to.deep.include({ + expect(requestBid.ext.prebid).to.deep.equal({ auctiontimestamp: 1510852447530, targeting: { includewinners: false, @@ -1442,7 +1446,7 @@ describe('S2S Adapter', function () { expect(requestBid).to.haveOwnProperty('ext'); expect(requestBid.ext).to.haveOwnProperty('prebid'); - expect(requestBid.ext.prebid).to.deep.include({ + expect(requestBid.ext.prebid).to.deep.equal({ auctiontimestamp: 1510852447530, cache: { vastxml: 'vastxml-set-though-extPrebid.cache.vastXml' diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 1ae023ae947..ed592c0cba5 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -2,7 +2,6 @@ import { attachIdSystem, auctionDelay, coreStorage, - getEidPermissions, init, requestBidsHook, setStoredConsentData, @@ -71,7 +70,7 @@ describe('User ID', function () { code, mediaTypes: {banner: {}, native: {}}, sizes: [[300, 200], [300, 600]], - bids: [{bidder: 'sampleBidder', params: {placementId: 'banner-only-bidder'}}, {bidder: 'anotherSampleBidder', params: {placementId: 'banner-only-bidder'}}] + bids: [{bidder: 'sampleBidder', params: {placementId: 'banner-only-bidder'}}] }; } @@ -1197,120 +1196,6 @@ describe('User ID', function () { }, {adUnits}); }); - it('eidPermissions fun with bidders', function (done) { - coreStorage.setCookie('sharedid', JSON.stringify({ - 'id': 'test222', - 'ts': 1590525289611 - }), (new Date(Date.now() + 5000).toUTCString())); - - setSubmoduleRegistry([sharedIdSubmodule]); - init(config); - config.setConfig({ - userSync: { - syncDelay: 0, - userIds: [ - { - name: 'sharedId', - bidders: [ - 'sampleBidder' - ], - storage: { - type: 'cookie', - name: 'sharedid', - expires: 28 - } - } - ] - } - }); - - requestBidsHook(function () { - const eidPermissions = getEidPermissions(); - expect(eidPermissions).to.deep.equal( - [ - {source: 'sharedid.org', bidders: ['sampleBidder']} - ] - ); - adUnits.forEach(unit => { - unit.bids.forEach(bid => { - if (bid.bidder === 'sampleBidder') { - expect(bid).to.have.deep.nested.property('userId.sharedid'); - expect(bid.userId.sharedid.id).to.equal('test222'); - expect(bid.userIdAsEids[0]).to.deep.equal({ - source: 'sharedid.org', - uids: [ - { - id: 'test222', - atype: 1, - ext: { - third: 'test222' - } - } - ] - }); - } - if (bid.bidder === 'anotherSampleBidder') { - expect(bid).to.not.have.deep.nested.property('userId.sharedid'); - expect(bid).to.not.have.property('userIdAsEids'); - } - }); - }); - coreStorage.setCookie('sharedid', '', EXPIRED_COOKIE_DATE); - done(); - }, {adUnits}); - }); - - it('eidPermissions fun without bidders', function (done) { - coreStorage.setCookie('sharedid', JSON.stringify({ - 'id': 'test222', - 'ts': 1590525289611 - }), (new Date(Date.now() + 5000).toUTCString())); - - setSubmoduleRegistry([sharedIdSubmodule]); - init(config); - config.setConfig({ - userSync: { - syncDelay: 0, - userIds: [ - { - name: 'sharedId', - storage: { - type: 'cookie', - name: 'sharedid', - expires: 28 - } - } - ] - } - }); - - requestBidsHook(function () { - const eidPermissions = getEidPermissions(); - expect(eidPermissions).to.deep.equal( - [] - ); - adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid).to.have.deep.nested.property('userId.sharedid'); - expect(bid.userId.sharedid.id).to.equal('test222'); - expect(bid.userIdAsEids[0]).to.deep.equal({ - source: 'sharedid.org', - uids: [ - { - id: 'test222', - atype: 1, - ext: { - third: 'test222' - } - }] - }); - }); - }); - coreStorage.setCookie('sharedid', '', EXPIRED_COOKIE_DATE); - done(); - }, {adUnits}); - }); - it('test hook from pubProvidedId config params', function (done) { setSubmoduleRegistry([pubProvidedIdSubmodule]); init(config); From a4c6efae61ca4966d0f8eacfd700c83c45cde0f1 Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Wed, 3 Feb 2021 15:14:49 -0500 Subject: [PATCH 145/181] Prebid 4.25.0 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d23415dca0..ceea005dfe6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.25.0-pre", + "version": "4.25.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 6cbb15e0248ed1e887ab2131b03e670443632cff Mon Sep 17 00:00:00 2001 From: Eric Harper Date: Wed, 3 Feb 2021 15:51:29 -0500 Subject: [PATCH 146/181] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ceea005dfe6..d8b6af04379 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.25.0", + "version": "4.26.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 28cbd2940a8cebb75987527168220e0a0d6b5a80 Mon Sep 17 00:00:00 2001 From: Skylinar <53079123+Skylinar@users.noreply.github.com> Date: Thu, 4 Feb 2021 07:34:34 +0100 Subject: [PATCH 147/181] smartxBidAdapter: new Feature - Made Out-Stream Player configurable (#6239) * Add smartclipBidAdapter * smartxBidAdapter.js - removed unused variables, removed debug, added window before the outstream related functions * - made outstream player configurable * remove wrong named files * camelcase * fix Co-authored-by: smartclip AdTechnology Co-authored-by: Gino Cirlini --- modules/smartxBidAdapter.js | 50 ++++++++++++++++++---- modules/smartxBidAdapter.md | 34 ++++++++++----- test/spec/modules/smartxBidAdapter_spec.js | 2 +- 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js index 4409e4e9dfb..804b25d1afc 100644 --- a/modules/smartxBidAdapter.js +++ b/modules/smartxBidAdapter.js @@ -328,16 +328,49 @@ export const spec = { } function createOutstreamScript(bid) { - // const slot = utils.getBidIdParameter('slot', bid.renderer.config.outstream_options); + // for SmartPlay 4.12 + function scPrebidClose(ele, completeCollapsed) { + if (completeCollapsed) { + document.getElementById(ele.id).style.display = 'none'; + } + } + + const confMinAdWidth = utils.getBidIdParameter('minAdWidth', bid.renderer.config.outstream_options) || 290; + const confMaxAdWidth = utils.getBidIdParameter('maxAdWidth', bid.renderer.config.outstream_options) || 900; + const confStartOpen = utils.getBidIdParameter('startOpen', bid.renderer.config.outstream_options) || 'false'; + const confEndingScreen = utils.getBidIdParameter('endingScreen', bid.renderer.config.outstream_options) || 'true'; + const confHeaderText = utils.getBidIdParameter('headerText', bid.renderer.config.outstream_options) || ''; + const confSkipOffset = utils.getBidIdParameter('skipOffset', bid.renderer.config.outstream_options) || 0; + const confDesiredBitrate = utils.getBidIdParameter('desiredBitrate', bid.renderer.config.outstream_options) || 1600; + const elementId = utils.getBidIdParameter('slot', bid.renderer.config.outstream_options) || bid.adUnitCode; + + // for SmartPlay 4.12 + let initCollapsed = true; + let completeCollapsed = true; + if (confStartOpen === 'true') { + initCollapsed = false; + } + if (confEndingScreen === 'true') { + completeCollapsed = false; + } + utils.logMessage('[SMARTX][renderer] Handle SmartX outstream renderer'); - const elementId = bid.adUnitCode; + let smartPlayObj = { - minAdWidth: 290, - maxAdWidth: 900, - elementLocator: { - allowInViewport: false, - minimumElementWidth: 290, - scanPixelsBelowViewport: 800 + minAdWidth: confMinAdWidth, + maxAdWidth: confMaxAdWidth, + headerText: confHeaderText, + skipOffset: confSkipOffset, + behaviourMatrix: { + init: { + 'collapsed': initCollapsed + }, + complete: { + 'collapsed': completeCollapsed + } + }, + environmentVars: { + desiredBitrate: confDesiredBitrate, }, onStartCallback: function (m, n) { try { @@ -351,6 +384,7 @@ function createOutstreamScript(bid) { }, onEndCallback: function (m, n) { try { + scPrebidClose(n, completeCollapsed); // for SmartPlay 4.12 window.sc_smartIntxtEnd(n); } catch (f) {} }, diff --git a/modules/smartxBidAdapter.md b/modules/smartxBidAdapter.md index a53af839e2b..082a36f3dde 100644 --- a/modules/smartxBidAdapter.md +++ b/modules/smartxBidAdapter.md @@ -29,11 +29,18 @@ This adapter requires setup and approval from the smartclip team. publisherId: '11986', siteId: '22860', bidfloor: 0.3, - bidfloorcur: "EUR", + bidfloorcur: 'EUR', at: 2, - cur: ["EUR"], + cur: ['EUR'], outstream_options: { - slot: 'video1' + slot: 'video1', + minAdWidth: 290, + maxAdWidth: 900, + headerText: '', + skipOffset: 0, + startOpen: 'true', + endingScreen: 'true', + desiredBitrate: 1600, }, } }], @@ -57,11 +64,18 @@ This adapter requires setup and approval from the smartclip team. publisherId: '11986', siteId: '22860', bidfloor: 0.3, - bidfloorcur: "EUR", + bidfloorcur: 'EUR', at: 2, - cur: ["EUR"], + cur: ['EUR'], outstream_options: { - slot: 'video1' + slot: 'video1', + minAdWidth: 290, + maxAdWidth: 900, + headerText: '', + skipOffset: 0, + startOpen: 'true', + endingScreen: 'true', + desiredBitrate: 1600, }, user: { data: [{ @@ -104,9 +118,9 @@ This adapter requires setup and approval from the smartclip team. publisherId: '11986', siteId: '22860', bidfloor: 0.3, - bidfloorcur: "EUR", + bidfloorcur: 'EUR', at: 2, - cur: ["EUR"] + cur: ['EUR'] } }], }]; @@ -129,9 +143,9 @@ This adapter requires setup and approval from the smartclip team. publisherId: '11986', siteId: '22860', bidfloor: 0.3, - bidfloorcur: "EUR", + bidfloorcur: 'EUR', at: 2, - cur: ["EUR"], + cur: ['EUR'], user: { data: [{ id: 'emq', diff --git a/test/spec/modules/smartxBidAdapter_spec.js b/test/spec/modules/smartxBidAdapter_spec.js index efc6abcc5fa..82c6642bd74 100644 --- a/test/spec/modules/smartxBidAdapter_spec.js +++ b/test/spec/modules/smartxBidAdapter_spec.js @@ -493,7 +493,7 @@ describe('The smartx adapter', function () { }; }); - it('should attempt to insert the EASI script', function () { + it('should attempt to insert the script', function () { var scriptTag; sinon.stub(window.document, 'getElementById').returns({ appendChild: sinon.stub().callsFake(function (script) { From 11a925d5fb13c48b369155ff4a33ed397b3c5c66 Mon Sep 17 00:00:00 2001 From: SKOCHERI <37454420+SKOCHERI@users.noreply.github.com> Date: Wed, 3 Feb 2021 22:47:02 -0800 Subject: [PATCH 148/181] Idl1 (#6242) * Renaming idLibrary to idImportLibrary * Renaming idLibrary to idImportLibrary Co-authored-by: skocheri --- modules/{idLibrary.js => idImportLibrary.js} | 2 +- modules/{idLibrary.md => idImportLibrary.md} | 4 ++-- .../{idLibrary_spec.js => idImportLibrary_spec.js} | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) rename modules/{idLibrary.js => idImportLibrary.js} (98%) rename modules/{idLibrary.md => idImportLibrary.md} (94%) rename test/spec/modules/{idLibrary_spec.js => idImportLibrary_spec.js} (82%) diff --git a/modules/idLibrary.js b/modules/idImportLibrary.js similarity index 98% rename from modules/idLibrary.js rename to modules/idImportLibrary.js index 0d8616c3f88..2a3a86cf270 100644 --- a/modules/idLibrary.js +++ b/modules/idImportLibrary.js @@ -240,4 +240,4 @@ export function setConfig(config) { associateIds(); } -config.getConfig('idLibrary', config => setConfig(config.idLibrary)); +config.getConfig('idImportLibrary', config => setConfig(config.idImportLibrary)); diff --git a/modules/idLibrary.md b/modules/idImportLibrary.md similarity index 94% rename from modules/idLibrary.md rename to modules/idImportLibrary.md index 28d40c389f3..3dd78ee25d8 100644 --- a/modules/idLibrary.md +++ b/modules/idImportLibrary.md @@ -1,4 +1,4 @@ -# ID Library +# ID Import Library ## Configuration Options @@ -13,7 +13,7 @@ ```javascript pbjs.setConfig({ - idLibrary: { + idImportLibrary: { target: 'username', url: 'https://example.com', debounce: 250, diff --git a/test/spec/modules/idLibrary_spec.js b/test/spec/modules/idImportLibrary_spec.js similarity index 82% rename from test/spec/modules/idLibrary_spec.js rename to test/spec/modules/idImportLibrary_spec.js index 682c2df1e44..699c2c43a94 100644 --- a/test/spec/modules/idLibrary_spec.js +++ b/test/spec/modules/idImportLibrary_spec.js @@ -1,5 +1,5 @@ import * as utils from 'src/utils.js'; -import * as idlibrary from 'modules/idLibrary.js'; +import * as idImportlibrary from 'modules/idImportLibrary.js'; var expect = require('chai').expect; @@ -20,7 +20,7 @@ describe('currency', function () { utils.logInfo.restore(); utils.logError.restore(); fakeCurrencyFileServer.restore(); - idlibrary.setConfig({}); + idImportlibrary.setConfig({}); }); describe('setConfig', function () { @@ -35,26 +35,26 @@ describe('currency', function () { }); it('results when no config available', function () { - idlibrary.setConfig({}); + idImportlibrary.setConfig({}); sinon.assert.called(utils.logError); }); it('results with config available', function () { - idlibrary.setConfig({ 'url': 'URL' }); + idImportlibrary.setConfig({ 'url': 'URL' }); sinon.assert.called(utils.logInfo); }); it('results with config default debounce ', function () { let config = { 'url': 'URL' } - idlibrary.setConfig(config); + idImportlibrary.setConfig(config); expect(config.debounce).to.be.equal(250); }); it('results with config default fullscan ', function () { let config = { 'url': 'URL' } - idlibrary.setConfig(config); + idImportlibrary.setConfig(config); expect(config.fullscan).to.be.equal(false); }); it('results with config fullscan ', function () { let config = { 'url': 'URL', 'fullscan': true } - idlibrary.setConfig(config); + idImportlibrary.setConfig(config); expect(config.fullscan).to.be.equal(true); }); }); From 4e1be70d08f2b8e9c038abc1f74c486289f7182f Mon Sep 17 00:00:00 2001 From: Nepomuk Seiler Date: Thu, 4 Feb 2021 08:02:27 +0100 Subject: [PATCH 149/181] Add the trade desk gvlid (#6263) UnifiedId only works with a vendor exception in the gdpr enforcement module. However this is not okay as the unifiedId matching endpoint doesn't take the consent into account and cookies are being dropped even without consent. --- modules/unifiedIdSystem.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/unifiedIdSystem.js b/modules/unifiedIdSystem.js index 3db4003c424..bc033f37992 100644 --- a/modules/unifiedIdSystem.js +++ b/modules/unifiedIdSystem.js @@ -18,6 +18,10 @@ export const unifiedIdSubmodule = { * @type {string} */ name: MODULE_NAME, + /** + * required for the gdpr enforcement module + */ + gvlid: 21, /** * decode the stored id value for passing to bid requests * @function From cca20295cd6d217422e0af2c5ce311523a0a9cf6 Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Thu, 4 Feb 2021 03:47:15 -0800 Subject: [PATCH 150/181] pick up wrapper family detail (#6272) --- modules/rubiconAnalyticsAdapter.js | 3 ++- test/spec/modules/rubiconAnalyticsAdapter_spec.js | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 679e86aa0a0..f216cbd6235 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -167,9 +167,10 @@ function sendMessage(auctionId, bidWonId) { referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), channel: 'web', }; - if (rubiConf.wrapperName || rubiConf.rule_name) { + if (rubiConf.wrapperName) { message.wrapper = { name: rubiConf.wrapperName, + family: rubiConf.wrapperFamily, rule: rubiConf.rule_name } } diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 16d25ec400c..71e5446ed06 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -1754,7 +1754,8 @@ describe('rubicon analytics adapter', function () { describe('wrapper details passed in', () => { it('should correctly pass in the wrapper details if provided', () => { config.setConfig({rubicon: { - wrapperName: '1001_wrapperName', + wrapperName: '1001_wrapperName_exp.4', + wrapperFamily: '1001_wrapperName', rule_name: 'na-mobile' }}); @@ -1771,7 +1772,8 @@ describe('rubicon analytics adapter', function () { const request = server.requests[0]; const message = JSON.parse(request.requestBody); expect(message.wrapper).to.deep.equal({ - name: '1001_wrapperName', + name: '1001_wrapperName_exp.4', + family: '1001_wrapperName', rule: 'na-mobile' }); From da13bdaa09b1725519e38cecaf55d75270c6fb2c Mon Sep 17 00:00:00 2001 From: Adprime <64427228+Adprime@users.noreply.github.com> Date: Thu, 4 Feb 2021 16:24:51 -0500 Subject: [PATCH 151/181] Add user sync (#6244) * initial * fix * remove redundant language mod, use player sizes in video traff * test modify * fix * Adding Tests * add keywords param * log * log * log * fix * add idl * add idl * fix test * lint * lint * fix * lint * lint * lint * lint * add sync * fix Co-authored-by: Aigolkin1991 Co-authored-by: Aiholkin --- modules/adprimeBidAdapter.js | 20 ++++++++++++++++++++ test/spec/modules/adprimeBidAdapter_spec.js | 10 ++++++++++ 2 files changed, 30 insertions(+) diff --git a/modules/adprimeBidAdapter.js b/modules/adprimeBidAdapter.js index 50303b82979..12d0410a821 100644 --- a/modules/adprimeBidAdapter.js +++ b/modules/adprimeBidAdapter.js @@ -4,6 +4,7 @@ import * as utils from '../src/utils.js'; const BIDDER_CODE = 'adprime'; const AD_URL = 'https://delta.adprime.com/?c=o&m=multi'; +const SYNC_URL = 'https://delta.adprime.com/?c=rtb&m=sync'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || @@ -104,6 +105,25 @@ export const spec = { } return response; }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncUrl = SYNC_URL + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + return [{ + type: 'image', + url: syncUrl + }]; + } + }; registerBidder(spec); diff --git a/test/spec/modules/adprimeBidAdapter_spec.js b/test/spec/modules/adprimeBidAdapter_spec.js index fe05634baae..8627941dc80 100644 --- a/test/spec/modules/adprimeBidAdapter_spec.js +++ b/test/spec/modules/adprimeBidAdapter_spec.js @@ -284,4 +284,14 @@ describe('AdprimebBidAdapter', function () { expect(serverResponses).to.be.an('array').that.is.empty; }); }); + describe('getUserSyncs', function () { + let userSync = spec.getUserSyncs(); + it('Returns valid URL and type', function () { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync[0].type).to.be.equal('image'); + expect(userSync[0].url).to.be.equal('https://delta.adprime.com/?c=rtb&m=sync'); + }); + }); }); From 1cd714f53b48a327c5b134915c965ca982638357 Mon Sep 17 00:00:00 2001 From: shikharsharma-zeotap Date: Fri, 5 Feb 2021 03:01:29 +0530 Subject: [PATCH 152/181] Zeotap id plus gvlid (#6260) * Add gvlid for ZeotapIdPlus module * Pass gvlid and module name to storage manager * add testcases to zeotapIdPlusIdSystem * remove unwanted code --- modules/zeotapIdPlusIdSystem.js | 20 ++++-- .../spec/modules/zeotapIdPlusIdSystem_spec.js | 64 ++++++++++++++++++- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/modules/zeotapIdPlusIdSystem.js b/modules/zeotapIdPlusIdSystem.js index d800286b00e..8f26cc827d6 100644 --- a/modules/zeotapIdPlusIdSystem.js +++ b/modules/zeotapIdPlusIdSystem.js @@ -9,23 +9,35 @@ import {submodule} from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; const ZEOTAP_COOKIE_NAME = 'IDP'; -export const storage = getStorageManager(); +const ZEOTAP_VENDOR_ID = 301; +const ZEOTAP_MODULE_NAME = 'zeotapIdPlus'; function readCookie() { - return storage.cookiesAreEnabled ? storage.getCookie(ZEOTAP_COOKIE_NAME) : null; + return storage.cookiesAreEnabled() ? storage.getCookie(ZEOTAP_COOKIE_NAME) : null; } function readFromLocalStorage() { - return storage.localStorageIsEnabled ? storage.getDataFromLocalStorage(ZEOTAP_COOKIE_NAME) : null; + return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(ZEOTAP_COOKIE_NAME) : null; } +export function getStorage() { + return getStorageManager(ZEOTAP_VENDOR_ID, ZEOTAP_MODULE_NAME); +} + +export const storage = getStorage(); + /** @type {Submodule} */ export const zeotapIdPlusSubmodule = { /** * used to link submodule with config * @type {string} */ - name: 'zeotapIdPlus', + name: ZEOTAP_MODULE_NAME, + /** + * Vendor ID of Zeotap + * @type {Number} + */ + gvlid: ZEOTAP_VENDOR_ID, /** * decode the stored id value for passing to bid requests * @function diff --git a/test/spec/modules/zeotapIdPlusIdSystem_spec.js b/test/spec/modules/zeotapIdPlusIdSystem_spec.js index 4f9e691f12e..9de6fa843bc 100644 --- a/test/spec/modules/zeotapIdPlusIdSystem_spec.js +++ b/test/spec/modules/zeotapIdPlusIdSystem_spec.js @@ -2,7 +2,8 @@ import { expect } from 'chai'; import find from 'core-js-pure/features/array/find.js'; import { config } from 'src/config.js'; import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; -import { storage, zeotapIdPlusSubmodule } from 'modules/zeotapIdPlusIdSystem.js'; +import { storage, getStorage, zeotapIdPlusSubmodule } from 'modules/zeotapIdPlusIdSystem.js'; +import * as storageManager from 'src/storageManager.js'; const ZEOTAP_COOKIE_NAME = 'IDP'; const ZEOTAP_COOKIE = 'THIS-IS-A-DUMMY-COOKIE'; @@ -43,6 +44,67 @@ function unsetLocalStorage() { } describe('Zeotap ID System', function() { + describe('Zeotap Module invokes StorageManager with appropriate arguments', function() { + let getStorageManagerSpy; + + beforeEach(function() { + getStorageManagerSpy = sinon.spy(storageManager, 'getStorageManager'); + }); + + it('when a stored Zeotap ID exists it is added to bids', function() { + let store = getStorage(); + expect(getStorageManagerSpy.calledOnce).to.be.true; + sinon.assert.calledWith(getStorageManagerSpy, 301, 'zeotapIdPlus'); + }); + }); + + describe('test method: getId calls storage methods to fetch ID', function() { + let cookiesAreEnabledStub; + let getCookieStub; + let localStorageIsEnabledStub; + let getDataFromLocalStorageStub; + + beforeEach(() => { + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + getCookieStub = sinon.stub(storage, 'getCookie'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(() => { + storage.cookiesAreEnabled.restore(); + storage.getCookie.restore(); + storage.localStorageIsEnabled.restore(); + storage.getDataFromLocalStorage.restore(); + unsetCookie(); + unsetLocalStorage(); + }); + + it('should check if cookies are enabled', function() { + let id = zeotapIdPlusSubmodule.getId(); + expect(cookiesAreEnabledStub.calledOnce).to.be.true; + }); + + it('should call getCookie if cookies are enabled', function() { + cookiesAreEnabledStub.returns(true); + let id = zeotapIdPlusSubmodule.getId(); + expect(cookiesAreEnabledStub.calledOnce).to.be.true; + expect(getCookieStub.calledOnce).to.be.true; + sinon.assert.calledWith(getCookieStub, 'IDP'); + }); + + it('should check for localStorage if cookies are disabled', function() { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(true) + let id = zeotapIdPlusSubmodule.getId(); + expect(cookiesAreEnabledStub.calledOnce).to.be.true; + expect(getCookieStub.called).to.be.false; + expect(localStorageIsEnabledStub.calledOnce).to.be.true; + expect(getDataFromLocalStorageStub.calledOnce).to.be.true; + sinon.assert.calledWith(getDataFromLocalStorageStub, 'IDP'); + }); + }); + describe('test method: getId', function() { afterEach(() => { unsetCookie(); From da1a6e9b284a088439b0b5442fcb252dad42d585 Mon Sep 17 00:00:00 2001 From: nyakove <43004249+nyakove@users.noreply.github.com> Date: Thu, 4 Feb 2021 23:39:00 +0200 Subject: [PATCH 153/181] adWMG adapter: add new parameter, fix minor bugs (#6265) * Support floorCPM parameter, fix some minor bugs * fix space-in-parens circleci error * example fix * clean usersync URL * spaces * spaces * add new unit tests, compatibility with IE11 * remove logInfo Co-authored-by: Mikhail Dykun --- modules/adWMGBidAdapter.js | 8 +++-- modules/adWMGBidAdapter.md | 9 ++--- test/spec/modules/adWMGBidAdapter_spec.js | 40 +++++++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/modules/adWMGBidAdapter.js b/modules/adWMGBidAdapter.js index 3a0a8a22274..87c40db51e6 100644 --- a/modules/adWMGBidAdapter.js +++ b/modules/adWMGBidAdapter.js @@ -113,14 +113,18 @@ export const spec = { return bidResponses; }, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - if (gdprConsent) { + if (gdprConsent && SYNC_ENDPOINT.indexOf('gdpr') === -1) { SYNC_ENDPOINT = utils.tryAppendQueryString(SYNC_ENDPOINT, 'gdpr', (gdprConsent.gdprApplies ? 1 : 0)); } - if (gdprConsent && typeof gdprConsent.consentString === 'string') { + if (gdprConsent && typeof gdprConsent.consentString === 'string' && SYNC_ENDPOINT.indexOf('gdpr_consent') === -1) { SYNC_ENDPOINT = utils.tryAppendQueryString(SYNC_ENDPOINT, 'gdpr_consent', gdprConsent.consentString); } + if (SYNC_ENDPOINT.slice(-1) === '&') { + SYNC_ENDPOINT = SYNC_ENDPOINT.slice(0, -1); + } + /* if (uspConsent) { SYNC_ENDPOINT = utils.tryAppendQueryString(SYNC_ENDPOINT, 'us_privacy', uspConsent); } */ diff --git a/modules/adWMGBidAdapter.md b/modules/adWMGBidAdapter.md index 8c277b803db..ec5541e6168 100644 --- a/modules/adWMGBidAdapter.md +++ b/modules/adWMGBidAdapter.md @@ -12,10 +12,11 @@ Module that connects to adWMG demand sources to fetch bids. Supports 'banner' ad # Bid Parameters -| Key | Required | Example | Description | -| --- | -------- | ------- | ----------- | -| `publisherId` | yes | `'5cebea3c9eea646c7b623d5e'` | publisher ID from WMG Dashboard | -| `IABCategories` | no | `['IAB1', 'IAB5']` | IAB ad categories for adUnit | +| Key | Required | Example | Description | +| --------------- | -------- | -----------------------------| ------------------------------- | +| `publisherId` | yes | `'5cebea3c9eea646c7b623d5e'` | publisher ID from WMG Dashboard | +| `IABCategories` | no | `['IAB1', 'IAB5']` | IAB ad categories for adUnit | +| `floorCPM` | no | `0.5` | Floor price for adUnit | # Test Parameters diff --git a/test/spec/modules/adWMGBidAdapter_spec.js b/test/spec/modules/adWMGBidAdapter_spec.js index 5c2364d454c..1f881897fd8 100644 --- a/test/spec/modules/adWMGBidAdapter_spec.js +++ b/test/spec/modules/adWMGBidAdapter_spec.js @@ -288,5 +288,45 @@ describe('adWMGBidAdapter', function () { expect(syncs[0].url).includes('gdpr=1'); expect(syncs[0].url).includes(`gdpr_consent=${gdprConsent.consentString}`); }); + + it('should not add GDPR consent params twice', function() { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': true + }; + const gdprConsent = { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }; + const gdprConsent2 = { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_7_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }; + const serverResponse = {}; + let syncs = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent); + syncs = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent2); + expect(syncs[0].url.match(/gdpr/g).length).to.equal(2); // gdpr + gdpr_consent + expect(syncs[0].url.match(/gdpr_consent/g).length).to.equal(1); + }); + + it('should delete \'&\' symbol at the end of usersync URL', function() { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': true + }; + const gdprConsent = { + consentString: 'CO9rhBTO9rhBTAcABBENBCCsAP_AAH_AACiQHItf_X_fb3_j-_59_9t0eY1f9_7_v20zjgeds-8Nyd_X_L8X42M7vB36pq4KuR4Eu3LBIQdlHOHcTUmw6IkVqTPsbk2Mr7NKJ7PEinMbe2dYGH9_n9XTuZKY79_s___z__-__v__7_f_r-3_3_vp9V---3YHIgEmGpfARZiWOBJNGlUKIEIVxIdACACihGFomsICVwU7K4CP0EDABAagIwIgQYgoxZBAAAAAElEQEgB4IBEARAIAAQAqQEIACNAEFgBIGAQACgGhYARQBCBIQZHBUcpgQESLRQTyVgCUXexhhCGUUANAg4AA.YAAAAAAAAAAA', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }; + const serverResponse = {}; + let syncs = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent); + expect(syncs[0].url.slice(-1)).to.not.equal('&'); + }); }); }); From 0a333497fe3d2cff6be93cb6cc465eaef3eb23a3 Mon Sep 17 00:00:00 2001 From: Ian Flournoy Date: Thu, 4 Feb 2021 17:02:02 -0500 Subject: [PATCH 154/181] [ParrableIdSystem] Supply iframe state to backend (#6278) * Add iframe detection * Remove forgotten .only Co-authored-by: Victor --- modules/parrableIdSystem.js | 3 ++- test/spec/modules/parrableIdSystem_spec.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index 9aa2b251f2c..32543e3863b 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -187,7 +187,8 @@ function fetchId(configParams) { const data = { eid, trackers: configParams.partner.split(','), - url: refererInfo.referer + url: refererInfo.referer, + isIframe: utils.inIframe(), }; const searchParams = { diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 046ab3a4005..f4f485affe9 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -116,7 +116,8 @@ describe('Parrable ID System', function() { expect(data).to.deep.equal({ eid: P_COOKIE_EID, trackers: P_CONFIG_MOCK.params.partner.split(','), - url: getRefererInfo().referer + url: getRefererInfo().referer, + isIframe: true }); server.requests[0].respond(200, From 8f776608ac1adcd2f909db78cdd67c73f8fa0515 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Fri, 5 Feb 2021 11:48:58 -0800 Subject: [PATCH 155/181] Bid Viewability Module (#6206) * introducing a new event, bidViewable * new module: bidViewability * details in bidViewability.md --- modules/bidViewability.js | 97 +++++++ modules/bidViewability.md | 49 ++++ src/adapterManager.js | 4 + src/constants.json | 3 +- test/spec/modules/bidViewability_spec.js | 297 +++++++++++++++++++++ test/spec/unit/core/adapterManager_spec.js | 32 +++ 6 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 modules/bidViewability.js create mode 100644 modules/bidViewability.md create mode 100644 test/spec/modules/bidViewability_spec.js diff --git a/modules/bidViewability.js b/modules/bidViewability.js new file mode 100644 index 00000000000..c3b72cda8d4 --- /dev/null +++ b/modules/bidViewability.js @@ -0,0 +1,97 @@ +// 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, triggerPixel } from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import adapterManager, { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; +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'; +const GPT_IMPRESSION_VIEWABLE_EVENT = 'impressionViewable'; + +export let isBidAdUnitCodeMatchingSlot = (bid, slot) => { + return (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode); +} + +export let getMatchingWinningBidForGPTSlot = (globalModuleConfig, slot) => { + return find(getGlobal().getAllWinningBids(), + // supports custom match function from config + bid => isFn(globalModuleConfig[CONFIG_CUSTOM_MATCH]) + ? globalModuleConfig[CONFIG_CUSTOM_MATCH](bid, slot) + : isBidAdUnitCodeMatchingSlot(bid, slot) + ) || null; +}; + +export let fireViewabilityPixels = (globalModuleConfig, bid) => { + if (globalModuleConfig[CONFIG_FIRE_PIXELS] === true && bid.hasOwnProperty(BID_VURL_ARRAY)) { + 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 => { + // 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) + }); + } +}; + +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 impressionViewableHandler = (globalModuleConfig, slot, event) => { + let respectiveBid = getMatchingWinningBidForGPTSlot(globalModuleConfig, slot); + if (respectiveBid === null) { + logWinningBidNotFound(slot); + } 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.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); + } +}; + +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 || []; + window.googletag.cmd.push(() => { + window.googletag.pubads().addEventListener(GPT_IMPRESSION_VIEWABLE_EVENT, function(event) { + impressionViewableHandler(globalModuleConfig, event.slot, event); + }); + }); + }); +} + +init() diff --git a/modules/bidViewability.md b/modules/bidViewability.md new file mode 100644 index 00000000000..78a1539fb1a --- /dev/null +++ b/modules/bidViewability.md @@ -0,0 +1,49 @@ +# 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 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 +- 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 + +# 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 +``` + pbjs.onEvent('bidViewable', function(bid){ + console.log('got bid details in bidViewable event', bid); + }); + +``` + +# Example of using config +``` + pbjs.setConfig({ + bidViewability: { + enabled: true, + firePixels: true, + customMatchFunction: function(bid, slot){ + console.log('using custom match function....'); + return bid.adUnitCode === slot.getAdUnitPath(); + } + } + }); +``` + +# Please Note: +- 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 + diff --git a/src/adapterManager.js b/src/adapterManager.js index 9c3ef7ac1d1..cb84607e130 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -606,4 +606,8 @@ adapterManager.callSetTargetingBidder = function(bidder, bid) { tryCallBidderMethod(bidder, 'onSetTargeting', bid); }; +adapterManager.callBidViewableBidder = function(bidder, bid) { + tryCallBidderMethod(bidder, 'onBidViewable', bid); +}; + export default adapterManager; 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", diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js new file mode 100644 index 00000000000..211dec090a5 --- /dev/null +++ b/test/spec/modules/bidViewability_spec.js @@ -0,0 +1,297 @@ +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, spy} from 'chai'; +import * as prebidGlobal from 'src/prebidGlobal.js'; +import { EVENTS } from 'src/constants.json'; +import adapterManager, { gdprDataHandler, uspDataHandler } from 'src/adapterManager.js'; +import parse from 'url-parse'; + +const GPT_SLOT = { + getAdUnitPath() { + return '/harshad/Jan/2021/'; + }, + + getSlotElementId() { + return 'DIV-1'; + } +}; + +const PBJS_WINNING_BID = { + 'adUnitCode': '/harshad/Jan/2021/', + 'bidderCode': 'pubmatic', + 'bidder': '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', + 'vurls': [ + 'https://domain-1.com/end-point?a=1', + 'https://domain-2.com/end-point/', + 'https://domain-3.com/end-point?a=1' + ] +}; + +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); + }); + }); + + describe('getMatchingWinningBidForGPTSlot', 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.getMatchingWinningBidForGPTSlot(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.getMatchingWinningBidForGPTSlot(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.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.getMatchingWinningBidForGPTSlot({}, 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('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); + PBJS_WINNING_BID.vurls.forEach((url, i) => { + let call = triggerPixelSpy.getCall(i); + expect(call.args[0]).to.equal(url); + }); + }); + + 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); + 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() { + let sandbox; + 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({ + 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); + }); + // adapterManager.callBidViewableBidder is called with required args + let call = callBidViewableBidderSpy.getCall(0); + 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); + 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); + // adapterManager.callBidViewableBidder is NOT called + expect(callBidViewableBidderSpy.callCount).to.equal(0); + // EVENTS.BID_VIEWABLE is NOT triggered + expect(eventsEmitSpy.callCount).to.equal(0); + }); + }); +}); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 00a0579e676..25b2307b943 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -404,6 +404,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 8d464b6715bf5995bb9ea455bf370cee61f743e7 Mon Sep 17 00:00:00 2001 From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com> Date: Fri, 5 Feb 2021 15:05:10 -0500 Subject: [PATCH 156/181] disable webdriver tests in trionBidAdapter spec (#6280) --- test/spec/modules/trionBidAdapter_spec.js | 82 +++++++++++------------ 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/test/spec/modules/trionBidAdapter_spec.js b/test/spec/modules/trionBidAdapter_spec.js index 596e8a3e2d9..ae329b4a028 100644 --- a/test/spec/modules/trionBidAdapter_spec.js +++ b/test/spec/modules/trionBidAdapter_spec.js @@ -146,47 +146,47 @@ describe('Trion adapter tests', function () { expect(bidUrlParams).to.include(getPublisherUrl()); }); - describe('webdriver', function () { - let originalWD; - - beforeEach(function () { - originalWD = window.navigator.webdriver; - }); - - afterEach(function () { - window.navigator['__defineGetter__']('webdriver', function () { - return originalWD; - }); - }); - - describe('is present', function () { - beforeEach(function () { - window.navigator['__defineGetter__']('webdriver', function () { - return 1; - }); - }); - - it('when there is non human traffic', function () { - let bidRequests = spec.buildRequests(TRION_BID_REQUEST); - let bidUrlParams = bidRequests[0].data; - expect(bidUrlParams).to.include('tr_wd=1'); - }); - }); - - describe('is not present', function () { - beforeEach(function () { - window.navigator['__defineGetter__']('webdriver', function () { - return 0; - }); - }); - - it('when there is not non human traffic', function () { - let bidRequests = spec.buildRequests(TRION_BID_REQUEST); - let bidUrlParams = bidRequests[0].data; - expect(bidUrlParams).to.include('tr_wd=0'); - }); - }); - }); + // describe('webdriver', function () { + // let originalWD; + + // beforeEach(function () { + // originalWD = window.navigator.webdriver; + // }); + + // afterEach(function () { + // window.navigator['__defineGetter__']('webdriver', function () { + // return originalWD; + // }); + // }); + + // describe('is present', function () { + // beforeEach(function () { + // window.navigator['__defineGetter__']('webdriver', function () { + // return 1; + // }); + // }); + + // it('when there is non human traffic', function () { + // let bidRequests = spec.buildRequests(TRION_BID_REQUEST); + // let bidUrlParams = bidRequests[0].data; + // expect(bidUrlParams).to.include('tr_wd=1'); + // }); + // }); + + // describe('is not present', function () { + // beforeEach(function () { + // window.navigator['__defineGetter__']('webdriver', function () { + // return 0; + // }); + // }); + + // it('when there is not non human traffic', function () { + // let bidRequests = spec.buildRequests(TRION_BID_REQUEST); + // let bidUrlParams = bidRequests[0].data; + // expect(bidUrlParams).to.include('tr_wd=0'); + // }); + // }); + // }); describe('document', function () { let originalHD; From ccd570bf76e13cc753f39ffe20465bfcdbb5f820 Mon Sep 17 00:00:00 2001 From: Ian Flournoy Date: Fri, 5 Feb 2021 15:08:30 -0500 Subject: [PATCH 157/181] [ParrableIdSystem] Supply Prebid library version to backend (#6279) * Add prebid version to data object * Renamed prebid to prebidVersion * Fix missing coma Co-authored-by: Victor --- modules/parrableIdSystem.js | 3 ++- test/spec/modules/parrableIdSystem_spec.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index 32543e3863b..237708aec57 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -188,7 +188,8 @@ function fetchId(configParams) { eid, trackers: configParams.partner.split(','), url: refererInfo.referer, - isIframe: utils.inIframe(), + prebidVersion: '$prebid.version$', + isIframe: utils.inIframe() }; const searchParams = { diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index f4f485affe9..9db5df2a518 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -117,6 +117,7 @@ describe('Parrable ID System', function() { eid: P_COOKIE_EID, trackers: P_CONFIG_MOCK.params.partner.split(','), url: getRefererInfo().referer, + prebidVersion: '$prebid.version$', isIframe: true }); From 86516aba8a6341cd6a82393c15438d0d73446d38 Mon Sep 17 00:00:00 2001 From: Ian Flournoy Date: Fri, 5 Feb 2021 15:11:36 -0500 Subject: [PATCH 158/181] [ParrableIdSystem] Accept list of partners as an array or string (#6277) * Accept partners as an array and fallthrough partner if no partners * Ensure that Parrable data object decodes with urlsafe base64 in tests * Fixed tests caused by typo in config property * Fix failing test due to accessing unexisting property 'partner' Co-authored-by: Victor --- modules/parrableIdSystem.js | 8 +- test/spec/modules/parrableIdSystem_spec.js | 89 ++++++++++++++++------ 2 files changed, 72 insertions(+), 25 deletions(-) diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index 237708aec57..afaf794513e 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -60,7 +60,7 @@ function isValidConfig(configParams) { utils.logError('User ID - parrableId submodule requires configParams'); return false; } - if (!configParams.partner) { + if (!configParams.partners && !configParams.partner) { utils.logError('User ID - parrableId submodule requires partner list'); return false; } @@ -183,10 +183,14 @@ function fetchId(configParams) { const eid = (parrableId) ? parrableId.eid : null; const refererInfo = getRefererInfo(); const uspString = uspDataHandler.getConsentData(); + const partners = configParams.partners || configParams.partner + const trackers = typeof partners === 'string' + ? partners.split(',') + : partners; const data = { eid, - trackers: configParams.partner.split(','), + trackers, url: refererInfo.referer, prebidVersion: '$prebid.version$', isIframe: utils.inIframe() diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 9db5df2a518..5a10529778c 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -18,7 +18,7 @@ const P_XHR_EID = '01.1588030911.test-new-eid' const P_CONFIG_MOCK = { name: 'parrableId', params: { - partner: 'parrable_test_partner_123,parrable_test_partner_456' + partners: 'parrable_test_partner_123,parrable_test_partner_456' } }; @@ -74,21 +74,21 @@ function removeParrableCookie() { storage.setCookie(P_COOKIE_NAME, '', EXPIRED_COOKIE_DATE); } +function decodeBase64UrlSafe(encBase64) { + const DEC = { + '-': '+', + '_': '/', + '.': '=' + }; + return encBase64.replace(/[-_.]/g, (m) => DEC[m]); +} + describe('Parrable ID System', function() { describe('parrableIdSystem.getId()', function() { describe('response callback function', function() { let logErrorStub; let callbackSpy = sinon.spy(); - let decodeBase64UrlSafe = function (encBase64) { - const DEC = { - '-': '+', - '_': '/', - '.': '=' - }; - return encBase64.replace(/[-_.]/g, (m) => DEC[m]); - } - beforeEach(function() { logErrorStub = sinon.stub(utils, 'logError'); callbackSpy.resetHistory(); @@ -115,7 +115,7 @@ describe('Parrable ID System', function() { expect(queryParams).to.not.have.property('us_privacy'); expect(data).to.deep.equal({ eid: P_COOKIE_EID, - trackers: P_CONFIG_MOCK.params.partner.split(','), + trackers: P_CONFIG_MOCK.params.partners.split(','), url: getRefererInfo().referer, prebidVersion: '$prebid.version$', isIframe: true @@ -162,7 +162,7 @@ describe('Parrable ID System', function() { it('should log an error and continue to callback if ajax request errors', function () { let callBackSpy = sinon.spy(); - let submoduleCallback = parrableIdSubmodule.getId({ params: {partner: 'prebid'} }).callback; + let submoduleCallback = parrableIdSubmodule.getId({ params: {partners: 'prebid'} }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; expect(request.url).to.contain('h.parrable.com'); @@ -237,7 +237,7 @@ describe('Parrable ID System', function() { it('permits an impression when no timezoneFilter is configured', function() { expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', } })).to.have.property('callback'); }); @@ -249,7 +249,7 @@ describe('Parrable ID System', function() { writeParrableCookie({ eid: P_COOKIE_EID }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } @@ -265,7 +265,7 @@ describe('Parrable ID System', function() { Intl.DateTimeFormat.returns({ resolvedOptions }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { allowedZones: [ allowedZone ] } @@ -279,7 +279,7 @@ describe('Parrable ID System', function() { Intl.DateTimeFormat.returns({ resolvedOptions }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } @@ -293,7 +293,7 @@ describe('Parrable ID System', function() { Intl.DateTimeFormat.returns({ resolvedOptions }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedZones: [ blockedZone ] } @@ -307,7 +307,7 @@ describe('Parrable ID System', function() { Intl.DateTimeFormat.returns({ resolvedOptions }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { allowedZones: [ timezone ], blockedZones: [ timezone ] @@ -337,7 +337,7 @@ describe('Parrable ID System', function() { writeParrableCookie({ eid: P_COOKIE_EID }); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } @@ -351,7 +351,7 @@ describe('Parrable ID System', function() { Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { allowedOffsets: [ allowedOffset ] } @@ -365,7 +365,7 @@ describe('Parrable ID System', function() { Date.prototype.getTimezoneOffset.returns(allowedOffset * 60); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } @@ -378,7 +378,7 @@ describe('Parrable ID System', function() { Date.prototype.getTimezoneOffset.returns(blockedOffset * 60); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { blockedOffsets: [ blockedOffset ] } @@ -391,7 +391,7 @@ describe('Parrable ID System', function() { Date.prototype.getTimezoneOffset.returns(offset * 60); expect(parrableIdSubmodule.getId({ params: { - partner: 'prebid-test', + partners: 'prebid-test', timezoneFilter: { allowedOffset: [ offset ], blockedOffsets: [ offset ] @@ -468,4 +468,47 @@ describe('Parrable ID System', function() { }, { adUnits }); }); }); + + describe('partners parsing', () => { + let callbackSpy = sinon.spy(); + + const partnersTestCase = [ + { + name: '"partners" as an array', + config: { params: { partners: ['parrable_test_partner_123', 'parrable_test_partner_456'] } }, + expected: ['parrable_test_partner_123', 'parrable_test_partner_456'] + }, + { + name: '"partners" as a string list', + config: { params: { partners: 'parrable_test_partner_123,parrable_test_partner_456' } }, + expected: ['parrable_test_partner_123', 'parrable_test_partner_456'] + }, + { + name: '"partners" as a string', + config: { params: { partners: 'parrable_test_partner_123' } }, + expected: ['parrable_test_partner_123'] + }, + { + name: '"partner" as a string list', + config: { params: { partner: 'parrable_test_partner_123,parrable_test_partner_456' } }, + expected: ['parrable_test_partner_123', 'parrable_test_partner_456'] + }, + { + name: '"partner" as string', + config: { params: { partner: 'parrable_test_partner_123' } }, + expected: ['parrable_test_partner_123'] + }, + ]; + partnersTestCase.forEach(testCase => { + it(`accepts config property ${testCase.name}`, () => { + parrableIdSubmodule.getId(testCase.config).callback(callbackSpy); + + let request = server.requests[0]; + let queryParams = utils.parseQS(request.url.split('?')[1]); + let data = JSON.parse(atob(decodeBase64UrlSafe(queryParams.data))); + + expect(data.trackers).to.deep.equal(testCase.expected); + }); + }); + }); }); From b7dcdf9e412bff45b374fb4ce21d3ebd25652ece Mon Sep 17 00:00:00 2001 From: mmoschovas <63253416+mmoschovas@users.noreply.github.com> Date: Sun, 7 Feb 2021 10:56:12 -0500 Subject: [PATCH 159/181] Rubicon Bid Adapter FPD Update (#6122) * Update to consolidate applying FPD to both banner and video requests. FPD will be merged using global defined FPD, ad unit FPD, and rubicon bidder param FPD. Validation logic with warning logs added * Refectored last push to: 1) Correct keywords bug 2) Revise error which looked for FPD in (user/context).ext.data as opposed to (user/context).data 3) General code cleanup * Consolidated other FPD data logic into new function * 1. Update to move pbadslot and adserver data into imp[] as opposed to parent. 2. Update to convert keywords passed through RP params to string if array found * Removed unnecessary conditional * Changed conditional to check for undefined type * Update to consolidate several lines of duplicate code into one location --- modules/rubiconBidAdapter.js | 160 ++++++++++---------- test/spec/modules/rubiconBidAdapter_spec.js | 63 +++++--- 2 files changed, 117 insertions(+), 106 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 2981bd4f3fa..395b7a693b2 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -254,46 +254,7 @@ export const spec = { utils.deepSetValue(data, 'source.ext.schain', bidRequest.schain); } - const siteData = Object.assign({}, bidRequest.params.inventory, config.getConfig('fpd.context')); - const userData = Object.assign({}, bidRequest.params.visitor, config.getConfig('fpd.user')); - if (!utils.isEmpty(siteData) || !utils.isEmpty(userData)) { - const bidderData = { - bidders: [ bidderRequest.bidderCode ], - config: { - fpd: {} - } - }; - - if (!utils.isEmpty(siteData)) { - bidderData.config.fpd.site = siteData; - } - - if (!utils.isEmpty(userData)) { - bidderData.config.fpd.user = userData; - } - - utils.deepSetValue(data, 'ext.prebid.bidderconfig.0', bidderData); - } - - /** - * Prebid AdSlot - * @type {(string|undefined)} - */ - const pbAdSlot = utils.deepAccess(bidRequest, 'fpd.context.pbAdSlot'); - if (typeof pbAdSlot === 'string' && pbAdSlot) { - utils.deepSetValue(data.imp[0].ext, 'context.data.pbadslot', pbAdSlot); - } - - /** - * Copy GAM AdUnit and Name to imp - */ - ['name', 'adSlot'].forEach(name => { - /** @type {(string|undefined)} */ - const value = utils.deepAccess(bidRequest, `fpd.context.adserver.${name}`); - if (typeof value === 'string' && value) { - utils.deepSetValue(data.imp[0].ext, `context.data.adserver.${name.toLowerCase()}`, value); - } - }); + applyFPD(bidRequest, VIDEO, data); // if storedAuctionResponse has been set, pass SRID if (bidRequest.storedAuctionResponse) { @@ -547,49 +508,7 @@ export const spec = { data['us_privacy'] = encodeURIComponent(bidderRequest.uspConsent); } - // visitor properties - const visitorData = Object.assign({}, params.visitor, config.getConfig('fpd.user')); - Object.keys(visitorData).forEach((key) => { - if (visitorData[key] != null && key !== 'keywords') { - data[`tg_v.${key}`] = typeof visitorData[key] === 'object' && !Array.isArray(visitorData[key]) - ? JSON.stringify(visitorData[key]) - : visitorData[key].toString(); // initialize array; - } - }); - - // inventory properties - const inventoryData = Object.assign({}, params.inventory, config.getConfig('fpd.context')); - Object.keys(inventoryData).forEach((key) => { - if (inventoryData[key] != null && key !== 'keywords') { - data[`tg_i.${key}`] = typeof inventoryData[key] === 'object' && !Array.isArray(inventoryData[key]) - ? JSON.stringify(inventoryData[key]) - : inventoryData[key].toString(); - } - }); - - // keywords - const keywords = (params.keywords || []).concat( - utils.deepAccess(config.getConfig('fpd.user'), 'keywords') || [], - utils.deepAccess(config.getConfig('fpd.context'), 'keywords') || []); - data.kw = Array.isArray(keywords) && keywords.length ? keywords.join(',') : ''; - - /** - * Prebid AdSlot - * @type {(string|undefined)} - */ - const pbAdSlot = utils.deepAccess(bidRequest, 'fpd.context.pbAdSlot'); - if (typeof pbAdSlot === 'string' && pbAdSlot) { - data['tg_i.pbadslot'] = pbAdSlot.replace(/^\/+/, ''); - } - - /** - * GAM Ad Unit - * @type {(string|undefined)} - */ - const gamAdUnit = utils.deepAccess(bidRequest, 'fpd.context.adServer.adSlot'); - if (typeof gamAdUnit === 'string' && gamAdUnit) { - data['tg_i.dfp_ad_unit_code'] = gamAdUnit.replace(/^\/+/, ''); - } + applyFPD(bidRequest, BANNER, data); if (config.getConfig('coppa') === true) { data['coppa'] = 1; @@ -949,6 +868,81 @@ function addVideoParameters(data, bidRequest) { data.imp[0].video.h = size[1] } +function applyFPD(bidRequest, mediaType, data) { + const bidFpd = { + user: {...bidRequest.params.visitor}, + context: {...bidRequest.params.inventory} + }; + + if (bidRequest.params.keywords) bidFpd.context.keywords = (utils.isArray(bidRequest.params.keywords)) ? bidRequest.params.keywords.join(',') : bidRequest.params.keywords; + + let fpd = utils.mergeDeep({}, config.getConfig('fpd') || {}, bidRequest.fpd || {}, bidFpd); + + const map = {user: {banner: 'tg_v.', code: 'user'}, context: {banner: 'tg_i.', code: 'site'}, adserver: 'dfp_ad_unit_code'}; + let obj = {}; + let impData = {}; + let keywords = []; + const validate = function(prop, key) { + if (typeof prop === 'object' && !Array.isArray(prop)) { + utils.logWarn('Rubicon: Filtered FPD key: ', key, ': Expected value to be string, integer, or an array of strings/ints'); + } else if (typeof prop !== 'undefined') { + return (Array.isArray(prop)) ? prop.filter(value => { + if (typeof value !== 'object' && typeof value !== 'undefined') return value.toString(); + + utils.logWarn('Rubicon: Filtered value: ', value, 'for key', key, ': Expected value to be string, integer, or an array of strings/ints'); + }).toString() : prop.toString(); + } + }; + + Object.keys(fpd).filter(value => fpd[value] && map[value] && typeof fpd[value] === 'object').forEach((type) => { + obj[map[type].code] = Object.keys(fpd[type]).filter(value => typeof fpd[type][value] !== 'undefined').reduce((result, key) => { + if (key === 'keywords') { + if (!Array.isArray(fpd[type][key]) && mediaType === BANNER) fpd[type][key] = [fpd[type][key]] + + result[key] = fpd[type][key]; + + if (mediaType === BANNER) keywords = keywords.concat(fpd[type][key]); + } else if (key === 'data') { + utils.mergeDeep(result, {ext: {data: fpd[type][key]}}); + } else if (key === 'adServer' || key === 'pbAdSlot') { + (key === 'adServer') ? ['name', 'adSlot'].forEach(name => { + const value = validate(fpd[type][key][name]); + if (value) utils.deepSetValue(impData, `adserver.${name.toLowerCase()}`, value.replace(/^\/+/, '')) + }) : impData[key.toLowerCase()] = fpd[type][key].replace(/^\/+/, '') + } else { + utils.mergeDeep(result, {ext: {data: {[key]: fpd[type][key]}}}); + } + + return result; + }, {}); + + if (mediaType === BANNER) { + let duplicate = (typeof obj[map[type].code].ext === 'object' && obj[map[type].code].ext.data) || {}; + + Object.keys(duplicate).forEach((key) => { + const val = (key === 'adserver') ? duplicate.adserver.adslot : validate(duplicate[key], key); + + if (val) data[(map[key]) ? `${map[type][BANNER]}${map[key]}` : `${map[type][BANNER]}${key}`] = val; + }); + } + }); + + Object.keys(impData).forEach((key) => { + if (mediaType === BANNER) { + (map[key]) ? data[`tg_i.${map[key]}`] = impData[key].adslot : data[`tg_i.${key.toLowerCase()}`] = impData[key]; + } else { + utils.mergeDeep(data.imp[0], {ext: {context: {data: {[key]: impData[key]}}}}); + } + }); + + if (mediaType === BANNER) { + let kw = validate(keywords, 'keywords'); + if (kw) data.kw = kw; + } else { + utils.mergeDeep(data, obj); + } +} + /** * @param sizes * @returns {*} diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 6944034a787..8c25d97dada 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -826,16 +826,22 @@ describe('the rubicon adapter', function () { }); }); - it('should use first party data from getConfig over the bid params, if present', () => { + it('should merge first party data from getConfig with the bid params, if present', () => { const context = { - keywords: ['e', 'f'], - rating: '4-star' + keywords: 'e,f', + rating: '4-star', + data: { + page: 'home' + } }; const user = { - keywords: ['d'], gender: 'M', yob: '1984', - geo: {country: 'ca'} + geo: {country: 'ca'}, + keywords: 'd', + data: { + age: 40 + } }; sandbox.stub(config, 'getConfig').callsFake(key => { @@ -849,14 +855,15 @@ describe('the rubicon adapter', function () { }); const expectedQuery = { - 'kw': 'a,b,c,d,e,f', + 'kw': 'a,b,c,d', 'tg_v.ucat': 'new', 'tg_v.lastsearch': 'iphone', 'tg_v.likes': 'sports,video games', 'tg_v.gender': 'M', + 'tg_v.age': '40', 'tg_v.yob': '1984', - 'tg_v.geo': '{"country":"ca"}', - 'tg_i.rating': '4-star', + 'tg_i.rating': '5-star', + 'tg_i.page': 'home', 'tg_i.prodtype': 'tech,mobile', }; @@ -1865,11 +1872,17 @@ describe('the rubicon adapter', function () { createVideoBidderRequest(); const context = { - keywords: ['e', 'f'], + data: { + page: 'home' + }, + keywords: 'e,f', rating: '4-star' }; const user = { - keywords: ['d'], + data: { + age: 31 + }, + keywords: 'd', gender: 'M', yob: '1984', geo: {country: 'ca'} @@ -1885,18 +1898,22 @@ describe('the rubicon adapter', function () { return utils.deepAccess(config, key); }); - const expected = [{ - bidders: ['rubicon'], - config: { - fpd: { - site: Object.assign({}, bidderRequest.bids[0].params.inventory, context), - user: Object.assign({}, bidderRequest.bids[0].params.visitor, user) - } - } - }]; - const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.ext.prebid.bidderconfig).to.deep.equal(expected); + + const expected = { + site: Object.assign({}, context, context.data, bidderRequest.bids[0].params.inventory), + user: Object.assign({}, user, user.data, bidderRequest.bids[0].params.visitor) + }; + + delete expected.site.data; + delete expected.user.data; + delete expected.site.keywords; + delete expected.user.keywords; + + expect(request.data.site.keywords).to.deep.equal('a,b,c'); + expect(request.data.user.keywords).to.deep.equal('d'); + expect(request.data.site.ext.data).to.deep.equal(expected.site); + expect(request.data.user.ext.data).to.deep.equal(expected.user); }); it('should include storedAuctionResponse in video bid request', function () { @@ -1935,7 +1952,7 @@ describe('the rubicon adapter', function () { createVideoBidderRequest(); bidderRequest.bids[0].fpd = { context: { - adserver: { + adServer: { adSlot: '1234567890', name: 'adServerName1' } @@ -2079,7 +2096,7 @@ describe('the rubicon adapter', function () { it('should not fail if keywords param is not an array', function () { bidderRequest.bids[0].params.keywords = 'a,b,c'; const slotParams = spec.createSlotParams(bidderRequest.bids[0], bidderRequest); - expect(slotParams.kw).to.equal(''); + expect(slotParams.kw).to.equal('a,b,c'); }); }); From 2640d0806a9b28a7e21e44dc46aeccbf83fdd0fd Mon Sep 17 00:00:00 2001 From: Scott Menzer Date: Mon, 8 Feb 2021 12:08:19 +0100 Subject: [PATCH 160/181] ID5 User Id Module: update a/b testing to be user based not request based (#6281) * convert A/B testing to be user-based, rather than request-based * update docs to say a/b testing is user based, not request based --- modules/id5IdSystem.js | 51 ++++++++++----- modules/id5IdSystem.md | 4 +- test/spec/modules/id5IdSystem_spec.js | 91 ++++++++++++++++++++------- 3 files changed, 105 insertions(+), 41 deletions(-) diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index baa2ff954dc..bf2365b938c 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -18,6 +18,7 @@ const NB_EXP_DAYS = 30; export const ID5_STORAGE_NAME = 'id5id'; export const ID5_PRIVACY_STORAGE_NAME = `${ID5_STORAGE_NAME}_privacy`; const LOCAL_STORAGE = 'html5'; +const ABTEST_RESOLUTION = 10000; // order the legacy cookie names in reverse priority order so the last // cookie in the array is the most preferred to use @@ -58,27 +59,18 @@ export const id5IdSubmodule = { } // check for A/B testing configuration and hide ID if in Control Group - let abConfig = getAbTestingConfig(config); - let controlGroup = false; - if ( - abConfig.enabled === true && - (!utils.isNumber(abConfig.controlGroupPct) || - abConfig.controlGroupPct < 0 || - abConfig.controlGroupPct > 1) - ) { + const abConfig = getAbTestingConfig(config); + const controlGroup = isInControlGroup(universalUid, abConfig.controlGroupPct); + if (abConfig.enabled === true && typeof controlGroup === 'undefined') { // A/B Testing is enabled, but configured improperly, so skip A/B testing utils.logError('User ID - ID5 submodule: A/B Testing controlGroupPct must be a number >= 0 and <= 1! Skipping A/B Testing'); - } else if ( - abConfig.enabled === true && - Math.random() < abConfig.controlGroupPct - ) { + } else if (abConfig.enabled === true && controlGroup === true) { // A/B Testing is enabled and user is in the Control Group, so do not share the ID5 ID - utils.logInfo('User ID - ID5 submodule: A/B Testing Enabled - request is in the Control Group, so the ID5 ID is NOT exposed'); + utils.logInfo('User ID - ID5 submodule: A/B Testing Enabled - user is in the Control Group, so the ID5 ID is NOT exposed'); universalUid = linkType = 0; - controlGroup = true; } else if (abConfig.enabled === true) { // A/B Testing is enabled but user is not in the Control Group, so ID5 ID is shared - utils.logInfo('User ID - ID5 submodule: A/B Testing Enabled - request is NOT in the Control Group, so the ID5 ID is exposed'); + utils.logInfo('User ID - ID5 submodule: A/B Testing Enabled - user is NOT in the Control Group, so the ID5 ID is exposed'); } let responseObj = { @@ -91,7 +83,7 @@ export const id5IdSubmodule = { }; if (abConfig.enabled === true) { - utils.deepSetValue(responseObj, 'id5id.ext.abTestingControlGroup', controlGroup); + utils.deepSetValue(responseObj, 'id5id.ext.abTestingControlGroup', (typeof controlGroup === 'undefined' ? false : controlGroup)); } return responseObj; @@ -296,4 +288,31 @@ function getAbTestingConfig(config) { return (config && config.params && config.params.abTesting) || { enabled: false }; } +/** + * Return a consistant random number between 0 and ABTEST_RESOLUTION-1 for this user + * Falls back to plain random if no user provided + * @param {string} userId + * @returns {number} + */ +function abTestBucket(userId) { + if (userId) { + return ((utils.cyrb53Hash(userId) % ABTEST_RESOLUTION) + ABTEST_RESOLUTION) % ABTEST_RESOLUTION; + } else { + return Math.floor(Math.random() * ABTEST_RESOLUTION); + } +} + +/** + * Return a consistant boolean if this user is within the control group ratio provided + * @param {string} userId + * @param {number} controlGroupPct - Ratio [0,1] of users expected to be in the control group + * @returns {boolean} + */ +export function isInControlGroup(userId, controlGroupPct) { + if (!utils.isNumber(controlGroupPct) || controlGroupPct < 0 || controlGroupPct > 1) { + return undefined; + } + return abTestBucket(userId) < controlGroupPct * ABTEST_RESOLUTION; +} + submodule('userId', id5IdSubmodule); diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md index 6b2192834fa..6a662361492 100644 --- a/modules/id5IdSystem.md +++ b/modules/id5IdSystem.md @@ -1,6 +1,6 @@ # ID5 Universal ID -The ID5 Universal ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 Universal ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 Universal ID and detailed integration docs, please visit [our documentation](https://console.id5.io/docs/public/prebid). We also recommend that you sign up for our [release notes](https://id5.io/universal-id/release-notes) to stay up-to-date with any changes to the implementation of the ID5 Universal ID in Prebid. +The ID5 Universal ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 Universal ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 Universal ID and detailed integration docs, please visit [our documentation](https://wiki.id5.io/x/BIAZ). We also recommend that you sign up for our [release notes](https://id5.io/universal-id/release-notes) to stay up-to-date with any changes to the implementation of the ID5 Universal ID in Prebid. ## ID5 Universal ID Registration @@ -65,4 +65,4 @@ pbjs.setConfig({ Publishers may want to test the value of the ID5 ID with their downstream partners. While there are various ways to do this, A/B testing is a standard approach. Instead of publishers manually enabling or disabling the ID5 User ID Module based on their control group settings (which leads to fewer calls to ID5, reducing our ability to recognize the user), we have baked this in to our module directly. -To turn on A/B Testing, simply edit the configuration (see above table) to enable it and set what percentage of requests you would like to set for the control group. The control group is the set of requests where an ID5 ID will not be exposed in to bid adapters or in the various user id functions available on the `pbjs` global. An additional value of `ext.abTestingControlGroup` will be set to `true` or `false` that can be used to inform reporting systems that the request was in the control group or not. It's important to note that the control group is request based, and not user based. In other words, from one page view to another, a user may be in or out of the control group. +To turn on A/B Testing, simply edit the configuration (see above table) to enable it and set what percentage of users you would like to set for the control group. The control group is the set of user where an ID5 ID will not be exposed in to bid adapters or in the various user id functions available on the `pbjs` global. An additional value of `ext.abTestingControlGroup` will be set to `true` or `false` that can be used to inform reporting systems that the user was in the control group or not. It's important to note that the control group is user based, and not request based. In other words, from one page view to another, a user will always be in or out of the control group. diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index 59fa8977fc8..7f104ff403d 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -7,7 +7,8 @@ import { expDaysStr, nbCacheName, getNbFromCache, - storeNbInCache + storeNbInCache, + isInControlGroup } from 'modules/id5IdSystem.js'; import { init, requestBidsHook, setSubmoduleRegistry, coreStorage } from 'modules/userId/index.js'; import { config } from 'src/config.js'; @@ -477,6 +478,9 @@ describe('ID5 ID System', function() { { enabled: true, controlGroupPct: true } ]; testInvalidAbTestingConfigsWithError.forEach((testAbTestingConfig) => { + it('should be undefined if ratio is invalid', () => { + expect(isInControlGroup('userId', testAbTestingConfig.controlGroupPct)).to.be.undefined; + }); it('should error if config is invalid, and always return an ID', function () { testConfig.params.abTesting = testAbTestingConfig; let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); @@ -493,6 +497,9 @@ describe('ID5 ID System', function() { { enabled: false, controlGroupPct: true } ]; testInvalidAbTestingConfigsWithoutError.forEach((testAbTestingConfig) => { + it('should be undefined if ratio is invalid', () => { + expect(isInControlGroup('userId', testAbTestingConfig.controlGroupPct)).to.be.undefined; + }); it('should not error if config is invalid but A/B testing is off, and always return an ID', function () { testConfig.params.abTesting = testAbTestingConfig; let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); @@ -509,6 +516,9 @@ describe('ID5 ID System', function() { { enabled: true, controlGroupPct: 1 } ]; testValidConfigs.forEach((testAbTestingConfig) => { + it('should not be undefined if ratio is valid', () => { + expect(isInControlGroup('userId', testAbTestingConfig.controlGroupPct)).to.not.be.undefined; + }); it('should not error if config is valid', function () { testConfig.params.abTesting = testAbTestingConfig; id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); @@ -555,34 +565,69 @@ describe('ID5 ID System', function() { randStub.restore(); }); - it('should expose ID when A/B testing is off', function () { - testConfig.params.abTesting = { - enabled: false, - controlGroupPct: 0.5 - }; + describe('IsInControlGroup', function () { + it('Nobody is in a 0% control group', function () { + expect(isInControlGroup('dsdndskhsdks', 0)).to.be.false; + expect(isInControlGroup('3erfghyuijkm', 0)).to.be.false; + expect(isInControlGroup('', 0)).to.be.false; + expect(isInControlGroup(undefined, 0)).to.be.false; + }); - let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); - expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); - }); + it('Everybody is in a 100% control group', function () { + expect(isInControlGroup('dsdndskhsdks', 1)).to.be.true; + expect(isInControlGroup('3erfghyuijkm', 1)).to.be.true; + expect(isInControlGroup('', 1)).to.be.true; + expect(isInControlGroup(undefined, 1)).to.be.true; + }); - it('should expose ID when not in control group', function () { - testConfig.params.abTesting = { - enabled: true, - controlGroupPct: 0.1 - }; + it('Being in the control group must be consistant', function () { + const inControlGroup = isInControlGroup('dsdndskhsdks', 0.5); + expect(inControlGroup === isInControlGroup('dsdndskhsdks', 0.5)).to.be.true; + expect(inControlGroup === isInControlGroup('dsdndskhsdks', 0.5)).to.be.true; + expect(inControlGroup === isInControlGroup('dsdndskhsdks', 0.5)).to.be.true; + }); - let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); - expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOn); + it('Control group ratio must be within a 10% error on a large sample', function () { + let nbInControlGroup = 0; + const sampleSize = 100; + for (let i = 0; i < sampleSize; i++) { + nbInControlGroup = nbInControlGroup + (isInControlGroup('R$*df' + i, 0.5) ? 1 : 0); + } + expect(nbInControlGroup).to.be.greaterThan(sampleSize / 2 - sampleSize / 10); + expect(nbInControlGroup).to.be.lessThan(sampleSize / 2 + sampleSize / 10); + }); }); - it('should not expose ID when in control group', function () { - testConfig.params.abTesting = { - enabled: true, - controlGroupPct: 0.5 - }; + describe('Decode', function() { + it('should expose ID when A/B testing is off', function () { + testConfig.params.abTesting = { + enabled: false, + controlGroupPct: 0.5 + }; - let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); - expect(decoded).to.deep.equal(expectedDecodedObjectWithoutIdAbOn); + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOff); + }); + + it('should expose ID when no one is in control group', function () { + testConfig.params.abTesting = { + enabled: true, + controlGroupPct: 0 + }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithIdAbOn); + }); + + it('should not expose ID when everyone is in control group', function () { + testConfig.params.abTesting = { + enabled: true, + controlGroupPct: 1 + }; + + let decoded = id5IdSubmodule.decode(ID5_STORED_OBJ, testConfig); + expect(decoded).to.deep.equal(expectedDecodedObjectWithoutIdAbOn); + }); }); }); }); From 727bf206f4e7c50215a3ae6ae2920cb304336953 Mon Sep 17 00:00:00 2001 From: bjorn-lw <32431346+bjorn-lw@users.noreply.github.com> Date: Mon, 8 Feb 2021 15:32:53 +0100 Subject: [PATCH 161/181] Shared ID gdpr support (#6275) * SharedId gdpr support * Reverted commented locally failing tests --- modules/id5IdSystem.js | 3 +- modules/pubCommonIdSystem.js | 20 +++++++-- modules/sharedIdSystem.js | 21 +++++++-- modules/userId/index.js | 3 +- test/spec/modules/sharedIdSystem_spec.js | 55 ++++++++++++++++++++++++ test/spec/modules/userId_spec.js | 53 +++++++++++++++++++++++ 6 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 test/spec/modules/sharedIdSystem_spec.js diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index bf2365b938c..6dade7d01f0 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -166,10 +166,11 @@ export const id5IdSubmodule = { * It's permissible to return neither, one, or both fields. * @function extendId * @param {SubmoduleConfig} config + * @param {ConsentData|undefined} consentData * @param {Object} cacheIdObj - existing id, if any * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ - extendId(config, cacheIdObj) { + extendId(config, consentData, cacheIdObj) { const partnerId = (config && config.params && config.params.partner) || 0; incrementNb(partnerId); return cacheIdObj; diff --git a/modules/pubCommonIdSystem.js b/modules/pubCommonIdSystem.js index cb0c07cefa8..339029120a8 100644 --- a/modules/pubCommonIdSystem.js +++ b/modules/pubCommonIdSystem.js @@ -136,6 +136,17 @@ function handleResponse(pubcid, callback, config) { } } +/** + * Builds and returns the shared Id URL with attached consent data if applicable + * @param {Object} consentData + * @return {string} + */ +function sharedIdUrl(consentData) { + if (!consentData || typeof consentData.gdprApplies !== 'boolean' || !consentData.gdprApplies) return SHAREDID_URL; + + return `${SHAREDID_URL}?gdpr=1&gdpr_consent=${consentData.consentString}` +} + /** * Wraps pixelCallback in order to call sharedid sync * @param {string} pubcid Pubcommon id value @@ -144,12 +155,12 @@ function handleResponse(pubcid, callback, config) { * @return {function(...[*]=)} */ -function getIdCallback(pubcid, pixelCallback, config) { +function getIdCallback(pubcid, pixelCallback, config, consentData) { return function (callback) { if (typeof pixelCallback === 'function') { pixelCallback(); } - ajax(SHAREDID_URL, handleResponse(pubcid, callback, config), undefined, {method: 'GET', withCredentials: true}); + ajax(sharedIdUrl(consentData), handleResponse(pubcid, callback, config), undefined, {method: 'GET', withCredentials: true}); } } @@ -227,7 +238,7 @@ export const pubCommonIdSubmodule = { } const pixelCallback = this.makeCallback(pixelUrl, newId); - const combinedCallback = enableSharedId ? getIdCallback(newId, pixelCallback, config) : pixelCallback; + const combinedCallback = enableSharedId ? getIdCallback(newId, pixelCallback, config, consentData) : pixelCallback; return {id: newId, callback: combinedCallback}; }, @@ -247,10 +258,11 @@ export const pubCommonIdSubmodule = { * * @function * @param {SubmoduleParams} [config] + * @param {ConsentData|undefined} consentData * @param {Object} storedId existing id * @returns {IdResponse|undefined} */ - extendId: function(config = {}, storedId) { + extendId: function(config = {}, consentData, storedId) { const {params: {extend = false, pixelUrl, enableSharedId = SHAREDID_DEFAULT_STATE} = {}} = config; if (extend) { diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index 49cac46f1df..762454af5fa 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -276,6 +276,17 @@ function detectPrng(root) { return () => Math.random(); } +/** + * Builds and returns the shared Id URL with attached consent data if applicable + * @param {Object} consentData + * @return {string} + */ +function sharedIdUrl(consentData) { + if (!consentData || typeof consentData.gdprApplies !== 'boolean' || !consentData.gdprApplies) return ID_SVC; + + return `${ID_SVC}?gdpr=1&gdpr_consent=${consentData.consentString}` +} + /** @type {Submodule} */ export const sharedIdSubmodule = { /** @@ -303,12 +314,13 @@ export const sharedIdSubmodule = { * performs action to obtain id and return a value. * @function * @param {SubmoduleConfig} [config] + * @param {ConsentData|undefined} consentData * @returns {sharedId} */ - getId(config) { + getId(config, consentData) { const resp = function (callback) { utils.logInfo('SharedId: Sharedid doesnt exists, new cookie creation'); - ajax(ID_SVC, idGenerationCallback(callback), undefined, {method: 'GET', withCredentials: true}); + ajax(sharedIdUrl(consentData), idGenerationCallback(callback), undefined, {method: 'GET', withCredentials: true}); }; return {callback: resp}; }, @@ -316,10 +328,11 @@ export const sharedIdSubmodule = { /** * performs actions even if the id exists and returns a value * @param config + * @param consentData * @param storedId * @returns {{callback: *}} */ - extendId(config, storedId) { + extendId(config, consentData, storedId) { const configParams = (config && config.params) || {}; utils.logInfo('SharedId: Existing shared id ' + storedId.id); const resp = function (callback) { @@ -329,7 +342,7 @@ export const sharedIdSubmodule = { const sharedIdPayload = {}; sharedIdPayload.sharedId = storedId.id; const payloadString = JSON.stringify(sharedIdPayload); - ajax(ID_SVC, existingIdCallback(storedId, callback), payloadString, {method: 'POST', withCredentials: true}); + ajax(sharedIdUrl(consentData), existingIdCallback(storedId, callback), payloadString, {method: 'POST', withCredentials: true}); } }; return {callback: resp}; diff --git a/modules/userId/index.js b/modules/userId/index.js index 9294311de69..da2c7b225fa 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -28,6 +28,7 @@ * It's permissible to return neither, one, or both fields. * @name Submodule#extendId * @param {SubmoduleConfig} config + * @param {ConsentData|undefined} consentData * @param {Object} storedId - existing id, if any * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ @@ -621,7 +622,7 @@ function populateSubmoduleId(submodule, consentData, storedConsentData, forceRef response = submodule.submodule.getId(submodule.config, consentData, storedId); } else if (typeof submodule.submodule.extendId === 'function') { // If the id exists already, give submodule a chance to decide additional actions that need to be taken - response = submodule.submodule.extendId(submodule.config, storedId); + response = submodule.submodule.extendId(submodule.config, consentData, storedId); } if (utils.isPlainObject(response)) { diff --git a/test/spec/modules/sharedIdSystem_spec.js b/test/spec/modules/sharedIdSystem_spec.js new file mode 100644 index 00000000000..904d6fe1c78 --- /dev/null +++ b/test/spec/modules/sharedIdSystem_spec.js @@ -0,0 +1,55 @@ +import { + sharedIdSubmodule, +} from 'modules/sharedIdSystem.js'; +import { server } from 'test/mocks/xhr.js'; + +let expect = require('chai').expect; + +describe('SharedId System', function() { + const SHAREDID_RESPONSE = {sharedId: 'testsharedid'}; + + describe('Xhr Requests from getId()', function() { + let callbackSpy = sinon.spy(); + + beforeEach(function() { + callbackSpy.resetHistory(); + }); + + afterEach(function () { + + }); + + it('should call shared id endpoint without consent data and handle a valid response', function () { + let submoduleCallback = sharedIdSubmodule.getId(undefined, undefined).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + expect(request.url).to.equal('https://id.sharedid.org/id'); + expect(request.withCredentials).to.be.true; + + request.respond(200, {}, JSON.stringify(SHAREDID_RESPONSE)); + + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg.id).to.equal(SHAREDID_RESPONSE.sharedId); + }); + + it('should call shared id endpoint with consent data and handle a valid response', function () { + let consentData = { + gdprApplies: true, + consentString: 'abc12345234', + }; + + let submoduleCallback = sharedIdSubmodule.getId(undefined, consentData).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + expect(request.url).to.equal('https://id.sharedid.org/id?gdpr=1&gdpr_consent=abc12345234'); + expect(request.withCredentials).to.be.true; + + request.respond(200, {}, JSON.stringify(SHAREDID_RESPONSE)); + + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg.id).to.equal(SHAREDID_RESPONSE.sharedId); + }); + }); +}); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index ed592c0cba5..0fcf03dde67 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -2037,6 +2037,8 @@ describe('User ID', function () { coreStorage.setCookie('pubcid_sharedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('unifiedid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('_parrable_eid', '', EXPIRED_COOKIE_DATE); + resetConsentData(); + delete window.__tcfapi; }); it('pubcid callback with url', function () { @@ -2171,6 +2173,57 @@ describe('User ID', function () { expect(server.requests[0].url).to.equal('https://id.sharedid.org/id'); expect(coreStorage.getCookie('pubcid_sharedid')).to.be.null; }); + + it('verify sharedid called with consent data when gdpr applies', function () { + let adUnits = [getAdUnitMock()]; + let customCfg = getConfigMock(['pubCommonId', 'pubcid', 'cookie']); + let consentConfig = { + cmpApi: 'iab', + timeout: 7500, + allowAuctionWithoutConsent: false + }; + customCfg = addConfig(customCfg, 'params', {pixelUrl: '/any/pubcid/url', enableSharedId: true}); + + server.respondWith('https://id.sharedid.org/id?gdpr=1&gdpr_consent=abc12345234', function(xhr) { + xhr.respond(200, {}, '{"sharedId":"testsharedid"}'); + }); + server.respondImmediately = true; + + let testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + vendor: {consents: {887: true}}, + purpose: { + consents: { + 1: true + } + } + }; + + window.__tcfapi = function () { }; + sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + setSubmoduleRegistry([pubCommonIdSubmodule]); + init(config); + config.setConfig(customCfg); + setConsentConfig(consentConfig); + + consentManagementRequestBidsHook(() => { + }, {}); + requestBidsHook((config) => { + }, {adUnits}); + + expect(utils.triggerPixel.called).to.be.false; + events.emit(CONSTANTS.EVENTS.AUCTION_END, {}); + expect(utils.triggerPixel.getCall(0).args[0]).to.include('/any/pubcid/url'); + + expect(server.requests[0].url).to.equal('https://id.sharedid.org/id?gdpr=1&gdpr_consent=abc12345234'); + expect(coreStorage.getCookie('pubcid_sharedid')).to.equal('testsharedid'); + }); }); describe('Set cookie behavior', function () { From ab9a8d276867182ff2fd191b6cd25c6182d55f55 Mon Sep 17 00:00:00 2001 From: Lemma Dev <54662130+lemmadev@users.noreply.github.com> Date: Mon, 8 Feb 2021 22:05:21 +0530 Subject: [PATCH 162/181] Lemma:set mediaType key value (#6006) * lemmaBidAdapter.js Added lemma bid adapter file * lemmaBidAdapter.md Added lemma bid adapter md file * lemmaBidAdapter_spec.js Added lemma bid adapter test spec file * Update lemmaBidAdapter.js Fixed automated code review alert comparison between inconvertible types * Update lemmaBidAdapter.js Fixed review changes * Update lemmaBidAdapter.md Correct parameter value. * Update lemmaBidAdapter.js Lemma Bid Adapter - v3.0 compliance * Update lemmaBidAdapter_spec.js Lemma Bid Adapter - v3.0 compliance * Update lemmaBidAdapter.md Lemma Bid Adapter - v3.0 compliance * Update lemmaBidAdapter.js Added user sync support into bid adapter. * updated include modules file extension. updated include modules js file extension. * Update lemmaBidAdapter_spec.js Added unit test for user sync feature. * Update lemmaBidAdapter.js Fixed format error. * Update lemmaBidAdapter_spec.js Fixed format error and typo error. * Set mediaType key value into bid object Set mediaType key value into the bid object. * Update lemmaBidAdapter.js remove duplicate function * Update lemmaBidAdapter.js Remove non supported code. * Update lemmaBidAdapter_spec.js Remove GDPR test cases. --- modules/lemmaBidAdapter.js | 27 +++++++++-------------- test/spec/modules/lemmaBidAdapter_spec.js | 18 --------------- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/modules/lemmaBidAdapter.js b/modules/lemmaBidAdapter.js index 5941802f97d..926761e5ab2 100644 --- a/modules/lemmaBidAdapter.js +++ b/modules/lemmaBidAdapter.js @@ -62,18 +62,6 @@ export var spec = { }, getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { let syncurl = USER_SYNC + 'pid=' + pubId; - - // Attaching GDPR Consent Params in UserSync url - if (gdprConsent) { - syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); - syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); - } - - // CCPA - if (uspConsent) { - syncurl += '&us_privacy=' + encodeURIComponent(uspConsent); - } - if (syncOptions.iframeEnabled) { return [{ type: 'iframe', @@ -122,9 +110,9 @@ function parseRTBResponse(request, response) { newBid.dealId = bid.dealid; } if (req.imp && req.imp.length > 0) { - newBid.mediaType = req.mediaType; req.imp.forEach(robj => { if (bid.impid === robj.id) { + _checkMediaType(bid.adm, newBid); switch (newBid.mediaType) { case BANNER: break; @@ -277,10 +265,6 @@ function _getDeviceObject(request) { function setOtherParams(request, ortbRequest) { var params = request && request.params ? request.params : null; - if (request && request.gdprConsent) { - ortbRequest.regs = { ext: { gdpr: request.gdprConsent.gdprApplies ? 1 : 0 } }; - ortbRequest.user = { ext: { consent: request.gdprConsent.consentString } }; - } if (params) { ortbRequest.tmax = params.tmax; ortbRequest.bcat = params.bcat; @@ -424,4 +408,13 @@ function parse(rawResp) { return null; } +function _checkMediaType(adm, newBid) { + // Create a regex here to check the strings + var videoRegex = new RegExp(/VAST.*version/); + if (videoRegex.test(adm)) { + newBid.mediaType = VIDEO; + } else { + newBid.mediaType = BANNER; + } +} registerBidder(spec); diff --git a/test/spec/modules/lemmaBidAdapter_spec.js b/test/spec/modules/lemmaBidAdapter_spec.js index a00c25d126c..a22f5650e50 100644 --- a/test/spec/modules/lemmaBidAdapter_spec.js +++ b/test/spec/modules/lemmaBidAdapter_spec.js @@ -346,24 +346,6 @@ describe('lemmaBidAdapter', function() { type: 'iframe', url: syncurl_iframe }]); }); - - it('CCPA/USP', function() { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&us_privacy=1NYN` - }]); - }); - - it('GDPR', function() { - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo` - }]); - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: false, consentString: 'foo' }, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&gdpr=0&gdpr_consent=foo` - }]); - expect(spec.getUserSyncs({ iframeEnabled: true }, {}, { gdprApplies: true, consentString: undefined }, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=` - }]); - }); }); }); }); From 19b4885892a14a8177c3b45284351b3492b1b8ad Mon Sep 17 00:00:00 2001 From: ix-certification Date: Mon, 8 Feb 2021 11:41:28 -0500 Subject: [PATCH 163/181] added support for addtlConsent (#6005) Co-authored-by: Ix-Prebid-Support --- modules/ixBidAdapter.js | 6 ++++++ test/spec/modules/ixBidAdapter_spec.js | 27 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 97341fbfd78..48a2e741c5a 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -341,6 +341,12 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r.user.ext = { consent: gdprConsent.consentString || '' }; + + if (gdprConsent.hasOwnProperty('addtlConsent') && gdprConsent.addtlConsent) { + r.user.ext.consented_providers_settings = { + consented_providers: gdprConsent.addtlConsent + } + } } } diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index edbcc5725ac..fca3f8f992e 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -1772,5 +1772,32 @@ describe('IndexexchangeAdapter', function () { expect(requestWithConsent.regs.ext.gdpr).to.equal(1); expect(requestWithConsent.regs.ext.us_privacy).to.equal('1YYN'); }); + + it('should contain `consented_providers_settings.consented_providers` & consent on user.ext when both are provided', function () { + const options = { + gdprConsent: { + consentString: '3huaa11=qu3198ae', + addtlConsent: '1~1.35.41.101', + } + }; + + const validBidWithConsent = spec.buildRequests(DEFAULT_BANNER_VALID_BID, options); + const requestWithConsent = JSON.parse(validBidWithConsent[0].data.r); + expect(requestWithConsent.user.ext.consented_providers_settings.consented_providers).to.equal('1~1.35.41.101'); + expect(requestWithConsent.user.ext.consent).to.equal('3huaa11=qu3198ae'); + }); + + it('should not contain `consented_providers_settings.consented_providers` on user.ext when consent is not provided', function () { + const options = { + gdprConsent: { + addtlConsent: '1~1.35.41.101', + } + }; + + const validBidWithConsent = spec.buildRequests(DEFAULT_BANNER_VALID_BID, options); + const requestWithConsent = JSON.parse(validBidWithConsent[0].data.r); + expect(utils.deepAccess(requestWithConsent, 'user.ext.consented_providers_settings')).to.not.exist; + expect(utils.deepAccess(requestWithConsent, 'user.ext.consent')).to.not.exist; + }); }); }); From 1654d53b8c7676d652c3bd35c74ca7c5413bf3a7 Mon Sep 17 00:00:00 2001 From: Missena <78362128+dev-missena@users.noreply.github.com> Date: Mon, 8 Feb 2021 22:49:01 +0100 Subject: [PATCH 164/181] Add bid adapter for Missena (#6247) * adds support for getFloor of video mediaTypes * adds test for calling getFloor with correct mediaType * checks that _getFloor converts string floors to float * Add bid adapter for Missena * Use publisher demo token in tests * Add Missena global vendor ID to spec * Use apiKey in the current bidRequest * Add referer info to payload Co-authored-by: Nick Llerandi Co-authored-by: Brandon Ling <51931757+blingster7@users.noreply.github.com> Co-authored-by: Brandon Ling --- modules/missenaBidAdapter.js | 94 ++++++++++++++ modules/missenaBidAdapter.md | 70 +++++++++++ test/spec/modules/missenaBidAdapter_spec.js | 131 ++++++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 modules/missenaBidAdapter.js create mode 100644 modules/missenaBidAdapter.md create mode 100644 test/spec/modules/missenaBidAdapter_spec.js diff --git a/modules/missenaBidAdapter.js b/modules/missenaBidAdapter.js new file mode 100644 index 00000000000..2b1d6bdb118 --- /dev/null +++ b/modules/missenaBidAdapter.js @@ -0,0 +1,94 @@ +import * as utils from '../src/utils.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'missena'; +const ENDPOINT_URL = 'https://bid.missena.io/'; + +export const spec = { + aliases: [BIDDER_CODE], + code: BIDDER_CODE, + gvlid: 687, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return typeof bid == 'object' && !!bid.params.apiKey; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + return validBidRequests.map((bidRequest) => { + const payload = { + request_id: bidRequest.bidId, + timeout: bidderRequest.timeout, + }; + + if (bidderRequest && bidderRequest.refererInfo) { + payload.referer = bidderRequest.refererInfo.referer; + payload.referer_canonical = bidderRequest.refererInfo.canonicalUrl; + } + + if (bidderRequest && bidderRequest.gdprConsent) { + payload.consent_string = bidderRequest.gdprConsent.consentString; + payload.consent_required = bidderRequest.gdprConsent.gdprApplies; + } + + return { + method: 'POST', + url: + ENDPOINT_URL + + '?' + + utils.formatQS({ + t: bidRequest.params.apiKey, + }), + data: JSON.stringify(payload), + }; + }); + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + const response = serverResponse.body; + + if (response && !response.timeout && !!response.ad) { + bidResponses.push(response); + } + + return bidResponses; + }, + + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ + onTimeout: function onTimeout(timeoutData) { + utils.logInfo('Missena - Timeout from adapter', timeoutData); + }, + + /** + * Register bidder specific code, which@ will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ + onBidWon: function (bid) { + utils.logInfo('Missena - Bid won', bid); + }, +}; + +registerBidder(spec); diff --git a/modules/missenaBidAdapter.md b/modules/missenaBidAdapter.md new file mode 100644 index 00000000000..d5fcacf04ab --- /dev/null +++ b/modules/missenaBidAdapter.md @@ -0,0 +1,70 @@ +# Overview + +``` +Module Name: Missena Bid Adapter +Module Type: Bidder Adapter +Maintainer: jney@missena.com +``` + +## Introduction + +Connects to Missena for bids. + +**Note:** this adapter doesn't support SafeFrame. + +Useful resources: + +- [README](../README.md#Build) +- [https://docs.prebid.org/dev-docs/bidder-adaptor.html](https://docs.prebid.org/dev-docs/bidder-adaptor.html) + +## Develop + +Setup the missena adapter in `integrationExamples/gpt/userId_example.html`. + +For example: + +```js +const AD_UNIT_CODE = "test-div"; +const PUBLISHER_MISSENA_TOKEN = "PA-34745704"; + +var adUnits = [ + { + code: AD_UNIT_CODE, + mediaTypes: { + banner: { + sizes: [1, 1], + }, + }, + bids: [ + { + bidder: "missena", + params: { + apiKey: PUBLISHER_MISSENA_TOKEN, + }, + }, + ], + }, +]; +``` + +Then start the demo app: + +```shell +gulp serve-fast --modules=missenaBidAdapter +``` + +And open [http://localhost:9999/integrationExamples/gpt/userId_example.html](http://localhost:9999/integrationExamples/gpt/userId_example.html) + +## Test + +```shell +gulp test --file test/spec/modules/missenaBidAdapter_spec.js +``` + +Add the `--watch` option to re-run unit tests whenever the source code changes. + +## Build + +```shell +gulp build --modules=missenaBidAdapter +``` diff --git a/test/spec/modules/missenaBidAdapter_spec.js b/test/spec/modules/missenaBidAdapter_spec.js new file mode 100644 index 00000000000..026e79c6d5a --- /dev/null +++ b/test/spec/modules/missenaBidAdapter_spec.js @@ -0,0 +1,131 @@ +import { expect } from 'chai'; +import { spec, _getPlatform } from 'modules/missenaBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('Missena Adapter', function () { + const adapter = newBidder(spec); + + const bidId = 'abc'; + + const bid = { + bidder: 'missena', + bidId: bidId, + sizes: [[1, 1]], + params: { + apiKey: 'PA-34745704', + }, + }; + + describe('codes', function () { + it('should return a bidder code of missena', function () { + expect(spec.code).to.equal('missena'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true if the apiKey param is present', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if the apiKey is missing', function () { + expect( + spec.isBidRequestValid(Object.assign(bid, { params: {} })) + ).to.equal(false); + }); + + it('should return false if the apiKey is an empty string', function () { + expect( + spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })) + ).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const consentString = 'AAAAAAAAA=='; + + const bidderRequest = { + gdprConsent: { + consentString: consentString, + gdprApplies: true, + }, + refererInfo: { + referer: 'https://referer', + canonicalUrl: 'https://canonical', + }, + }; + + const requests = spec.buildRequests([bid, bid], bidderRequest); + const request = requests[0]; + const payload = JSON.parse(request.data); + + it('should return as many server requests as bidder requests', function () { + expect(requests.length).to.equal(2); + }); + + it('should have a post method', function () { + expect(request.method).to.equal('POST'); + }); + + it('should send the bidder id', function () { + expect(payload.request_id).to.equal(bidId); + }); + + it('should send referer information to the request', function () { + expect(payload.referer).to.equal('https://referer'); + expect(payload.referer_canonical).to.equal('https://canonical'); + }); + + it('should send gdpr consent information to the request', function () { + expect(payload.consent_string).to.equal(consentString); + expect(payload.consent_required).to.equal(true); + }); + }); + + describe('interpretResponse', function () { + const serverResponse = { + requestId: bidId, + cpm: 0.5, + currency: 'USD', + ad: '', + }; + + const serverTimeoutResponse = { + requestId: bidId, + timeout: true, + ad: '', + }; + + const serverEmptyAdResponse = { + requestId: bidId, + cpm: 0.5, + currency: 'USD', + ad: '', + }; + + it('should return a proper bid response', function () { + const result = spec.interpretResponse({ body: serverResponse }, bid); + + expect(result.length).to.equal(1); + + expect(Object.keys(result[0])).to.have.members( + Object.keys(serverResponse) + ); + }); + + it('should return an empty response when the server answers with a timeout', function () { + const result = spec.interpretResponse( + { body: serverTimeoutResponse }, + bid + ); + expect(result).to.deep.equal([]); + }); + + it('should return an empty response when the server answers with an empty ad', function () { + const result = spec.interpretResponse( + { body: serverEmptyAdResponse }, + bid + ); + expect(result).to.deep.equal([]); + }); + }); +}); From c27ff14b31c5a7a08695eda7dc929c768dbbb809 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Tue, 9 Feb 2021 02:12:10 -0800 Subject: [PATCH 165/181] Gulp test file example added in readme (#6287) --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 40df62ccee4..3e541080bf9 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,11 @@ To run the unit tests: gulp test ``` +To run the unit tests for a perticular file (example for pubmaticBidAdapter_spec.js): +```bash +gulp test --file "test/spec/modules/pubmaticBidAdapter_spec.js" +``` + To generate and view the code coverage reports: ```bash From dde585bbd38d16157668a955690b00989dcf100b Mon Sep 17 00:00:00 2001 From: Pooja Pasawala Date: Tue, 9 Feb 2021 02:38:53 -0800 Subject: [PATCH 166/181] Sharethrough: Add support for ID5, Shared ID, and Live Intent ID (#6261) * Update prebid adapter universal ids to include ID5, SharedID, and LiveIntent ID. [#176447070](https://www.pivotaltracker.com/story/show/176447070) Co-authored-by: Mathieu Pheulpin * Addressing review [#176447070] * Quick rewrite [#176447070] * Address ID5 review, forward linkType to adserver * Reformatting SharedID to align with ID5 Co-authored-by: Mathieu Pheulpin --- modules/sharethroughBidAdapter.js | 48 +++++++++++++------ .../modules/sharethroughBidAdapter_spec.js | 42 +++++++++++++++- 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 89484b1c68b..24be8673615 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,7 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; -const VERSION = '3.3.0'; +const VERSION = '3.3.1'; const BIDDER_CODE = 'sharethrough'; const STR_ENDPOINT = 'https://btlr.sharethrough.com/WYu2BXv1/v1'; const DEFAULT_SIZE = [1, 1]; @@ -31,6 +31,8 @@ export const sharethroughAdapterSpec = { strVersion: VERSION }; + Object.assign(query, handleUniversalIds(bidRequest)); + const nonHttp = sharethroughInternal.getProtocol().indexOf('http') < 0; query.secure = nonHttp || (sharethroughInternal.getProtocol().indexOf('https') > -1); @@ -46,20 +48,6 @@ export const sharethroughAdapterSpec = { query.us_privacy = bidderRequest.uspConsent } - if (bidRequest.userId && bidRequest.userId.tdid) { - query.ttduid = bidRequest.userId.tdid; - } - - if (bidRequest.userId && bidRequest.userId.pubcid) { - query.pubcid = bidRequest.userId.pubcid; - } else if (bidRequest.crumbs && bidRequest.crumbs.pubcid) { - query.pubcid = bidRequest.crumbs.pubcid; - } - - if (bidRequest.userId && bidRequest.userId.idl_env) { - query.idluid = bidRequest.userId.idl_env; - } - if (bidRequest.schain) { query.schain = JSON.stringify(bidRequest.schain); } @@ -147,6 +135,36 @@ export const sharethroughAdapterSpec = { onSetTargeting: (bid) => {} }; +function handleUniversalIds(bidRequest) { + if (!bidRequest.userId) return {}; + + const universalIds = {}; + + const ttd = utils.deepAccess(bidRequest, 'userId.tdid'); + if (ttd) universalIds.ttduid = ttd; + + const pubc = utils.deepAccess(bidRequest, 'userId.pubcid') || utils.deepAccess(bidRequest, 'crumbs.pubcid'); + if (pubc) universalIds.pubcid = pubc; + + const idl = utils.deepAccess(bidRequest, 'userId.idl_env'); + if (idl) universalIds.idluid = idl; + + const id5 = utils.deepAccess(bidRequest, 'userId.id5id.uid'); + if (id5) { + universalIds.id5uid = { id: id5 }; + const id5link = utils.deepAccess(bidRequest, 'userId.id5id.ext.linkType'); + if (id5link) universalIds.id5uid.linkType = id5link; + } + + const lipb = utils.deepAccess(bidRequest, 'userId.lipb.lipbid'); + if (lipb) universalIds.liuid = lipb; + + const shd = utils.deepAccess(bidRequest, 'userId.sharedid'); + if (shd) universalIds.shduid = shd; // object with keys: id & third + + return universalIds; +} + function getLargestSize(sizes) { function area(size) { return size[0] * size[1]; diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index cd9071a6098..b3451a09dde 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { sharethroughAdapterSpec, sharethroughInternal } from 'modules/sharethroughBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as utils from '../../../src/utils.js'; const spec = newBidder(sharethroughAdapterSpec).getSpec(); const bidRequests = [ @@ -15,7 +16,20 @@ const bidRequests = [ userId: { tdid: 'fake-tdid', pubcid: 'fake-pubcid', - idl_env: 'fake-identity-link' + idl_env: 'fake-identity-link', + id5id: { + uid: 'fake-id5id', + ext: { + linkType: 2 + } + }, + sharedid: { + id: 'fake-sharedid', + third: 'fake-sharedthird' + }, + lipb: { + lipbid: 'fake-lipbid' + } }, crumbs: { pubcid: 'fake-pubcid-in-crumbs-obj' @@ -329,6 +343,15 @@ describe('sharethrough adapter spec', function() { expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); }); + it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + + ' crumbs object of the bidrequest', function() { + const bidData = utils.deepClone(bidRequests); + delete bidData[0].userId.pubcid; + + const bidRequest = spec.buildRequests(bidData)[0]; + expect(bidRequest.data.pubcid).to.eq('fake-pubcid-in-crumbs-obj'); + }); + it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + ' crumbs object of the bidrequest', function() { const bidRequest = spec.buildRequests(bidRequests)[0]; @@ -341,6 +364,23 @@ describe('sharethrough adapter spec', function() { expect(bidRequest.data.idluid).to.eq('fake-identity-link'); }); + it('should add the id5uid parameter if a bid request contains a value for ID5', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.id5uid.id).to.eq('fake-id5id'); + expect(bidRequest.data.id5uid.linkType).to.eq(2); + }); + + it('should add the shduid parameter if a bid request contains a value for Shared ID', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.shduid.id).to.eq('fake-sharedid'); + expect(bidRequest.data.shduid.third).to.eq('fake-sharedthird'); + }); + + it('should add the liuid parameter if a bid request contains a value for LiveIntent ID', function() { + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.liuid).to.eq('fake-lipbid'); + }); + it('should add Sharethrough specific parameters', function() { const builtBidRequests = spec.buildRequests(bidRequests); expect(builtBidRequests[0]).to.deep.include({ From 73cfeb537a9ffbca707e80c1c171d14f57b46869 Mon Sep 17 00:00:00 2001 From: lowendavid <66423906+lowendavid@users.noreply.github.com> Date: Tue, 9 Feb 2021 11:39:14 +0100 Subject: [PATCH 167/181] SmartAdServer Bid Adapter: image sync and noAd (#6236) * SIM-889 Now we have image based sync * SIM-889 Added test to check noad and image sync * SIM-889 Fixing indenting issues --- modules/smartadserverBidAdapter.js | 12 +++- .../modules/smartadserverBidAdapter_spec.js | 69 ++++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index ed9003e3b4d..bb9364c72c3 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -135,7 +135,7 @@ export const spec = { const bidResponses = []; let response = serverResponse.body; try { - if (response) { + if (response && !response.isNoAd) { const bidRequest = JSON.parse(bidRequestString.data); let bidResponse = { @@ -147,7 +147,8 @@ export const spec = { dealId: response.dealId, currency: response.currency, netRevenue: response.isNetCpm, - ttl: response.ttl + ttl: response.ttl, + dspPixels: response.dspPixels }; if (bidRequest.mediaType === VIDEO) { @@ -182,6 +183,13 @@ export const spec = { type: 'iframe', url: serverResponses[0].body.cSyncUrl }); + } else if (syncOptions.pixelEnabled && serverResponses.length > 0 && serverResponses[0].body.dspPixels !== undefined) { + serverResponses[0].body.dspPixels.forEach(function(pixel) { + syncs.push({ + type: 'image', + url: pixel + }); + }); } return syncs; } diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index e3bca240a47..749de43b9af 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -115,7 +115,41 @@ describe('Smart bid adapter tests', function () { ttl: 300, adUrl: 'http://awesome.fake.url', ad: '< --- awesome script --- >', - cSyncUrl: 'http://awesome.fake.csync.url' + cSyncUrl: 'http://awesome.fake.csync.url', + isNoAd: false + } + }; + + var BID_RESPONSE_IS_NO_AD = { + body: { + cpm: 12, + width: 300, + height: 250, + creativeId: 'zioeufg', + currency: 'GBP', + isNetCpm: true, + ttl: 300, + adUrl: 'http://awesome.fake.url', + ad: '< --- awesome script --- >', + cSyncUrl: 'http://awesome.fake.csync.url', + isNoAd: true + } + }; + + var BID_RESPONSE_IMAGE_SYNC = { + body: { + cpm: 12, + width: 300, + height: 250, + creativeId: 'zioeufg', + currency: 'GBP', + isNetCpm: true, + ttl: 300, + adUrl: 'http://awesome.fake.url', + ad: '< --- awesome script --- >', + cSyncUrl: 'http://awesome.fake.csync.url', + isNoAd: false, + dspPixels: ['pixelOne', 'pixelTwo', 'pixelThree'] } }; @@ -149,6 +183,18 @@ describe('Smart bid adapter tests', function () { expect(requestContent).to.have.property('ckid').and.to.equal(42); }); + it('Verify parse response with no ad', function () { + const request = spec.buildRequests(DEFAULT_PARAMS); + const bids = spec.interpretResponse(BID_RESPONSE_IS_NO_AD, request[0]); + expect(bids).to.have.lengthOf(0); + + expect(function () { + spec.interpretResponse(BID_RESPONSE_IS_NO_AD, { + data: 'invalid Json' + }) + }).to.not.throw(); + }); + it('Verify parse response', function () { const request = spec.buildRequests(DEFAULT_PARAMS); const bids = spec.interpretResponse(BID_RESPONSE, request[0]); @@ -258,6 +304,27 @@ describe('Smart bid adapter tests', function () { expect(syncs).to.have.lengthOf(0); }); + it('Verifies user sync using dspPixels', function () { + var syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, [BID_RESPONSE_IMAGE_SYNC]); + expect(syncs).to.have.lengthOf(3); + expect(syncs[0].type).to.equal('image'); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: false + }, [BID_RESPONSE_IMAGE_SYNC]); + expect(syncs).to.have.lengthOf(0); + + syncs = spec.getUserSyncs({ + iframeEnabled: false, + pixelEnabled: true + }, []); + expect(syncs).to.have.lengthOf(0); + }); + describe('gdpr tests', function () { afterEach(function () { config.resetConfig(); From fae47aab59b29b54421b57cfc22ab9271b436ebf Mon Sep 17 00:00:00 2001 From: iskmerof Date: Wed, 10 Feb 2021 06:19:01 -0500 Subject: [PATCH 168/181] Add client Alias Adkernel (#6291) Adding "bcm" alias to Adkernel adapter --- modules/adkernelBidAdapter.js | 2 +- test/spec/modules/adkernelBidAdapter_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 20ed65fe2e2..03fa5c2b2d9 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -52,7 +52,7 @@ const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => { export const spec = { code: 'adkernel', - aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon', 'andbeyond', 'adbite', 'houseofpubs', 'torchad', 'stringads'], + aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon', 'andbeyond', 'adbite', 'houseofpubs', 'torchad', 'stringads', 'bcm'], supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 4454aa00a71..0d772423a22 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -556,7 +556,7 @@ describe('Adkernel adapter', function () { describe('adapter configuration', () => { it('should have aliases', () => { - expect(spec.aliases).to.have.lengthOf(11); + expect(spec.aliases).to.have.lengthOf(12); }); }); From 4a7b465d6bd640046b7da3675e203082caed2cd4 Mon Sep 17 00:00:00 2001 From: Ian Flournoy Date: Wed, 10 Feb 2021 06:55:26 -0500 Subject: [PATCH 169/181] [ParrableIdSystem] Add GVLID and handle TC Consent data (#6283) * Added GDPR support * Remove forgotten .only Co-authored-by: Victor --- modules/parrableIdSystem.js | 22 ++++++++++++--- test/spec/modules/parrableIdSystem_spec.js | 32 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index afaf794513e..f072de8736f 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -14,12 +14,13 @@ import { getStorageManager } from '../src/storageManager.js'; const PARRABLE_URL = 'https://h.parrable.com/prebid'; const PARRABLE_COOKIE_NAME = '_parrable_id'; +const PARRABLE_GVLID = 928; const LEGACY_ID_COOKIE_NAME = '_parrable_eid'; const LEGACY_OPTOUT_COOKIE_NAME = '_parrable_optout'; const ONE_YEAR_MS = 364 * 24 * 60 * 60 * 1000; const EXPIRE_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:00 GMT'; -const storage = getStorageManager(); +const storage = getStorageManager(PARRABLE_GVLID); function getExpirationDate() { const oneYearFromNow = new Date(utils.timestamp() + ONE_YEAR_MS); @@ -167,7 +168,7 @@ function shouldFilterImpression(configParams, parrableId) { return !isAllowed() || isBlocked(); } -function fetchId(configParams) { +function fetchId(configParams, gdprConsentData) { if (!isValidConfig(configParams)) return; let parrableId = readCookie(); @@ -183,6 +184,8 @@ function fetchId(configParams) { const eid = (parrableId) ? parrableId.eid : null; const refererInfo = getRefererInfo(); const uspString = uspDataHandler.getConsentData(); + const gdprApplies = (gdprConsentData && typeof gdprConsentData.gdprApplies === 'boolean' && gdprConsentData.gdprApplies); + const gdprConsentString = (gdprConsentData && gdprApplies && gdprConsentData.consentString) || ''; const partners = configParams.partners || configParams.partner const trackers = typeof partners === 'string' ? partners.split(',') @@ -198,6 +201,7 @@ function fetchId(configParams) { const searchParams = { data: encodeBase64UrlSafe(btoa(JSON.stringify(data))), + gdpr: gdprApplies ? 1 : 0, _rand: Math.random() }; @@ -205,6 +209,10 @@ function fetchId(configParams) { searchParams.us_privacy = uspString; } + if (gdprApplies) { + searchParams.gdpr_consent = gdprConsentString; + } + const options = { method: 'GET', withCredentials: true @@ -251,7 +259,7 @@ function fetchId(configParams) { callback, id: parrableId }; -}; +} /** @type {Submodule} */ export const parrableIdSubmodule = { @@ -260,6 +268,12 @@ export const parrableIdSubmodule = { * @type {string} */ name: 'parrableId', + /** + * Global Vendor List ID + * @type {number} + */ + gvlid: PARRABLE_GVLID, + /** * decode the stored id value for passing to bid requests * @function @@ -282,7 +296,7 @@ export const parrableIdSubmodule = { */ getId(config, gdprConsentData, currentStoredId) { const configParams = (config && config.params) || {}; - return fetchId(configParams); + return fetchId(configParams, gdprConsentData); } }; diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 5a10529778c..f0aac7666f9 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -210,6 +210,38 @@ describe('Parrable ID System', function() { removeParrableCookie(); }); }); + + describe('GDPR consent', () => { + let callbackSpy = sinon.spy(); + + const config = { + params: { + partner: 'partner' + } + }; + + const gdprConsentTestCases = [ + { consentData: { gdprApplies: true, consentString: 'expectedConsentString' }, expected: { gdpr: 1, gdpr_consent: 'expectedConsentString' } }, + { consentData: { gdprApplies: false, consentString: 'expectedConsentString' }, expected: { gdpr: 0 } }, + { consentData: { gdprApplies: true, consentString: undefined }, expected: { gdpr: 1, gdpr_consent: '' } }, + { consentData: { gdprApplies: 'yes', consentString: 'expectedConsentString' }, expected: { gdpr: 0 } }, + { consentData: undefined, expected: { gdpr: 0 } } + ]; + + gdprConsentTestCases.forEach((testCase, index) => { + it(`should call user sync url with the gdprConsent - case ${index}`, () => { + parrableIdSubmodule.getId(config, testCase.consentData).callback(callbackSpy); + + if (testCase.expected.gdpr === 1) { + expect(server.requests[0].url).to.contain('gdpr=' + testCase.expected.gdpr); + expect(server.requests[0].url).to.contain('gdpr_consent=' + testCase.expected.gdpr_consent); + } else { + expect(server.requests[0].url).to.contain('gdpr=' + testCase.expected.gdpr); + expect(server.requests[0].url).to.not.contain('gdpr_consent'); + } + }) + }); + }); }); describe('parrableIdSystem.decode()', function() { From 8e2501e881d6f2790d474e1393b56d9af5400478 Mon Sep 17 00:00:00 2001 From: Matt Kendall <1870166+mkendall07@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:24:04 -0500 Subject: [PATCH 170/181] 4.26.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d8b6af04379..97020126830 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.26.0-pre", + "version": "4.26.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From ff0b015c96c1c707ca66a66505a350c935adac62 Mon Sep 17 00:00:00 2001 From: Matt Kendall <1870166+mkendall07@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:38:25 -0500 Subject: [PATCH 171/181] 4.27.0-pre --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97020126830..45493de7aea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.26.0", + "version": "4.27.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From a037c5455b038b25c0be07a1625aac4761ef7f3e Mon Sep 17 00:00:00 2001 From: pnh-pubx <73683023+pnh-pubx@users.noreply.github.com> Date: Thu, 11 Feb 2021 04:34:49 +0530 Subject: [PATCH 172/181] Updated data mapping of winning bid and auction logs in pubxai analytics adapter (#6285) Co-authored-by: Phaneendra Hegde --- modules/pubxaiAnalyticsAdapter.js | 13 +- .../modules/pubxaiAnalyticsAdapter_spec.js | 164 ++++++------------ 2 files changed, 64 insertions(+), 113 deletions(-) diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index 7e2f5061621..894752d607b 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -27,9 +27,13 @@ var pubxaiAnalyticsAdapter = Object.assign(adapter( if (eventType === CONSTANTS.EVENTS.BID_TIMEOUT) { args.forEach(item => { mapBidResponse(item, 'timeout'); }); } else if (eventType === CONSTANTS.EVENTS.AUCTION_INIT) { - events.auctionInit = args; + events.floorDetail = {}; + if (typeof args.bidderRequests[0].bids[0] !== 'undefined' && typeof args.bidderRequests[0].bids[0].floorData !== 'undefined') { + Object.assign(events.floorDetail, args.bidderRequests[0].bids[0].floorData); + } auctionTimestamp = args.timestamp; } else if (eventType === CONSTANTS.EVENTS.BID_REQUESTED) { + events.bids = []; mapBidRequests(args).forEach(item => { events.bids.push(item) }); } else if (eventType === CONSTANTS.EVENTS.BID_RESPONSE) { mapBidResponse(args, 'response'); @@ -56,6 +60,8 @@ function mapBidRequests(params) { adUnitCode: bid.adUnitCode, requestId: bid.bidderRequestId, auctionId: bid.auctionId, + placementId: bid.params.placementId, + floorData: bid.floorData, transactionId: bid.transactionId, sizes: utils.parseSizesInput(bid.mediaTypes.banner.sizes).toString(), renderStatus: 1, @@ -134,8 +140,9 @@ pubxaiAnalyticsAdapter.shouldFireEventRequest = function (samplingRate = 1) { function send(data, status) { if (pubxaiAnalyticsAdapter.shouldFireEventRequest(initOptions.samplingRate)) { let location = utils.getWindowLocation(); - if (typeof data !== 'undefined' && typeof data.auctionInit !== 'undefined') { - Object.assign(data.auctionInit, { host: location.host, path: location.pathname, search: location.search }); + if (typeof data !== 'undefined') { + data.pageDetail = {}; + Object.assign(data.pageDetail, { host: location.host, path: location.pathname, search: location.search }); } data.initOptions = initOptions; diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index 91c81dcae8d..95245f2c6c9 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -27,6 +27,8 @@ describe('pubxai analytics adapter', function() { pubxId: '6c415fc0-8b0e-4cf5-be73-01526a4db625' }; + let location = utils.getWindowLocation(); + let prebidEvent = { 'auctionInit': { 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', @@ -219,6 +221,12 @@ describe('pubxai analytics adapter', function() { 'originalCpm': 0.5, 'originalCurrency': 'USD', 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false, 'floorValue': 0.4, 'floorRule': '/19968336/header-bid-tag-1|banner', 'floorCurrency': 'USD', @@ -381,6 +389,12 @@ describe('pubxai analytics adapter', function() { 'originalCpm': 0.5, 'originalCurrency': 'USD', 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false, 'floorValue': 0.4, 'floorRule': '/19968336/header-bid-tag-1|banner', 'floorCurrency': 'USD', @@ -449,6 +463,12 @@ describe('pubxai analytics adapter', function() { 'originalCpm': 0.5, 'originalCurrency': 'USD', 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false, 'floorValue': 0.4, 'floorRule': '/19968336/header-bid-tag-1|banner', 'floorCurrency': 'USD', @@ -488,9 +508,13 @@ describe('pubxai analytics adapter', function() { 'params': [{ 'placementId': 13144370 }] - } + }, + 'pageDetail': { + 'host': location.host, + 'path': location.pathname, + 'search': location.search + }, }; - let location = utils.getWindowLocation(); let expectedAfterBid = { 'bids': [{ @@ -509,6 +533,12 @@ describe('pubxai analytics adapter', function() { 'mediaType': 'banner', 'statusMessage': 'Bid available', 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false, 'floorValue': 0.4, 'floorRule': '/19968336/header-bid-tag-1|banner', 'floorCurrency': 'USD', @@ -527,118 +557,21 @@ describe('pubxai analytics adapter', function() { 'timeToRespond': 267, 'responseTimestamp': 1603865707449, 'platform': navigator.platform, + 'placementId': 13144370, 'deviceType': getDeviceType() }], - 'auctionInit': { + 'pageDetail': { 'host': location.host, 'path': location.pathname, - 'search': location.search, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'timestamp': 1603865707180, - 'auctionStatus': 'inProgress', - 'adUnits': [{ - 'code': '/19968336/header-bid-tag-1', - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } - }, - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 - }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'new model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloor', - 'fetchStatus': 'success' - } - }], - 'sizes': [ - [ - 300, - 250 - ] - ], - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294' - }], - 'adUnitCodes': [ - '/19968336/header-bid-tag-1' - ], - 'bidderRequests': [{ - 'bidderCode': 'appnexus', - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'bidderRequestId': '184cbc05bb90ba', - 'bids': [{ - 'bidder': 'appnexus', - 'params': { - 'placementId': 13144370 - }, - 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', - 'floorData': { - 'skipped': false, - 'skipRate': 0, - 'modelVersion': 'new model 1.0', - 'location': 'fetch', - 'floorProvider': 'PubXFloor', - 'fetchStatus': 'success' - }, - 'mediaTypes': { - 'banner': { - 'sizes': [ - [ - 300, - 250 - ] - ] - } - }, - 'adUnitCode': '/19968336/header-bid-tag-1', - 'transactionId': '41ec8eaf-3e7c-4a8b-8344-ab796ff6e294', - 'sizes': [ - [ - 300, - 250 - ] - ], - 'bidId': '248f9a4489835e', - 'bidderRequestId': '184cbc05bb90ba', - 'src': 'client', - 'bidRequestsCount': 1, - 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 - }], - 'auctionStart': 1603865707180, - 'timeout': 1000, - 'refererInfo': { - 'referer': 'http://local-pnh.net:8080/stream/', - 'reachedTop': true, - 'isAmp': false, - 'numIframes': 0, - 'stack': [ - 'http://local-pnh.net:8080/stream/' - ], - 'canonicalUrl': null - }, - 'start': 1603865707182 - }], - 'noBids': [], - 'bidsReceived': [], - 'winningBids': [], - 'timeout': 1000, - 'config': { - 'samplingRate': '1', - 'pubxId': '6c415fc0-8b0e-4cf5-be73-01526a4db625' - } + 'search': location.search + }, + 'floorDetail': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false }, 'initOptions': initOptions }; @@ -660,6 +593,12 @@ describe('pubxai analytics adapter', function() { 'status': 'rendered', 'statusMessage': 'Bid available', 'floorData': { + 'fetchStatus': 'success', + 'floorProvider': 'PubXFloor', + 'location': 'fetch', + 'modelVersion': 'new model 1.0', + 'skipRate': 0, + 'skipped': false, 'floorValue': 0.4, 'floorRule': '/19968336/header-bid-tag-1|banner', 'floorCurrency': 'USD', @@ -680,6 +619,11 @@ describe('pubxai analytics adapter', function() { 'platform': navigator.platform, 'deviceType': getDeviceType() }, + 'pageDetail': { + 'host': location.host, + 'path': location.pathname, + 'search': location.search + }, 'initOptions': initOptions } From c9e8869ddd867a475efa4531d594ed422252aa8c Mon Sep 17 00:00:00 2001 From: TheMediaGrid <44166371+TheMediaGrid@users.noreply.github.com> Date: Thu, 11 Feb 2021 14:41:38 +0300 Subject: [PATCH 173/181] Grid Bid Adapter: Added video protocols to the ad request (#6299) --- modules/gridBidAdapter.js | 6 +++++- test/spec/modules/gridBidAdapter_spec.js | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index 6e610b67e0e..b296fe39ae4 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -365,7 +365,7 @@ function _addBidResponse(serverBid, bidRequest, bidResponses) { } function createVideoRequest(bid, mediaType) { - const {playerSize, mimes, durationRangeSec} = mediaType; + const {playerSize, mimes, durationRangeSec, protocols} = mediaType; const size = (playerSize || bid.sizes || [])[0]; if (!size) return; @@ -380,6 +380,10 @@ function createVideoRequest(bid, mediaType) { result.maxduration = durationRangeSec[1]; } + if (protocols && protocols.length) { + result.protocols = protocols; + } + return result; } diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index 084c67562e6..b4e87119111 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -105,7 +105,8 @@ describe('TheMediaGrid Adapter', function () { 'sizes': [[728, 90]], 'mediaTypes': { 'video': { - 'playerSize': [[400, 600]] + 'playerSize': [[400, 600]], + 'protocols': [1, 2, 3] }, 'banner': { 'sizes': [[728, 90]] @@ -281,7 +282,8 @@ describe('TheMediaGrid Adapter', function () { }, 'video': { 'w': 400, - 'h': 600 + 'h': 600, + 'protocols': [1, 2, 3] } }] }); From f672209110ef30b9cdf96048d4e8285d43716b4a Mon Sep 17 00:00:00 2001 From: bretg Date: Thu, 11 Feb 2021 06:52:17 -0500 Subject: [PATCH 174/181] Rubicon Bid Adapter: updated transactionId to auctionId for OpenRTB (#6298) --- modules/rubiconBidAdapter.js | 4 ++-- test/spec/modules/rubiconBidAdapter_spec.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 395b7a693b2..e9f25c9411e 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -153,7 +153,7 @@ export const spec = { test: config.getConfig('debug') ? 1 : 0, cur: ['USD'], source: { - tid: bidRequest.transactionId + tid: bidRequest.auctionId }, tmax: bidderRequest.timeout, imp: [{ @@ -424,7 +424,7 @@ export const spec = { 'rp_floor': (params.floor = parseFloat(params.floor)) >= 0.01 ? params.floor : undefined, 'rp_secure': '1', 'tk_flint': `${rubiConf.int_type || DEFAULT_INTEGRATION}_v$prebid.version$`, - 'x_source.tid': bidRequest.transactionId, + 'x_source.tid': bidRequest.auctionId, 'x_source.pchain': params.pchain, 'p_screen_res': _getScreenResolution(), 'tk_user_key': params.userId, diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 8c25d97dada..5b881299210 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -415,7 +415,7 @@ describe('the rubicon adapter', function () { 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, - 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', + 'x_source.tid': 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', 'x_source.pchain': 'GAM:11111-reseller1:22222', 'p_screen_res': /\d+x\d+/, 'tk_user_key': '12346', @@ -572,7 +572,7 @@ describe('the rubicon adapter', function () { 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, - 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', + 'x_source.tid': 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', 'p_screen_res': /\d+x\d+/, 'tk_user_key': '12346', 'kw': 'a,b,c', @@ -895,7 +895,7 @@ describe('the rubicon adapter', function () { 'rp_secure': /[01]/, 'rand': '0.1', 'tk_flint': INTEGRATION, - 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', + 'x_source.tid': 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', 'p_screen_res': /\d+x\d+/, 'tk_user_key': '12346', 'kw': 'a,b,c', @@ -2067,7 +2067,7 @@ describe('the rubicon adapter', function () { 'p_pos': 'atf', 'rp_secure': /[01]/, 'tk_flint': INTEGRATION, - 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', + 'x_source.tid': 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', 'p_screen_res': /\d+x\d+/, 'tk_user_key': '12346', 'kw': 'a,b,c', From 2702e964cd0d540002450ddacd776f7c1b635794 Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Fri, 12 Feb 2021 02:50:29 -0800 Subject: [PATCH 175/181] Fix for Issue 6117: Added Module Name in Build to Comments (#6297) --- gulpfile.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index b7a9e442a8c..51536992bd0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -31,7 +31,7 @@ const execa = require('execa'); var prebid = require('./package.json'); var dateString = 'Updated : ' + (new Date()).toISOString().substring(0, 10); -var banner = '/* <%= prebid.name %> v<%= prebid.version %>\n' + dateString + ' */\n'; +var banner = '/* <%= prebid.name %> v<%= prebid.version %>\n' + dateString + '\nModules: <%= modules %> */\n'; var port = 9999; const FAKE_SERVER_HOST = argv.host ? argv.host : 'localhost'; const FAKE_SERVER_PORT = 4444; @@ -157,12 +157,13 @@ function makeWebpackPkg() { const analyticsSources = helpers.getAnalyticsSources(); const moduleSources = helpers.getModulePaths(externalModules); + const modulesString = (externalModules.length > 0) ? externalModules.join(', ') : 'All available modules in current version.'; return gulp.src([].concat(moduleSources, analyticsSources, 'src/prebid.js')) .pipe(helpers.nameModules(externalModules)) .pipe(webpackStream(cloned, webpack)) .pipe(uglify()) - .pipe(gulpif(file => file.basename === 'prebid-core.js', header(banner, { prebid: prebid }))) + .pipe(gulpif(file => file.basename === 'prebid-core.js', header(banner, { prebid: prebid, modules: modulesString }))) .pipe(gulp.dest('build/dist')); } From a6c3986c0a306f4dc50b149d4a5f617282741a96 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Fri, 12 Feb 2021 06:39:20 -0500 Subject: [PATCH 176/181] map tripleliftBidAdapter.js tl_source to bid.meta.mediaType (#6303) --- modules/tripleliftBidAdapter.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index f97165f3d1c..029cdd68331 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -322,6 +322,14 @@ function _buildResponseObject(bidderRequest, bid) { if (bid.adomain && bid.adomain.length) { bidResponse.meta.advertiserDomains = bid.adomain; } + + if (bid.tl_source && bid.tl_source == 'hdx') { + bidResponse.meta.mediaType = 'banner'; + } + + if (bid.tl_source && bid.tl_source == 'tlx') { + bidResponse.meta.mediaType = 'native'; + } }; return bidResponse; } From 62ca969575e82eb1ae5416e412982d562d74c2d5 Mon Sep 17 00:00:00 2001 From: prebidtappx <77485538+prebidtappx@users.noreply.github.com> Date: Fri, 12 Feb 2021 15:54:43 +0100 Subject: [PATCH 177/181] Tappx Bid Adapter: new bidder adapter added (#6233) * ADD: tappx bid adapter * FIX: replace .includes by .indexOf * UPDATE: Expand the test coverage * FIX: format spacing tests * FIX: get auctionId from validBidRequests * UPDATE: add bannerMediaType tablet sizes * FIX: get timeout from bidderRequest.timeout * UPDATE: replace the way to get the hostname * UPDATE: adding support multiple bid requests in a single call * UPDATE: remove hardcoded test payload param Co-authored-by: marc_tappx --- modules/tappxBidAdapter.js | 289 ++++++++++++++++++++++ modules/tappxBidAdapter.md | 37 +++ test/spec/modules/tappxBidAdapter_spec.js | 86 +++++++ 3 files changed, 412 insertions(+) create mode 100644 modules/tappxBidAdapter.js create mode 100644 modules/tappxBidAdapter.md create mode 100644 test/spec/modules/tappxBidAdapter_spec.js diff --git a/modules/tappxBidAdapter.js b/modules/tappxBidAdapter.js new file mode 100644 index 00000000000..7782f151802 --- /dev/null +++ b/modules/tappxBidAdapter.js @@ -0,0 +1,289 @@ +'use strict'; + +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'tappx'; +const TTL = 360; +const CUR = 'USD'; +var HOST; +var hostDomain; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + if ((bid.params == null) || (bid.params.endpoint == null) || (bid.params.tappxkey == null)) { + utils.logWarn(`[TAPPX]: Please review the mandatory Tappx parameters. ${JSON.stringify(bid)}`); + return false; + } + return true; + }, + + /** + * Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test. + * Make a server request from the list of BidRequests. + * + * @param {*} validBidRequests + * @param {*} bidderRequest + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + let requests = []; + validBidRequests.forEach(oneValidRequest => { + requests.push(buildOneRequest(oneValidRequest, bidderRequest)); + }); + return requests; + }, + + /** + * Parse the response and generate one or more bid objects. + * + * @param {*} serverResponse + * @param {*} originalRequest + */ + interpretResponse: function(serverResponse, originalRequest) { + const responseBody = serverResponse.body; + if (!serverResponse.body) { + utils.logWarn('[TAPPX]: Empty response body HTTP 204, no bids'); + return []; + } + + const bids = []; + responseBody.seatbid.forEach(serverSeatBid => { + serverSeatBid.bid.forEach(serverBid => { + bids.push(interpretBannerBid(serverBid, originalRequest)); + }); + }); + + return bids; + }, + + /** + * If the publisher allows user-sync activity, the platform will call this function and the adapter may register pixels and/or iframe user syncs. + * + * @param {*} syncOptions + * @param {*} serverResponses + * @param {*} gdprConsent + */ + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let url = `https://${hostDomain}/cs/usersync.php?`; + + // GDPR & CCPA + if (gdprConsent) { + url += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + url += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (uspConsent) { + url += '&us_privacy=' + encodeURIComponent(uspConsent); + } + + // SyncOptions + if (syncOptions.iframeEnabled) { + url += '&type=iframe' + return [{ + type: 'iframe', + url: url + }]; + } else { + url += '&type=img' + return [{ + type: 'image', + url: url + }]; + } + } +} + +/** + * Parse the response and generate one bid object. + * + * @param {object} serverBid Bid by OpenRTB 2.5 + * @returns {object} Prebid banner bidObject + */ +function interpretBannerBid(serverBid, request) { + return { + requestId: request.bids.bidId, + cpm: serverBid.price, + currency: serverBid.cur ? serverBid.cur : CUR, + width: serverBid.w, + height: serverBid.h, + ad: serverBid.adm, + ttl: TTL, + creativeId: serverBid.crid, + netRevenue: true, + mediaType: BANNER, + } +} + +/** +* Build and makes the request +* +* @param {*} validBidRequests +* @param {*} bidderRequest +* @return response ad +*/ +function buildOneRequest(validBidRequests, bidderRequest) { + HOST = utils.deepAccess(validBidRequests, 'params.host'); + hostDomain = HOST.split('/', 1)[0]; + + const ENDPOINT = utils.deepAccess(validBidRequests, 'params.endpoint'); + const TAPPXKEY = utils.deepAccess(validBidRequests, 'params.tappxkey'); + const BIDFLOOR = utils.deepAccess(validBidRequests, 'params.bidfloor'); + const bannerMediaType = utils.deepAccess(validBidRequests, 'mediaTypes.banner'); + const { refererInfo } = bidderRequest; + + // let requests = []; + let payload = {}; + let publisher = {}; + let tagid; + let api = {}; + + // > App/Site object + if (utils.deepAccess(validBidRequests, 'params.app')) { + let app = {}; + app.name = utils.deepAccess(validBidRequests, 'params.app.name'); + app.bundle = utils.deepAccess(validBidRequests, 'params.app.bundle'); + app.domain = utils.deepAccess(validBidRequests, 'params.app.domain'); + publisher.name = utils.deepAccess(validBidRequests, 'params.app.publisher.name'); + publisher.domain = utils.deepAccess(validBidRequests, 'params.app.publisher.domain'); + tagid = `${app.name}_typeAdBanVid_${getOs()}`; + payload.app = app; + api[0] = utils.deepAccess(validBidRequests, 'params.api') ? utils.deepAccess(validBidRequests, 'params.api') : [3, 5]; + } else { + let site = {}; + site.name = (bidderRequest && refererInfo) ? utils.parseUrl(refererInfo.referer).hostname : window.location.hostname; + site.bundle = (bidderRequest && refererInfo) ? utils.parseUrl(refererInfo.referer).hostname : window.location.hostname; + site.domain = (bidderRequest && refererInfo) ? utils.parseUrl(refererInfo.referer).hostname : window.location.hostname; + publisher.name = (bidderRequest && refererInfo) ? utils.parseUrl(refererInfo.referer).hostname : window.location.hostname; + publisher.domain = (bidderRequest && refererInfo) ? utils.parseUrl(refererInfo.referer).hostname : window.location.hostname; + tagid = `${site.name}_typeAdBanVid_${getOs()}`; + payload.site = site; + } + // < App/Site object + + // > Imp object + let imp = {}; + let w; + let h; + + if (bannerMediaType) { + let banner = {}; + w = bannerMediaType.sizes[0][0]; + h = bannerMediaType.sizes[0][1]; + banner.w = w; + banner.h = h; + if ( + ((bannerMediaType.sizes[0].indexOf(480) >= 0) && (bannerMediaType.sizes[0].indexOf(320) >= 0)) || + ((bannerMediaType.sizes[0].indexOf(768) >= 0) && (bannerMediaType.sizes[0].indexOf(1024) >= 0))) { + banner.pos = 7 + } else { + banner.pos = 4 + } + + banner.api = api; + + let format = {}; + format[0] = {}; + format[0].w = w; + format[0].h = h; + banner.format = format; + + imp.banner = banner; + } + + imp.id = validBidRequests.bidId; + imp.tagid = tagid; + imp.secure = 1; + + imp.bidfloor = utils.deepAccess(validBidRequests, 'params.bidfloor'); + // < Imp object + + // > Device object + let device = {}; + // Mandatory + device.os = getOs(); + device.ip = 'peer'; + device.ua = navigator.userAgent; + device.ifa = validBidRequests.ifa; + + // Optional + device.h = screen.height; + device.w = screen.width; + device.dnt = utils.getDNT() ? 1 : 0; + device.language = getLanguage(); + device.make = navigator.vendor ? navigator.vendor : ''; + + let geo = {}; + geo.country = utils.deepAccess(validBidRequests, 'params.geo.country'); + // < Device object + + // > Params + let params = {}; + params.host = 'tappx.com'; + params.tappxkey = TAPPXKEY; + params.endpoint = ENDPOINT; + params.bidfloor = BIDFLOOR; + // < Params + + // > GDPR + let regs = {}; + regs.gdpr = 0; + if (!(bidderRequest.gdprConsent == null)) { + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { regs.gdpr = bidderRequest.gdprConsent.gdprApplies; } + if (regs.gdpr) { regs.consent = bidderRequest.gdprConsent.consentString; } + } + + // CCPA + regs.ext = {}; + if (!(bidderRequest.uspConsent == null)) { + regs.ext.us_privacy = bidderRequest.uspConsent; + } + + // COPPA compliance + if (config.getConfig('coppa') === true) { + regs.coppa = config.getConfig('coppa') === true ? 1 : 0; + } + // < GDPR + + // > Payload + payload.id = validBidRequests.auctionId; + payload.test = utils.deepAccess(validBidRequests, 'params.test') ? 1 : 0; + payload.at = 1; + payload.tmax = bidderRequest.timeout ? bidderRequest.timeout : 600; + payload.bidder = BIDDER_CODE; + payload.imp = [imp]; + + payload.device = device; + payload.params = params; + payload.regs = regs; + // < Payload + + return { + method: 'POST', + url: `https://${HOST}/${ENDPOINT}?type_cnn=prebidjs`, + data: JSON.stringify(payload), + bids: validBidRequests + }; +} + +function getLanguage() { + const language = navigator.language ? 'language' : 'userLanguage'; + return navigator[language].split('-')[0]; +} + +function getOs() { + let ua = navigator.userAgent; + if (ua == null) { return 'unknown'; } else if (ua.match(/(iPhone|iPod|iPad)/)) { return 'ios'; } else if (ua.match(/Android/)) { return 'android'; } else if (ua.match(/Window/)) { return 'windows'; } else { return 'unknown'; } +} + +registerBidder(spec); diff --git a/modules/tappxBidAdapter.md b/modules/tappxBidAdapter.md new file mode 100644 index 00000000000..d9ffd98b6c5 --- /dev/null +++ b/modules/tappxBidAdapter.md @@ -0,0 +1,37 @@ +# Overview +``` +Module Name: Tappx Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@tappx.com +``` + +# Description +Module that connects to :tappx demand sources. +Please use ```tappx``` as the bidder code. +Ads sizes available: [320,50], [300,250], [320,480], [1024,768], [728,90] + +# Banner Test Parameters +``` + var adUnits = [ + { + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[320,50]] + } + }, + bids: [ + { + bidder: "tappx", + params: { + host: "testing.ssp.tappx.com/rtb/v2/", + tappxkey: "pub-1234-android-1234", + endpoint: "ZZ1234PBJS", + bidfloor: 0.005, + test: true // Optional for testing purposes + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/tappxBidAdapter_spec.js b/test/spec/modules/tappxBidAdapter_spec.js new file mode 100644 index 00000000000..c4410d8ce5e --- /dev/null +++ b/test/spec/modules/tappxBidAdapter_spec.js @@ -0,0 +1,86 @@ +import { assert } from 'chai'; +import {spec} from 'modules/tappxBidAdapter'; + +describe('Tappx adapter tests', function () { + describe('isBidRequestValid', function () { + let bid = { bidder: 'tappx', params: { host: 'testing.ssp.tappx.com', tappxkey: 'pub-1234-test-1234', endpoint: 'ZZ1234PBJS', bidfloor: 0.005 } }; + + it('should return true when required params found', function () { + assert(spec.isBidRequestValid(bid)); + }); + + it('should return false when required params are missing', function () { + const bid = { + host: 'testing.ssp.tappx.com' + }; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + }); + + describe('buildRequest', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + // Web Test + let validBidRequests = [{'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com', 'tappxkey': 'pub-1234-android-1234', 'endpoint': 'ZZ1234PBJS', 'bidfloor': 0.005, 'api': [3, 5]}, 'crumbs': {'pubcid': 'df2144f7-673f-4440-83f5-cd4a73642d99'}, 'fpd': {'context': {'adServer': {'name': 'gam', 'adSlot': '/19968336/header-bid-tag-0'}, 'pbAdSlot': '/19968336/header-bid-tag-0'}}, 'mediaTypes': {'banner': {'sizes': [[320, 480]]}}, 'adUnitCode': 'div-1', 'transactionId': '713f2c01-61e3-45b5-9e4e-2b163033f3d6', 'sizes': [[320, 480]], 'bidId': '27818d05971607', 'bidderRequestId': '1320551a307df5', 'auctionId': '3f1281d3-9860-4657-808d-3c1d42231ef3', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}] + // App Test + let validAppBidRequests = [{'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com', 'tappxkey': 'pub-1234-android-1234', 'endpoint': 'ZZ1234PBJS', 'bidfloor': 0.005, 'api': [3, 5], 'app': {'name': 'Tappx Test', 'bundle': 'com.test.tappx', 'domain': 'tappx.com', 'publisher': { 'name': 'Tappx', 'domain': 'tappx.com' }}}, 'crumbs': {'pubcid': 'df2144f7-673f-4440-83f5-cd4a73642d99'}, 'fpd': {'context': {'adServer': {'name': 'gam', 'adSlot': '/19968336/header-bid-tag-0'}, 'pbAdSlot': '/19968336/header-bid-tag-0'}}, 'mediaTypes': {'banner': {'sizes': [[320, 50]]}}, 'adUnitCode': 'div-1', 'transactionId': '713f2c01-61e3-45b5-9e4e-2b163033f3d6', 'sizes': [[320, 50]], 'bidId': '27818d05971607', 'bidderRequestId': '1320551a307df5', 'auctionId': '3f1281d3-9860-4657-808d-3c1d42231ef3', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}] + let bidderRequest = {'bidderCode': 'tappx', 'auctionId': '6cca2192-2262-468b-8a3c-d00c58a5d911', 'bidderRequestId': '1ae5c6a02684df', 'bids': [{'bidder': 'tappx', 'params': {'host': 'tests.tappx.com', 'tappxkey': 'pub-1234-test-1234', 'endpoint': 'ZZ1234PBJS', 'bidfloor': 0.005}, 'fpd': {'context': {'adServer': {'name': 'gam', 'adSlot': '/19968336/header-bid-tag-0'}, 'pbAdSlot': '/19968336/header-bid-tag-0'}}, 'mediaTypes': {'banner': {'sizes': [[320, 480]]}}, 'adUnitCode': 'banner-ad-div', 'transactionId': 'c44cdbde-ab6d-47a0-8dde-6b4ff7909a35', 'sizes': [[320, 50]], 'bidId': '2e3a5feb30cfe4', 'bidderRequestId': '1ae5c6a02684df', 'auctionId': '6cca2192-2262-468b-8a3c-d00c58a5d911', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0}], 'auctionStart': 1611308859094, 'timeout': 700, 'refererInfo': {'referer': 'http://tappx.local:9999/integrationExamples/gpt/gdpr_hello_world.html?pbjs_debug=true', 'reachedTop': true, 'isAmp': false, 'numIframes': 0, 'stack': ['http://tappx.local:9999/integrationExamples/gpt/gdpr_hello_world.html?pbjs_debug=true'], 'canonicalUrl': null}, 'gdprConsent': {'consentString': consentString, 'vendorData': {'metadata': 'BO-JeiTPABAOkAAABAENABA', 'gdprApplies': true, 'hasGlobalScope': false, 'cookieVersion': 1, 'created': '2020-12-09T09:22:09.900Z', 'lastUpdated': '2021-01-14T15:44:03.600Z', 'cmpId': 0, 'cmpVersion': 1, 'consentScreen': 0, 'consentLanguage': 'EN', 'vendorListVersion': 1, 'maxVendorId': 0, 'purposeConsents': {}, 'vendorConsents': {}}, 'gdprApplies': true, 'apiVersion': 1}, 'uspConsent': '1YCC', 'start': 1611308859099}; + + it('should add gdpr/usp consent information to the request', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request[0].data); + + expect(payload.regs.gdpr).to.exist.and.to.be.true; + expect(payload.regs.consent).to.exist.and.to.equal(consentString); + expect(payload.regs.ext.us_privacy).to.exist; + }); + + it('should properly build a banner request', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request[0].url).to.match(/^(http|https):\/\/(.*)\.tappx\.com\/.+/); + expect(request[0].method).to.equal('POST'); + + const data = JSON.parse(request[0].data); + expect(data.site).to.not.equal(null); + expect(data.imp).to.have.lengthOf(1); + expect(data.imp[0].bidfloor, data).to.not.be.null; + expect(data.imp[0].banner).to.not.equal(null); + expect(data.imp[0].banner.w).to.be.oneOf([320, 50, 250, 480]); + expect(data.imp[0].banner.h).to.be.oneOf([320, 50, 250, 480]); + }); + + it('should properly build a banner request with app params', function () { + const request = spec.buildRequests(validAppBidRequests, bidderRequest); + expect(request[0].url).to.match(/^(http|https):\/\/(.*)\.tappx\.com\/.+/); + expect(request[0].method).to.equal('POST'); + + const data = JSON.parse(request[0].data); + expect(data.site).to.not.equal(null); + expect(data.imp).to.have.lengthOf(1); + expect(data.imp[0].bidfloor, data).to.not.be.null; + expect(data.imp[0].banner).to.not.equal(null); + expect(data.imp[0].banner.w).to.be.oneOf([320, 50, 250, 480]); + expect(data.imp[0].banner.h).to.be.oneOf([320, 50, 250, 480]); + }); + }); + + describe('interpretResponse', function () { + const bidRequest = { + data: {}, + bids: [ {'bidder': 'tappx', 'params': {'host': 'testing.ssp.tappx.com', 'tappxkey': 'pub-1234-android-1234', 'endpoint': 'ZZ1234PBJS', 'bidfloor': 0.05, 'api': [3, 5]}, 'crumbs': {'pubcid': 'df2144f7-673f-4440-83f5-cd4a73642d99'}, 'fpd': {'context': {'adServer': {'name': 'gam', 'adSlot': '/19968336/header-bid-tag-0'}, 'pbAdSlot': '/19968336/header-bid-tag-0'}}, 'mediaTypes': {'banner': {'sizes': [[320, 480]]}}, 'adUnitCode': 'div-1', 'transactionId': '47dd44e8-e7db-417c-a8f1-621a2e1a117d', 'sizes': [[320, 480]], 'bidId': '2170932097e505', 'bidderRequestId': '140ba7a1ab7aeb', 'auctionId': '1c54b4f1-645f-44e6-b8ae-5d43c923ef1c', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, 'bidderWinsCount': 0} ] + }; + + const serverResponse = {'body': {'id': '1c54b4f1-645f-44e6-b8ae-5d43c923ef1c', 'bidid': 'bid3811165568213389257', 'seatbid': [{'seat': '1', 'group': 0, 'bid': [{'id': '3811165568213389257', 'impid': 1, 'price': 0.05, 'adm': "\t", 'w': 320, 'h': 480, 'lurl': 'http://testing.ssp.tappx.com/rtb/RTBv2Loss?id=3811165568213389257&ep=ZZ1234PBJS&au=test&bu=localhost&sz=320x480&pu=0.005&pt=0.01&cid=&crid=&adv=&aid=${AUCTION_ID}&bidid=${AUCTION_BID_ID}&impid=${AUCTION_IMP_ID}&sid=${AUCTION_SEAT_ID}&adid=${AUCTION_AD_ID}&ap=${AUCTION_PRICE}&cur=${AUCTION_CURRENCY}&mbr=${AUCTION_MBR}&l=${AUCTION_LOSS}', 'cid': '01744fbb521e9fb10ffea926190effea', 'crid': 'a13cf884e66e7c660afec059c89d98b6', 'adomain': []}]}], 'cur': 'USD'}, 'headers': {}}; + it('receive reponse with single placement', function () { + const bids = spec.interpretResponse(serverResponse, bidRequest); + const bid = bids[0]; + expect(bid.cpm).to.exist; + expect(bid.ad).to.match(/^