From b9ebe16b0c93798b0aa6281846d9f647ad5d6382 Mon Sep 17 00:00:00 2001 From: Scott Date: Thu, 13 Aug 2020 17:43:03 +0200 Subject: [PATCH 01/43] user id module refresh ids when consent changes (#5451) * first cut at making the userId module aware of user consent choices so it can refresh the ID if consent changes * typos in comments * fix failing tests * refactor consent changes tests to prepare for adding tcf v2 tests * an update in 4.0 changed the interface for the `setStoredValue()` method which caused the previous code to break. Here I changed the code to read/write the consent data cookie to just do it directly rather than use the code for handling storing the actual id objects. --- modules/userId/index.js | 82 ++++++++++- test/spec/modules/userId_spec.js | 234 +++++++++++++++++++++++++++++-- 2 files changed, 300 insertions(+), 16 deletions(-) diff --git a/modules/userId/index.js b/modules/userId/index.js index 2a37723e3a0..afdd93a57ba 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -124,6 +124,10 @@ const COOKIE = 'cookie'; const LOCAL_STORAGE = 'html5'; const DEFAULT_SYNC_DELAY = 500; const NO_AUCTION_DELAY = 0; +const CONSENT_DATA_COOKIE_STORAGE_CONFIG = { + name: '_pbjs_userid_consent_data', + expires: 30 // 30 days expiration, which should match how often consent is refreshed by CMPs +}; export const coreStorage = getCoreStorageManager('userid'); /** @type {string[]} */ @@ -221,6 +225,72 @@ function getStoredValue(storage, key = undefined) { return storedValue; } +/** + * makes an object that can be stored with only the keys we need to check. + * excluding the vendorConsents object since the consentString is enough to know + * if consent has changed without needing to have all the details in an object + * @param consentData + * @returns {{apiVersion: number, gdprApplies: boolean, consentString: string}} + */ +function makeStoredConsentDataObject(consentData) { + const storedConsentData = { + consentString: '', + gdprApplies: false, + apiVersion: 0 + }; + + if (consentData) { + storedConsentData.consentString = consentData.consentString; + storedConsentData.gdprApplies = consentData.gdprApplies; + storedConsentData.apiVersion = consentData.apiVersion; + } + + return storedConsentData; +} + +/** + * puts the current consent data into cookie storage + * @param consentData + */ +export function setStoredConsentData(consentData) { + try { + const expiresStr = (new Date(Date.now() + (CONSENT_DATA_COOKIE_STORAGE_CONFIG.expires * (60 * 60 * 24 * 1000)))).toUTCString(); + coreStorage.setCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name, JSON.stringify(makeStoredConsentDataObject(consentData)), expiresStr, 'Lax'); + } catch (error) { + utils.logError(error); + } +} + +/** + * get the stored consent data from local storage, if any + * @returns {string} + */ +function getStoredConsentData() { + let storedValue; + try { + storedValue = JSON.parse(coreStorage.getCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name)); + } catch (e) { + utils.logError(e); + } + return storedValue; +} + +/** + * test if the consent object stored locally matches the current consent data. + * if there is nothing in storage, return true and we'll do an actual comparison next time. + * this way, we don't force a refresh for every user when this code rolls out + * @param storedConsentData + * @param consentData + * @returns {boolean} + */ +function storedConsentDataMatchesConsentData(storedConsentData, consentData) { + return ( + typeof storedConsentData === 'undefined' || + storedConsentData === null || + utils.deepEqual(storedConsentData, makeStoredConsentDataObject(consentData)) + ); +} + /** * test if consent module is present, applies, and is valid for local storage or cookies (purpose 1) * @param {ConsentData} consentData @@ -308,7 +378,7 @@ function addIdDataToAdUnitBids(adUnits, submodules) { } /** - * This is a common function that will initalize subModules if not already done and it will also execute subModule callbacks + * This is a common function that will initialize subModules if not already done and it will also execute subModule callbacks */ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { let delayed = false; @@ -411,6 +481,10 @@ export const validateGdprEnforcement = hook('sync', function (submodules, consen * @returns {SubmoduleContainer[]} initialized submodules */ function initSubmodules(submodules, consentData) { + // we always want the latest consentData stored, even if we don't execute any submodules + const storedConsentData = getStoredConsentData(); + setStoredConsentData(consentData); + // gdpr consent with purpose one is required, otherwise exit immediately let {userIdModules, hasValidated} = validateGdprEnforcement(submodules, consentData); if (!hasValidated && !hasGDPRConsent(consentData)) { @@ -432,8 +506,8 @@ function initSubmodules(submodules, consentData) { refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000); } - if (!storedId || refreshNeeded) { - // No previously saved id. Request one from submodule. + if (!storedId || refreshNeeded || !storedConsentDataMatchesConsentData(storedConsentData, consentData)) { + // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. response = submodule.submodule.getId(submodule.config.params, 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 @@ -569,7 +643,7 @@ export function init(config) { utils.logInfo(`${MODULE_NAME} - opt-out cookie found, exit module`); return; } - // _pubcid_optout is checked for compatiblility with pubCommonId + // _pubcid_optout is checked for compatibility with pubCommonId if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && (coreStorage.getDataFromLocalStorage('_pbjs_id_optout') || coreStorage.getDataFromLocalStorage('_pubcid_optout'))) { utils.logInfo(`${MODULE_NAME} - opt-out localStorage found, exit module`); return; diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index a0b7d68bcce..d9671aabc84 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -6,7 +6,8 @@ import { setSubmoduleRegistry, syncDelay, coreStorage, - setStoredValue + setStoredValue, + setStoredConsentData } from 'modules/userId/index.js'; import {createEidsArray} from 'modules/userId/eids.js'; import {config} from 'src/config.js'; @@ -14,6 +15,8 @@ import * as utils from 'src/utils.js'; import events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import {getGlobal} from 'src/prebidGlobal.js'; +import {setConsentConfig, requestBidsHook as consentManagementRequestBidsHook, resetConsentData} from 'modules/consentManagement.js'; +import {gdprDataHandler} from 'src/adapterManager.js'; import {unifiedIdSubmodule} from 'modules/unifiedIdSystem.js'; import {pubCommonIdSubmodule} from 'modules/pubCommonIdSystem.js'; import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; @@ -28,6 +31,7 @@ import {server} from 'test/mocks/xhr.js'; let assert = require('chai').assert; let expect = require('chai').expect; const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; +const CONSENT_LOCAL_STORAGE_NAME = '_pbjs_userid_consent_data'; describe('User ID', function() { function getConfigMock(configArr1, configArr2, configArr3, configArr4, configArr5, configArr6, configArr7, configArr8) { @@ -87,6 +91,10 @@ describe('User ID', function() { localStorage.removeItem('_pubcid_optout'); }); + beforeEach(function() { + coreStorage.setCookie(CONSENT_LOCAL_STORAGE_NAME, '', EXPIRED_COOKIE_DATE); + }); + describe('Decorate Ad Units', function() { beforeEach(function() { coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); @@ -211,8 +219,9 @@ describe('User ID', function() { }); }); }); - // Because the cookie exists already, there should be no setCookie call by default - expect(coreStorage.setCookie.callCount).to.equal(0); + // Because the cookie exists already, there should be no setCookie call by default; the only setCookie call is + // to store consent data + expect(coreStorage.setCookie.callCount).to.equal(1); }); it('Extend cookie', function() { @@ -237,8 +246,9 @@ describe('User ID', function() { }); }); }); - // Because extend is true, the cookie will be updated even if it exists already - expect(coreStorage.setCookie.callCount).to.equal(1); + // Because extend is true, the cookie will be updated even if it exists already. The second setCookie call + // is for storing consentData + expect(coreStorage.setCookie.callCount).to.equal(2); }); it('Disable auto create', function() { @@ -259,7 +269,8 @@ describe('User ID', function() { expect(bid).to.not.have.deep.nested.property('userIdAsEids'); }); }); - expect(coreStorage.setCookie.callCount).to.equal(0); + // setCookie is called once in order to store consentData + expect(coreStorage.setCookie.callCount).to.equal(1); }); it('pbjs.getUserIds', function() { @@ -1447,16 +1458,16 @@ describe('User ID', function() { describe('Set cookie behavior', function() { let coreStorageSpy; - beforeEach(function() { + beforeEach(function () { coreStorageSpy = sinon.spy(coreStorage, 'setCookie'); }); - afterEach(function() { + afterEach(function () { coreStorageSpy.restore(); }); it('should allow submodules to override the domain', function () { const submodule = { submodule: { - domainOverride: function() { + domainOverride: function () { return 'foo.com' } }, @@ -1472,9 +1483,7 @@ describe('User ID', function() { it('should pass null for domain if submodule does not override the domain', function () { const submodule = { - submodule: { - - }, + submodule: {}, config: { storage: { type: 'cookie' @@ -1485,4 +1494,205 @@ describe('User ID', function() { expect(coreStorage.setCookie.getCall(0).args[4]).to.equal(null); }); }); + + describe('Consent changes determine getId refreshes', function() { + let expStr; + let adUnits; + + const mockIdCookieName = 'MOCKID'; + let mockGetId = sinon.stub(); + let mockDecode = sinon.stub(); + let mockExtendId = sinon.stub(); + const mockIdSystem = { + name: 'mockId', + getId: mockGetId, + decode: mockDecode, + extendId: mockExtendId + }; + const userIdConfig = { + userSync: { + userIds: [{ + name: 'mockId', + storage: { + name: 'MOCKID', + type: 'cookie', + refreshInSeconds: 30 + } + }], + auctionDelay: 5 + } + }; + + let cmpStub; + let testConsentData; + const consentConfig = { + cmpApi: 'iab', + timeout: 7500, + allowAuctionWithoutConsent: false + }; + + const sharedBeforeFunction = function() { + // clear cookies + expStr = (new Date(Date.now() + 25000).toUTCString()); + coreStorage.setCookie(mockIdCookieName, '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie(`${mockIdCookieName}_last`, '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie(CONSENT_LOCAL_STORAGE_NAME, '', EXPIRED_COOKIE_DATE); + + // init + adUnits = [getAdUnitMock()]; + init(config); + + // init id system + attachIdSystem(mockIdSystem); + config.setConfig(userIdConfig); + } + const sharedAfterFunction = function () { + config.resetConfig(); + mockGetId.reset(); + mockDecode.reset(); + mockExtendId.reset(); + cmpStub.restore(); + resetConsentData(); + delete window.__cmp; + delete window.__tcfapi; + }; + + describe('TCF v1', function() { + testConsentData = { + gdprApplies: true, + consentData: 'xyz', + apiVersion: 1 + }; + + beforeEach(function () { + sharedBeforeFunction(); + + // init v1 consent management + window.__cmp = function () { }; + delete window.__tcfapi; + cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + args[2](testConsentData); + }); + setConsentConfig(consentConfig); + }); + + afterEach(function() { + sharedAfterFunction(); + }); + + it('does not call getId if no stored consent data and refresh is not needed', function () { + coreStorage.setCookie(mockIdCookieName, JSON.stringify({ id: '1234' }), expStr); + coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); + + let innerAdUnits; + consentManagementRequestBidsHook(() => { }, {}); + requestBidsHook((config) => { innerAdUnits = config.adUnits }, { adUnits }); + + sinon.assert.notCalled(mockGetId); + sinon.assert.calledOnce(mockDecode); + sinon.assert.calledOnce(mockExtendId); + + let consent = gdprDataHandler.getConsentData(); + let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); + expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); + expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); + expect(userIdStoredConsent.consentString).to.equal(consent.consentString); + expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); + }); + + it('calls getId if no stored consent data but refresh is needed', function () { + coreStorage.setCookie(mockIdCookieName, JSON.stringify({ id: '1234' }), expStr); + coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 60 * 1000).toUTCString()), expStr); + + let innerAdUnits; + consentManagementRequestBidsHook(() => { }, {}); + requestBidsHook((config) => { innerAdUnits = config.adUnits }, { adUnits }); + + sinon.assert.calledOnce(mockGetId); + sinon.assert.calledOnce(mockDecode); + sinon.assert.notCalled(mockExtendId); + + let consent = gdprDataHandler.getConsentData(); + let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); + expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); + expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); + expect(userIdStoredConsent.consentString).to.equal(consent.consentString); + expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); + }); + + it('calls getId if empty stored consent and refresh not needed', function () { + coreStorage.setCookie(mockIdCookieName, JSON.stringify({ id: '1234' }), expStr); + coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); + + setStoredConsentData(); + + let innerAdUnits; + consentManagementRequestBidsHook(() => { }, {}); + requestBidsHook((config) => { innerAdUnits = config.adUnits }, { adUnits }); + + sinon.assert.calledOnce(mockGetId); + sinon.assert.calledOnce(mockDecode); + sinon.assert.notCalled(mockExtendId); + + let consent = gdprDataHandler.getConsentData(); + let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); + expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); + expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); + expect(userIdStoredConsent.consentString).to.equal(consent.consentString); + expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); + }); + + it('calls getId if stored consent does not match current consent and refresh not needed', function () { + coreStorage.setCookie(mockIdCookieName, JSON.stringify({ id: '1234' }), expStr); + coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); + + setStoredConsentData({ + gdprApplies: testConsentData.gdprApplies, + consentString: 'abc', + apiVersion: testConsentData.apiVersion + }); + + let innerAdUnits; + consentManagementRequestBidsHook(() => { }, {}); + requestBidsHook((config) => { innerAdUnits = config.adUnits }, { adUnits }); + + sinon.assert.calledOnce(mockGetId); + sinon.assert.calledOnce(mockDecode); + sinon.assert.notCalled(mockExtendId); + + let consent = gdprDataHandler.getConsentData(); + let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); + expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); + expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); + expect(userIdStoredConsent.consentString).to.equal(consent.consentString); + expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); + }); + + it('does not call getId if stored consent matches current consent and refresh not needed', function () { + coreStorage.setCookie(mockIdCookieName, JSON.stringify({ id: '1234' }), expStr); + coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); + + setStoredConsentData({ + gdprApplies: testConsentData.gdprApplies, + consentString: testConsentData.consentData, + apiVersion: testConsentData.apiVersion + }); + + let innerAdUnits; + consentManagementRequestBidsHook(() => { }, {}); + requestBidsHook((config) => { innerAdUnits = config.adUnits }, { adUnits }); + + sinon.assert.notCalled(mockGetId); + sinon.assert.calledOnce(mockDecode); + sinon.assert.calledOnce(mockExtendId); + + let consent = gdprDataHandler.getConsentData(); + let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); + expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); + expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); + expect(userIdStoredConsent.consentString).to.equal(consent.consentString); + expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); + }); + }); + }); }); From ef00b3f171e8c394a3a978b7cc7a193fecb8aa51 Mon Sep 17 00:00:00 2001 From: akiselicki-liveramp <65005198+akiselicki-liveramp@users.noreply.github.com> Date: Thu, 13 Aug 2020 20:02:42 +0200 Subject: [PATCH 02/43] Fix v2CmpResponseCallback handle (#5564) * Fix v2CmpResponseCallback handle * Add case when gdpr doesn't apply to v2CmpResponseCallback Co-authored-by: Aleksandar Kiselicki --- modules/consentManagement.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index a5ed134420e..0c48d8c854c 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -100,7 +100,9 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { function v2CmpResponseCallback(tcfData, success) { utils.logInfo('Received a response from CMP', tcfData); if (success) { - if (tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { + if (tcfData.gdprApplies === false) { + cmpSuccess(tcfData, hookConfig); + } else if (tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { cmpSuccess(tcfData, hookConfig); } else if (tcfData.eventStatus === 'cmpuishown' && tcfData.tcString && tcfData.purposeOneTreatment === true) { cmpSuccess(tcfData, hookConfig); From c34786df2138b3a75c532f8974301db23676ad71 Mon Sep 17 00:00:00 2001 From: Neelanjan Sen <14229985+Fawke@users.noreply.github.com> Date: Thu, 13 Aug 2020 23:47:19 +0530 Subject: [PATCH 03/43] TCFv2.0 Purpose 7 (#5444) * TCF v2.0 enforcement * test/spec/modules/gdprEnforcement_spec.js * add check for gdpr version * add logInfo message * remove comment and store value of PURPOSES in an object * add gvlid check * add unit tests for validateRules function * remove purposeId parameter from validateRules function * add extra tests * make failing unit test case pass * deprecate allowAuctionWithouConsent with tcf 2 workflow * add extra checks for defaults * remove tcf 2 test page * add strict gvlid check * add comments and shorten log messages * shorted log messages * add unit tests for setEnforcementConfig * add gvlid for alias and gvlMapping support * remove gvlid check * add support to add gvlid for aliases * add enableAnalytics hook * purpose 7 implementation: 1.hook added 2.new field to set gvlid for analytics adapters * add enableAnalytics hook * emit tcf2 events * fix regression * modify mechanism of event emitted after auction end * add unit test for enableAnalyticsHook * add unit test for auction end event Co-authored-by: Jaimin Panchal --- modules/appnexusAnalyticsAdapter.js | 3 +- modules/gdprEnforcement.js | 119 ++++++++++++++++++++-- src/adapterManager.js | 12 ++- src/constants.json | 4 +- src/prebid.js | 15 ++- test/spec/modules/gdprEnforcement_spec.js | 116 +++++++++++++++++++-- 6 files changed, 242 insertions(+), 27 deletions(-) diff --git a/modules/appnexusAnalyticsAdapter.js b/modules/appnexusAnalyticsAdapter.js index d697d31cdd3..868b317d7d4 100644 --- a/modules/appnexusAnalyticsAdapter.js +++ b/modules/appnexusAnalyticsAdapter.js @@ -13,7 +13,8 @@ var appnexusAdapter = adapter({ adapterManager.registerAnalyticsAdapter({ adapter: appnexusAdapter, - code: 'appnexus' + code: 'appnexus', + gvlid: 32 }); export default appnexusAdapter; diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 0a32441c813..97eaedd92be 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -16,9 +16,13 @@ import { EVENTS } from '../src/constants.json'; const TCF2 = { 'purpose1': { id: 1, name: 'storage' }, - 'purpose2': { id: 2, name: 'basicAds' } + 'purpose2': { id: 2, name: 'basicAds' }, + 'purpose7': { id: 7, name: 'measurement' } } +/* + These rules would be used if `consentManagement.gdpr.rules` is undefined by the publisher. +*/ const DEFAULT_RULES = [{ purpose: 'storage', enforcePurpose: true, @@ -33,9 +37,21 @@ const DEFAULT_RULES = [{ export let purpose1Rule; export let purpose2Rule; -let addedDeviceAccessHook = false; +export let purpose7Rule; + export let enforcementRules; +const storageBlocked = []; +const biddersBlocked = []; +const analyticsBlocked = []; + +let addedDeviceAccessHook = false; + +/** + * Returns gvlId for Bid Adapters. If a bidder does not have an associated gvlId, it returns 'undefined'. + * @param {string=} bidderCode - The 'code' property on the Bidder spec. + * @retuns {number} gvlId + */ function getGvlid(bidderCode) { let gvlid; bidderCode = bidderCode || config.getCurrentBidder(); @@ -53,6 +69,11 @@ function getGvlid(bidderCode) { return gvlid; } +/** + * Returns gvlId for userId module. If a userId modules does not have an associated gvlId, it returns 'undefined'. + * @param {Object} userIdModule + * @retuns {number} gvlId + */ function getGvlidForUserIdModule(userIdModule) { let gvlId; const gvlMapping = config.getConfig('gvlMapping'); @@ -64,6 +85,22 @@ function getGvlidForUserIdModule(userIdModule) { return gvlId; } +/** + * Returns gvlId for analytics adapters. If a analytics adapter does not have an associated gvlId, it returns 'undefined'. + * @param {string} code - 'provider' property on the analytics adapter config + * @returns {number} gvlId + */ +function getGvlidForAnalyticsAdapter(code) { + let gvlId; + const gvlMapping = config.getConfig('gvlMapping'); + if (gvlMapping && gvlMapping[code]) { + gvlId = gvlMapping[code]; + } else { + gvlId = adapterManager.getAnalyticsAdapter(code).gvlid; + } + return gvlId; +} + /** * This function takes in a rule and consentData and validates against the consentData provided. Depending on what it returns, * the caller may decide to suppress a TCF-sensitive activity. @@ -136,8 +173,9 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) { result.valid = true; fn.call(this, gvlid, moduleName, result); } else { - curModule && utils.logWarn(`Device access denied for ${curModule} by TCF2`); + curModule && utils.logWarn(`TCF2 denied device access for ${curModule}`); result.valid = false; + storageBlocked.push(curModule); fn.call(this, gvlid, moduleName, result); } } else { @@ -168,6 +206,7 @@ export function userSyncHook(fn, ...args) { fn.call(this, ...args); } else { utils.logWarn(`User sync not allowed for ${curBidder}`); + storageBlocked.push(curBidder); } } else { // The module doesn't enforce TCF1.1 strings @@ -195,10 +234,11 @@ export function userIdHook(fn, submodules, consentData) { return submodule; } else { utils.logWarn(`User denied permission to fetch user id for ${moduleName} User id module`); + storageBlocked.push(moduleName); } return undefined; }).filter(module => module) - fn.call(this, userIdModules, {...consentData, hasValidated: true}); + fn.call(this, userIdModules, { ...consentData, hasValidated: true }); } else { // The module doesn't enforce TCF1.1 strings fn.call(this, submodules, consentData); @@ -209,8 +249,8 @@ export function userIdHook(fn, submodules, consentData) { } /** - * Checks if a bidder is allowed in Auction. - * Enforces "purpose 2 (basic ads)" of TCF v2.0 spec + * Checks if bidders are allowed in the auction. + * Enforces "purpose 2 (Basic Ads)" of TCF v2.0 spec * @param {Function} fn - Function reference to the original function. * @param {Array} adUnits */ @@ -218,17 +258,15 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { const consentData = gdprDataHandler.getConsentData(); if (consentData && consentData.gdprApplies) { if (consentData.apiVersion === 2) { - const disabledBidders = []; adUnits.forEach(adUnit => { adUnit.bids = adUnit.bids.filter(bid => { const currBidder = bid.bidder; const gvlId = getGvlid(currBidder); - if (includes(disabledBidders, currBidder)) return false; + if (includes(biddersBlocked, currBidder)) return false; const isAllowed = !!validateRules(purpose2Rule, consentData, currBidder, gvlId); if (!isAllowed) { utils.logWarn(`TCF2 blocked auction for ${currBidder}`); - events.emit(EVENTS.BIDDER_BLOCKED, currBidder); - disabledBidders.push(currBidder); + biddersBlocked.push(currBidder); } return isAllowed; }); @@ -243,8 +281,64 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { } } +/** + * Checks if Analytics adapters are allowed to send data to their servers for furhter processing. + * Enforces "purpose 7 (Measurement)" of TCF v2.0 spec + * @param {Function} fn - Function reference to the original function. + * @param {Array} config - Configuration object passed to pbjs.enableAnalytics() + */ +export function enableAnalyticsHook(fn, config) { + const consentData = gdprDataHandler.getConsentData(); + if (consentData && consentData.gdprApplies) { + if (consentData.apiVersion === 2) { + if (!utils.isArray(config)) { + config = [config] + } + config = config.filter(conf => { + const analyticsAdapterCode = conf.provider; + const gvlid = getGvlidForAnalyticsAdapter(analyticsAdapterCode); + const isAllowed = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid); + if (!isAllowed) { + analyticsBlocked.push(analyticsAdapterCode); + utils.logWarn(`TCF2 blocked analytics adapter ${conf.provider}`); + } + return isAllowed; + }); + fn.call(this, config); + } else { + // This module doesn't enforce TCF1.1 strings + fn.call(this, config); + } + } else { + fn.call(this, config); + } +} + +/** + * Compiles the TCF2.0 enforcement results into an object, which is emitted as an event payload to "tcf2Enforcement" event. + */ +function emitTCF2FinalResults() { + // remove null and duplicate values + const formatArray = function (arr) { + return arr.filter((i, k) => i !== null && arr.indexOf(i) === k); + } + const tcf2FinalResults = { + storageBlocked: formatArray(storageBlocked), + biddersBlocked: formatArray(biddersBlocked), + analyticsBlocked: formatArray(analyticsBlocked) + }; + + events.emit(EVENTS.TCF2_ENFORCEMENT, tcf2FinalResults); +} + +events.on(EVENTS.AUCTION_END, emitTCF2FinalResults); + +/* + Set of callback functions used to detect presence of a TCF rule, passed as the second argument to find(). +*/ const hasPurpose1 = (rule) => { return rule.purpose === TCF2.purpose1.name } const hasPurpose2 = (rule) => { return rule.purpose === TCF2.purpose2.name } +const hasPurpose7 = (rule) => { return rule.purpose === TCF2.purpose7.name } /** * A configuration function that initializes some module variables, as well as adds hooks @@ -261,6 +355,7 @@ export function setEnforcementConfig(config) { purpose1Rule = find(enforcementRules, hasPurpose1); purpose2Rule = find(enforcementRules, hasPurpose2); + purpose7Rule = find(enforcementRules, hasPurpose7); if (!purpose1Rule) { purpose1Rule = DEFAULT_RULES[0]; @@ -280,6 +375,10 @@ export function setEnforcementConfig(config) { if (purpose2Rule) { getHook('makeBidRequests').before(makeBidRequestsHook); } + + if (purpose7Rule) { + getHook('enableAnalyticsCb').before(enableAnalyticsHook); + } } config.getConfig('consentManagement', config => setEnforcementConfig(config.consentManagement)); diff --git a/src/adapterManager.js b/src/adapterManager.js index 06ccba9787e..5124ae99694 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -468,11 +468,11 @@ adapterManager.aliasBidAdapter = function (bidderCode, alias, options) { } }; -adapterManager.registerAnalyticsAdapter = function ({adapter, code}) { +adapterManager.registerAnalyticsAdapter = function ({adapter, code, gvlid}) { if (adapter && code) { if (typeof adapter.enableAnalytics === 'function') { adapter.code = code; - _analyticsRegistry[code] = adapter; + _analyticsRegistry[code] = { adapter, gvlid }; } else { utils.logError(`Prebid Error: Analytics adaptor error for analytics "${code}" analytics adapter must implement an enableAnalytics() function`); @@ -488,7 +488,7 @@ adapterManager.enableAnalytics = function (config) { } utils._each(config, adapterConfig => { - var adapter = _analyticsRegistry[adapterConfig.provider]; + var adapter = _analyticsRegistry[adapterConfig.provider].adapter; if (adapter) { adapter.enableAnalytics(adapterConfig); } else { @@ -496,12 +496,16 @@ adapterManager.enableAnalytics = function (config) { ${adapterConfig.provider}.`); } }); -}; +} adapterManager.getBidAdapter = function(bidder) { return _bidderRegistry[bidder]; }; +adapterManager.getAnalyticsAdapter = function(code) { + return _analyticsRegistry[code]; +} + // the s2sTesting module is injected when it's loaded rather than being imported // importing it causes the packager to include it even when it's not explicitly included in the build export function setS2STestingModule(module) { diff --git a/src/constants.json b/src/constants.json index 946c43754d5..1b5feda6a05 100644 --- a/src/constants.json +++ b/src/constants.json @@ -36,8 +36,8 @@ "BEFORE_REQUEST_BIDS": "beforeRequestBids", "REQUEST_BIDS": "requestBids", "ADD_AD_UNITS": "addAdUnits", - "AD_RENDER_FAILED" : "adRenderFailed", - "BIDDER_BLOCKED": "bidderBlocked" + "AD_RENDER_FAILED": "adRenderFailed", + "TCF2_ENFORCEMENT": "tcf2Enforcement" }, "AD_RENDER_FAILED_REASON" : { "PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocuemnt", diff --git a/src/prebid.js b/src/prebid.js index 67402a995cf..827ba2e2270 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -536,8 +536,9 @@ $$PREBID_GLOBAL$$.requestBids = hook('async', function ({ bidsBackHandler, timeo auction.callBids(); }); -export function executeStorageCallbacks(fn, reqBidsConfigObj) { +export function executeCallbacks(fn, reqBidsConfigObj) { runAll(storageCallbacks); + runAll(enableAnalyticsCallbacks); fn.call(this, reqBidsConfigObj); function runAll(queue) { var queued; @@ -548,7 +549,7 @@ export function executeStorageCallbacks(fn, reqBidsConfigObj) { } // This hook will execute all storage callbacks which were registered before gdpr enforcement hook was added. Some bidders, user id modules use storage functions when module is parsed but gdpr enforcement hook is not added at that stage as setConfig callbacks are yet to be called. Hence for such calls we execute all the stored callbacks just before requestBids. At this hook point we will know for sure that gdprEnforcement module is added or not -$$PREBID_GLOBAL$$.requestBids.before(executeStorageCallbacks, 49); +$$PREBID_GLOBAL$$.requestBids.before(executeCallbacks, 49); /** * @@ -667,13 +668,21 @@ $$PREBID_GLOBAL$$.createBid = function (statusCode) { * @param {Object} config.options The options for this particular analytics adapter. This will likely vary between adapters. * @alias module:pbjs.enableAnalytics */ -$$PREBID_GLOBAL$$.enableAnalytics = function (config) { + +// Stores 'enableAnalytics' callbacks for later execution. +const enableAnalyticsCallbacks = []; + +const enableAnalyticsCb = hook('async', function (config) { if (config && !utils.isEmpty(config)) { utils.logInfo('Invoking $$PREBID_GLOBAL$$.enableAnalytics for: ', config); adapterManager.enableAnalytics(config); } else { utils.logError('$$PREBID_GLOBAL$$.enableAnalytics should be called with option {}'); } +}, 'enableAnalyticsCb'); + +$$PREBID_GLOBAL$$.enableAnalytics = function (config) { + enableAnalyticsCallbacks.push(enableAnalyticsCb.bind(this, config)); }; /** diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 9b02f74f4bb..d5c8d7bfb88 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,11 +1,20 @@ -import { deviceAccessHook, setEnforcementConfig, userSyncHook, userIdHook, makeBidRequestsHook, validateRules, enforcementRules, purpose1Rule, purpose2Rule } from 'modules/gdprEnforcement.js'; +import { + deviceAccessHook, + setEnforcementConfig, + userSyncHook, + userIdHook, + makeBidRequestsHook, + validateRules, + enforcementRules, + purpose1Rule, + purpose2Rule, + enableAnalyticsHook +} from 'modules/gdprEnforcement.js'; import { config } from 'src/config.js'; import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { validateStorageEnforcement } from 'src/storageManager.js'; -import { executeStorageCallbacks } from 'src/prebid.js'; import events from 'src/events.js'; -import { EVENTS } from 'src/constants.json'; describe('gdpr enforcement', function () { let nextFnSpy; @@ -36,7 +45,8 @@ describe('gdpr enforcement', function () { 'consents': { '1': true, '2': true, - '3': true + '3': true, + '7': true }, 'legitimateInterests': { '1': false, @@ -87,7 +97,7 @@ describe('gdpr enforcement', function () { after(function () { validateStorageEnforcement.getHooks({ hook: deviceAccessHook }).remove(); - $$PREBID_GLOBAL$$.requestBids.getHooks({ hook: executeStorageCallbacks }).remove(); + $$PREBID_GLOBAL$$.requestBids.getHooks().remove(); adapterManager.makeBidRequests.getHooks({ hook: makeBidRequestsHook }).remove(); }) @@ -662,8 +672,6 @@ describe('gdpr enforcement', function () { }], []); expect(logWarnSpy.calledOnce).to.equal(true); - expect(emitEventSpy.calledOnce).to.equal(true); - sinon.assert.calledWith(emitEventSpy, EVENTS.BIDDER_BLOCKED, 'bidder_2'); }); it('should skip validation checks if GDPR version is not equal to "2"', function () { @@ -694,6 +702,71 @@ describe('gdpr enforcement', function () { }); }); + describe('enableAnalyticsHook', function () { + let sandbox; + let adapterManagerStub; + + const MOCK_ANALYTICS_ADAPTER_CONFIG = [{ + provider: 'analyticsAdapter_A', + options: {} + }, { + provider: 'analyticsAdapter_B', + options: {} + }, { + provider: 'analyticsAdapter_C', + options: {} + }]; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + gdprDataHandlerStub = sandbox.stub(gdprDataHandler, 'getConsentData'); + adapterManagerStub = sandbox.stub(adapterManager, 'getAnalyticsAdapter'); + logWarnSpy = sandbox.spy(utils, 'logWarn'); + nextFnSpy = sandbox.spy(); + }); + + afterEach(function() { + config.resetConfig(); + sandbox.restore(); + }); + + it('should block analytics adapter which does not have consent and allow the one(s) which have consent', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'measurement', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: ['analyticsAdapter_B'] + }] + } + }); + + const consentData = {}; + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.apiVersion = 2; + consentData.gdprApplies = true; + + gdprDataHandlerStub.returns(consentData); + adapterManagerStub.withArgs('analyticsAdapter_A').returns({ gvlid: 3 }); + adapterManagerStub.withArgs('analyticsAdapter_B').returns({ gvlid: 5 }); + adapterManagerStub.withArgs('analyticsAdapter_C').returns({ gvlid: 1 }); + + enableAnalyticsHook(nextFnSpy, MOCK_ANALYTICS_ADAPTER_CONFIG); + + // Assertions + expect(nextFnSpy.calledOnce).to.equal(true); + sinon.assert.calledWith(nextFnSpy, [{ + provider: 'analyticsAdapter_B', + options: {} + }, { + provider: 'analyticsAdapter_C', + options: {} + }]); + expect(logWarnSpy.calledOnce).to.equal(true); + }); + }); + describe('validateRules', function () { const createGdprRule = (purposeName = 'storage', enforcePurpose = true, enforceVendor = true, vendorExceptions = []) => ({ purpose: purposeName, @@ -965,4 +1038,33 @@ describe('gdpr enforcement', function () { expect(enforcementRules).to.deep.equal(rules); }); }); + + describe('TCF2FinalResults', function() { + let sandbox; + beforeEach(function() { + sandbox = sinon.createSandbox(); + sandbox.spy(events, 'emit'); + }); + afterEach(function() { + config.resetConfig(); + sandbox.restore(); + }); + it('should emit TCF2 enforcement data on auction end', function() { + const rules = [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: false + }, { + purpose: 'basicAds', + enforcePurpose: false, + enforceVendor: false + }] + setEnforcementConfig({gdpr: { rules }}); + + events.emit('auctionEnd', {}) + + // Assertions + sinon.assert.calledWith(events.emit.getCall(1), 'tcf2Enforcement', sinon.match.object); + }) + }); }); From a761b3173b4e0a14fb85fe9f592140fc657263e2 Mon Sep 17 00:00:00 2001 From: Matt Kendall <1870166+mkendall07@users.noreply.github.com> Date: Thu, 13 Aug 2020 15:13:19 -0400 Subject: [PATCH 04/43] prebid.js 4.3.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3594d6d7621..b61e3e338eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.3.0-pre", + "version": "4.3.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 22a59bdc3aa27d0adfad02a65d0a6222bf45d23d Mon Sep 17 00:00:00 2001 From: Matt Kendall <1870166+mkendall07@users.noreply.github.com> Date: Thu, 13 Aug 2020 15:27:04 -0400 Subject: [PATCH 05/43] 4.4.0-pre --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b61e3e338eb..3cb04dc6f8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.3.0", + "version": "4.4.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 7ede93b9f4b20cb08aea0faa88e95c2fbf75aadd Mon Sep 17 00:00:00 2001 From: Harshad Mane Date: Fri, 14 Aug 2020 06:31:45 -0700 Subject: [PATCH 06/43] PubMatic to support passing content object set in pbjs.setConfig (#5592) * added support for pubcommon, digitrust, id5id * added support for IdentityLink * changed the source for id5 * added unit test cases * changed source param for identityLink * read content object from config and send it in site.content and app.content * do not use content object from config if content object is present in app as app.content * fixed the test-cases * app.content related test cases --- modules/pubmaticBidAdapter.js | 10 +++ test/spec/modules/pubmaticBidAdapter_spec.js | 73 ++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 2e52fd27cf1..437e37de83e 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -865,6 +865,11 @@ export const spec = { payload.site.page = conf.kadpageurl.trim() || 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')); @@ -910,6 +915,11 @@ export const spec = { // not copying domain from site as it is a derived value from page payload.app.publisher = payload.site.publisher; payload.app.ext = payload.site.ext || UNDEFINED; + // We will also need to pass content object in app.content if app object is also set into the config; + // BUT do not use content object from config if content object is present in app as app.content + if (typeof payload.app.content !== 'object') { + payload.app.content = payload.site.content || UNDEFINED; + } delete payload.site; } diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index dd46646abc8..64f88f0c906 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -776,6 +776,23 @@ describe('PubMatic adapter', function () { expect(data.source.ext.schain).to.deep.equal(bidRequests[0].schain); }); + it('Set content from config, set site.content', function() { + let sandbox = sinon.sandbox.create(); + const content = { + 'id': 'alpha-numeric-id' + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + content: content + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.site.content).to.deep.equal(content); + sandbox.restore(); + }); + it('Merge the device info from config', function() { let sandbox = sinon.sandbox.create(); sandbox.stub(config, 'getConfig').callsFake((key) => { @@ -840,6 +857,62 @@ describe('PubMatic adapter', function () { sandbox.restore(); }); + it('Set app, content from config, copy publisher and ext from site, unset site, config.content in app.content', function() { + let sandbox = sinon.sandbox.create(); + const content = { + 'id': 'alpha-numeric-id' + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + content: content, + app: { + bundle: 'org.prebid.mobile.demoapp', + domain: 'prebid.org' + } + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); + expect(data.app.domain).to.equal('prebid.org'); + expect(data.app.publisher.id).to.equal(bidRequests[0].params.publisherId); + expect(data.app.ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); + expect(data.app.content).to.deep.equal(content); + expect(data.site).to.not.exist; + sandbox.restore(); + }); + + it('Set app.content, content from config, copy publisher and ext from site, unset site, config.app.content in app.content', function() { + let sandbox = sinon.sandbox.create(); + const content = { + 'id': 'alpha-numeric-id' + }; + const appContent = { + id: 'app-content-id-2' + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + content: content, + app: { + bundle: 'org.prebid.mobile.demoapp', + domain: 'prebid.org', + content: appContent + } + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); + expect(data.app.domain).to.equal('prebid.org'); + expect(data.app.publisher.id).to.equal(bidRequests[0].params.publisherId); + expect(data.app.ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); + expect(data.app.content).to.deep.equal(appContent); + expect(data.site).to.not.exist; + sandbox.restore(); + }); + it('Request params check: without adSlot', function () { delete bidRequests[0].params.adSlot; let request = spec.buildRequests(bidRequests); From db02d8a252b928a052748a2e307876c46708d560 Mon Sep 17 00:00:00 2001 From: pm-shashank-jain <40654031+pm-shashank-jain@users.noreply.github.com> Date: Fri, 14 Aug 2020 19:11:50 +0530 Subject: [PATCH 07/43] PubMatic: Support for Outstream Renderer (BlueBilyWig Renderer) (#5553) * changes to support native in pubmaticbid adapter * Removed port from endpoint * Removed protocol from endpoint * Formatting * Fix request payload * Updated test case * Changed request and response as per ortb spec * Change in request and response * Removed comments and extra code * Code Review comments * Code Review Comments and Test cases for request and response * Removed data type as all data asset types are handled * Code Review Changes * Code Review Comments * Supporting both banner and native and sending 0x0 in case of native * Bug Fixes * Bug response not processed by prebid * Change warning message * Fixed typo * Do not send request in case of invalid native bid * Do not send request in case of invalid native requests * objects converted to strings in log for debug purposes * Fixed logic to check for required parmas * Fixed typo for stringify * documentation for native * Review comments from Prebid * Typo * Typo * Updated pub id for native * Code Review * Support for pubid * Test Cases for PubCommonId in PubMatic adapter * Delete yarn.lock * Rename adaptermanager.js to adapterManager.js * Rename yieldNexusBidAdapter.js to yieldnexusBidAdapter.js * Rename yieldNexusBidAdapter.md to yieldnexusBidAdapter.md * Rename yieldNexusBidAdapter_spec.js to yieldnexusBidAdapter_spec.yieldnexusBidAdaptera * Rename yieldnexusBidAdapter_spec.yieldnexusBidAdaptera to yieldnexusBidAdapter_spec.js * bluebillywig outstream player support in pubmatic adapter * removed pubcommon id test cases * BBW Renderer --- modules/pubmaticBidAdapter.js | 91 ++++++++++++- modules/pubmaticBidAdapter.md | 1 + test/spec/modules/pubmaticBidAdapter_spec.js | 132 ++++++++++++++++++- 3 files changed, 219 insertions(+), 5 deletions(-) diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 437e37de83e..d21854a57c4 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -2,6 +2,7 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; import {config} from '../src/config.js'; +import { Renderer } from '../src/Renderer.js'; const BIDDER_CODE = 'pubmatic'; const LOG_WARN_PREFIX = 'PubMatic: '; @@ -14,6 +15,8 @@ const UNDEFINED = undefined; const DEFAULT_WIDTH = 0; const DEFAULT_HEIGHT = 0; const PREBID_NATIVE_HELP_LINK = 'http://prebid.org/dev-docs/show-native-ads.html'; +const PUBLICATION = 'pubmatic'; // Your publication on Blue Billywig, potentially with environment (e.g. publication.bbvms.com or publication.test.bbvms.com) +const RENDERER_URL = 'https://pubmatic.bbvms.com/r/'.concat('$RENDERER', '.js'); // URL of the renderer application const CUSTOM_PARAMS = { 'kadpageurl': '', // Custom page url 'gender': '', // User gender @@ -104,6 +107,60 @@ const dealChannelValues = { 5: 'PREF', 6: 'PMPG' }; +// BB stands for Blue BillyWig +const BB_RENDERER = { + bootstrapPlayer: function(bid) { + const config = { + code: bid.adUnitCode, + }; + + if (bid.vastXml) config.vastXml = bid.vastXml; + else if (bid.vastUrl) config.vastUrl = bid.vastUrl; + + if (!bid.vastXml && !bid.vastUrl) { + utils.logWarn(`${LOG_WARN_PREFIX}: No vastXml or vastUrl on bid, bailing...`); + return; + } + + const rendererId = BB_RENDERER.getRendererId(PUBLICATION, bid.rendererCode); + + const ele = document.getElementById(bid.adUnitCode); // NB convention + + let renderer; + + for (let rendererIndex = 0; rendererIndex < window.bluebillywig.renderers.length; rendererIndex++) { + if (window.bluebillywig.renderers[rendererIndex]._id === rendererId) { + renderer = window.bluebillywig.renderers[rendererIndex]; + break; + } + } + + if (renderer) renderer.bootstrap(config, ele); + else utils.logWarn(`${LOG_WARN_PREFIX}: Couldn't find a renderer with ${rendererId}`); + }, + newRenderer: function(rendererCode, adUnitCode) { + var rendererUrl = RENDERER_URL.replace('$RENDERER', rendererCode); + const renderer = Renderer.install({ + url: rendererUrl, + loaded: false, + adUnitCode + }); + + try { + renderer.setRender(BB_RENDERER.outstreamRender); + } catch (err) { + utils.logWarn(`${LOG_WARN_PREFIX}: Error tying to setRender on renderer`, err); + } + + return renderer; + }, + outstreamRender: function(bid) { + bid.renderer.push(function() { BB_RENDERER.bootstrapPlayer(bid) }); + }, + getRendererId: function(pub, renderer) { + return `${pub}-${renderer}`; // NB convention! + } +}; let publisherId = 0; let isInvalidNativeRequest = false; @@ -760,6 +817,23 @@ function _handleDealCustomTargetings(payload, dctrArr, validBidRequests) { } } +function _assignRenderer(newBid, request) { + let bidParams, context, adUnitCode; + if (request.bidderRequest && request.bidderRequest.bids) { + for (let bidderRequestBidsIndex = 0; bidderRequestBidsIndex < request.bidderRequest.bids.length; bidderRequestBidsIndex++) { + if (request.bidderRequest.bids[bidderRequestBidsIndex].bidId === newBid.requestId) { + bidParams = request.bidderRequest.bids[bidderRequestBidsIndex].params; + context = request.bidderRequest.bids[bidderRequestBidsIndex].mediaTypes[VIDEO].context; + adUnitCode = request.bidderRequest.bids[bidderRequestBidsIndex].adUnitCode; + } + } + if (context && context === 'outstream' && bidParams && bidParams.outstreamAU && adUnitCode) { + newBid.rendererCode = bidParams.outstreamAU; + newBid.renderer = BB_RENDERER.newRenderer(newBid.rendererCode, adUnitCode); + } + } +}; + export const spec = { code: BIDDER_CODE, gvlid: 76, @@ -782,6 +856,19 @@ export const spec = { utils.logWarn(LOG_WARN_PREFIX + 'Error: For video ads, mimes is mandatory and must specify atlease 1 mime value. Call to OpenBid will not be sent for ad unit:' + JSON.stringify(bid)); return false; } + if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { + if (!bid.mediaTypes[VIDEO].hasOwnProperty('context')) { + 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); + return false; + } + } else { + utils.logError(`${LOG_WARN_PREFIX}: mediaTypes or mediaTypes.video is not specified. Rejecting bid: `, bid); + return false; + } } return true; } @@ -926,7 +1013,8 @@ export const spec = { return { method: 'POST', url: ENDPOINT, - data: JSON.stringify(payload) + data: JSON.stringify(payload), + bidderRequest: bidderRequest }; }, @@ -976,6 +1064,7 @@ export const spec = { newBid.width = bid.hasOwnProperty('w') ? bid.w : req.video.w; newBid.height = bid.hasOwnProperty('h') ? bid.h : req.video.h; newBid.vastXml = bid.adm; + _assignRenderer(newBid, request); break; case NATIVE: _parseNativeResponse(bid, newBid); diff --git a/modules/pubmaticBidAdapter.md b/modules/pubmaticBidAdapter.md index a045bed3e2b..cd9398477f4 100644 --- a/modules/pubmaticBidAdapter.md +++ b/modules/pubmaticBidAdapter.md @@ -25,6 +25,7 @@ var adUnits = [ bidder: 'pubmatic', params: { publisherId: '156209', // required + oustreamAU: 'renderer_test_pubmatic', // required if mediaTypes-> video-> context is 'outstream'. This value can be get by BlueBillyWig Team. adSlot: 'pubmatic_test2', // optional pmzoneid: 'zone1, zone11', // optional lat: '40.712775', // optional diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 64f88f0c906..0f51a8df61c 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -26,6 +26,9 @@ describe('PubMatic adapter', function () { let bannerBidResponse; let videoBidResponse; let schainConfig; + let outstreamBidRequest; + let validOutstreamBidRequest; + let outstreamVideoBidResponse; beforeEach(function () { schainConfig = { @@ -55,7 +58,7 @@ describe('PubMatic adapter', function () { } }, params: { - publisherId: '301', + publisherId: '5670', adSlot: '/15671365/DMDemo@300x250:0', kadfloor: '1.2', pmzoneid: 'aabc, ddef', @@ -656,7 +659,89 @@ describe('PubMatic adapter', function () { }] }] } - } + }; + outstreamBidRequest = + [ + { + code: 'video1', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'outstream' + } + }, + bidder: 'pubmatic', + bidId: '47acc48ad47af5', + requestId: '0fb4905b-1234-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + params: { + publisherId: '5670', + outstreamAU: 'pubmatic-test', + adSlot: 'Div1@0x0', // ad_id or tagid + video: { + mimes: ['video/mp4', 'video/x-flv'], + skippable: true, + minduration: 5, + maxduration: 30 + } + } + } + ]; + + validOutstreamBidRequest = { + auctionId: '92489f71-1bf2-49a0-adf9-000cea934729', + auctionStart: 1585918458868, + bidderCode: 'pubmatic', + bidderRequestId: '47acc48ad47af5', + bids: [{ + adUnitCode: 'video1', + auctionId: '92489f71-1bf2-49a0-adf9-000cea934729', + bidId: '47acc48ad47af5', + bidRequestsCount: 1, + bidder: 'pubmatic', + bidderRequestId: '47acc48ad47af5', + mediaTypes: { + video: { + context: 'outstream' + } + }, + params: { + publisherId: '5670', + outstreamAU: 'pubmatic-test', + adSlot: 'Div1@0x0', // ad_id or tagid + video: { + mimes: ['video/mp4', 'video/x-flv'], + skippable: true, + minduration: 5, + maxduration: 30 + } + }, + sizes: [[768, 432], [640, 480], [630, 360]], + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' + }], + start: 11585918458869, + timeout: 3000 + }; + + outstreamVideoBidResponse = { + 'body': { + 'id': '93D3BAD6-E2E2-49FB-9D89-920B1761C865', + 'seatbid': [{ + 'bid': [{ + 'id': '0fb4905b-1234-4152-86be-c6f6d259ba99', + 'impid': '47acc48ad47af5', + 'price': 1.3, + 'adm': 'Acudeo CompatibleVAST 2.0 Instream Test 1VAST 2.0 Instream Test 1https://dsptracker.com/{PSPM}00:00:04https://www.pubmatic.com', + 'h': 250, + 'w': 300, + 'ext': { + 'deal_channel': 6 + } + }] + }] + } + }; }); describe('implementation', function () { @@ -725,7 +810,17 @@ describe('PubMatic adapter', function () { let request = spec.buildRequests(bidRequests); expect(request.url).to.equal('https://hbopenbid.pubmatic.com/translator?source=prebid-client'); expect(request.method).to.equal('POST'); - }); + }); + + it('should return bidderRequest property', function() { + let request = spec.buildRequests(bidRequests, validOutstreamBidRequest); + expect(request.bidderRequest).to.equal(validOutstreamBidRequest); + }); + + it('bidderRequest should be undefined if bidderRequest is not present', function() { + let request = spec.buildRequests(bidRequests); + expect(request.bidderRequest).to.be.undefined; + }); it('test flag not sent when pubmaticTest=true is absent in page url', function() { let request = spec.buildRequests(bidRequests); @@ -2454,7 +2549,6 @@ describe('PubMatic adapter', function () { it('should check for valid video mediaType in case of multiformat request', function() { let request = spec.buildRequests(videoBidRequests); let response = spec.interpretResponse(videoBidResponse, request); - expect(response[0].mediaType).to.equal('video'); }); @@ -2464,6 +2558,36 @@ describe('PubMatic adapter', function () { expect(response[0].mediaType).to.equal('native'); }); + + it('should assign renderer if bid is video and request is for outstream', function() { + let request = spec.buildRequests(outstreamBidRequest, validOutstreamBidRequest); + let response = spec.interpretResponse(outstreamVideoBidResponse, request); + expect(response[0].renderer).to.exist; + }); + + it('should not assign renderer if bidderRequest is not present', function() { + let request = spec.buildRequests(outstreamBidRequest); + let response = spec.interpretResponse(outstreamVideoBidResponse, request); + expect(response[0].renderer).to.not.exist; + }); + + it('should not assign renderer if bid is video and request is for instream', function() { + let request = spec.buildRequests(videoBidRequests); + let response = spec.interpretResponse(videoBidResponse, request); + expect(response[0].renderer).to.not.exist; + }); + + it('should not assign renderer if bid is native', function() { + let request = spec.buildRequests(nativeBidRequests); + let response = spec.interpretResponse(nativeBidResponse, request); + expect(response[0].renderer).to.not.exist; + }); + + it('should not assign renderer if bid is of banner', function() { + let request = spec.buildRequests(bidRequests); + let response = spec.interpretResponse(bidResponses, request); + expect(response[0].renderer).to.not.exist; + }); }); describe('getUserSyncs', function() { From 84e2361cdf6fd82c5d4b1de3a74dab2e33e7f9c4 Mon Sep 17 00:00:00 2001 From: Dan Harton Date: Mon, 17 Aug 2020 10:24:48 -0700 Subject: [PATCH 08/43] AdButler Bid Adapter: Add Doceree as alias (#5598) --- modules/adbutlerBidAdapter.js | 2 +- modules/docereeBidAdapter.md | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 modules/docereeBidAdapter.md diff --git a/modules/adbutlerBidAdapter.js b/modules/adbutlerBidAdapter.js index 06c2eea5d67..10edd8ae3e3 100644 --- a/modules/adbutlerBidAdapter.js +++ b/modules/adbutlerBidAdapter.js @@ -9,7 +9,7 @@ const BIDDER_CODE = 'adbutler'; export const spec = { code: BIDDER_CODE, pageID: Math.floor(Math.random() * 10e6), - aliases: ['divreach'], + aliases: ['divreach', 'doceree'], isBidRequestValid: function (bid) { return !!(bid.params.accountID && bid.params.zoneID); diff --git a/modules/docereeBidAdapter.md b/modules/docereeBidAdapter.md new file mode 100644 index 00000000000..9e3d4bbe1b8 --- /dev/null +++ b/modules/docereeBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +Module Name: Doceree Bidder Adapter +Module Type: Bidder Adapter + +# Description + +Connects to Doceree demand source to fetch bids. +Please use ```doceree``` as the bidder code. + +# Test Parameters +``` + var adUnits = [ + { + code: 'desktop-banner-ad-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "doceree", + params: { + accountID: '167283', + zoneID: '445501', + domain: 'adbserver.doceree.com', + extra: { + tuid: '1234-abcd' + } + } + } + ] + }, + ]; +``` From 952e0fb651316bdd8d35af60d1d1cc3c76d06647 Mon Sep 17 00:00:00 2001 From: rtuschkany <35923908+rtuschkany@users.noreply.github.com> Date: Mon, 17 Aug 2020 19:26:53 +0200 Subject: [PATCH 09/43] Digitrust removal, broader identity support, floor-module support (#5599) * Digitrust remove, broader identity support, floor-module support * Digitrust removal, broader identity support, floor-module support --- modules/connectadBidAdapter.js | 95 +++++-------------- test/spec/modules/connectadBidAdapter_spec.js | 24 ++++- 2 files changed, 45 insertions(+), 74 deletions(-) diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js index 8b34df563ec..04459ec1f09 100644 --- a/modules/connectadBidAdapter.js +++ b/modules/connectadBidAdapter.js @@ -2,6 +2,7 @@ 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'; +import {createEidsArray} from './userId/eids.js'; const BIDDER_CODE = 'connectad'; const BIDDER_CODE_ALIAS = 'connectadrealtime'; @@ -18,8 +19,6 @@ export const spec = { }, buildRequests: function(validBidRequests, bidderRequest) { - let digitrust; - let ret = { method: 'POST', url: '', @@ -41,7 +40,8 @@ export const spec = { screensize: getScreenSize(), dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, language: navigator.language, - ua: navigator.userAgent + ua: navigator.userAgent, + pversion: '$prebid.version$' }); // coppa compliance @@ -69,80 +69,19 @@ export const spec = { utils.deepSetValue(data, 'user.ext.us_privacy', bidderRequest.uspConsent); } - // Digitrust Support - const bidRequestDigitrust = utils.deepAccess(validBidRequests[0], 'userId.digitrustid.data'); - if (bidRequestDigitrust && (!bidRequestDigitrust.privacy || !bidRequestDigitrust.privacy.optout)) { - digitrust = { - id: bidRequestDigitrust.id, - keyv: bidRequestDigitrust.keyv - } - } - - if (digitrust) { - utils.deepSetValue(data, 'user.ext.digitrust', { - id: digitrust.id, - keyv: digitrust.keyv - }) - } - - if (validBidRequests[0].userId && typeof validBidRequests[0].userId === 'object' && (validBidRequests[0].userId.tdid || validBidRequests[0].userId.pubcid || validBidRequests[0].userId.lipb || validBidRequests[0].userId.id5id || validBidRequests[0].userId.parrableId)) { - utils.deepSetValue(data, 'user.ext.eids', []); - - if (validBidRequests[0].userId.tdid) { - data.user.ext.eids.push({ - source: 'adserver.org', - uids: [{ - id: validBidRequests[0].userId.tdid, - ext: { - rtiPartner: 'TDID' - } - }] - }); - } - - if (validBidRequests[0].userId.pubcid) { - data.user.ext.eids.push({ - source: 'pubcommon', - uids: [{ - id: validBidRequests[0].userId.pubcid, - }] - }); - } - - if (validBidRequests[0].userId.id5id) { - data.user.ext.eids.push({ - source: 'id5-sync.com', - uids: [{ - id: validBidRequests[0].userId.id5id, - }] - }); - } - - if (validBidRequests[0].userId.parrableId) { - data.user.ext.eids.push({ - source: 'parrable.com', - uids: [{ - id: validBidRequests[0].userId.parrableId.eid, - }] - }); - } - - if (validBidRequests[0].userId.lipb && validBidRequests[0].userId.lipb.lipbid) { - data.user.ext.eids.push({ - source: 'liveintent.com', - uids: [{ - id: validBidRequests[0].userId.lipb.lipbid - }] - }); - } + // EIDS Support + if (validBidRequests[0].userId) { + data.user.ext.eids = createEidsArray(validBidRequests[0].userId); } validBidRequests.map(bid => { const placement = Object.assign({ id: bid.transactionId, divName: bid.bidId, + pisze: bid.mediaTypes.banner.sizes[0] || bid.sizes[0], sizes: bid.mediaTypes.banner.sizes, - adTypes: getSize(bid.mediaTypes.banner.sizes || bid.sizes) + adTypes: getSize(bid.mediaTypes.banner.sizes || bid.sizes), + floor: getBidFloor(bid) }, bid.params); if (placement.networkId && placement.siteId) { @@ -277,6 +216,22 @@ sizeMap[331] = '320x250'; sizeMap[3301] = '320x267'; sizeMap[2730] = '728x250'; +function getBidFloor(bidRequest) { + let floorInfo = {}; + + if (typeof bidRequest.getFloor === 'function') { + floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'banner', + size: '*' + }); + } + + let floor = floorInfo.floor || 0; + + return floor; +} + function getSize(sizes) { const result = []; sizes.forEach(function(size) { diff --git a/test/spec/modules/connectadBidAdapter_spec.js b/test/spec/modules/connectadBidAdapter_spec.js index 626018241c4..aef4fb562a7 100644 --- a/test/spec/modules/connectadBidAdapter_spec.js +++ b/test/spec/modules/connectadBidAdapter_spec.js @@ -14,7 +14,8 @@ describe('ConnectAd Adapter', function () { bidder: 'conntectad', params: { siteId: 123456, - networkId: 123456 + networkId: 123456, + bidfloor: 0.50 }, adUnitCode: '/19968336/header-bid-tag-1', mediaTypes: { @@ -46,8 +47,7 @@ describe('ConnectAd Adapter', function () { bidderRequestId: '1c56ad30b9b8ca8', transactionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', userId: { - tdid: '123456', - digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}} + tdid: '123456' } }]; @@ -154,6 +154,23 @@ describe('ConnectAd Adapter', function () { expect(requestparse.placements[0].networkId).to.equal(123456); }); + it('should process floors module if available', function() { + const floorInfo = { + currency: 'USD', + floor: 5.20 + }; + bidRequests[0].getFloor = () => floorInfo; + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.placements[0].floor).to.equal(5.20); + }); + + it('should be 0 if no floormodule is available', function() { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.placements[0].floor).to.equal(0); + }); + it('should contain gdpr info', function () { const request = spec.buildRequests(bidRequests, bidderRequest); const requestparse = JSON.parse(request.data); @@ -227,7 +244,6 @@ describe('ConnectAd Adapter', function () { const requestparse = JSON.parse(request.data); expect(requestparse.user.ext.eids).to.be.an('array'); expect(requestparse.user.ext.eids[0].uids[0].id).to.equal('123456'); - expect(requestparse.user.ext.digitrust.id).to.equal('DTID'); }); it('should add referer info', function () { From 7696428cf8165fd2ccac3e92f5fdede198cd8ac7 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Mon, 17 Aug 2020 15:52:41 -0400 Subject: [PATCH 10/43] Adds support for additional consent (#5600) * add addtlConsent consent to consent object * add unit test for additional consent * Update consentManagement.js * Update index.js * Update prebidServerBidAdapter_spec.js * condense else if --- modules/consentManagement.js | 5 ++- modules/prebidServerBidAdapter/index.js | 3 ++ test/spec/modules/consentManagement_spec.js | 26 ++++++++++++++++ .../modules/prebidServerBidAdapter_spec.js | 31 +++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 0c48d8c854c..90fe24735db 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -371,7 +371,7 @@ function cmpFailed(errMsg, hookConfig, extraArgs) { } /** - * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanger.js for later in the auction + * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanager.js for later in the auction * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) */ function storeConsentData(cmpConsentObject) { @@ -387,6 +387,9 @@ function storeConsentData(cmpConsentObject) { vendorData: (cmpConsentObject) || undefined, gdprApplies: cmpConsentObject && typeof cmpConsentObject.gdprApplies === 'boolean' ? cmpConsentObject.gdprApplies : gdprScope }; + if (cmpConsentObject.addtlConsent && utils.isStr(cmpConsentObject.addtlConsent)) { + consentData.addtlConsent = cmpConsentObject.addtlConsent; + }; } consentData.apiVersion = cmpVersion; gdprDataHandler.setConsentData(consentData); diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 1007305b324..b3d559d956f 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -641,6 +641,9 @@ const OPEN_RTB_PROTOCOL = { } utils.deepSetValue(request, 'regs.ext.gdpr', gdprApplies); utils.deepSetValue(request, 'user.ext.consent', firstBidRequest.gdprConsent.consentString); + if (firstBidRequest.gdprConsent.addtlConsent && typeof firstBidRequest.gdprConsent.addtlConsent === 'string') { + utils.deepSetValue(request, 'user.ext.ConsentedProvidersSettings.consented_providers', firstBidRequest.gdprConsent.addtlConsent); + } } // US Privacy (CCPA) support diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 3ebebfef1ee..deaacbc5a28 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -643,6 +643,32 @@ describe('consentManagement', function () { expect(consent.apiVersion).to.equal(2); }); + it('performs lookup check and stores consentData for a valid existing user with additional consent', function () { + let testConsentData = { + tcString: 'abc12345234', + addtlConsent: 'superduperstring', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded' + }; + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + setConsentConfig(goodConfigWithAllowAuction); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consent = gdprDataHandler.getConsentData(); + sinon.assert.notCalled(utils.logError); + expect(didHookReturn).to.be.true; + expect(consent.consentString).to.equal(testConsentData.tcString); + expect(consent.addtlConsent).to.equal(testConsentData.addtlConsent); + expect(consent.gdprApplies).to.be.true; + expect(consent.apiVersion).to.equal(2); + }); + it('throws an error when processCmpData check fails + does not call requestBids callbcack even when allowAuction is true', function () { let testConsentData = {}; let bidsBackHandlerReturn = false; diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index b75ede0e9db..5abe068c100 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -547,6 +547,37 @@ describe('S2S Adapter', function () { expect(requestBid.user).to.not.exist; }); + it('adds additional consent information to ortb2 request depending on presence of module', function () { + let ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + + let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: ortb2Config }; + config.setConfig(consentConfig); + + let gdprBidRequest = utils.deepClone(BID_REQUESTS); + gdprBidRequest[0].gdprConsent = { + consentString: 'abc123', + addtlConsent: 'superduperconsent', + gdprApplies: true + }; + + adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + let requestBid = JSON.parse(server.requests[0].requestBody); + + expect(requestBid.regs.ext.gdpr).is.equal(1); + expect(requestBid.user.ext.consent).is.equal('abc123'); + expect(requestBid.user.ext.ConsentedProvidersSettings.consented_providers).is.equal('superduperconsent'); + + config.resetConfig(); + config.setConfig({ s2sConfig: CONFIG }); + + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + requestBid = JSON.parse(server.requests[1].requestBody); + + expect(requestBid.regs).to.not.exist; + expect(requestBid.user).to.not.exist; + }); + it('check gdpr info gets added into cookie_sync request: have consent data', function () { let cookieSyncConfig = utils.deepClone(CONFIG); cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync'; From 97713dd0646f5bae4578ff323a865f74c7e017e0 Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Mon, 17 Aug 2020 15:23:02 -0500 Subject: [PATCH 11/43] pass along providers (#5610) --- modules/rubiconAnalyticsAdapter.js | 26 ++++++++----------- .../modules/rubiconAnalyticsAdapter_spec.js | 20 ++++++++++---- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 7a1bdce84d5..6defd7b45ae 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -40,8 +40,6 @@ const cache = { timeouts: {}, }; -const validFloorProviders = ['rubicon', 'rubi', 'magnite', 'mgni']; - export function getHostNameFromReferer(referer) { try { rubiconAdapter.referrerHostname = utils.parseUrl(referer, {noDecodeWholeURL: true}).hostname; @@ -205,12 +203,13 @@ function sendMessage(auctionId, bidWonId) { } else { auction.floors = utils.pick(auctionCache.floorData, [ 'location', - 'modelName', () => auctionCache.floorData.modelVersion, + 'modelVersion as modelName', 'skipped', 'enforcement', () => utils.deepAccess(auctionCache.floorData, 'enforcements.enforceJS'), 'dealsEnforced', () => utils.deepAccess(auctionCache.floorData, 'enforcements.floorDeals'), 'skipRate', - 'fetchStatus' + 'fetchStatus', + 'floorProvider as provider' ]); } } @@ -285,7 +284,7 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { if (previousBidResponse && previousBidResponse.bidPriceUSD > responsePrice) { return previousBidResponse; } - let bidResponse = utils.pick(bid, [ + return utils.pick(bid, [ 'bidPriceUSD', () => responsePrice, 'dealId', 'status', @@ -295,12 +294,9 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { 'height' ]), 'seatBidId', + 'floorValue', () => utils.deepAccess(bid, 'floorData.floorValue'), + 'floorRule', () => utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined ]); - if (auctionFloorData) { - bidResponse.floorValue = utils.deepAccess(bid, 'floorData.floorValue'); - bidResponse.floorRule = utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined - } - return bidResponse; } let samplingFactor = 1; @@ -380,9 +376,9 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.bids = {}; cacheEntry.bidsWon = {}; cacheEntry.referrer = args.bidderRequests[0].refererInfo.referer; - const floorProvider = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData.floorProvider'); - if (validFloorProviders.indexOf(floorProvider) !== -1) { - cacheEntry.floorData = {...args.bidderRequests[0].bids[0].floorData}; + const floorData = utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData'); + if (floorData) { + cacheEntry.floorData = {...floorData}; } cache.auctions[args.auctionId] = cacheEntry; break; @@ -468,7 +464,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { bid.adUnit.adSlot = args.floorData.matchedFields.gptSlot; } // if we have not set enforcements yet set it - if (auctionEntry.floorData && !auctionEntry.floorData.enforcements && utils.deepAccess(args, 'floorData.enforcements')) { + if (!utils.deepAccess(auctionEntry, 'floorData.enforcements') && utils.deepAccess(args, 'floorData.enforcements')) { auctionEntry.floorData.enforcements = {...args.floorData.enforcements}; } if (!bid) { @@ -492,7 +488,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { }; } bid.clientLatencyMillis = Date.now() - cache.auctions[args.auctionId].timestamp; - bid.bidResponse = parseBidResponse(args, bid.bidResponse, auctionEntry.floorData); + bid.bidResponse = parseBidResponse(args, bid.bidResponse); break; case BIDDER_DONE: args.bids.forEach(bid => { diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 466435d9652..0c2c83a4b37 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -743,7 +743,8 @@ describe('rubicon analytics adapter', function () { enforcement: true, dealsEnforced: false, skipRate: 15, - fetchStatus: 'error' + fetchStatus: 'error', + provider: 'rubicon' }); // first adUnit's adSlot expect(message.auctions[0].adUnits[0].adSlot).to.equal('12345/sports'); @@ -765,19 +766,28 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); }); - it('should not send floor info if provider is not rubicon', function () { + it('should still send floor info if provider is not rubicon', function () { let message = performFloorAuction('randomProvider') // verify our floor stuff is passed // top level floor info - expect(message.auctions[0].floors).to.be.undefined; + expect(message.auctions[0].floors).to.deep.equal({ + location: 'setConfig', + modelName: 'someModelName', + skipped: false, + enforcement: true, + dealsEnforced: false, + skipRate: 15, + fetchStatus: 'error', + provider: 'randomProvider' + }); // first adUnit's adSlot expect(message.auctions[0].adUnits[0].adSlot).to.equal('12345/sports'); // since no other bids, we set adUnit status to no-bid expect(message.auctions[0].adUnits[0].status).to.equal('no-bid'); // first adUnits bid is rejected expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected'); - expect(message.auctions[0].adUnits[0].bids[0].bidResponse.floorValue).to.be.undefined; + expect(message.auctions[0].adUnits[0].bids[0].bidResponse.floorValue).to.equal(4); // if bid rejected should take cpmAfterAdjustments val expect(message.auctions[0].adUnits[0].bids[0].bidResponse.bidPriceUSD).to.equal(2.1); @@ -787,7 +797,7 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].status).to.equal('success'); // second adUnits bid is success expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); - expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.be.undefined; + expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); }); From f983af22b8817bb23eaf1dd666769cd2ff260306 Mon Sep 17 00:00:00 2001 From: Dan Bogdan <43830380+EMXDigital@users.noreply.github.com> Date: Tue, 18 Aug 2020 03:54:05 -0400 Subject: [PATCH 12/43] Emx bid adapter: gdpr user sync update (#5611) * adding ccpa support for emx_digital adapter * emx_digital ccpa compliance: lint fix * emx 3.0 compliance update * fix outstream renderer issue, update test spec * refactor formatVideoResponse function to use core-js/find * Added GVLID to Adapter, Updated getUserSyncs to accept and leverage gdprConsent * Added testing coverage for gdpr in getUserSyncs Co-authored-by: Nick Colletti Co-authored-by: Nick Colletti Co-authored-by: Kiyoshi Hara Co-authored-by: Dan Bogdan --- modules/emx_digitalBidAdapter.js | 14 +++++++++-- .../modules/emx_digitalBidAdapter_spec.js | 23 ++++++++++++++----- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/modules/emx_digitalBidAdapter.js b/modules/emx_digitalBidAdapter.js index 6688d15d8e9..fa58481548a 100644 --- a/modules/emx_digitalBidAdapter.js +++ b/modules/emx_digitalBidAdapter.js @@ -162,6 +162,7 @@ export const emxAdapter = { export const spec = { code: BIDDER_CODE, + gvlid: 183, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function (bid) { if (!bid || !bid.params) { @@ -279,12 +280,21 @@ export const spec = { } return emxBidResponses; }, - getUserSyncs: function (syncOptions) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { const syncs = []; if (syncOptions.iframeEnabled) { + let url = 'https://biddr.brealtime.com/check.html'; + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + // add 'gdpr' only if 'gdprApplies' is defined + if (typeof gdprConsent.gdprApplies === 'boolean') { + url += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + url += `?gdpr_consent=${gdprConsent.consentString}`; + } + } syncs.push({ type: 'iframe', - url: 'https://biddr.brealtime.com/check.html' + url: url }); } return syncs; diff --git a/test/spec/modules/emx_digitalBidAdapter_spec.js b/test/spec/modules/emx_digitalBidAdapter_spec.js index 7be8a2ce5ac..138786b9c74 100644 --- a/test/spec/modules/emx_digitalBidAdapter_spec.js +++ b/test/spec/modules/emx_digitalBidAdapter_spec.js @@ -612,12 +612,23 @@ describe('emx_digital Adapter', function () { }); describe('getUserSyncs', function () { - let syncOptionsIframe = { iframeEnabled: true }; - let syncOptionsPixel = { pixelEnabled: true }; - it('Should push the correct sync type depending on the config', function () { - let iframeSync = spec.getUserSyncs(syncOptionsIframe); - expect(iframeSync.length).to.equal(1); - expect(iframeSync[0].type).to.equal('iframe'); + it('should register the iframe sync url', function () { + let syncs = spec.getUserSyncs({ + iframeEnabled: true + }); + expect(syncs).to.not.be.an('undefined'); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + }); + + it('should pass gdpr params', function () { + let syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, { + gdprApplies: false, consentString: 'test' + }); + expect(syncs).to.not.be.an('undefined'); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.contains('gdpr=0'); }); }); }); From cc58b2320f2f6808d478103a0f420fb40cf4ef5f Mon Sep 17 00:00:00 2001 From: hbanalytics <55453525+hbanalytics@users.noreply.github.com> Date: Tue, 18 Aug 2020 11:18:53 +0300 Subject: [PATCH 13/43] Yieldone Analytics Adapter: Fix empty events (#5617) * Added Y1 Analytics Adapter * rename y1AnalyticsAdapter in yieldoneAnalyticsAdapter * Yieldone Bid Adapter: fixes from lint check * Yieldone Analytics Adapter: fix endpoint protocol * Added spec file for yieldone Analytics Adapter * Add adUnitName to analytics data for Yieldone Analytics Adapter * Fix yieldone Analytics Adapter to log only id from adUnitPath * Fix bug with timeout event in Yieldone Analytics Adapter * Update yieldone analytics adapter to remove excess 'ad' field from data * Update yieldone analytics adapter * Yieldone Analytics Adapter: remove dispensable events from log * Platform One Analytics Adapter: fixes after review * Fix empty events in Yieldone Analytics Adapter --- modules/yieldoneAnalyticsAdapter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/yieldoneAnalyticsAdapter.js b/modules/yieldoneAnalyticsAdapter.js index dd40e205b07..542c0917708 100644 --- a/modules/yieldoneAnalyticsAdapter.js +++ b/modules/yieldoneAnalyticsAdapter.js @@ -123,7 +123,7 @@ const yieldoneAnalytics = Object.assign(adapter({analyticsType}), { auctionManager.getAdUnitCodes(), auctionManager.getBidsReceived() ); - if (yieldoneAnalytics.eventsStorage[currentAuctionId]) { + if (yieldoneAnalytics.eventsStorage[currentAuctionId] && yieldoneAnalytics.eventsStorage[currentAuctionId].events.length) { yieldoneAnalytics.eventsStorage[currentAuctionId].page = {url: referrers[currentAuctionId]}; yieldoneAnalytics.eventsStorage[currentAuctionId].pubId = pubId; yieldoneAnalytics.eventsStorage[currentAuctionId].wrapper_version = '$prebid.version$'; @@ -139,8 +139,8 @@ const yieldoneAnalytics = Object.assign(adapter({analyticsType}), { } } }, - sendStat(events, auctionId) { - if (!events) return; + sendStat(data, auctionId) { + if (!data || !data.events || !data.events.length) return; delete yieldoneAnalytics.eventsStorage[auctionId]; ajax( url, @@ -148,7 +148,7 @@ const yieldoneAnalytics = Object.assign(adapter({analyticsType}), { success: function() {}, error: function() {} }, - JSON.stringify(events), + JSON.stringify(data), { method: 'POST' } From 8646ba7511abc447db20e9e99c3db474a75dd7e6 Mon Sep 17 00:00:00 2001 From: BrightMountainMedia <69471268+BrightMountainMediaInc@users.noreply.github.com> Date: Tue, 18 Aug 2020 14:09:28 +0530 Subject: [PATCH 14/43] Add Bright Mountain Media Bid Adapter (#5593) * Add Bright Mountain Media Bid Adapter * Fix missing quotes around placement_id string * Update maintainer email --- modules/brightMountainMediaBidAdapter.js | 89 +++++++++++ modules/brightMountainMediaBidAdapter.md | 34 +++++ .../brightMountainMediaBidAdapter_spec.js | 139 ++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 modules/brightMountainMediaBidAdapter.js create mode 100644 modules/brightMountainMediaBidAdapter.md create mode 100644 test/spec/modules/brightMountainMediaBidAdapter_spec.js diff --git a/modules/brightMountainMediaBidAdapter.js b/modules/brightMountainMediaBidAdapter.js new file mode 100644 index 00000000000..5a285be71c0 --- /dev/null +++ b/modules/brightMountainMediaBidAdapter.js @@ -0,0 +1,89 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const BIDDER_CODE = 'brightmountainmedia'; +const AD_URL = 'https://console.brightmountainmedia.com/hb/bid'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && bid.params.placement_id); + }, + + buildRequests: (validBidRequests, bidderRequest) => { + let winTop = window; + let location; + try { + location = new URL(bidderRequest.refererInfo.referer) + winTop = window.top; + } catch (e) { + location = winTop.location; + utils.logMessage(e); + }; + let placements = []; + let request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language : '', + 'secure': 1, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + if (bidderRequest) { + if (bidderRequest.gdprConsent) { + request.gdpr_consent = bidderRequest.gdprConsent.consentString || 'ALL' + request.gdpr_require = bidderRequest.gdprConsent.gdprApplies ? 1 : 0 + } + } + for (let i = 0; i < validBidRequests.length; i++) { + let bid = validBidRequests[i]; + let traff = bid.params.traffic || BANNER + let placement = { + placementId: bid.params.placement_id, + bidId: bid.bidId, + sizes: bid.mediaTypes[traff].sizes, + traffic: traff + }; + if (bid.schain) { + placement.schain = bid.schain; + } + placements.push(placement); + } + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + try { + serverResponse = serverResponse.body; + for (let i = 0; i < serverResponse.length; i++) { + let resItem = serverResponse[i]; + + response.push(resItem); + } + } catch (e) { + utils.logMessage(e); + }; + return response; + }, + + getUserSyncs: (syncOptions) => { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: 'https://console.brightmountainmedia.com:4444/cookieSync' + }]; + } + }, + +}; + +registerBidder(spec); diff --git a/modules/brightMountainMediaBidAdapter.md b/modules/brightMountainMediaBidAdapter.md new file mode 100644 index 00000000000..a9900b5264d --- /dev/null +++ b/modules/brightMountainMediaBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: Bright Mountain Media Bidder Adapter +Module Type: Bidder Adapter +Maintainer: dev@brightmountainmedia.com +``` + +# Description + +Connects to Bright Mountain Media exchange for bids. + +Bright Mountain Media bid adapter currently supports Banner. + +# Test Parameters +``` + var adUnits = [ + code: 'placementid_0', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'brightmountainmedia', + params: { + placement_id: '5f21784949be82079d08c', + traffic: 'banner' + } + } + ] + ]; +``` \ No newline at end of file diff --git a/test/spec/modules/brightMountainMediaBidAdapter_spec.js b/test/spec/modules/brightMountainMediaBidAdapter_spec.js new file mode 100644 index 00000000000..3b7e46e55a7 --- /dev/null +++ b/test/spec/modules/brightMountainMediaBidAdapter_spec.js @@ -0,0 +1,139 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/brightMountainMediaBidAdapter.js'; + +describe('brightMountainMediaBidAdapter_spec', function () { + let bid = { + bidId: '2dd581a2b6281d', + bidder: 'brightmountainmedia', + bidderRequestId: '145e1d6a7837c9', + params: { + placement_id: '123qwerty' + }, + placementCode: 'placementid_0', + auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + transactionId: '3bb2f6da-87a6-4029-aeb0-bfe951372e62', + }; + let bidderRequest = { + bidderCode: 'brightmountainmedia', + auctionId: 'fffffff-ffff-ffff-ffff-ffffffffffff', + bidderRequestId: 'ffffffffffffff', + start: 1472239426002, + auctionStart: 1472239426000, + timeout: 5000, + uspConsent: '1YN-', + refererInfo: { + referer: 'http://www.example.com', + reachedTop: true, + }, + bids: [bid] + } + + describe('isBidRequestValid', function () { + it('Should return true when when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false when required params are not passed', function () { + bid.params = {} + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://console.brightmountainmedia.com/hb/bid'); + }); + + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + let placements = data['placements']; + for (let i = 0; i < placements.length; i++) { + let placement = placements[i]; + expect(placement).to.have.all.keys('placementId', 'bidId', 'traffic', 'sizes'); + expect(placement.placementId).to.be.a('string'); + expect(placement.bidId).to.be.a('string'); + expect(placement.traffic).to.be.a('string'); + expect(placement.sizes).to.be.an('array'); + } + }); + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + let resObject = { + body: [{ + requestId: '123', + mediaType: 'banner', + cpm: 0.3, + width: 320, + height: 50, + ad: '

Hello ad

', + ttl: 1000, + creativeId: '123asd', + netRevenue: true, + currency: 'USD' + }] + }; + let serverResponses = spec.interpretResponse(resObject); + it('Returns an array of valid server responses if response object is valid', function () { + expect(serverResponses).to.be.an('array').that.is.not.empty; + for (let i = 0; i < serverResponses.length; i++) { + let dataItem = serverResponses[i]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'mediaType'); + expect(dataItem.requestId).to.be.a('string'); + expect(dataItem.cpm).to.be.a('number'); + expect(dataItem.width).to.be.a('number'); + expect(dataItem.height).to.be.a('number'); + expect(dataItem.ad).to.be.a('string'); + expect(dataItem.ttl).to.be.a('number'); + expect(dataItem.creativeId).to.be.a('string'); + expect(dataItem.netRevenue).to.be.a('boolean'); + expect(dataItem.currency).to.be.a('string'); + expect(dataItem.mediaType).to.be.a('string'); + } + it('Returns an empty array if invalid response is passed', function () { + serverResponses = spec.interpretResponse('invalid_response'); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + }); + + describe('getUserSyncs', function () { + let syncoptionsIframe = { + 'iframeEnabled': 'true' + } + it('should return iframe sync option', function () { + expect(spec.getUserSyncs(syncoptionsIframe)).to.be.an('array').with.lengthOf(1); + expect(spec.getUserSyncs(syncoptionsIframe)[0].type).to.exist; + expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.exist; + expect(spec.getUserSyncs(syncoptionsIframe)[0].type).to.equal('iframe') + expect(spec.getUserSyncs(syncoptionsIframe)[0].url).to.equal('https://console.brightmountainmedia.com:4444/cookieSync') + }); + }); +}); From 38e078ef99804b2320b60c868a2375470e917185 Mon Sep 17 00:00:00 2001 From: liranbaruch Date: Tue, 18 Aug 2020 12:38:17 +0300 Subject: [PATCH 15/43] Added IronSource bidder tests and adapter according to specs (#5568) * Added IronSource bidder tests and adapter according to specs * Update tests according to request --- modules/ironsourceBidAdapter.js | 234 ++++++++++++ modules/ironsourceBidAdapter.md | 49 +++ .../spec/modules/ironsourceBidAdapter_spec.js | 339 ++++++++++++++++++ 3 files changed, 622 insertions(+) create mode 100644 modules/ironsourceBidAdapter.js create mode 100644 modules/ironsourceBidAdapter.md create mode 100644 test/spec/modules/ironsourceBidAdapter_spec.js diff --git a/modules/ironsourceBidAdapter.js b/modules/ironsourceBidAdapter.js new file mode 100644 index 00000000000..795302762cd --- /dev/null +++ b/modules/ironsourceBidAdapter.js @@ -0,0 +1,234 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; + +const SUPPORTED_AD_TYPES = [VIDEO]; +const BIDDER_CODE = 'ironSource'; +const BIDDER_VERSION = '4.0.0'; +const TTL = 360; +const SELLER_ENDPOINT = 'https://hb.yellowblue.io/hb'; +const SUPPORTED_SYNC_METHODS = { + IFRAME: 'iframe', + PIXEL: 'pixel' +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: function(bidRequest) { + return !!(bidRequest.params.isOrg); + }, + buildRequests: function (bidRequests, bidderRequest) { + if (bidRequests.length === 0) { + return []; + } + + const requests = []; + + bidRequests.forEach(bid => { + requests.push(buildVideoRequest(bid, bidderRequest)); + }); + + return requests; + }, + interpretResponse: function({body}) { + const bidResponses = []; + + const bidResponse = { + requestId: body.requestId, + cpm: body.cpm, + width: body.width, + height: body.height, + creativeId: body.requestId, + currency: body.currency, + netRevenue: body.netRevenue, + ttl: TTL, + vastXml: body.vastXml, + mediaType: VIDEO + }; + + bidResponses.push(bidResponse); + + return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (syncOptions.iframeEnabled && response.body.userSyncURL) { + syncs.push({ + type: 'iframe', + url: response.body.userSyncURL + }); + } + if (syncOptions.pixelEnabled && utils.isArray(response.body.userSyncPixels)) { + const pixels = response.body.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }) + syncs.push(...pixels) + } + } + return syncs; + } +}; + +registerBidder(spec); + +/** + * Build the video request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function buildVideoRequest(bid, bidderRequest) { + const sellerParams = generateParameters(bid, bidderRequest); + return { + method: 'GET', + url: SELLER_ENDPOINT, + data: sellerParams + }; +} + +/** + * Get the the ad size from the bid + * @param bid {bid} + * @returns {Array} + */ +function getSizes(bid) { + if (utils.deepAccess(bid, 'mediaTypes.video.sizes')) { + return bid.mediaTypes.video.sizes[0]; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + return bid.sizes[0]; + } + return []; +} + +/** + * Get schain string value + * @param schainObject {Object} + * @returns {string} + */ +function getSupplyChain(schainObject) { + if (utils.isEmpty(schainObject)) { + return ''; + } + let scStr = `${schainObject.ver},${schainObject.complete}`; + schainObject.nodes.forEach((node) => { + scStr += '!'; + scStr += `${getEncodedValIfNotEmpty(node.asi)},`; + scStr += `${getEncodedValIfNotEmpty(node.sid)},`; + scStr += `${getEncodedValIfNotEmpty(node.hp)},`; + scStr += `${getEncodedValIfNotEmpty(node.rid)},`; + scStr += `${getEncodedValIfNotEmpty(node.name)},`; + scStr += `${getEncodedValIfNotEmpty(node.domain)}`; + }); + return scStr; +} + +/** + * Get encoded node value + * @param val {string} + * @returns {string} + */ +function getEncodedValIfNotEmpty(val) { + return !utils.isEmpty(val) ? encodeURIComponent(val) : ''; +} + +/** + * Get preferred user-sync method based on publisher configuration + * @param bidderCode {string} + * @returns {string} + */ +function getAllowedSyncMethod(filterSettings, bidderCode) { + const iframeConfigsToCheck = ['all', 'iframe']; + const pixelConfigToCheck = 'image'; + if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { + return SUPPORTED_SYNC_METHODS.IFRAME; + } + if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { + return SUPPORTED_SYNC_METHODS.PIXEL; + } +} + +/** + * Check if sync rule is supported + * @param syncRule {Object} + * @param bidderCode {string} + * @returns {boolean} + */ +function isSyncMethodAllowed(syncRule, bidderCode) { + if (!syncRule) { + return false; + } + const isInclude = syncRule.filter === 'include'; + const bidders = utils.isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; + return isInclude && utils.contains(bidders, bidderCode); +} + +/** + * Generate query parameters for the request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function generateParameters(bid, bidderRequest) { + const timeout = config.getConfig('bidderTimeout'); + const { syncEnabled, filterSettings } = config.getConfig('userSync'); + const [ width, height ] = getSizes(bid); + const { params } = bid; + const { bidderCode } = bidderRequest; + const domain = window.location.hostname; + + const requestParams = { + auction_start: utils.timestamp(), + ad_unit_code: utils.getBidIdParameter('adUnitCode', bid), + tmax: timeout, + width: width, + height: height, + publisher_id: params.isOrg, + floor_price: params.floorPrice, + ua: navigator.userAgent, + bid_id: utils.getBidIdParameter('bidId', bid), + bidder_request_id: utils.getBidIdParameter('bidderRequestId', bid), + transaction_id: utils.getBidIdParameter('transactionId', bid), + session_id: utils.getBidIdParameter('auctionId', bid), + publisher_name: domain, + site_domain: domain, + bidder_version: BIDDER_VERSION + }; + + if (syncEnabled) { + const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + requestParams.cs_method = allowedSyncMethod; + } + } + + if (bidderRequest.uspConsent) { + requestParams.us_privacy = bidderRequest.uspConsent; + } + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + requestParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + requestParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + + if (params.ifa) { + requestParams.ifa = params.ifa; + } + + if (bid.schain) { + requestParams.schain = getSupplyChain(bid.schain); + } + + if (bidderRequest && bidderRequest.refererInfo) { + requestParams.referrer = utils.deepAccess(bidderRequest, 'refererInfo.referer'); + requestParams.page_url = utils.deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href'); + } + + return requestParams; +} diff --git a/modules/ironsourceBidAdapter.md b/modules/ironsourceBidAdapter.md new file mode 100644 index 00000000000..378a344b672 --- /dev/null +++ b/modules/ironsourceBidAdapter.md @@ -0,0 +1,49 @@ +#Overview + +Module Name: IronSource Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: prebid-digital-brands@ironsrc.com + + +# Description + +Module that connects to IronSource's demand sources. + +The IronSource adapter requires setup and approval from the IronSource. Please reach out to prebid-digital-brands@ironsrc.com to create an IronSource account. + +The adapter supports Video(instream). For the integration, IronSource returns content as vastXML and requires the publisher to define the cache url in config passed to Prebid for it to be valid in the auction. + +# Bid Parameters +## Video + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `isOrg` | required | String | IronSource publisher Id provided by your IronSource representative | "56f91cd4d3e3660002000033" +| `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 +| `ifa` | optional | String | The ID for advertisers (also referred to as "IDFA") | "XXX-XXX" + +# Test Parameters +```javascript +var adUnits = [ + { + code: 'dfp-video-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + } + }, + bids: [{ + bidder: 'ironSource', + params: { + isOrg: '56f91cd4d3e3660002000033', // Required + floorPrice: 2.00, // Optional + ifa: 'XXX-XXX', // Optional + } + }] + } + ]; +``` diff --git a/test/spec/modules/ironsourceBidAdapter_spec.js b/test/spec/modules/ironsourceBidAdapter_spec.js new file mode 100644 index 00000000000..0ddc12f235c --- /dev/null +++ b/test/spec/modules/ironsourceBidAdapter_spec.js @@ -0,0 +1,339 @@ +import { expect } from 'chai'; +import { spec } from 'modules/ironsourceBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { VIDEO } from '../../../src/mediaTypes.js'; + +const ENDPOINT = 'https://hb.yellowblue.io/hb'; +const TTL = 360; + +describe('ironsourceAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'isOrg': 'jdye8weeyirk00000001' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'isOrg': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'isOrg': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const bidderRequest = { + bidderCode: 'ironSource', + } + + it('sends bid request to ENDPOINT via GET', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('should send the correct bid Id', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.bid_id).to.equal('299ffc8cca0b87'); + } + }); + + it('should send the correct width and height', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('width', 640); + expect(request.data).to.have.property('height', 480); + } + }); + + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.setConfig({ + userSync: { + syncEnabled: true + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'pixel'); + } + }); + + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithUSP); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('us_privacy', '1YNN'); + } + }); + + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('us_privacy'); + } + }); + + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('gdpr'); + expect(request.data).to.not.have.property('gdpr_consent'); + } + }); + + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('gdpr', true); + expect(request.data).to.have.property('gdpr_consent', 'test-consent-string'); + } + }); + + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('schain', '1.0,1!indirectseller.com,00001,,,,'); + } + }); + }); + + describe('interpretResponse', function () { + const response = { + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + netRevenue: true, + currency: 'USD' + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + requestId: '21e12606d47ba7', + cpm: 12.5, + width: 640, + height: 480, + creativeId: '21e12606d47ba7', + currency: 'USD', + netRevenue: true, + ttl: TTL, + vastXml: '', + mediaType: VIDEO + } + ]; + const result = spec.interpretResponse({ body: [response] }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + }; + + const iframeSyncResponse = { + body: { + userSyncURL: 'https://iframe-sync-url.test' + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); + + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); + }); + }) +}); From 0e182b79a174453ddc19cfb185bba58ff580065a Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Tue, 18 Aug 2020 10:57:28 -0500 Subject: [PATCH 16/43] add provider when noData (#5621) --- modules/rubiconAnalyticsAdapter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 6defd7b45ae..0bee4d926fc 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -198,7 +198,8 @@ function sendMessage(auctionId, bidWonId) { if (auctionCache.floorData.location === 'noData') { auction.floors = utils.pick(auctionCache.floorData, [ 'location', - 'fetchStatus' + 'fetchStatus', + 'floorProvider as provider' ]); } else { auction.floors = utils.pick(auctionCache.floorData, [ From 6c61de8e501cf6c107ab99eb373fea2d46c463f1 Mon Sep 17 00:00:00 2001 From: mefjush Date: Tue, 18 Aug 2020 20:07:31 +0200 Subject: [PATCH 17/43] Configure Adhese gvlid, filter out empty params to reduce the Adhese request size (#5602) --- modules/adheseBidAdapter.js | 17 +++++++++++------ test/spec/modules/adheseBidAdapter_spec.js | 8 ++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/modules/adheseBidAdapter.js b/modules/adheseBidAdapter.js index cd5a44f34d8..606e56c13d5 100644 --- a/modules/adheseBidAdapter.js +++ b/modules/adheseBidAdapter.js @@ -4,10 +4,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'adhese'; +const GVLID = 553; const USER_SYNC_BASE_URL = 'https://user-sync.adhese.com/iframe/user_sync.html'; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bid) { @@ -112,12 +114,15 @@ function mergeTargets(targets, target) { if (target) { Object.keys(target).forEach(function (key) { const val = target[key]; - const values = Array.isArray(val) ? val : [val]; - if (targets[key]) { - const distinctValues = values.filter(v => targets[key].indexOf(v) < 0); - targets[key].push.apply(targets[key], distinctValues); - } else { - targets[key] = values; + const dirtyValues = Array.isArray(val) ? val : [val]; + const values = dirtyValues.filter(v => v === 0 || v); + if (values.length > 0) { + if (targets[key]) { + const distinctValues = values.filter(v => targets[key].indexOf(v) < 0); + targets[key].push.apply(targets[key], distinctValues); + } else { + targets[key] = values; + } } }); } diff --git a/test/spec/modules/adheseBidAdapter_spec.js b/test/spec/modules/adheseBidAdapter_spec.js index 0743ddd211d..aa4872641b4 100644 --- a/test/spec/modules/adheseBidAdapter_spec.js +++ b/test/spec/modules/adheseBidAdapter_spec.js @@ -95,6 +95,14 @@ describe('AdheseAdapter', function () { expect(JSON.parse(req.data).parameters).to.deep.include({ 'ci': [ 'london', '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; + expect(params).to.not.have.any.keys('aa', 'bb', 'cc', 'dd'); + expect(params).to.deep.include({ 'ee': [ 0, 1 ], 'ff': [ 0 ], 'gg': [ 'x', 'y' ] }); + }); + it('should include gdpr consent param', function () { let req = spec.buildRequests([ minimalBid() ], bidderRequest); From 28a4c345306824cbad81f8de70551603fc40ae24 Mon Sep 17 00:00:00 2001 From: "Isaac A. Dettman" Date: Tue, 18 Aug 2020 12:08:26 -0700 Subject: [PATCH 18/43] Update rubicon adapter for liveramp userid requirements change (#5620) * Add microadBidAdapter * Remove unnecessary encodeURIComponent from microadBidAdapter * Submit Advangelists Prebid Adapter * Submit Advangelists Prebid Adapter 1.1 * Correct procudtion endpoint for prebid * analytics update with wrapper name * reverted error merge * update changed default value of netRevenue to true * Re-add rubicon analytics without deprecated getTopWindowUrl util * Cache referrer on auction_init instead of bid_requested * merged remote master changes * update liveramp userid support Co-authored-by: nakamoto Co-authored-by: Chandra Prakash Co-authored-by: Eric Harper Co-authored-by: TJ Eastmond Co-authored-by: Mark Monday Co-authored-by: msm0504 <51493331+msm0504@users.noreply.github.com> --- modules/rubiconBidAdapter.js | 4 ++-- test/spec/modules/rubiconBidAdapter_spec.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 0c563987331..47ca694dc43 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -289,7 +289,7 @@ export const spec = { // support identityLink (aka LiveRamp) if (bidRequest.userId.idl_env) { data.user.ext.eids.push({ - source: 'liveramp.com', + source: 'liveramp_idl', uids: [{ id: bidRequest.userId.idl_env }] @@ -554,7 +554,7 @@ export const spec = { // support identityLink (aka LiveRamp) if (bidRequest.userId.idl_env) { - data['tpid_liveramp.com'] = bidRequest.userId.idl_env; + data['x_liverampidl'] = bidRequest.userId.idl_env; } } diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index c21a9b879f1..808d4840418 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1343,7 +1343,7 @@ describe('the rubicon adapter', function () { }); describe('LiveRamp support', function () { - it('should send tpid_liveramp.com when userId defines idl_env', function () { + it('should send x_liverampidl when userId defines idl_env', function () { const clonedBid = utils.deepClone(bidderRequest.bids[0]); clonedBid.userId = { idl_env: '1111-2222-3333-4444' @@ -1351,7 +1351,7 @@ describe('the rubicon adapter', function () { let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); - expect(data['tpid_liveramp.com']).to.equal('1111-2222-3333-4444'); + expect(data['x_liverampidl']).to.equal('1111-2222-3333-4444'); }); }); }) @@ -1546,7 +1546,7 @@ describe('the rubicon adapter', function () { expect(post.user.ext.tpid.source).to.equal('liveintent.com'); expect(post.user.ext.tpid.uid).to.equal('0000-1111-2222-3333'); // LiveRamp should exist - expect(post.user.ext.eids[1].source).to.equal('liveramp.com'); + expect(post.user.ext.eids[1].source).to.equal('liveramp_idl'); expect(post.user.ext.eids[1].uids[0].id).to.equal('1111-2222-3333-4444'); expect(post.rp).that.is.an('object'); expect(post.rp.target).that.is.an('object'); From 3b5a747bdb50fb120405258ca3abc5c41c3b7b71 Mon Sep 17 00:00:00 2001 From: yuvalgg <57989766+yuvalgg@users.noreply.github.com> Date: Tue, 18 Aug 2020 23:17:40 +0300 Subject: [PATCH 19/43] Add intentIq (#5624) --- modules/.submodules.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/.submodules.json b/modules/.submodules.json index 58ab8798fa5..dd40557c35b 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -10,7 +10,8 @@ "criteoIdSystem", "netIdSystem", "identityLinkIdSystem", - "sharedIdSystem" + "sharedIdSystem", + "intentIqIdSystem" ], "adpod": [ "freeWheelAdserverVideo", From fb72d518e3cd68d5dedffd46416db6a0cdfc0ef5 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Tue, 18 Aug 2020 17:21:44 -0400 Subject: [PATCH 20/43] Adds advertiserDomains to kargo adapter (#5625) * Update kargoBidAdapter.js * Update kargoBidAdapter_spec.js --- modules/kargoBidAdapter.js | 3 ++- test/spec/modules/kargoBidAdapter_spec.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 31c35f4afe3..03767efc135 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -65,7 +65,8 @@ export const spec = { let meta; if (adUnit.metadata && adUnit.metadata.landingPageDomain) { meta = { - clickUrl: adUnit.metadata.landingPageDomain + clickUrl: adUnit.metadata.landingPageDomain, + advertiserDomains: [adUnit.metadata.landingPageDomain] }; } bidResponses.push({ diff --git a/test/spec/modules/kargoBidAdapter_spec.js b/test/spec/modules/kargoBidAdapter_spec.js index be73e140839..9dbcca8e331 100644 --- a/test/spec/modules/kargoBidAdapter_spec.js +++ b/test/spec/modules/kargoBidAdapter_spec.js @@ -505,7 +505,8 @@ describe('kargo adapter tests', function () { netRevenue: true, currency: 'USD', meta: { - clickUrl: 'https://foobar.com' + clickUrl: 'https://foobar.com', + advertiserDomains: ['https://foobar.com'] } }, { requestId: '3', From c0850e91eccff86a31142d3444099690aefb3e45 Mon Sep 17 00:00:00 2001 From: Itay Nave <38345760+itaynave@users.noreply.github.com> Date: Wed, 19 Aug 2020 00:23:11 +0300 Subject: [PATCH 21/43] Adding alias (#5622) * Support new aniview bid adapter * Support new aniview bid adapter * Support new aniview bid adapter * Support new aniview bid adapter * Fix Consent parameters * Update aniviewBidAdapter.js V3 support * Update aniviewBidAdapter.js * Update aniviewBidAdapter.js Update refererInfo * Update aniviewBidAdapter.js Fix tabs and spaces * Update aniviewBidAdapter.js fixes * Update aniviewBidAdapter.js * Update aniviewBidAdapter.js Add ccpa support * Update aniviewBidAdapter.js Typo * Update aniviewBidAdapter.js * Update aniviewBidAdapter.js * Fix size and sample Fixed sizes from playerSize Updated md sample * Fix tabs * Fix sizes * Recheck * Add tgt parameter * Update sample * Add support for cookie sync + tests * Add support for cookie sync + tests * Add support for cookie sync + tests * Support aliases Support aliases * Update Update * Fix lint Fix lint * Update spec Update spec * Aniview Bid Adapter: Added the new alias Co-authored-by: Roman Shevchenko --- modules/aniviewBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/aniviewBidAdapter.js b/modules/aniviewBidAdapter.js index f0fab83aeea..e1f61a30abc 100644 --- a/modules/aniviewBidAdapter.js +++ b/modules/aniviewBidAdapter.js @@ -252,7 +252,7 @@ function getUserSyncs(syncOptions, serverResponses) { export const spec = { code: BIDDER_CODE, - aliases: ['selectmediavideo'], + aliases: ['avantisvideo', 'selectmediavideo'], supportedMediaTypes: [VIDEO], isBidRequestValid, buildRequests, From b01fe091418727ed24d8011794c5a94e011873dd Mon Sep 17 00:00:00 2001 From: Adam Browning <19834421+adam-browning@users.noreply.github.com> Date: Wed, 19 Aug 2020 00:28:23 +0300 Subject: [PATCH 22/43] oneVideo Adapter: User sync pixel updates (VIDEOPUB-17981) (#5583) * updated DBM sync pixel to ups/57304 + removed yahoo sync pixel * updated unit tests * updated GDPR tests * Update package-lock.json * Update package-lock.json * update local branch to origin * added sync pixel unit tests * Update package-lock.json --- modules/oneVideoBidAdapter.js | 13 ++++--------- test/spec/modules/oneVideoBidAdapter_spec.js | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/modules/oneVideoBidAdapter.js b/modules/oneVideoBidAdapter.js index 26b0a7dccc2..c5bc054ac04 100644 --- a/modules/oneVideoBidAdapter.js +++ b/modules/oneVideoBidAdapter.js @@ -4,12 +4,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'oneVideo'; export const spec = { code: 'oneVideo', - VERSION: '3.0.3', + VERSION: '3.0.4', 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://cm.g.doubleclick.net/pixel?google_nid=adaptv_dbm&google_cm&google_sc', - SYNC_ENDPOINT2: 'https://pr-bh.ybp.yahoo.com/sync/adaptv_ortb/{combo_uid}', - SYNC_ENDPOINT3: 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=adaptv&ttd_tpi=1', + SYNC_ENDPOINT1: 'https://pixel.advertising.com/ups/57304/sync?gdpr=&gdpr_consent=&_origin=0&redir=true', + SYNC_ENDPOINT2: 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=adaptv&ttd_tpi=1', supportedMediaTypes: ['video', 'banner'], /** * Determines whether or not the given bid request is valid. @@ -136,17 +135,13 @@ export const spec = { type: 'image', url: spec.SYNC_ENDPOINT1 }, - { - type: 'image', - url: spec.SYNC_ENDPOINT2 - }, { type: 'image', url: `https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0` + encodeURI(`&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}`) }, { type: 'image', - url: spec.SYNC_ENDPOINT3 + url: spec.SYNC_ENDPOINT2 }]; } } diff --git a/test/spec/modules/oneVideoBidAdapter_spec.js b/test/spec/modules/oneVideoBidAdapter_spec.js index a91e9ac27e8..ae29bcd48ec 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.3'; + const VERSION = '3.0.4'; 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); @@ -593,14 +593,25 @@ describe('OneVideoBidAdapter', function () { it('should get correct user sync when iframeEnabled', function () { let pixel = spec.getUserSyncs({pixelEnabled: true}, {}, {gdprApplies: true, consentString: GDPR_CONSENT_STRING}) - expect(pixel[2].type).to.equal('image'); - expect(pixel[2].url).to.equal('https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=1&gdpr_consent=' + GDPR_CONSENT_STRING + '&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0&gdpr=1&gdpr_consent=' + encodeURI(GDPR_CONSENT_STRING)); + expect(pixel[1].type).to.equal('image'); + expect(pixel[1].url).to.equal('https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=1&gdpr_consent=' + GDPR_CONSENT_STRING + '&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0&gdpr=1&gdpr_consent=' + encodeURI(GDPR_CONSENT_STRING)); }); it('should default to gdprApplies=0 when consentData is undefined', function () { let pixel = spec.getUserSyncs({pixelEnabled: true}, {}, undefined); - expect(pixel[2].url).to.equal('https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=0&gdpr_consent=&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0&gdpr=0&gdpr_consent='); + expect(pixel[1].url).to.equal('https://sync-tm.everesttech.net/upi/pid/m7y5t93k?gdpr=0&gdpr_consent=&redir=https%3A%2F%2Fpixel.advertising.com%2Fups%2F55986%2Fsync%3Fuid%3D%24%7BUSER_ID%7D%26_origin%3D0&gdpr=0&gdpr_consent='); }); }); + + describe('verify sync pixels', function () { + let pixel = spec.getUserSyncs({pixelEnabled: true}, {}, undefined); + it('should be UPS sync pixel for DBM', function () { + expect(pixel[0].url).to.equal('https://pixel.advertising.com/ups/57304/sync?gdpr=&gdpr_consent=&_origin=0&redir=true') + }); + + it('should be TTD sync pixel', function () { + expect(pixel[2].url).to.equal('https://match.adsrvr.org/track/cmf/generic?ttd_pid=adaptv&ttd_tpi=1') + }); + }) }); }); From e1100af152b35946c6d2e1dcd0efb7aa0da30491 Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Wed, 19 Aug 2020 10:44:43 -0700 Subject: [PATCH 23/43] surround getFloor in try catch (#5628) --- modules/rubiconBidAdapter.js | 30 ++++++++++++++------- test/spec/modules/rubiconBidAdapter_spec.js | 29 +++++++++++++++++++- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 47ca694dc43..5d4c8d8f78c 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -203,11 +203,16 @@ export const spec = { let bidFloor; if (typeof bidRequest.getFloor === 'function' && !config.getConfig('rubicon.disableFloors')) { - let floorInfo = bidRequest.getFloor({ - currency: 'USD', - mediaType: 'video', - size: parseSizes(bidRequest, 'video') - }); + let floorInfo; + try { + floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'video', + size: parseSizes(bidRequest, 'video') + }); + } catch (e) { + utils.logError('Rubicon: getFloor threw an error: ', e); + } bidFloor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; } else { bidFloor = parseFloat(utils.deepAccess(bidRequest, 'params.floor')); @@ -527,11 +532,16 @@ export const spec = { // If floors module is enabled and we get USD floor back, send it in rp_hard_floor else undfined if (typeof bidRequest.getFloor === 'function' && !config.getConfig('rubicon.disableFloors')) { - let floorInfo = bidRequest.getFloor({ - currency: 'USD', - mediaType: 'banner', - size: '*' - }); + let floorInfo; + try { + floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'video', + size: parseSizes(bidRequest, 'video') + }); + } catch (e) { + utils.logError('Rubicon: getFloor threw an error: ', e); + } data['rp_hard_floor'] = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; } diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 808d4840418..cddf190fda4 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -13,7 +13,8 @@ describe('the rubicon adapter', function () { let sandbox, bidderRequest, sizeMap, - getFloorResponse; + getFloorResponse, + logErrorSpy; /** * @typedef {Object} sizeMapConverted @@ -272,6 +273,7 @@ describe('the rubicon adapter', function () { beforeEach(function () { sandbox = sinon.sandbox.create(); + logErrorSpy = sinon.spy(utils, 'logError'); getFloorResponse = {}; bidderRequest = { bidderCode: 'rubicon', @@ -343,6 +345,7 @@ describe('the rubicon adapter', function () { afterEach(function () { sandbox.restore(); + utils.logError.restore(); }); describe('MAS mapping / ordering', function () { @@ -1597,6 +1600,30 @@ describe('the rubicon adapter', function () { [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); expect(request.data.imp[0].bidfloor).to.equal(1.23); }); + + it('should continue with auction and log error if getFloor throws one', function () { + createVideoBidderRequest(); + // default getFloor response is empty object so should not break and not send hard_floor + bidderRequest.bids[0].getFloor = () => { + throw new Error('An exception!'); + }; + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); + + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // log error called + expect(logErrorSpy.calledOnce).to.equal(true); + + // should have an imp + expect(request.data.imp).to.exist.and.to.be.a('array'); + expect(request.data.imp).to.have.lengthOf(1); + + // should be NO bidFloor + expect(request.data.imp[0].bidfloor).to.be.undefined; + }); + it('should add alias name to PBS Request', function() { createVideoBidderRequest(); From 23af840a1045b5e97e947378fc6b3f2deeb8afa5 Mon Sep 17 00:00:00 2001 From: omerBrowsi <54346241+omerBrowsi@users.noreply.github.com> Date: Wed, 19 Aug 2020 22:20:09 +0300 Subject: [PATCH 24/43] Extending the Real Time Data Module (#5519) * real time data module, browsi sub module for real time data, new hook bidsBackCallback, fix for config unsubscribe * change timeout&primary ad server only to auctionDelay update docs * support multiple providers * change promise to callbacks configure submodule on submodules.json * bug fixes * use Prebid ajax * tests fix * browsi real time data provider improvements * real time data module, browsi sub module for real time data, new hook bidsBackCallback, fix for config unsubscribe * change timeout&primary ad server only to auctionDelay update docs * support multiple providers * change promise to callbacks configure submodule on submodules.json * bug fixes * use Prebid ajax * tests fix * browsi real time data provider improvements * RTD module extend #4610 * add hook for submodule init variables naming --- modules/browsiRtdProvider.js | 32 +- modules/rtdModule/index.js | 214 +++++++--- src/targeting.js | 5 +- test/spec/modules/realTimeModule_spec.js | 484 +++++++++++------------ 4 files changed, 425 insertions(+), 310 deletions(-) diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 9317786fb8d..3aff3c6aac6 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -37,6 +37,8 @@ let _moduleParams = {}; let _data = null; /** @type {null | function} */ let _dataReadyCallback = null; +/** @type {string} */ +const DEF_KEYNAME = 'browsiViewability'; /** * add browsi script to page @@ -117,16 +119,14 @@ function waitForData(callback) { function sendDataToModule(adUnits, onDone) { try { waitForData(_predictionsData => { - const _predictions = _predictionsData.p; - if (!_predictions || !Object.keys(_predictions).length) { - return onDone({}); - } + const _predictions = _predictionsData.p || {}; let dataToReturn = adUnits.reduce((rp, cau) => { const adUnitCode = cau && cau.code; if (!adUnitCode) { return rp } const adSlot = getSlotByCode(adUnitCode); const identifier = adSlot ? getMacroId(_predictionsData.pmd, adSlot) : adUnitCode; const predictionData = _predictions[identifier]; + rp[adUnitCode] = getKVObject(-1, _predictionsData.kn); if (!predictionData) { return rp } if (predictionData.p) { @@ -160,7 +160,7 @@ function getAllSlots() { function getKVObject(p, keyName) { const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); let prObject = {}; - prObject[((_moduleParams['keyName'] || keyName).toString())] = prValue.toString(); + prObject[((_moduleParams['keyName'] || keyName || DEF_KEYNAME).toString())] = prValue.toString(); return prObject; } /** @@ -231,7 +231,7 @@ function evaluate(macro, divId, adUnit, replacer) { * @param {string} url server url with query params */ function getPredictionsFromServer(url) { - let ajax = ajaxBuilder(_moduleParams.auctionDelay || _moduleParams.timeout || DEF_TIMEOUT); + let ajax = ajaxBuilder(_moduleParams.auctionDelay || _moduleParams.timeout); ajax(url, { @@ -286,21 +286,26 @@ export const browsiSubmodule = { * @param {adUnit[]} adUnits * @param {function} onDone */ - getData: sendDataToModule + getData: sendDataToModule, + init: init }; -export function init(config) { +function init(config, gdpr, usp) { + return true; +} + +export function beforeInit(config) { const confListener = config.getConfig(MODULE_NAME, ({realTimeData}) => { try { _moduleParams = realTimeData.dataProviders && realTimeData.dataProviders.filter( pr => pr.name && pr.name.toLowerCase() === 'browsi')[0].params; + confListener(); _moduleParams.auctionDelay = realTimeData.auctionDelay; - _moduleParams.timeout = realTimeData.timeout; + _moduleParams.timeout = realTimeData.timeout || DEF_TIMEOUT; } catch (e) { _moduleParams = {}; } if (_moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { - confListener(); collectData(); } else { utils.logError('missing params for Browsi provider'); @@ -308,5 +313,8 @@ export function init(config) { }); } -submodule('realTimeData', browsiSubmodule); -init(config); +function registerSubModule() { + submodule('realTimeData', browsiSubmodule); +} +registerSubModule(); +beforeInit(config); diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 3aa7753d204..9acd484cec8 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -23,14 +23,56 @@ */ /** - * @interface ModuleConfig + * @property + * @summary used to link submodule with config + * @name RtdSubmodule#config + * @type {Object} */ /** - * @property - * @summary sub module name - * @name ModuleConfig#name - * @type {string} + * @function + * @summary init sub module + * @name RtdSubmodule#init + * @param {Object} config + * @param {Object} gdpr settings + * @param {Object} usp settings + * @return {boolean} false to remove sub module + */ + +/** + * @function? + * @summary on auction init event + * @name RtdSubmodule#auctionInit + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @function? + * @summary on auction end event + * @name RtdSubmodule#auctionEnd + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @function? + * @summary on bid request event + * @name RtdSubmodule#updateBidRequest + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @function? + * @summary on bid response event + * @name RtdSubmodule#updateBidResponse + * @param {Object} data + * @param {SubmoduleConfig} config + */ + +/** + * @interface ModuleConfig */ /** @@ -40,18 +82,43 @@ * @type {number} */ +/** + * @property + * @summary timeout (if no auction dealy) + * @name ModuleConfig#timeout + * @type {number} + */ + +/** + * @property + * @summary list of sub modules + * @name ModuleConfig#dataProviders + * @type {SubmoduleConfig[]} + */ + +/** + * @interface SubModuleConfig + */ + /** * @property * @summary params for provide (sub module) - * @name ModuleConfig#params + * @name SubModuleConfig#params * @type {Object} */ /** * @property - * @summary timeout (if no auction dealy) - * @name ModuleConfig#timeout - * @type {number} + * @summary name + * @name ModuleConfig#name + * @type {string} + */ + +/** + * @property + * @summary delay auction for this sub module + * @name ModuleConfig#waitForIt + * @type {boolean} */ import {getGlobal} from '../../src/prebidGlobal.js'; @@ -59,15 +126,21 @@ import {config} from '../../src/config.js'; import {targeting} from '../../src/targeting.js'; import {getHook, module} from '../../src/hook.js'; import * as utils from '../../src/utils.js'; +import events from '../../src/events.js'; +import CONSTANTS from '../../src/constants.json'; +import {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; +import find from 'core-js-pure/features/array/find.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; /** @type {number} */ const DEF_TIMEOUT = 1000; /** @type {RtdSubmodule[]} */ -let subModules = []; +export let subModules = []; /** @type {ModuleConfig} */ let _moduleConfig; +/** @type {SubmoduleConfig[]} */ +let _dataProviders = []; /** * enable submodule in User ID @@ -85,6 +158,9 @@ export function init(config) { } confListener(); // unsubscribe config listener _moduleConfig = realTimeData; + _dataProviders = realTimeData.dataProviders; + getHook('makeBidRequests').before(initSubModules); + setEventsListeners(); if (typeof (_moduleConfig.auctionDelay) === 'undefined') { _moduleConfig.auctionDelay = 0; } @@ -97,61 +173,82 @@ export function init(config) { }); } +/** + * call each sub module init function by config order + * if no init function / init return failure / module not configured - remove it from submodules list + */ +export function initSubModules(next, adUnits, auctionStart, auctionId, cbTimeout, labels) { + let subModulesByOrder = []; + _dataProviders.forEach(provider => { + const sm = find(subModules, s => s.name === provider.name); + const initResponse = sm && sm.init && sm.init(provider, gdprDataHandler.getConsentData(), uspDataHandler.getConsentData()); + if (initResponse) { + subModulesByOrder.push(Object.assign(sm, {config: provider})); + } + }); + subModules = subModulesByOrder; + next(adUnits, auctionStart, auctionId, cbTimeout, labels) +} + +/** + * call each sub module event function by config order + */ +function setEventsListeners() { + events.on(CONSTANTS.EVENTS.AUCTION_INIT, (args) => { + subModules.forEach(sm => { sm.auctionInit && sm.auctionInit(args, sm.config) }) + }); + events.on(CONSTANTS.EVENTS.AUCTION_END, (args) => { + subModules.forEach(sm => { sm.auctionEnd && sm.auctionEnd(args, sm.config) }) + }); + events.on(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, (args) => { + subModules.forEach(sm => { sm.updateBidRequest && sm.updateBidRequest(args, sm.config) }) + }); + events.on(CONSTANTS.EVENTS.BID_RESPONSE, (args) => { + subModules.forEach(sm => { sm.updateBidResponse && sm.updateBidResponse(args, sm.config) }) + }); +} + /** * get data from sub module * @param {AdUnit[]} adUnits received from auction * @param {function} callback callback function on data received */ -function getProviderData(adUnits, callback) { - const callbackExpected = subModules.length; - let dataReceived = []; - let processDone = false; - const dataWaitTimeout = setTimeout(() => { - processDone = true; - callback(dataReceived); - }, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); +export function getProviderData(adUnits, callback) { + /** + * invoke callback if one of the conditions met: + * timeout reached + * all submodules answered + * all sub modules configured "waitForIt:true" answered (as long as there is at least one configured) + */ + const waitForSubModulesLength = subModules.filter(sm => sm.config && sm.config.waitForIt).length; + let callbacksExpected = waitForSubModulesLength || subModules.length; + const shouldWaitForAllSubModules = waitForSubModulesLength === 0; + let dataReceived = {}; + let processDone = false; + const dataWaitTimeout = setTimeout(done, _moduleConfig.auctionDelay || _moduleConfig.timeout || DEF_TIMEOUT); subModules.forEach(sm => { - sm.getData(adUnits, onDataReceived); + sm.getData(adUnits, onDataReceived.bind(sm)); }); function onDataReceived(data) { if (processDone) { return } - dataReceived.push(data); - if (dataReceived.length === callbackExpected) { - processDone = true; + dataReceived[this.name] = data; + if (shouldWaitForAllSubModules || (this.config && this.config.waitForIt)) { + callbacksExpected-- + } + if (callbacksExpected <= 0) { clearTimeout(dataWaitTimeout); - callback(dataReceived); + done(); } } -} -/** - * delete invalid data received from provider - * this is to ensure working flow for GPT - * @param {Object} data received from provider - * @return {Object} valid data for GPT targeting - */ -export function validateProviderDataForGPT(data) { - // data must be an object, contains object with string as value - if (typeof data !== 'object') { - return {}; - } - for (let key in data) { - if (data.hasOwnProperty(key)) { - for (let innerKey in data[key]) { - if (data[key].hasOwnProperty(innerKey)) { - if (typeof data[key][innerKey] !== 'string') { - utils.logWarn(`removing ${key}: {${innerKey}:${data[key][innerKey]} } from GPT targeting because of invalid type (must be string)`); - delete data[key][innerKey]; - } - } - } - } + function done() { + processDone = true; + callback(dataReceived); } - return data; } /** @@ -163,7 +260,7 @@ export function validateProviderDataForGPT(data) { export function setTargetsAfterRequestBids(next, adUnits) { getProviderData(adUnits, (data) => { if (data && Object.keys(data).length) { - const _mergedData = deepMerge(data); + const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); if (Object.keys(_mergedData).length) { setDataForPrimaryAdServer(_mergedData); } @@ -172,6 +269,22 @@ export function setTargetsAfterRequestBids(next, adUnits) { }); } +/** + * return an array providers data in reverse order,so the data merge will be according to correct config order + * @param {Submodule[]} modules + * @param {Object} data - data retrieved from providers + * @return {array} reversed order ready for merge + */ +function setDataOrderByProvider(modules, data) { + let rd = []; + for (let i = modules.length; i--; i > 0) { + if (data[modules[i].name]) { + rd.push(data[modules[i].name]) + } + } + return rd; +} + /** * deep merge array of objects * @param {array} arr - objects array @@ -207,7 +320,7 @@ export function deepMerge(arr) { export function requestBidsHook(fn, reqBidsConfigObj) { getProviderData(reqBidsConfigObj.adUnits || getGlobal().adUnits, (data) => { if (data && Object.keys(data).length) { - const _mergedData = deepMerge(data); + const _mergedData = deepMerge(setDataOrderByProvider(subModules, data)); if (Object.keys(_mergedData).length) { setDataForPrimaryAdServer(_mergedData); addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, _mergedData); @@ -222,7 +335,6 @@ export function requestBidsHook(fn, reqBidsConfigObj) { * @param {Object} data - key values to set */ function setDataForPrimaryAdServer(data) { - data = validateProviderDataForGPT(data); if (utils.isGptPubadsDefined()) { targeting.setTargetingForGPT(data, null) } else { @@ -247,5 +359,5 @@ function addIdDataToAdUnitBids(adUnits, data) { }); } -init(config); module('realTimeData', attachRealTimeDataProvider); +init(config); diff --git a/src/targeting.js b/src/targeting.js index 5873eeeb559..1b1e14fd4a6 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -325,7 +325,10 @@ export function newTargeting(auctionManager) { Object.keys(targetingConfig).filter(customSlotMatching ? customSlotMatching(slot) : isAdUnitCodeMatchingSlot(slot)) .forEach(targetId => Object.keys(targetingConfig[targetId]).forEach(key => { - let valueArr = targetingConfig[targetId][key].split(','); + let valueArr = targetingConfig[targetId][key]; + if (typeof valueArr === 'string') { + valueArr = valueArr.split(','); + } valueArr = (valueArr.length > 1) ? [valueArr] : valueArr; valueArr.map((value) => { utils.logMessage(`Attempting to set key value for slot: ${slot.getSlotElementId()} key: ${key} value: ${value}`); diff --git a/test/spec/modules/realTimeModule_spec.js b/test/spec/modules/realTimeModule_spec.js index ca149fe7a44..f47068724d1 100644 --- a/test/spec/modules/realTimeModule_spec.js +++ b/test/spec/modules/realTimeModule_spec.js @@ -1,27 +1,176 @@ -import { - init, - requestBidsHook, - setTargetsAfterRequestBids, - deepMerge, - validateProviderDataForGPT -} from 'modules/rtdModule/index.js'; -import { - init as browsiInit, - addBrowsiTag, - isIdMatchingAdUnit, - setData, - getMacroId -} from 'modules/browsiRtdProvider.js'; -import { - init as audigentInit, - setData as setAudigentData -} from 'modules/audigentRtdProvider.js'; +import * as rtdModule from 'modules/rtdModule/index.js'; import { config } from 'src/config.js'; -import { makeSlot } from '../integration/faker/googletag.js'; - -let expect = require('chai').expect; +import {makeSlot} from '../integration/faker/googletag.js'; +import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; + +const validSM = { + name: 'validSM', + init: () => { return true }, + getData: (adUnits, onDone) => { + setTimeout(() => { + return onDone({'key': 'validSM'}) + }, 500) + } +}; + +const validSMWait = { + name: 'validSMWait', + init: () => { return true }, + getData: (adUnits, onDone) => { + setTimeout(() => { + return onDone({'ad1': {'key': 'validSMWait'}}) + }, 50) + } +}; + +const invalidSM = { + name: 'invalidSM' +}; + +const failureSM = { + name: 'failureSM', + init: () => { return false } +}; + +const nonConfSM = { + name: 'nonConfSM', + init: () => { return true } +}; + +const conf = { + 'realTimeData': { + 'auctionDelay': 250, + dataProviders: [ + { + 'name': 'validSMWait', + 'waitForIt': true, + }, + { + 'name': 'validSM', + 'waitForIt': false, + }, + { + 'name': 'invalidSM' + }, + { + 'name': 'failureSM' + }] + } +}; + +function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: { banner: {}, native: {} }, + sizes: [[300, 200], [300, 600]], + bids: [{ bidder: 'sampleBidder', params: { placementId: 'banner-only-bidder' } }] + }; +} describe('Real time module', function () { + after(function () { + config.resetConfig(); + }); + + beforeEach(function () { + config.setConfig(conf); + }); + + it('should use only valid modules', function (done) { + rtdModule.attachRealTimeDataProvider(validSM); + rtdModule.attachRealTimeDataProvider(invalidSM); + rtdModule.attachRealTimeDataProvider(failureSM); + rtdModule.attachRealTimeDataProvider(nonConfSM); + rtdModule.attachRealTimeDataProvider(validSMWait); + rtdModule.initSubModules(afterInitSubModules); + function afterInitSubModules() { + expect(rtdModule.subModules).to.eql([validSMWait, validSM]); + done(); + } + rtdModule.init(config); + }); + + it('should only wait for must have sub modules', function (done) { + rtdModule.getProviderData([], (data) => { + expect(data).to.eql({validSMWait: {'ad1': {'key': 'validSMWait'}}}); + done(); + }) + }); + + it('deep merge object', function () { + const obj1 = { + id1: { + key: 'value', + key2: 'value2' + }, + id2: { + k: 'v' + } + }; + const obj2 = { + id1: { + key3: 'value3' + } + }; + const obj3 = { + id3: { + key: 'value' + } + }; + const expected = { + id1: { + key: 'value', + key2: 'value2', + key3: 'value3' + }, + id2: { + k: 'v' + }, + id3: { + key: 'value' + } + }; + + const merged = rtdModule.deepMerge([obj1, obj2, obj3]); + assert.deepEqual(expected, merged); + }); + + it('check module using bidsBackCallback', function (done) { + // set slot + const slot = makeSlot({ code: '/code1', divId: 'ad1' }); + window.googletag.pubads().setSlots([slot]); + + function afterBidHook() { + expect(slot.getTargeting().length).to.equal(1); + expect(slot.getTargeting()[0].key).to.equal('validSMWait'); + done(); + } + rtdModule.setTargetsAfterRequestBids(afterBidHook, []); + }); + + it('check module using requestBidsHook', function (done) { + // set slot + const slotsB = makeSlot({ code: '/code1', divId: 'ad1' }); + window.googletag.pubads().setSlots([slotsB]); + let adUnits = [getAdUnitMock('ad1')]; + + function afterBidHook(data) { + expect(slotsB.getTargeting().length).to.equal(1); + expect(slotsB.getTargeting()[0].key).to.equal('validSMWait'); + + data.adUnits.forEach(unit => { + unit.bids.forEach(bid => { + expect(bid.realTimeData).to.have.property('key'); + expect(bid.realTimeData.key).to.equal('validSMWait'); + }); + }); + done(); + } + rtdModule.requestBidsHook(afterBidHook, { adUnits: adUnits }); + }); +}); + +describe('browsi Real time data sub module', function () { const conf = { 'realTimeData': { 'auctionDelay': 250, @@ -33,250 +182,93 @@ describe('Real time module', function () { 'pubKey': 'testPub', 'keyName': 'bv' } - }, { - 'name': 'audigent' }] } }; - const predictions = { - p: { - 'browsiAd_2': { - 'w': [ - '/57778053/Browsi_Demo_Low', - '/57778053/Browsi_Demo_300x250' - ], - 'p': 0.07 - }, - 'browsiAd_1': { - 'w': [], - 'p': 0.06 - }, - 'browsiAd_3': { - 'w': [], - 'p': 0.53 - }, - 'browsiAd_4': { - 'w': [ - '/57778053/Browsi_Demo' - ], - 'p': 0.85 - } - } - }; - - const audigentSegments = { - audigent_segments: { 'a': 1, 'b': 2 } - } - - function getAdUnitMock(code = 'adUnit-code') { - return { - code, - mediaTypes: { banner: {}, native: {} }, - sizes: [[300, 200], [300, 600]], - bids: [{ bidder: 'sampleBidder', params: { placementId: 'banner-only-bidder' } }] - }; - } - - function createSlots() { - const slot1 = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - const slot2 = makeSlot({ code: '/57778053/Browsi', divId: 'browsiAd_1' }); - return [slot1, slot2]; - } - - describe('Real time module with browsi provider', function () { - afterEach(function () { - $$PREBID_GLOBAL$$.requestBids.removeAll(); - }); + beforeEach(function () { + config.setConfig(conf); + }); - after(function () { - config.resetConfig(); - }); + after(function () { + config.resetConfig(); + }); - it('check module using bidsBackCallback', function () { - let adUnits1 = [getAdUnitMock('browsiAd_1')]; - let targeting = []; - init(config); - browsiInit(config); - config.setConfig(conf); - setData(predictions); - - // set slot - const slots = createSlots(); - window.googletag.pubads().setSlots(slots); - - function afterBidHook() { - slots.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); + it('should init and return true', function () { + browsiRTD.beforeInit(config); + expect(browsiRTD.browsiSubmodule.init()).to.equal(true) + }); - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); - } - setTargetsAfterRequestBids(afterBidHook, adUnits1, true); - }); + it('should create browsi script', function () { + const script = browsiRTD.addBrowsiTag('scriptUrl.com'); + expect(script.getAttribute('data-sitekey')).to.equal('testKey'); + expect(script.getAttribute('data-pubkey')).to.equal('testPub'); + expect(script.async).to.equal(true); + expect(script.prebidData.kn).to.equal(conf.realTimeData.dataProviders[0].params.keyName); + }); - it('check module using requestBidsHook', function () { - let adUnits1 = [getAdUnitMock('browsiAd_1')]; - let targeting = []; - let dataReceived = null; - - // set slot - const slotsB = createSlots(); - window.googletag.pubads().setSlots(slotsB); - - function afterBidHook(data) { - dataReceived = data; - slotsB.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); + it('should match placement with ad unit', function () { + const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - expect(targeting.indexOf('bv')).to.be.greaterThan(-1); - dataReceived.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('bv'); - }); - }); - } - requestBidsHook(afterBidHook, { adUnits: adUnits1 }); - }); + const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250']); // true + const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true + const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_Low']); // false + const test4 = browsiRTD.isIdMatchingAdUnit(slot, []); // true - it('check object deep merge', function () { - const obj1 = { - id1: { - key: 'value', - key2: 'value2' - }, - id2: { - k: 'v' - } - }; - const obj2 = { - id1: { - key3: 'value3' - } - }; - const obj3 = { - id3: { - key: 'value' - } - }; - const expected = { - id1: { - key: 'value', - key2: 'value2', - key3: 'value3' - }, - id2: { - k: 'v' - }, - id3: { - key: 'value' - } - }; + expect(test1).to.equal(true); + expect(test2).to.equal(true); + expect(test3).to.equal(false); + expect(test4).to.equal(true); + }); - const merged = deepMerge([obj1, obj2, obj3]); - assert.deepEqual(expected, merged); - }); + it('should return correct macro values', function () { + const slot = makeSlot({ code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1' }); - it('check data validation for GPT targeting', function () { - // non strings values should be removed - const obj = { - valid: {'key': 'value'}, - invalid: {'key': ['value']}, - combine: { - 'a': 'value', - 'b': [] - } - }; + slot.setTargeting('test', ['test', 'value']); + // slot getTargeting doesn't act like GPT so we can't expect real value + const macroResult = browsiRTD.getMacroId({p: '/'}, slot); + expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); - const expected = { - valid: {'key': 'value'}, - invalid: {}, - combine: { - 'a': 'value', - } - }; - const validationResult = validateProviderDataForGPT(obj); - assert.deepEqual(expected, validationResult); - }); + const macroResultB = browsiRTD.getMacroId({}, slot); + expect(macroResultB).to.equal('browsiAd_1'); - it('check browsi sub module', function () { - const script = addBrowsiTag('scriptUrl.com'); - expect(script.getAttribute('data-sitekey')).to.equal('testKey'); - expect(script.getAttribute('data-pubkey')).to.equal('testPub'); - expect(script.async).to.equal(true); - - const slots = createSlots(); - const test1 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_300x250']); // true - const test2 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true - const test3 = isIdMatchingAdUnit(slots[0], ['/57778053/Browsi_Demo_Low']); // false - const test4 = isIdMatchingAdUnit(slots[0], []); // true - - expect(test1).to.equal(true); - expect(test2).to.equal(true); - expect(test3).to.equal(false); - expect(test4).to.equal(true); - - // macro results - slots[0].setTargeting('test', ['test', 'value']); - // slot getTargeting doesn't act like GPT so we can't expect real value - const macroResult = getMacroId({p: '/'}, slots[0]); - expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); - - const macroResultB = getMacroId({}, slots[0]); - expect(macroResultB).to.equal('browsiAd_1'); - - const macroResultC = getMacroId({p: '', s: {s: 0, e: 1}}, slots[0]); - expect(macroResultC).to.equal('/'); - }) + const macroResultC = browsiRTD.getMacroId({p: '', s: {s: 0, e: 1}}, slot); + expect(macroResultC).to.equal('/'); }); - describe('Real time module with Audigent provider', function () { - before(function () { - init(config); - audigentInit(config); - config.setConfig(conf); - setAudigentData(audigentSegments); + describe('should return data to RTD module', function () { + it('should return empty if no ad units defined', function (done) { + browsiRTD.setData({}); + browsiRTD.browsiSubmodule.getData([], onDone); + function onDone(data) { + expect(data).to.eql({}); + done(); + } }); - afterEach(function () { - $$PREBID_GLOBAL$$.requestBids.removeAll(); - config.resetConfig(); + it('should return NA if no prediction for ad unit', function (done) { + const adUnits = [getAdUnitMock('adMock')]; + browsiRTD.setData({}); + browsiRTD.browsiSubmodule.getData(adUnits, onDone); + function onDone(data) { + expect(data).to.eql({adMock: {bv: 'NA'}}); + done(); + } }); - it('check module using requestBidsHook', function () { - let adUnits1 = [getAdUnitMock('audigentAd_1')]; - let targeting = []; - let dataReceived = null; - - // set slot - const slotsB = createSlots(); - window.googletag.pubads().setSlots(slotsB); - - function afterBidHook(data) { - dataReceived = data; - slotsB.map(s => { - targeting = []; - s.getTargeting().map(value => { - targeting.push(Object.keys(value).toString()); - }); - }); - - dataReceived.adUnits.forEach(unit => { - unit.bids.forEach(bid => { - expect(bid.realTimeData).to.have.property('audigent_segments'); - expect(bid.realTimeData.audigent_segments).to.deep.equal(audigentSegments.audigent_segments); - }); - }); + it('should return prediction from server', function (done) { + const adUnits = [getAdUnitMock('hasPrediction')]; + const data = { + p: {'hasPrediction': {p: 0.234}}, + kn: 'bv', + pmd: undefined + }; + browsiRTD.setData(data); + browsiRTD.browsiSubmodule.getData(adUnits, onDone); + function onDone(data) { + expect(data).to.eql({hasPrediction: {bv: '0.20'}}); + done(); } - - requestBidsHook(afterBidHook, { adUnits: adUnits1 }); - }); - }); + }) + }) }); From f0e4433b07b8270bf8f741c735726b0c80a34918 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Thu, 20 Aug 2020 05:53:26 -0400 Subject: [PATCH 25/43] Update dfpAdServerVideo.js to allow for vast4 (#5608) * Update dfpAdServerVideo.js This had a bug in which all requests were hardcoded to vast 3, although pubs may have selected vast 3 or 4 * Update dfpAdServerVideo_spec.js * Update dfpAdServerVideo_spec.js --- modules/dfpAdServerVideo.js | 2 +- test/spec/modules/dfpAdServerVideo_spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 1d999fdbacc..9fe8c26e4f6 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -42,7 +42,7 @@ import { auctionManager } from '../src/auctionManager.js'; const defaultParamConstants = { env: 'vp', gdfp_req: 1, - output: 'xml_vast3', + output: 'vast', unviewed_position_start: 1, }; diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index cd412530d2b..c0ecb9cad5e 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -38,7 +38,7 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('env', 'vp'); expect(queryParams).to.have.property('gdfp_req', '1'); expect(queryParams).to.have.property('iu', 'my/adUnit'); - expect(queryParams).to.have.property('output', 'xml_vast3'); + expect(queryParams).to.have.property('output', 'vast'); expect(queryParams).to.have.property('sz', '640x480'); expect(queryParams).to.have.property('unviewed_position_start', '1'); expect(queryParams).to.have.property('url'); @@ -375,7 +375,7 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('env', 'vp'); expect(queryParams).to.have.property('gdfp_req', '1'); expect(queryParams).to.have.property('iu', 'my/adUnit'); - expect(queryParams).to.have.property('output', 'xml_vast3'); + expect(queryParams).to.have.property('output', 'vast'); expect(queryParams).to.have.property('sz', '640x480'); expect(queryParams).to.have.property('unviewed_position_start', '1'); expect(queryParams).to.have.property('url'); From ce00f4c3f6672a857255c439a9d67fca3285326b Mon Sep 17 00:00:00 2001 From: invibes <51820283+invibes@users.noreply.github.com> Date: Thu, 20 Aug 2020 18:55:17 +0300 Subject: [PATCH 26/43] purposes in call for invibes (#5533) * added tcf 2.0 * Updated adapter to support gdprEnforcement * reverted storage manager initialization * add purposes in call * send purposes in string array * InvibesBidAdapter - gdpr updates * [InvibesBidAdapter] GDPR - purpose adjustments * [InvibesBidAdapter] fixed tests for new alg & reordered adapter checks * add tc string Co-authored-by: florin_nedelcu_invibes Co-authored-by: Cristian Grigoras Co-authored-by: raduchept --- modules/invibesBidAdapter.js | 198 ++++++-------------- test/spec/modules/invibesBidAdapter_spec.js | 168 +++++++++-------- 2 files changed, 145 insertions(+), 221 deletions(-) diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index 220aed47e15..50ed7d5f57c 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -8,7 +8,7 @@ const CONSTANTS = { SYNC_ENDPOINT: 'https://k.r66net.com/GetUserSync', TIME_TO_LIVE: 300, DEFAULT_CURRENCY: 'EUR', - PREBID_VERSION: 2, + PREBID_VERSION: 4, METHOD: 'GET', INVIBES_VENDOR_ID: 436 }; @@ -39,9 +39,7 @@ export const spec = { }, getUserSyncs: function (syncOptions) { if (syncOptions.iframeEnabled) { - handlePostMessage(); const syncUrl = buildSyncUrl(); - return { type: 'iframe', url: syncUrl @@ -55,6 +53,7 @@ registerBidder(spec); // some state info is required: cookie info, unique user visit id const topWin = getTopMostWindow(); let invibes = topWin.invibes = topWin.invibes || {}; +invibes.purposes = invibes.purposes || [false, false, false, false, false, false, false, false, false, false]; let _customUserSync; function isBidRequestValid(bid) { @@ -89,13 +88,11 @@ function buildRequest(bidRequests, bidderRequest) { _customUserSync = _customUserSync || bidRequest.params.customUserSync; }); - invibes.visitId = invibes.visitId || generateRandomId(); + invibes.optIn = invibes.optIn || readGdprConsent(bidderRequest.gdprConsent); - cookieDomain = detectTopmostCookieDomain(); + invibes.visitId = invibes.visitId || generateRandomId(); invibes.noCookies = invibes.noCookies || invibes.getCookie('ivNoCookie'); - invibes.optIn = invibes.optIn || invibes.getCookie('ivOptIn') || readGdprConsent(bidderRequest.gdprConsent); - - initDomainId(invibes.domainOptions); + let lid = initDomainId(invibes.domainOptions); const currentQueryStringParams = parseQueryStringParams(); @@ -117,14 +114,16 @@ function buildRequest(bidRequests, bidderRequest) { width: topWin.innerWidth, height: topWin.innerHeight, - noc: !cookieDomain, oi: invibes.optIn, - kw: keywords + kw: keywords, + purposes: invibes.purposes.toString(), + + tc: invibes.gdpr_consent }; - if (invibes.dom.id) { - data.lId = invibes.dom.id; + if (lid) { + data.lId = lid; } const parametersToPassForward = 'videoaddebug,advs,bvci,bvid,istop,trybvid,trybvci'.split(','); @@ -278,6 +277,8 @@ function renderCreative(bidModel) { function getCappedCampaignsAsString() { const key = 'ivvcap'; + if (!invibes.optIn || !invibes.purposes[0]) { return ''; } + let loadData = function () { try { return JSON.parse(storage.getDataFromLocalStorage(key)) || {}; @@ -348,49 +349,48 @@ function buildSyncUrl() { return syncUrl; } -function handlePostMessage() { - try { - if (window.addEventListener) { - window.addEventListener('message', acceptPostMessage); - } - } catch (e) { } -} - -function acceptPostMessage(e) { - let msg = e.data || {}; - if (msg.ivbscd === 1) { - invibes.setCookie(msg.name, msg.value, msg.exdays, msg.domain); - } else if (msg.ivbscd === 2) { - invibes.dom.graduate(); - } -} - function readGdprConsent(gdprConsent) { if (gdprConsent && gdprConsent.vendorData) { + invibes.gdpr_consent = getVendorConsentData(gdprConsent.vendorData); + if (!gdprConsent.vendorData.gdprApplies || gdprConsent.vendorData.hasGlobalConsent) { + var index; + for (index = 0; index < invibes.purposes; ++index) { + invibes.purposes[index] = true; + } return 2; } let purposeConsents = getPurposeConsents(gdprConsent.vendorData); if (purposeConsents == null) { return 0; } - let properties = Object.keys(purposeConsents); - let purposeConsentsCounter = getPurposeConsentsCounter(gdprConsent.vendorData); + let purposesLength = getPurposeConsentsCounter(gdprConsent.vendorData); - if (properties.length < purposeConsentsCounter) { + if (purposeConsents instanceof Array) { + for (let i = 0; i < purposesLength && i < purposeConsents.length; i++) { + invibes.purposes[i] = !((purposeConsents[i] === false || purposeConsents[i] === 'false' || purposeConsents[i] == null)); + } + } else if (typeof purposeConsents === 'object' && purposeConsents !== null) { + let i = 0; + for (let prop in purposeConsents) { + if (i === purposesLength) { + break; + } + + if (purposeConsents.hasOwnProperty(prop)) { + invibes.purposes[i] = !((purposeConsents[prop] === false || purposeConsents[prop] === 'false' || purposeConsents[prop] == null)); + i++; + } + } + } else { return 0; } - for (let i = 0; i < purposeConsentsCounter; i++) { - if (!purposeConsents[properties[i]] || purposeConsents[properties[i]] === 'false') { return 0; } - } - + let invibesVendorId = CONSTANTS.INVIBES_VENDOR_ID.toString(10); let vendorConsents = getVendorConsents(gdprConsent.vendorData); - if (vendorConsents == null || vendorConsents[CONSTANTS.INVIBES_VENDOR_ID.toString(10)] == null) { - return 4; - } + if (vendorConsents == null || vendorConsents[invibesVendorId] == null) { return 4; } - if (vendorConsents[CONSTANTS.INVIBES_VENDOR_ID.toString(10)] === false) { return 0; } + if (vendorConsents[invibesVendorId] === false) { return 0; } return 2; } @@ -418,6 +418,13 @@ function getPurposeConsents(vendorData) { return null; } +function getVendorConsentData(vendorData) { + if (vendorData.purpose && vendorData.purpose.consents) { + if (vendorData.tcString != null) { return vendorData.tcString; } + } + return vendorData.consentData; +}; + function getVendorConsents(vendorData) { if (vendorData.vendor && vendorData.vendor.consents) { return vendorData.vendor.consents; @@ -443,55 +450,15 @@ invibes.Uid = { } }; -let cookieDomain; invibes.getCookie = function (name) { if (!storage.cookiesAreEnabled()) { return; } - let i, x, y; - let cookies = document.cookie.split(';'); - for (i = 0; i < cookies.length; i++) { - x = cookies[i].substr(0, cookies[i].indexOf('=')); - y = cookies[i].substr(cookies[i].indexOf('=') + 1); - x = x.replace(/^\s+|\s+$/g, ''); - if (x === name) { - return unescape(y); - } - } -}; -invibes.setCookie = function (name, value, exdays, domain) { - if (!storage.cookiesAreEnabled()) { return; } - let whiteListed = name == 'ivNoCookie' || name == 'IvbsCampIdsLocal'; - if (invibes.noCookies && !whiteListed && (exdays || 0) >= 0) { return; } - if (exdays > 365) { exdays = 365; } - domain = domain || cookieDomain; - let exdate = new Date(); - let exms = exdays * 24 * 60 * 60 * 1000; - exdate.setTime(exdate.getTime() + exms); - storage.setCookie(name, value, exdate.toUTCString(), undefined, domain); -}; + if (!invibes.optIn || !invibes.purposes[0]) { return; } -let detectTopmostCookieDomain = function () { - let testCookie = invibes.Uid.generate(); - let hostParts = location.hostname.split('.'); - if (hostParts.length === 1) { - return location.hostname; - } - for (let i = hostParts.length - 1; i >= 0; i--) { - let domain = '.' + hostParts.slice(i).join('.'); - invibes.setCookie(testCookie, testCookie, 1, domain); - let val = invibes.getCookie(testCookie); - if (val === testCookie) { - invibes.setCookie(testCookie, testCookie, -1, domain); - return domain; - } - } + return storage.getCookie(name); }; let initDomainId = function (options) { - if (invibes.dom) { return; } - - options = options || {}; - let cookiePersistence = { cname: 'ivbsdid', load: function () { @@ -499,75 +466,16 @@ let initDomainId = function (options) { try { return JSON.parse(str); } catch (e) { } - }, - save: function (obj) { - invibes.setCookie(this.cname, JSON.stringify(obj), 365); - } - }; - - let persistence = options.persistence || cookiePersistence; - let state; - let minHC = 2; - - let validGradTime = function (state) { - if (!state.cr) { return false; } - let min = 151 * 10e9; - if (state.cr < min) { - return false; } - let now = new Date().getTime(); - let age = now - state.cr; - let minAge = 24 * 60 * 60 * 1000; - return age > minAge; - }; - - state = persistence.load() || { - id: invibes.Uid.generate(), - cr: new Date().getTime(), - hc: 1, }; - if (state.id.match(/\./)) { - state.id = invibes.Uid.generate(); - } - - let graduate = function () { - if (!state.cr) { return; } - delete state.cr; - delete state.hc; - persistence.save(state); - setId(); - }; + options = options || {}; - let regenerateId = function () { - state.id = invibes.Uid.generate(); - persistence.save(state); - }; + var persistence = options.persistence || cookiePersistence; - let setId = function () { - invibes.dom = { - get id() { - return (!state.cr && invibes.optIn > 0) ? state.id : undefined; - }, - get tempId() { - return (invibes.optIn > 0) ? state.id : undefined; - }, - graduate: graduate, - regen: regenerateId - }; - }; + let state = persistence.load(); - if (state.cr && !options.noVisit) { - if (state.hc < minHC) { - state.hc++; - } - if ((state.hc >= minHC && validGradTime(state)) || options.skipGraduation) { - graduate(); - } - } - persistence.save(state); - setId(); - ivLogger.info('Did=' + invibes.dom.id); + return state ? (state.id || state.tempId) : undefined; }; let keywords = (function () { diff --git a/test/spec/modules/invibesBidAdapter_spec.js b/test/spec/modules/invibesBidAdapter_spec.js index b75c6a4578f..442039504f8 100644 --- a/test/spec/modules/invibesBidAdapter_spec.js +++ b/test/spec/modules/invibesBidAdapter_spec.js @@ -137,11 +137,19 @@ describe('invibesBidAdapter:', function () { }); it('has capped ids if local storage variable is correctly formatted', function () { + top.window.invibes.optIn = 1; + top.window.invibes.purposes = [true, false, false, false, false, false, false, false, false, false]; localStorage.ivvcap = '{"9731":[1,1768600800000]}'; const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); expect(request.data.capCounts).to.equal('9731=1'); }); + it('does not have capped ids if local storage variable is correctly formatted but no opt in', function () { + localStorage.ivvcap = '{"9731":[1,1768600800000]}'; + const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); + expect(request.data.capCounts).to.equal(''); + }); + it('does not have capped ids if local storage variable is incorrectly formatted', function () { localStorage.ivvcap = ':[1,1574334216992]}'; const request = spec.buildRequests(bidRequests, {auctionStart: Date.now()}); @@ -178,104 +186,102 @@ describe('invibesBidAdapter:', function () { expect(JSON.parse(request.data.bidParamsJson).placementIds).to.contain(bidRequests[1].params.placementId); }); - it('uses cookies', function () { - global.document.cookie = 'ivNoCookie=1'; + it('sends undefined lid when no cookie', function () { let request = spec.buildRequests(bidRequests); expect(request.data.lId).to.be.undefined; }); - it('doesnt send the domain id if not graduated', function () { - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":1522929537626,"hc":1}'; - let request = spec.buildRequests(bidRequests); - expect(request.data.lId).to.not.exist; - }); - - it('try to graduate but not enough count - doesnt send the domain id', function () { - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":1521818537626,"hc":0}'; - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - }); - - it('try to graduate but not old enough - doesnt send the domain id', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":' + Date.now() + ',"hc":5}'; - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - }); - - it('graduate and send the domain id', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - stubDomainOptions(new StubbedPersistence('{"id":"dvdjkams6nkq","cr":1521818537626,"hc":7}')); - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.exist; - }); - - it('send the domain id if already graduated', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi"}')); - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.exist; - expect(top.window.invibes.dom.tempId).to.exist; - }); - - it('send the domain id after replacing it with new format', function () { + it('sends lid when comes on cookie', function () { + top.window.invibes.optIn = 1; + top.window.invibes.purposes = [true, false, false, false, false, false, false, false, false, false]; + global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":' + Date.now() + ',"hc":0}'; let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.lId).to.exist; - expect(top.window.invibes.dom.tempId).to.exist; }); - it('dont send the domain id if consent declined on tcf v1', function () { + it('should send purpose 1', function () { let bidderRequest = { gdprConsent: { vendorData: { gdprApplies: true, hasGlobalConsent: false, - vendorConsents: {436: false} + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } } } }; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.not.exist; - expect(request.data.oi).to.equal(0); + expect(request.data.purposes.split(',')[0]).to.equal('true'); }); - it('dont send the domain id if consent declined on tcf v2', function () { + it('should send purpose 2 & 7', function () { let bidderRequest = { gdprConsent: { vendorData: { gdprApplies: true, hasGlobalConsent: false, - vendor: {consents: {436: false}} + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } } } }; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.not.exist; - expect(request.data.oi).to.equal(0); - }); - - it('dont send the domain id if no consent', function () { - let bidderRequest = {}; - stubDomainOptions(new StubbedPersistence('{"id":"f8zoh044p9oi.8537626"}')); - let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.not.exist; + expect(request.data.purposes.split(',')[1] && request.data.purposes.split(',')[6]).to.equal('true'); }); - it('try to init id but was already loaded on page - does not increment the id again', function () { - let bidderRequest = {gdprConsent: {vendorData: {vendorConsents: {436: true}}}}; - global.document.cookie = 'ivbsdid={"id":"dvdjkams6nkq","cr":1521818537626,"hc":0}'; + it('should send purpose 9', function () { + let bidderRequest = { + gdprConsent: { + vendorData: { + gdprApplies: true, + hasGlobalConsent: false, + vendor: {consents: {436: true}}, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + 10: true + } + } + } + } + }; let request = spec.buildRequests(bidRequests, bidderRequest); - request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.lId).to.not.exist; - expect(top.window.invibes.dom.tempId).to.exist; + expect(request.data.purposes.split(',')[9]).to.equal('true'); }); it('should send oi = 0 when vendorData is null', function () { @@ -344,7 +350,7 @@ describe('invibesBidAdapter:', function () { expect(request.data.oi).to.equal(0); }); - it('should send oi = 0 when purpose consents weren\'t approved on tcf v2', function () { + it('should send oi = 2 when purpose consents weren\'t approved on tcf v2', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -369,10 +375,10 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); - it('should send oi = 0 when purpose consents are less then 10 on tcf v2', function () { + it('should send oi = 2 when purpose consents are less then 10 on tcf v2', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -392,7 +398,7 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); it('should send oi = 4 when vendor consents are null on tcf v2', function () { @@ -527,7 +533,7 @@ describe('invibesBidAdapter:', function () { expect(request.data.oi).to.equal(2); }); - it('should send oi = 0 when purpose consents weren\'t approved on tcf v1', function () { + it('should send oi = 2 when purpose consents weren\'t approved on tcf v1', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -545,10 +551,10 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); - it('should send oi = 0 when purpose consents are less then 5 on tcf v1', function () { + it('should send oi = 2 when purpose consents are less then 5 on tcf v1', function () { let bidderRequest = { gdprConsent: { vendorData: { @@ -564,7 +570,7 @@ describe('invibesBidAdapter:', function () { } }; let request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.oi).to.equal(0); + expect(request.data.oi).to.equal(2); }); it('should send oi = 0 when vendor consents for invibes are false on tcf v1', function () { @@ -669,8 +675,8 @@ describe('invibesBidAdapter:', function () { context('when the response is valid', function () { it('responds with a valid bid', function () { - top.window.invibes.setCookie('a', 'b', 370); - top.window.invibes.setCookie('c', 'd', 0); + // top.window.invibes.setCookie('a', 'b', 370); + // top.window.invibes.setCookie('c', 'd', 0); let result = spec.interpretResponse({body: response}, {bidRequests}); expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); @@ -705,6 +711,16 @@ describe('invibesBidAdapter:', function () { expect(response.url).to.include(SYNC_ENDPOINT); expect(response.url).to.include('optIn'); expect(response.url).to.include('ivvbks'); + }); + + it('returns an iframe with params including if enabled', function () { + top.window.invibes.optIn = 1; + global.document.cookie = 'ivvbks=17639.0,1,2;ivbsdid={"id":"dvdjkams6nkq","cr":' + Date.now() + ',"hc":0}'; + let response = spec.getUserSyncs({iframeEnabled: true}); + expect(response.type).to.equal('iframe'); + expect(response.url).to.include(SYNC_ENDPOINT); + expect(response.url).to.include('optIn'); + expect(response.url).to.include('ivvbks'); expect(response.url).to.include('ivbsdid'); }); From 2c0ea15334bd756aa2bd0dbcc37c921f2b95abcb Mon Sep 17 00:00:00 2001 From: "Isaac A. Dettman" Date: Thu, 20 Aug 2020 08:57:28 -0700 Subject: [PATCH 27/43] Update rubicon adapter for sharedid userid support (#5627) * Add microadBidAdapter * Remove unnecessary encodeURIComponent from microadBidAdapter * Submit Advangelists Prebid Adapter * Submit Advangelists Prebid Adapter 1.1 * Correct procudtion endpoint for prebid * analytics update with wrapper name * reverted error merge * update changed default value of netRevenue to true * Re-add rubicon analytics without deprecated getTopWindowUrl util * Cache referrer on auction_init instead of bid_requested * merged remote master changes * update liveramp userid support * changed source value to all lowercase * update share id name * add unit test for shareid eid * update shareid obj paths Co-authored-by: nakamoto Co-authored-by: Chandra Prakash Co-authored-by: Eric Harper Co-authored-by: TJ Eastmond Co-authored-by: Mark Monday Co-authored-by: msm0504 <51493331+msm0504@users.noreply.github.com> --- modules/rubiconBidAdapter.js | 21 +++++++++++++++- test/spec/modules/rubiconBidAdapter_spec.js | 28 ++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 5d4c8d8f78c..0e41bf6c6ad 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -248,7 +248,7 @@ export const spec = { } if (bidRequest.userId && typeof bidRequest.userId === 'object' && - (bidRequest.userId.tdid || bidRequest.userId.pubcid || bidRequest.userId.lipb || bidRequest.userId.idl_env)) { + (bidRequest.userId.tdid || bidRequest.userId.pubcid || bidRequest.userId.lipb || bidRequest.userId.idl_env || bidRequest.userId.sharedid)) { utils.deepSetValue(data, 'user.ext.eids', []); if (bidRequest.userId.tdid) { @@ -300,6 +300,20 @@ export const spec = { }] }); } + + // support shared id + if (bidRequest.userId.sharedid) { + data.user.ext.eids.push({ + source: 'sharedid.org', + uids: [{ + id: bidRequest.userId.sharedid.id, + atype: 3, + ext: { + third: bidRequest.userId.sharedid.third + } + }] + }); + } } if (config.getConfig('coppa') === true) { @@ -566,6 +580,11 @@ export const spec = { if (bidRequest.userId.idl_env) { data['x_liverampidl'] = bidRequest.userId.idl_env; } + + // support shared id + if (bidRequest.userId.sharedid) { + data['eid_sharedid.org'] = `${bidRequest.userId.sharedid.id}^3^${bidRequest.userId.sharedid.third}`; + } } if (bidderRequest.gdprConsent) { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index cddf190fda4..1d5769b9ac1 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -224,7 +224,11 @@ describe('the rubicon adapter', function () { lipbid: '0000-1111-2222-3333', segments: ['segA', 'segB'] }, - idl_env: '1111-2222-3333-4444' + idl_env: '1111-2222-3333-4444', + sharedid: { + id: '1111', + third: '2222' + } }; bid.storedAuctionResponse = 11111; } @@ -1357,6 +1361,22 @@ describe('the rubicon adapter', function () { expect(data['x_liverampidl']).to.equal('1111-2222-3333-4444'); }); }); + + describe('SharedID support', function () { + it('should send sharedid when userId defines sharedId', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + clonedBid.userId = { + sharedid: { + id: '1111', + third: '2222' + } + }; + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_sharedid.org']).to.equal('1111^3^2222'); + }); + }); }) describe('Prebid AdSlot', function () { @@ -1551,6 +1571,12 @@ describe('the rubicon adapter', function () { // LiveRamp should exist expect(post.user.ext.eids[1].source).to.equal('liveramp_idl'); expect(post.user.ext.eids[1].uids[0].id).to.equal('1111-2222-3333-4444'); + // SharedId should exist + expect(post.user.ext.eids[2].source).to.equal('sharedid.org'); + expect(post.user.ext.eids[2].uids[0].id).to.equal('1111'); + expect(post.user.ext.eids[2].uids[0].atype).to.equal(3); + expect(post.user.ext.eids[2].uids[0].ext.third).to.equal('2222'); + expect(post.rp).that.is.an('object'); expect(post.rp.target).that.is.an('object'); expect(post.rp.target.LIseg).that.is.an('array'); From 1a38f40ad0d29478e68f76851a14dc8206ae4069 Mon Sep 17 00:00:00 2001 From: Michael Griego Date: Thu, 20 Aug 2020 10:59:01 -0500 Subject: [PATCH 28/43] Allow hp:0 in supply chain nodes passed to Rubicon (#5629) --- modules/rubiconBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 0e41bf6c6ad..2768b41da21 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -1195,7 +1195,7 @@ export function hasValidSupplyChainParams(schain) { if (!schain.nodes) return isValid; isValid = schain.nodes.reduce((status, node) => { if (!status) return status; - return requiredFields.every(field => node[field]); + return requiredFields.every(field => node.hasOwnProperty(field)); }, true); if (!isValid) utils.logError('Rubicon: required schain params missing'); return isValid; From a1692da2779e60144efb51cb129fbba7bf0bdbe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E6=80=9D=E6=95=8F?= <506374983@qq.com> Date: Fri, 21 Aug 2020 00:02:48 +0800 Subject: [PATCH 29/43] =?UTF-8?q?Add=20a=20new=20bidder=20adapter=20?= =?UTF-8?q?=EF=BC=9AmediagoBidderAdapter=20(#5614)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * mediago bid adaptor * 修改去掉固定ip * mediago bid adapter: add test spec * spec code style * change into prod EP and auto size. * delete other param * 修改为强制https协议 --- modules/mediagoBidAdapter.js | 362 ++++++++++++++++++++ modules/mediagoBidAdapter.md | 33 ++ test/spec/modules/mediagoBidAdapter_spec.js | 101 ++++++ 3 files changed, 496 insertions(+) create mode 100644 modules/mediagoBidAdapter.js create mode 100644 modules/mediagoBidAdapter.md create mode 100644 test/spec/modules/mediagoBidAdapter_spec.js diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js new file mode 100644 index 00000000000..68eb95ddae3 --- /dev/null +++ b/modules/mediagoBidAdapter.js @@ -0,0 +1,362 @@ +/** + * gulp serve --modules=mediagoBidAdapter --nolint --notest + */ + +import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'mediago'; +// const PROTOCOL = window.document.location.protocol; +const ENDPOINT_URL = + // ((PROTOCOL === 'https:') ? 'https' : 'http') + + 'https://rtb-us.mediago.io/api/bid?tn='; +const TIME_TO_LIVE = 500; +// const ENDPOINT_URL = '/api/bid?tn='; +const storage = getStorageManager(); +let globals = {}; +let itemMaps = {}; + +/** + * 获取随机id + * @param {number} a random number from 0 to 15 + * @return {string} random number or random string + */ +function getRandomId( + a // placeholder +) { + return a // if the placeholder was passed, return + ? ( // a random number from 0 to 15 + a ^ // unless b is 8, + Math.random() * // in which case + 16 >> // a random number from + a / 4 // 8 to 11 + ).toString(16) // in hexadecimal + : ( // or otherwise a concatenated string: + [1e7] + // 10000000 + + 1e3 + // -1000 + + 4e3 + // -4000 + + 8e3 + // -80000000 + + 1e11 // -100000000000, + ).replace( // replacing + /[018]/g, // zeroes, ones, and eights with + getRandomId // random hex digits + ); +} + +/* ----- mguid:start ------ */ +const COOKIE_KEY_MGUID = '__mguid_'; + +/** + * 获取用户id + * @return {string} + */ +const getUserID = () => { + const i = storage.getCookie(COOKIE_KEY_MGUID); + + if (i === null) { + const uuid = utils.generateUUID(); + storage.setCookie(COOKIE_KEY_MGUID, uuid); + return uuid; + } + return i; +}; + +/* ----- mguid:end ------ */ + +/** + * 获取一个对象的某个值,如果没有则返回空字符串 + * @param {Object} obj 对象 + * @param {...string} keys 键名 + * @return {any} + */ +function getProperty(obj, ...keys) { + let o = obj; + + for (let key of keys) { + // console.log(key, o); + if (o && o[key]) { + o = o[key]; + } else { + return ''; + } + } + return o; +} + +/** + * 是不是移动设备或者平板 + * @return {boolean} + */ +function isMobileAndTablet() { + let check = false; + (function(a) { + let reg1 = new RegExp(['(android|bb\d+|meego)', + '.+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)', + '|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone', + '|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap', + '|windows ce|xda|xiino|android|ipad|playbook|silk' + ].join(''), 'i'); + let reg2 = new RegExp(['1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)', + '|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )', + '|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell', + '|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)', + '|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene', + '|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c', + '|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom', + '|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)', + '|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)', + '|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]', + '|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)', + '|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio', + '|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms', + '|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al', + '|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)', + '|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|', + 'v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)', + '|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-', + '|your|zeto|zte\-' + ].join(''), 'i'); + if (reg1.test(a) || + reg2.test(a.substr(0, 4))) { + check = true; + } + })(navigator.userAgent || navigator.vendor || window.opera); + return check; +} + +/** + * 将尺寸转为RTB识别的尺寸 + * + * @param {Array|Object} requestSizes 配置尺寸 + * @return {Object} + */ +function transformSizes(requestSizes) { + let sizes = []; + let sizeObj = {}; + + if (utils.isArray(requestSizes) && requestSizes.length === 2 && + !utils.isArray(requestSizes[0])) { + sizeObj.width = parseInt(requestSizes[0], 10); + sizeObj.height = parseInt(requestSizes[1], 10); + sizes.push(sizeObj); + } else if (typeof requestSizes === 'object') { + for (let i = 0; i < requestSizes.length; i++) { + let size = requestSizes[i]; + sizeObj = {}; + sizeObj.width = parseInt(size[0], 10); + sizeObj.height = parseInt(size[1], 10); + sizes.push(sizeObj); + } + } + + return sizes; +} + +/** + * 获取广告位配置 + * @param {Array} validBidRequests an an array of bids + * @param {Object} bidderRequest The master bidRequest object + * @return {Object} + */ +function getItems(validBidRequests, bidderRequest) { + let items = []; + items = validBidRequests.map((req, i) => { + let ret = {}; + let mediaTypes = getProperty(req, 'mediaTypes'); + + let sizes = transformSizes(getProperty(req, 'sizes')); + let matchSize; + + // 确认尺寸是否符合我们要求 + for (let size of sizes) { + if (size.width === 300 && size.height === 250) { + matchSize = size; + break; + } + } + + // if (mediaTypes.native) {} + // banner广告类型 + if (mediaTypes.banner) { + let id = '' + (i + 1); + ret = { + id: id, + // bidFloor: 0, // todo + banner: { + h: matchSize.height, + w: matchSize.width, + pos: 1, + } + }; + itemMaps[id] = { + req, + ret + }; + } + + return ret; + }); + return items; +} + +/** + * 获取rtb请求参数 + * + * @param {Array} validBidRequests an an array of bids + * @param {Object} bidderRequest The master bidRequest object + * @return {Object} + */ +function getParam(validBidRequests, bidderRequest) { + // console.log(validBidRequests, bidderRequest); + let isMobile = isMobileAndTablet() ? 1 : 0; + let isTest = 0; + let auctionId = getProperty(bidderRequest, 'auctionId') || getRandomId(); + let items = getItems(validBidRequests, bidderRequest); + + const domain = document.domain; + const location = utils.deepAccess(bidderRequest, 'refererInfo.referer'); + + if (items && items.length) { + let c = { + 'id': 'mgprebidjs_' + auctionId, + 'test': +isTest, + 'at': 1, + 'cur': ['USD'], + 'device': { + // 'dnt':0, + // 'devicetype':2, + 'js': 1, + 'os': navigator.platform || '', + 'ua': navigator.userAgent, + 'language': /en/.test(navigator.language) ? 'en' : navigator.language, + // 'geo':{ + // 'country':'USA' + // } + }, + 'user': { + 'id': getUserID() // todo + }, + 'site': { + 'name': domain, + 'domain': domain, + 'page': location, + 'ref': location, + 'mobile': isMobile, + 'cat': [], // todo + 'publisher': { // todo + 'id': domain, + 'name': domain + } + }, + 'imp': items + }; + return c; + } else { + return null; + } +} + +export const spec = { + code: BIDDER_CODE, + // aliases: ['ex'], // short code + /** + * 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) { + // console.log('mediago', { + // bid + // }); + if (bid.params.token) { + globals['token'] = bid.params.token; + } + return !!(bid.params.token); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {Array} validBidRequests an an array of bids + * @param {Object} bidderRequest The master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + let payload = getParam(validBidRequests, bidderRequest); + + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: ENDPOINT_URL + globals['token'], + data: payloadString, + }; + }, + + /** + * 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 bids = getProperty(serverResponse, 'body', 'seatbid', 0, 'bid'); + const cur = getProperty(serverResponse, 'body', 'cur'); + + const bidResponses = []; + + for (let bid of bids) { + let impid = getProperty(bid, 'impid'); + if (itemMaps[impid]) { + let bidId = getProperty(itemMaps[impid], 'req', 'bidId'); + const bidResponse = { + requestId: bidId, + cpm: getProperty(bid, 'price'), + width: getProperty(bid, 'w'), + height: getProperty(bid, 'h'), + creativeId: getProperty(bid, 'crid'), + dealId: '', + currency: cur, + netRevenue: true, + ttl: TIME_TO_LIVE, + // referrer: REFERER, + ad: getProperty(bid, 'adm') + }; + bidResponses.push(bidResponse); + } + } + + return bidResponses; + }, + + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ + onTimeout: function(data) { + // console.log('onTimeout', data); + // Bidder specifc code + }, + + /** + * 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) { + // console.log('onBidWon', bid); + // Bidder specific code + }, + + /** + * Register bidder specific code, which will execute when the adserver targeting has been set for a bid from this bidder + * @param {Bid} The bid of which the targeting has been set + */ + onSetTargeting: function(bid) { + // console.log('onSetTargeting', bid); + // Bidder specific code + } +}; +registerBidder(spec); diff --git a/modules/mediagoBidAdapter.md b/modules/mediagoBidAdapter.md new file mode 100644 index 00000000000..87c38f662a3 --- /dev/null +++ b/modules/mediagoBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: MediaGo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: fangsimin@baidu.com +``` + +# Description + +Module that connects to MediaGo's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: "mediago", + params: { + token: '' // required, send email to ext_mediago_am@baidu.com to get the corresponding token + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/mediagoBidAdapter_spec.js b/test/spec/modules/mediagoBidAdapter_spec.js new file mode 100644 index 00000000000..25cfc72672b --- /dev/null +++ b/test/spec/modules/mediagoBidAdapter_spec.js @@ -0,0 +1,101 @@ +import { + expect +} from 'chai'; +import { + spec +} from 'modules/mediagoBidAdapter.js'; + +describe('mediago:BidAdapterTests', function() { + let bidRequestData = { + 'bidderCode': 'mediago', + 'auctionId': '7fae02a9-0195-472f-ba94-708d3bc2c0d9', + 'bidderRequestId': '4fec04e87ad785', + 'bids': [{ + 'bidder': 'mediago', + 'params': { + 'token': '85a6b01e41ac36d49744fad726e3655d' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '5e24a2ce-db03-4565-a8a3-75dbddca9377', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '54d73f19c9d47a', + 'bidderRequestId': '4fec04e87ad785', + 'auctionId': '7fae02a9-0195-472f-ba94-708d3bc2c0d9', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }] + }; + let request = []; + + it('mediago:validate_pub_params', function() { + expect( + spec.isBidRequestValid({ + bidder: 'mediago', + params: { + token: ['85a6b01e41ac36d49744fad726e3655d'] + } + }) + ).to.equal(true); + }); + + it('mediago:validate_generated_params', function() { + request = spec.buildRequests(bidRequestData.bids, bidRequestData); + // console.log(request); + let req_data = JSON.parse(request.data); + expect(req_data.imp).to.have.lengthOf(1); + }); + + it('mediago:validate_response_params', function() { + let serverResponse = { + body: { + 'id': '7244645c-a81a-4760-8fd6-9d908d2c4a44', + 'seatbid': [{ + 'bid': [{ + 'id': 'aa86796a857ebedda9a2d7128a87dab1', + 'impid': '1', + 'price': 0.05, + 'nurl': 'http://d21uzv52i0cqie.cloudfront.net/api/winnotice?tn=341443089c0cb829164455a42d216ee3\u0026winloss=1\u0026id=aa86796a857ebedda9a2d7128a87dab1\u0026seat_id=${AUCTION_SEAT_ID}\u0026currency=${AUCTION_CURRENCY}\u0026bid_id=${AUCTION_BID_ID}\u0026ad_id=${AUCTION_AD_ID}\u0026loss=${AUCTION_LOSS}\u0026imp_id=1\u0026price=${AUCTION_PRICE}\u0026test=0\u0026time=1597714943\u0026adp=Dtnz0O4U8sdAU-XGGijCAgMbjDIeMGyCLXeSg1laXxM\u0026dsp_id=22\u0026url=-BFDu2NYtc4PYTplFW_2JcnDSRVLOOfaERbwJABjFyG6NUB4ywA3dUaXt5zPlyCUpBCOxjH9gk4E6yWTshzuSfQSx7g_TxvcXYUgh7YtY9NQZxx14InmNCTsezqID5UztV7llz8SXWHQ-ZsutH1nJIZzl1jH3i2uCPi91shqIZLN1bLJ5guAr5O4WyxVeOqIKyD_GiVcY9Olm51iI_3wgwFyDEN_dIDv-ObgNxpbPD0L11-62bjhGw3__7RuEo6XLdox-g46Fcqk6i0zayfsPM4QeMAhWJ4lsg-xswSI0YAfzyoOIeTWB78mdpt_GmN5PKZZPqyO7VkbwHEasn-mTyYTddbz5v2fzEkRO0AQZtAZx96PANGrNvcOHnRVmCdkzN96b5Ur1_8ipdyzHOFRtJ-z_KmKaxig6himvMCePozZvrvihiGhigP4RGiFT7ytVYKHyUGAV2PF5SwtgnB0uGCltd7o1CLhZyZEQNgE7LSESyGztZ5kM9N_VZV9gPZVhvlJDfYFNRW9i6D2pZxV0Gd63rA9gpeUJ3mhbkj-B27VRKrNTBSrwIAU7P0RPD5_opl3G8nPD1Ce2vKuQK8qynHWQblfeA61nDok-fRezSKbzwepqi8oxXadFrCmN7KxP_mPqA794xYzIw5-mS64NA', + 'burl': 'http://d21uzv52i0cqie.cloudfront.net/api/winnotice?tn=341443089c0cb829164455a42d216ee3\u0026winloss=2\u0026id=aa86796a857ebedda9a2d7128a87dab1\u0026seat_id=${AUCTION_SEAT_ID}\u0026currency=${AUCTION_CURRENCY}\u0026bid_id=${AUCTION_BID_ID}\u0026ad_id=${AUCTION_AD_ID}\u0026loss=${AUCTION_LOSS}\u0026imp_id=1\u0026price=${AUCTION_PRICE}\u0026test=0\u0026time=1597714943\u0026adp=Dtnz0O4U8sdAU-XGGijCAgMbjDIeMGyCLXeSg1laXxM\u0026dsp_id=22\u0026url=dXerAvyp4zYQzsQ56eGB4JtiA4yFaYlTqcHffccrvCg', + 'lurl': 'http://d21uzv52i0cqie.cloudfront.net/api/winnotice?tn=341443089c0cb829164455a42d216ee3\u0026winloss=0\u0026id=aa86796a857ebedda9a2d7128a87dab1\u0026seat_id=${AUCTION_SEAT_ID}\u0026currency=${AUCTION_CURRENCY}\u0026bid_id=${AUCTION_BID_ID}\u0026ad_id=${AUCTION_AD_ID}\u0026loss=${AUCTION_LOSS}\u0026imp_id=1\u0026price=${AUCTION_PRICE}\u0026test=0\u0026time=1597714943\u0026adp=Dtnz0O4U8sdAU-XGGijCAgMbjDIeMGyCLXeSg1laXxM\u0026dsp_id=22\u0026url=ptSxg_vR7-fdx-WAkkUADJb__BntE5a6-RSeYdUewvk', + 'adm': '\u003clink rel=\"stylesheet\" href=\"//cdn.mediago.io/js/style/style_banner_300*250.css\"\u003e\u003cdiv id=\"mgcontainer-583ce3286b442001205b2fb9a5488efc\" class=\"mediago-placement imgTopTitleBottom\" style=\"position:relative;width:298px;height:248px;overflow:hidden\"\u003e\u003ca href=\"http://trace.mediago.io/api/bidder/track?tn=341443089c0cb829164455a42d216ee3\u0026price=PRMC8pCHtH55ipgXubUs8jlsKTBxWRSEweH8Mee_aUQ\u0026evt=102\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026campaignid=1003540\u0026impid=25-300x175-1\u0026offerid=1107782\u0026test=0\u0026time=1597714943\u0026cp=GJA03gjm-ugPTmN7prOpvhfu1aA04IgpTcW4oRX22Lg\u0026clickid=25_aa86796a857ebedda9a2d7128a87dab1_25-300x175-1\u0026acid=164\u0026trackingid=583ce3286b442001205b2fb9a5488efc\u0026uid=6dda6c6b70eb4e2d9ab3469d921f2c74\u0026jt=2\u0026url=PQFFci337KgCVkx7KTgRItClLaWH0lgTcIlgBRTpfKngVJES4uKLfxXz9mjLcDWIbWQcEVVk_gfTcIaK8oKO2YyVG5lc3hjZeZr0VaIDHbWggPJaqtfDK9T0HZIKvrpe\" target=\"_blank\"\u003e\u003cimg alt=\"Robert Vowed To Keep Silent, But Decided To Put The People First And Speak\" src=\"https://d2cli4kgl5uxre.cloudfront.net/ML/b5e361889beef5eaf69987384b7a56e8__300x175.png\" style=\"height:70%;width:100%;border-width:0;border:none;\"\u003e\u003ch3 class=\"title\" style=\"font-size:16px;\"\u003eRobert Vowed To Keep Silent, But Decided To Put The People First And Speak\u003c/h3\u003e\u003c/a\u003e\u003cspan class=\"source\"\u003e\u003ca class=\"sourcename\" href=\"//www.mediago.io\" target=\"_blank\"\u003e\u003cspan\u003eAd\u003c/span\u003e \u003c/a\u003e\u003ca class=\"srcnameadslabelurl\" href=\"//www.mediago.io/privacy\" target=\"_blank\"\u003e\u003cspan\u003eViral Net News\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e\u003c/div\u003e\u003cscript\u003e!function(e){var t={};function n(o){if(t[o])return t[o].exports;var r=t[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){\"undefined\"!=typeof Symbol\u0026\u0026Symbol.toStringTag\u0026\u0026Object.defineProperty(e,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(e,\"__esModule\",{value:!0})},n.t=function(e,t){if(1\u0026t\u0026\u0026(e=n(e)),8\u0026t)return e;if(4\u0026t\u0026\u0026\"object\"==typeof e\u0026\u0026e\u0026\u0026e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,\"default\",{enumerable:!0,value:e}),2\u0026t\u0026\u0026\"string\"!=typeof e)for(var r in e)n.d(o,r,function(t){return e[t]}.bind(null,r));return o},n.n=function(e){var t=e\u0026\u0026e.__esModule?function(){return e.default}:function(){return e};return n.d(t,\"a\",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p=\"\",n(n.s=24)}({24:function(e,t,n){\"use strict\";function o(e){var t=new Image;t.src=e,t.style=\"display:none;visibility:hidden\",t.width=0,t.height=0,document.body.appendChild(t)}o(\"http://d21uzv52i0cqie.cloudfront.net/api/bidder/track?tn=341443089c0cb829164455a42d216ee3\u0026price=PRMC8pCHtH55ipgXubUs8jlsKTBxWRSEweH8Mee_aUQ\u0026evt=101\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026campaignid=1003540\u0026impid=25-300x175-1\u0026offerid=1107782\u0026test=0\u0026time=1597714943\u0026cp=GJA03gjm-ugPTmN7prOpvhfu1aA04IgpTcW4oRX22Lg\u0026acid=164\u0026trackingid=583ce3286b442001205b2fb9a5488efc\u0026uid=6dda6c6b70eb4e2d9ab3469d921f2c74\");var r=document.getElementById(\"mgcontainer-583ce3286b442001205b2fb9a5488efc\"),i=!1;!function e(){setTimeout((function(){var t,n;!i\u0026\u0026(t=r,n=window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight,(t.getBoundingClientRect()\u0026\u0026t.getBoundingClientRect().top)\u003c=n-.75*(t.offsetHeight||t.clientHeight))?(i=!0,o(\"http://d21uzv52i0cqie.cloudfront.net/api/bidder/track?tn=341443089c0cb829164455a42d216ee3\u0026price=PRMC8pCHtH55ipgXubUs8jlsKTBxWRSEweH8Mee_aUQ\u0026evt=104\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026campaignid=1003540\u0026impid=25-300x175-1\u0026offerid=1107782\u0026test=0\u0026time=1597714943\u0026cp=GJA03gjm-ugPTmN7prOpvhfu1aA04IgpTcW4oRX22Lg\u0026acid=164\u0026trackingid=583ce3286b442001205b2fb9a5488efc\u0026uid=6dda6c6b70eb4e2d9ab3469d921f2c74\u0026sid=16__11__13\u0026format=\u0026crid=b5e361889beef5eaf69987384b7a56e8\")):e()}),500)}()}});\u003c/script\u003e\u003cscript type=\"text/javascript\" src=\"http://d21uzv52i0cqie.cloudfront.net/api/track?tn=341443089c0cb829164455a42d216ee3\u0026price=${AUCTION_PRICE}\u0026evt=5\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026impid=1\u0026offerid=\u0026tagid=\u0026test=0\u0026time=1597714943\u0026adp=5zrCZ2rC-WLafYkNpeTnzA72tDqVZUlOA3Js6_eJjYU\u0026dsp_id=22\u0026cp=${cp}\u0026url=\u0026type=script\"\u003e\u003c/script\u003e\u003cscript\u003edocument.addEventListener\u0026\u0026document.addEventListener(\"click\",function(){var a=document.createElement(\"script\");a.src=\"http://d21uzv52i0cqie.cloudfront.net/api/track?tn=341443089c0cb829164455a42d216ee3\u0026price=${AUCTION_PRICE}\u0026evt=6\u0026rid=aa86796a857ebedda9a2d7128a87dab1\u0026impid=1\u0026offerid=\u0026tagid=\u0026test=0\u0026time=1597714943\u0026adp=5zrCZ2rC-WLafYkNpeTnzA72tDqVZUlOA3Js6_eJjYU\u0026dsp_id=22\u0026cp=${cp}\u0026url=\u0026clickid=25_aa86796a857ebedda9a2d7128a87dab1_1\";document.body.appendChild(a)});\u003c/script\u003e', + 'cid': '1003540', + 'crid': 'b5e361889beef5eaf69987384b7a56e8', + 'w': 300, + 'h': 250 + }] + }], + 'cur': 'USD' + } + }; + + let bids = spec.interpretResponse(serverResponse); + // console.log({ + // bids + // }); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + + expect(bid.creativeId).to.equal('b5e361889beef5eaf69987384b7a56e8'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.currency).to.equal('USD'); + }); +}); From 0d8d9bf65bff06386b0dea66b158afd4a326e3b0 Mon Sep 17 00:00:00 2001 From: Scott Date: Thu, 20 Aug 2020 18:30:34 +0200 Subject: [PATCH 30/43] updated userid module to stop caching the entire consent object (#5641) * updated userid module to stop caching the entire consent object but rather just a hash of it, since all we need it for is comparison purposes. * IE doesn't support Math.imul, so providing a polyfill for it when necessary * use `===` to compare consent values; convert hashes to a string when returning them * add test for string response and fix @returns doc * don't use polyfills! --- modules/userId/index.js | 13 ++++------- src/utils.js | 40 ++++++++++++++++++++++++++++++++ test/spec/modules/userId_spec.js | 35 ---------------------------- test/spec/utils_spec.js | 20 ++++++++++++++++ 4 files changed, 65 insertions(+), 43 deletions(-) diff --git a/modules/userId/index.js b/modules/userId/index.js index afdd93a57ba..bcc0f68b2b0 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -232,7 +232,7 @@ function getStoredValue(storage, key = undefined) { * @param consentData * @returns {{apiVersion: number, gdprApplies: boolean, consentString: string}} */ -function makeStoredConsentDataObject(consentData) { +function makeStoredConsentDataHash(consentData) { const storedConsentData = { consentString: '', gdprApplies: false, @@ -244,8 +244,7 @@ function makeStoredConsentDataObject(consentData) { storedConsentData.gdprApplies = consentData.gdprApplies; storedConsentData.apiVersion = consentData.apiVersion; } - - return storedConsentData; + return utils.simpleHash(JSON.stringify(storedConsentData)); } /** @@ -255,7 +254,7 @@ function makeStoredConsentDataObject(consentData) { export function setStoredConsentData(consentData) { try { const expiresStr = (new Date(Date.now() + (CONSENT_DATA_COOKIE_STORAGE_CONFIG.expires * (60 * 60 * 24 * 1000)))).toUTCString(); - coreStorage.setCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name, JSON.stringify(makeStoredConsentDataObject(consentData)), expiresStr, 'Lax'); + coreStorage.setCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name, makeStoredConsentDataHash(consentData), expiresStr, 'Lax'); } catch (error) { utils.logError(error); } @@ -266,13 +265,11 @@ export function setStoredConsentData(consentData) { * @returns {string} */ function getStoredConsentData() { - let storedValue; try { - storedValue = JSON.parse(coreStorage.getCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name)); + return coreStorage.getCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name); } catch (e) { utils.logError(e); } - return storedValue; } /** @@ -287,7 +284,7 @@ function storedConsentDataMatchesConsentData(storedConsentData, consentData) { return ( typeof storedConsentData === 'undefined' || storedConsentData === null || - utils.deepEqual(storedConsentData, makeStoredConsentDataObject(consentData)) + storedConsentData === makeStoredConsentDataHash(consentData) ); } diff --git a/src/utils.js b/src/utils.js index f0fa57c4cff..6801a7dc1d1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1215,3 +1215,43 @@ export function mergeDeep(target, ...sources) { return mergeDeep(target, ...sources); } + +/** + * returns a hash of a string using a fast algorithm + * source: https://stackoverflow.com/a/52171480/845390 + * @param str + * @param seed (optional) + * @returns {string} + */ +export function simpleHash(str, seed = 0) { + // IE doesn't support imul + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul#Polyfill + let imul = function(opA, opB) { + if (isFn(Math.imul)) { + return Math.imul(opA, opB); + } else { + opB |= 0; // ensure that opB is an integer. opA will automatically be coerced. + // floating points give us 53 bits of precision to work with plus 1 sign bit + // automatically handled for our convienence: + // 1. 0x003fffff /*opA & 0x000fffff*/ * 0x7fffffff /*opB*/ = 0x1fffff7fc00001 + // 0x1fffff7fc00001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ + var result = (opA & 0x003fffff) * opB; + // 2. We can remove an integer coersion from the statement above because: + // 0x1fffff7fc00001 + 0xffc00000 = 0x1fffffff800001 + // 0x1fffffff800001 < Number.MAX_SAFE_INTEGER /*0x1fffffffffffff*/ + if (opA & 0xffc00000) result += (opA & 0xffc00000) * opB | 0; + return result | 0; + } + }; + + let h1 = 0xdeadbeef ^ seed; + let h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = imul(h1 ^ ch, 2654435761); + h2 = imul(h2 ^ ch, 1597334677); + } + h1 = imul(h1 ^ (h1 >>> 16), 2246822507) ^ imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = imul(h2 ^ (h2 >>> 16), 2246822507) ^ imul(h1 ^ (h1 >>> 13), 3266489909); + return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(); +} diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index d9671aabc84..fd7e8a76972 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -1591,13 +1591,6 @@ describe('User ID', function() { sinon.assert.notCalled(mockGetId); sinon.assert.calledOnce(mockDecode); sinon.assert.calledOnce(mockExtendId); - - let consent = gdprDataHandler.getConsentData(); - let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); - expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); - expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); - expect(userIdStoredConsent.consentString).to.equal(consent.consentString); - expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); }); it('calls getId if no stored consent data but refresh is needed', function () { @@ -1611,13 +1604,6 @@ describe('User ID', function() { sinon.assert.calledOnce(mockGetId); sinon.assert.calledOnce(mockDecode); sinon.assert.notCalled(mockExtendId); - - let consent = gdprDataHandler.getConsentData(); - let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); - expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); - expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); - expect(userIdStoredConsent.consentString).to.equal(consent.consentString); - expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); }); it('calls getId if empty stored consent and refresh not needed', function () { @@ -1633,13 +1619,6 @@ describe('User ID', function() { sinon.assert.calledOnce(mockGetId); sinon.assert.calledOnce(mockDecode); sinon.assert.notCalled(mockExtendId); - - let consent = gdprDataHandler.getConsentData(); - let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); - expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); - expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); - expect(userIdStoredConsent.consentString).to.equal(consent.consentString); - expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); }); it('calls getId if stored consent does not match current consent and refresh not needed', function () { @@ -1659,13 +1638,6 @@ describe('User ID', function() { sinon.assert.calledOnce(mockGetId); sinon.assert.calledOnce(mockDecode); sinon.assert.notCalled(mockExtendId); - - let consent = gdprDataHandler.getConsentData(); - let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); - expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); - expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); - expect(userIdStoredConsent.consentString).to.equal(consent.consentString); - expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); }); it('does not call getId if stored consent matches current consent and refresh not needed', function () { @@ -1685,13 +1657,6 @@ describe('User ID', function() { sinon.assert.notCalled(mockGetId); sinon.assert.calledOnce(mockDecode); sinon.assert.calledOnce(mockExtendId); - - let consent = gdprDataHandler.getConsentData(); - let userIdStoredConsent = JSON.parse(coreStorage.getCookie(CONSENT_LOCAL_STORAGE_NAME)); - expect(userIdStoredConsent.gdprApplies).to.equal(consent.gdprApplies); - expect(userIdStoredConsent.gdprApplies).to.equal(testConsentData.gdprApplies); - expect(userIdStoredConsent.consentString).to.equal(consent.consentString); - expect(userIdStoredConsent.consentString).to.equal(testConsentData.consentData); }); }); }); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index fca59633ebe..dbaad919bd3 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -1177,5 +1177,25 @@ describe('Utils', function () { } expect(utils.deepEqual(obj1, obj2)).to.equal(false); }); + + describe('simpleHash', function() { + it('should return the same hash for the same string', function() { + const stringOne = 'string1'; + expect(utils.simpleHash(stringOne)).to.equal(utils.simpleHash(stringOne)); + }); + it('should return a different hash for the same string with different seeds', function() { + const stringOne = 'string1'; + expect(utils.simpleHash(stringOne, 1)).to.not.equal(utils.simpleHash(stringOne, 2)); + }); + it('should return a different hash for different strings with the same seed', function() { + const stringOne = 'string1'; + const stringTwo = 'string2'; + expect(utils.simpleHash(stringOne)).to.not.equal(utils.simpleHash(stringTwo)); + }); + it('should return a string value, not a number', function() { + const stringOne = 'string1'; + expect(typeof utils.simpleHash(stringOne)).to.equal('string'); + }); + }); }); }); From b4c6b47bf90f48e6ffcab8f7e5aa287de0119a57 Mon Sep 17 00:00:00 2001 From: Josh Larson Date: Thu, 20 Aug 2020 12:38:22 -0500 Subject: [PATCH 31/43] New bidder & analytics adapter: Concert (attempt #2) (#5623) * Add concert bid adapter, doc and tests. * Add analytics adapter * Add email * fix alert from lgtm * try to fix test for ie 11 * Handle USP string for PPID * Fix linking error * Debug: Find out why IE11 is failing * More debugging * More debugging * Attempt to store queue in-prototype * Revert "Attempt to store queue in-prototype" This reverts commit 829ad84485369fbae3f65c738b37d1af2bbc4625. * More debugging * More debugging * Remove Array.includes to support IE11 Co-authored-by: Messay Bekele Co-authored-by: Messay Bekele Co-authored-by: Andrew Amato --- modules/concertAnalyticsAdapter.js | 120 ++++++++++ modules/concertAnalyticsAdapter.md | 11 + modules/concertBidAdapter.js | 208 +++++++++++++++++ modules/concertBidAdapter.md | 33 +++ .../modules/concertAnalyticsAdapter_spec.js | 157 +++++++++++++ test/spec/modules/concertBidAdapter_spec.js | 219 ++++++++++++++++++ 6 files changed, 748 insertions(+) create mode 100644 modules/concertAnalyticsAdapter.js create mode 100644 modules/concertAnalyticsAdapter.md create mode 100644 modules/concertBidAdapter.js create mode 100644 modules/concertBidAdapter.md create mode 100644 test/spec/modules/concertAnalyticsAdapter_spec.js create mode 100644 test/spec/modules/concertBidAdapter_spec.js diff --git a/modules/concertAnalyticsAdapter.js b/modules/concertAnalyticsAdapter.js new file mode 100644 index 00000000000..a81d07e63b5 --- /dev/null +++ b/modules/concertAnalyticsAdapter.js @@ -0,0 +1,120 @@ +import {ajax} from '../src/ajax.js'; +import adapter from '../src/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager from '../src/adapterManager.js'; +import * as utils from '../src/utils.js'; + +const analyticsType = 'endpoint'; + +// We only want to send about 1% of events for sampling purposes +const SAMPLE_RATE_PERCENTAGE = 1 / 100; +const pageIncludedInSample = sampleAnalytics(); + +const url = 'https://bids.concert.io/analytics'; + +const { + EVENTS: { + BID_RESPONSE, + BID_WON, + AUCTION_END + } +} = CONSTANTS; + +let queue = []; + +let concertAnalytics = Object.assign(adapter({url, analyticsType}), { + track({ eventType, args }) { + switch (eventType) { + case BID_RESPONSE: + if (args.bidder !== 'concert') break; + queue.push(mapBidEvent(eventType, args)); + break; + + case BID_WON: + if (args.bidder !== 'concert') break; + queue.push(mapBidEvent(eventType, args)); + break; + + case AUCTION_END: + // Set a delay, as BID_WON events will come after AUCTION_END events + setTimeout(() => sendEvents(), 3000); + break; + + default: + break; + } + } +}); + +function mapBidEvent(eventType, args) { + const { adId, auctionId, cpm, creativeId, width, height, timeToRespond } = args; + const [gamCreativeId, concertRequestId] = getConcertRequestId(creativeId); + + const payload = { + event: eventType, + concert_rid: concertRequestId, + adId, + auctionId, + creativeId: gamCreativeId, + position: args.adUnitCode, + url: window.location.href, + cpm, + width, + height, + timeToRespond + } + + return payload; +} + +/** + * In order to pass back the concert_rid from CBS, it is tucked into the `creativeId` + * slot in the bid response and combined with a pipe `|`. This method splits the creative ID + * and the concert_rid. + * + * @param {string} creativeId + */ +function getConcertRequestId(creativeId) { + if (!creativeId || creativeId.indexOf('|') < 0) return [null, null]; + + return creativeId.split('|'); +} + +function sampleAnalytics() { + return Math.random() <= SAMPLE_RATE_PERCENTAGE; +} + +function sendEvents() { + concertAnalytics.eventsStorage = queue; + + if (!queue.length) return; + + if (!pageIncludedInSample) { + utils.logMessage('Page not included in sample for Concert Analytics'); + return; + } + + try { + const body = JSON.stringify(queue); + ajax(url, () => queue = [], body, { + contentType: 'application/json', + method: 'POST' + }); + } catch (err) { utils.logMessage('Concert Analytics error') } +} + +// save the base class function +concertAnalytics.originEnableAnalytics = concertAnalytics.enableAnalytics; +concertAnalytics.eventsStorage = []; + +// override enableAnalytics so we can get access to the config passed in from the page +concertAnalytics.enableAnalytics = function (config) { + concertAnalytics.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: concertAnalytics, + code: 'concert' +}); + +export default concertAnalytics; diff --git a/modules/concertAnalyticsAdapter.md b/modules/concertAnalyticsAdapter.md new file mode 100644 index 00000000000..7c9b6d22703 --- /dev/null +++ b/modules/concertAnalyticsAdapter.md @@ -0,0 +1,11 @@ +# Overview + +``` +Module Name: Concert Analytics Adapter +Module Type: Analytics Adapter +Maintainer: support@concert.io +``` + +# Description + +Analytics adapter for concert. \ No newline at end of file diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js new file mode 100644 index 00000000000..d153ddf9ee2 --- /dev/null +++ b/modules/concertBidAdapter.js @@ -0,0 +1,208 @@ + +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js' + +const BIDDER_CODE = 'concert'; +const CONCERT_ENDPOINT = 'https://bids.concert.io'; +const USER_SYNC_URL = 'https://cdn.concert.io/lib/bids/sync.html'; + +export const spec = { + code: BIDDER_CODE, + /** + * 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.partnerId) { + utils.logWarn('Missing partnerId bid parameter'); + return false; + } + + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @param {bidderRequest} - + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + utils.logMessage(validBidRequests); + utils.logMessage(bidderRequest); + let payload = { + meta: { + prebidVersion: '$prebid.version$', + pageUrl: bidderRequest.refererInfo.referer, + screen: [window.screen.width, window.screen.height].join('x'), + debug: utils.debugTurnedOn(), + uid: getUid(bidderRequest), + optedOut: hasOptedOutOfPersonalization(), + adapterVersion: '1.1.0', + uspConsent: bidderRequest.uspConsent, + gdprConsent: bidderRequest.gdprConsent + } + } + + payload.slots = validBidRequests.map(bidRequest => { + let slot = { + name: bidRequest.adUnitCode, + bidId: bidRequest.bidId, + transactionId: bidRequest.transactionId, + sizes: bidRequest.sizes, + partnerId: bidRequest.params.partnerId, + slotType: bidRequest.params.slotType + } + + return slot; + }); + + utils.logMessage(payload); + + return { + method: 'POST', + url: `${CONCERT_ENDPOINT}/bids/prebid`, + 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) { + utils.logMessage(serverResponse); + utils.logMessage(bidRequest); + + const serverBody = serverResponse.body; + + if (!serverBody || typeof serverBody !== 'object') { + return []; + } + + let bidResponses = []; + + bidResponses = serverBody.bids.map(bid => { + return { + requestId: bid.bidId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + ad: bid.ad, + ttl: bid.ttl, + creativeId: bid.creativeId, + netRevenue: bid.netRevenue, + currency: bid.currency + } + }); + + if (utils.debugTurnedOn() && serverBody.debug) { + utils.logMessage(`CONCERT`, serverBody.debug); + } + + utils.logMessage(bidResponses); + return bidResponses; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @param {gdprConsent} object GDPR consent object. + * @param {uspConsent} string US Privacy String. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + if (syncOptions.iframeEnabled && !hasOptedOutOfPersonalization()) { + let params = []; + + if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean')) { + params.push(`gdpr_applies=${gdprConsent.gdprApplies ? '1' : '0'}`); + } + if (gdprConsent && (typeof gdprConsent.consentString === 'string')) { + params.push(`gdpr_consent=${gdprConsent.consentString}`); + } + if (uspConsent && (typeof uspConsent === 'string')) { + params.push(`usp_consent=${uspConsent}`); + } + + syncs.push({ + type: 'iframe', + url: USER_SYNC_URL + (params.length > 0 ? `?${params.join('&')}` : '') + }); + } + return syncs; + }, + + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ + onTimeout: function(data) { + utils.logMessage('concert bidder timed out'); + utils.logMessage(data); + }, + + /** + * 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.logMessage('concert bidder won bid'); + utils.logMessage(bid); + } + +} + +registerBidder(spec); + +const storage = getStorageManager(); + +/** + * Check or generate a UID for the current user. + */ +function getUid(bidderRequest) { + if (hasOptedOutOfPersonalization() || !consentAllowsPpid(bidderRequest)) { + return false; + } + + const CONCERT_UID_KEY = 'c_uid'; + + let uid = storage.getDataFromLocalStorage(CONCERT_UID_KEY); + + if (!uid) { + uid = utils.generateUUID(); + storage.setDataInLocalStorage(CONCERT_UID_KEY, uid); + } + + return uid; +} + +/** + * Whether the user has opted out of personalization. + */ +function hasOptedOutOfPersonalization() { + const CONCERT_NO_PERSONALIZATION_KEY = 'c_nap'; + + return storage.getDataFromLocalStorage(CONCERT_NO_PERSONALIZATION_KEY) === 'true'; +} + +/** + * Whether the privacy consent strings allow personalization. + * + * @param {BidderRequest} bidderRequest Object which contains any data consent signals + */ +function consentAllowsPpid(bidderRequest) { + /* NOTE: We cannot easily test GDPR consent, without the + * `consent-string` npm module; so will have to rely on that + * happening on the bid-server. */ + return !(bidderRequest.uspConsent === 'string' && + bidderRequest.uspConsent.toUpperCase().substring(0, 2) === '1YY') +} diff --git a/modules/concertBidAdapter.md b/modules/concertBidAdapter.md new file mode 100644 index 00000000000..faf774946d1 --- /dev/null +++ b/modules/concertBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: Concert Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@concert.io +``` + +# Description + +Module that connects to Concert demand sources + +# Test Paramters +``` + var adUnits = [ + { + code: 'desktop_leaderboard_variable', + mediaTypes: { + banner: { + sizes: [[1030, 590]] + } + } + bids: [ + { + bidder: "concert", + params: { + partnerId: 'test_partner' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/test/spec/modules/concertAnalyticsAdapter_spec.js b/test/spec/modules/concertAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..b0aad2f3156 --- /dev/null +++ b/test/spec/modules/concertAnalyticsAdapter_spec.js @@ -0,0 +1,157 @@ +import concertAnalytics from 'modules/concertAnalyticsAdapter.js'; +import { expect } from 'chai'; +const sinon = require('sinon'); +let adapterManager = require('src/adapterManager').default; +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('ConcertAnalyticsAdapter', function() { + let sandbox; + let xhr; + let requests; + let clock; + let timestamp = 1896134400; + let auctionId = '9f894496-10fe-4652-863d-623462bf82b8'; + let timeout = 1000; + + before(function () { + sandbox = sinon.createSandbox(); + xhr = sandbox.useFakeXMLHttpRequest(); + requests = []; + + xhr.onCreate = function (request) { + requests.push(request); + }; + clock = sandbox.useFakeTimers(1896134400); + }); + + after(function () { + sandbox.restore(); + }); + + describe('track', function() { + beforeEach(function () { + sandbox.stub(events, 'getEvents').returns([]); + + adapterManager.enableAnalytics({ + provider: 'concert' + }); + }); + + afterEach(function () { + events.getEvents.restore(); + concertAnalytics.eventsStorage = []; + concertAnalytics.disableAnalytics(); + }); + + it('should catch all events', function() { + sandbox.spy(concertAnalytics, 'track'); + + fireBidEvents(events); + sandbox.assert.callCount(concertAnalytics.track, 5); + }); + + it('should report data for BID_RESPONSE, BID_WON events', function() { + fireBidEvents(events); + clock.tick(3000 + 1000); + + const eventsToReport = ['bidResponse', 'bidWon']; + for (var i = 0; i < concertAnalytics.eventsStorage.length; i++) { + expect(eventsToReport.indexOf(concertAnalytics.eventsStorage[i].event)).to.be.above(-1); + } + + for (var i = 0; i < eventsToReport.length; i++) { + expect(concertAnalytics.eventsStorage.some(function(event) { + return event.event === eventsToReport[i] + })).to.equal(true); + } + }); + + it('should report data in the shape expected by analytics endpoint', function() { + fireBidEvents(events); + clock.tick(3000 + 1000); + + const requiredFields = ['event', 'concert_rid', 'adId', 'auctionId', 'creativeId', 'position', 'url', 'cpm', 'width', 'height', 'timeToRespond']; + + for (var i = 0; i < requiredFields.length; i++) { + expect(concertAnalytics.eventsStorage[0]).to.have.property(requiredFields[i]); + } + }); + }); + + const adUnits = [{ + code: 'desktop_leaderboard_variable', + sizes: [[1030, 590]], + mediaTypes: { + banner: { + sizes: [[1030, 590]] + } + }, + bids: [ + { + bidder: 'concert', + params: { + partnerId: 'test_partner' + } + } + ] + }]; + + const bidResponse = { + 'bidderCode': 'concert', + 'width': 1030, + 'height': 590, + 'statusMessage': 'Bid available', + 'adId': '642f13fe18ab7dc', + 'requestId': '4062fba2e039919', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 6, + 'ad': '', + 'ttl': 360, + 'creativeId': '138308483085|62bac030-a5d3-11ea-b3be-55590c8153a5', + 'netRevenue': false, + 'currency': 'USD', + 'originalCpm': 6, + 'originalCurrency': 'USD', + 'auctionId': '9f894496-10fe-4652-863d-623462bf82b8', + 'responseTimestamp': 1591213790366, + 'requestTimestamp': 1591213790017, + 'bidder': 'concert', + 'adUnitCode': 'desktop_leaderboard_variable', + 'timeToRespond': 349, + 'status': 'rendered', + 'params': [ + { + 'partnerId': 'cst' + } + ] + } + + const bidWon = { + 'adId': '642f13fe18ab7dc', + 'mediaType': 'banner', + 'requestId': '4062fba2e039919', + 'cpm': 6, + 'creativeId': '138308483085|62bac030-a5d3-11ea-b3be-55590c8153a5', + 'currency': 'USD', + 'netRevenue': false, + 'ttl': 360, + 'auctionId': '9f894496-10fe-4652-863d-623462bf82b8', + 'statusMessage': 'Bid available', + 'responseTimestamp': 1591213790366, + 'requestTimestamp': 1591213790017, + 'bidder': 'concert', + 'adUnitCode': 'desktop_leaderboard_variable', + 'sizes': [[1030, 590]], + 'size': [1030, 590] + } + + function fireBidEvents(events) { + events.emit(constants.EVENTS.AUCTION_INIT, {timestamp, auctionId, timeout, adUnits}); + events.emit(constants.EVENTS.BID_REQUESTED, {bidder: 'concert'}); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponse); + events.emit(constants.EVENTS.AUCTION_END, {}); + events.emit(constants.EVENTS.BID_WON, bidWon); + } +}); diff --git a/test/spec/modules/concertBidAdapter_spec.js b/test/spec/modules/concertBidAdapter_spec.js new file mode 100644 index 00000000000..df999f45df9 --- /dev/null +++ b/test/spec/modules/concertBidAdapter_spec.js @@ -0,0 +1,219 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { spec } from 'modules/concertBidAdapter.js'; +import { getStorageManager } from '../../../src/storageManager.js' + +describe('ConcertAdapter', function () { + let bidRequests; + let bidRequest; + let bidResponse; + + beforeEach(function () { + bidRequests = [ + { + bidder: 'concert', + params: { + partnerId: 'foo', + slotType: 'fizz' + }, + adUnitCode: 'desktop_leaderboard_variable', + bidId: 'foo', + transactionId: '', + sizes: [[1030, 590]] + } + ]; + + bidRequest = { + refererInfo: { + referer: 'https://www.google.com' + }, + uspConsent: '1YYY', + gdprConsent: {} + }; + + bidResponse = { + body: { + bids: [ + { + bidId: '16d2e73faea32d9', + cpm: '6', + width: '1030', + height: '590', + ad: '', + ttl: '360', + creativeId: '123349|a7d62700-a4bf-11ea-829f-ad3b0b7a9383', + netRevenue: false, + currency: 'USD' + } + ] + } + } + }); + + describe('spec.isBidRequestValid', function() { + it('should return when it recieved all the required params', function() { + const bid = bidRequests[0]; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when partner id is missing', function() { + const bid = { + bidder: 'concert', + params: {} + } + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('spec.buildRequests', function() { + it('should build a payload object with the shape expected by server', function() { + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + expect(payload).to.have.property('meta'); + expect(payload).to.have.property('slots'); + + const metaRequiredFields = ['prebidVersion', 'pageUrl', 'screen', 'debug', 'uid', 'optedOut', 'adapterVersion', 'uspConsent', 'gdprConsent']; + const slotsRequiredFields = ['name', 'bidId', 'transactionId', 'sizes', 'partnerId', 'slotType']; + + metaRequiredFields.forEach(function(field) { + expect(payload.meta).to.have.property(field); + }); + slotsRequiredFields.forEach(function(field) { + expect(payload.slots[0]).to.have.property(field); + }); + }); + + it('should not generate uid if the user has opted out', function() { + const storage = getStorageManager(); + storage.setDataInLocalStorage('c_nap', 'true'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.uid).to.equal(false); + }); + + it('should generate uid if the user has not opted out', function() { + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.uid).to.not.equal(false); + }); + + it('should grab uid from local storage if it exists', function() { + const storage = getStorageManager(); + storage.setDataInLocalStorage('c_uid', 'foo'); + storage.removeDataFromLocalStorage('c_nap'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.uid).to.equal('foo'); + }); + }); + + describe('spec.interpretResponse', function() { + it('should return bids in the shape expected by prebid', function() { + const bids = spec.interpretResponse(bidResponse, bidRequest); + const requiredFields = ['requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', 'netRevenue', 'currency']; + + requiredFields.forEach(function(field) { + expect(bids[0]).to.have.property(field); + }); + }); + + it('should return empty bids if there is no response from server', function() { + const bids = spec.interpretResponse({ body: null }, bidRequest); + expect(bids).to.have.lengthOf(0); + }); + + it('should return empty bids if there are no bids from the server', function() { + const bids = spec.interpretResponse({ body: {bids: []} }, bidRequest); + expect(bids).to.have.lengthOf(0); + }); + }); + + describe('spec.getUserSyncs', function() { + it('should not register syncs when iframe is not enabled', function() { + const opts = { + iframeEnabled: false + } + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync).to.have.lengthOf(0); + }); + + it('should not register syncs when the user has opted out', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.setDataInLocalStorage('c_nap', 'true'); + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync).to.have.lengthOf(0); + }); + + it('should set gdprApplies flag to 1 if the user is in area where GDPR applies', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: true + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('gdpr_applies=1'); + }); + + it('should set gdprApplies flag to 1 if the user is in area where GDPR applies', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: false + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('gdpr_applies=0'); + }); + + it('should set gdpr consent param with the user\'s choices on consent', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: false, + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==' + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('gdpr_consent=BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + }); + + it('should set ccpa consent param with the user\'s choices on consent', function() { + const opts = { + iframeEnabled: true + }; + const storage = getStorageManager(); + storage.removeDataFromLocalStorage('c_nap'); + + bidRequest.gdprConsent = { + gdprApplies: false, + uspConsent: '1YYY' + }; + + const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(sync[0].url).to.have.string('usp_consent=1YY'); + }); + }); +}); From 40e5c7da5ee1ead9b29de8072a91759de214452e Mon Sep 17 00:00:00 2001 From: Scott Date: Thu, 20 Aug 2020 19:54:21 +0200 Subject: [PATCH 32/43] rename simpleHash to cyrb53Hash to make it clearer which hash function is actually used. (#5644) --- modules/userId/index.js | 2 +- src/utils.js | 2 +- test/spec/utils_spec.js | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/userId/index.js b/modules/userId/index.js index bcc0f68b2b0..14f7ad3599b 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -244,7 +244,7 @@ function makeStoredConsentDataHash(consentData) { storedConsentData.gdprApplies = consentData.gdprApplies; storedConsentData.apiVersion = consentData.apiVersion; } - return utils.simpleHash(JSON.stringify(storedConsentData)); + return utils.cyrb53Hash(JSON.stringify(storedConsentData)); } /** diff --git a/src/utils.js b/src/utils.js index 6801a7dc1d1..591c1d1bb2b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1223,7 +1223,7 @@ export function mergeDeep(target, ...sources) { * @param seed (optional) * @returns {string} */ -export function simpleHash(str, seed = 0) { +export function cyrb53Hash(str, seed = 0) { // IE doesn't support imul // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul#Polyfill let imul = function(opA, opB) { diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index dbaad919bd3..6494ead78e7 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -1178,23 +1178,23 @@ describe('Utils', function () { expect(utils.deepEqual(obj1, obj2)).to.equal(false); }); - describe('simpleHash', function() { + describe('cyrb53Hash', function() { it('should return the same hash for the same string', function() { const stringOne = 'string1'; - expect(utils.simpleHash(stringOne)).to.equal(utils.simpleHash(stringOne)); + expect(utils.cyrb53Hash(stringOne)).to.equal(utils.cyrb53Hash(stringOne)); }); it('should return a different hash for the same string with different seeds', function() { const stringOne = 'string1'; - expect(utils.simpleHash(stringOne, 1)).to.not.equal(utils.simpleHash(stringOne, 2)); + expect(utils.cyrb53Hash(stringOne, 1)).to.not.equal(utils.cyrb53Hash(stringOne, 2)); }); it('should return a different hash for different strings with the same seed', function() { const stringOne = 'string1'; const stringTwo = 'string2'; - expect(utils.simpleHash(stringOne)).to.not.equal(utils.simpleHash(stringTwo)); + expect(utils.cyrb53Hash(stringOne)).to.not.equal(utils.cyrb53Hash(stringTwo)); }); it('should return a string value, not a number', function() { const stringOne = 'string1'; - expect(typeof utils.simpleHash(stringOne)).to.equal('string'); + expect(typeof utils.cyrb53Hash(stringOne)).to.equal('string'); }); }); }); From 67d184a7f7410268973d75acabf351f8a63d642d Mon Sep 17 00:00:00 2001 From: Steve Alliance Date: Thu, 20 Aug 2020 14:06:47 -0400 Subject: [PATCH 33/43] DMX support for video instream and more user id support (#5618) * adding DMX test @97%, two files added one updated * Update districtm_spec.js * Update districtmDMX.js * adding all districtm needed file * remove legacy file * remove typo || 0 in the test method * force default to return a valid width and height * update unit test code for failing test * changed class for an object * remove package-lock.json * change file name for dmx adapter * renamed files * restaure package-lock.json * update to last package-lock state * update gdpr user consent * fix sizes issue * Documentation updates Adding the readme.md info * update file name and update unit testing import file location * current machine state * lint correction * remove variable assigment duplicate * adding CCPA support for DMX * adding test for ccpa and gdpr * districtm dmx adding deal id field * idsync support ccpa & gdpr * Adding code getProtocols and gegtApi function + video object support * Resolve userId detection on dmx * adding support for video and vast support inline and wrap * Change video setting to be only taken from mediaTypes object from placement setting * Adding documentation for video support * Support for bid floor modules and remove deprecated user id module * fixed caniuse error for ie11 Array.includes and add advertiser domain in meta object * replace includes array method for indexOf * switch anoter includes array function for indexOf Co-authored-by: Steve Alliance Co-authored-by: Luis Co-authored-by: Steve Alliance Co-authored-by: Steve Alliance Co-authored-by: steve-a-districtm --- modules/districtmDMXBidAdapter.js | 160 ++++++++++++++++-- modules/districtmDmxBidAdapter.md | 61 ++++++- .../modules/districtmDmxBidAdapter_spec.js | 154 ++++++++++++++++- 3 files changed, 358 insertions(+), 17 deletions(-) diff --git a/modules/districtmDMXBidAdapter.js b/modules/districtmDMXBidAdapter.js index 03dca0b607d..21f3b7b3586 100644 --- a/modules/districtmDMXBidAdapter.js +++ b/modules/districtmDMXBidAdapter.js @@ -1,14 +1,26 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'districtmDMX'; const DMXURI = 'https://dmx.districtm.io/b/v1'; +const VIDEO_MAPPING = { + playback_method: { + 'auto_play_sound_on': 1, + 'auto_play_sound_off': 2, + 'click_to_play': 3, + 'mouse_over': 4, + 'viewport_sound_on': 5, + 'viewport_sound_off': 6 + } +}; export const spec = { code: BIDDER_CODE, - supportedFormat: ['banner'], + supportedFormat: [BANNER, VIDEO], + supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid(bid) { return !!(bid.params.dmxid && bid.params.memberid); }, @@ -27,14 +39,23 @@ export const spec = { nBid.requestId = nBid.impid; nBid.width = nBid.w || width; nBid.height = nBid.h || height; + nBid.mediaType = bid.mediaTypes && bid.mediaTypes.video ? 'video' : null; + if (nBid.mediaType) { + nBid.vastXml = cleanVast(nBid.adm); + } if (nBid.dealid) { nBid.dealId = nBid.dealid; } + nBid.uuid = nBid.bidId; nBid.ad = nBid.adm; nBid.netRevenue = true; nBid.creativeId = nBid.crid; nBid.currency = 'USD'; nBid.ttl = 60; + nBid.meta = nBid.meta || {}; + if (nBid.adomain && nBid.adomain.length > 0) { + nBid.meta.advertiserDomains = nBid.adomain; + } return nBid; } else { oBid.cpm = oBid.price; @@ -83,12 +104,19 @@ export const spec = { } let eids = []; - if (bidRequest && bidRequest.userId) { - bindUserId(eids, utils.deepAccess(bidRequest, `userId.idl_env`), 'liveramp.com', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.digitrustid.data.id`), 'digitru.st', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.id5id`), 'id5-sync.com', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.pubcid`), 'pubcid.org', 1); - bindUserId(eids, utils.deepAccess(bidRequest, `userId.tdid`), 'adserver.org', 1); + if (bidRequest[0] && bidRequest[0].userId) { + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.idl_env`), 'liveramp.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.id5id`), 'id5-sync.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.pubcid`), 'pubcid.org', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.tdid`), 'adserver.org', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.criteoId`), 'criteo.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.britepoolid`), 'britepool.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.lipb.lipbid`), 'liveintent.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.intentiqid`), 'intentiq.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.lotamePanoramaId`), 'lotame.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.parrableId`), 'parrable.com', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.netId`), 'netid.de', 1); + bindUserId(eids, utils.deepAccess(bidRequest[0], `userId.sharedid`), 'sharedid.org', 1); dmxRequest.user = dmxRequest.user || {}; dmxRequest.user.ext = dmxRequest.user.ext || {}; dmxRequest.user.ext.eids = eids; @@ -122,14 +150,34 @@ export const spec = { obj.id = dmx.bidId; obj.tagid = String(dmx.params.dmxid); obj.secure = 1; - obj.banner = { - topframe: 1, - w: cleanSizes(dmx.sizes, 'w'), - h: cleanSizes(dmx.sizes, 'h'), - format: cleanSizes(dmx.sizes).map(s => { - return {w: s[0], h: s[1]}; - }).filter(obj => typeof obj.w === 'number' && typeof obj.h === 'number') - }; + obj.bidfloor = getFloor(dmx); + if (dmx.mediaTypes && dmx.mediaTypes.video) { + obj.video = { + topframe: 1, + skip: dmx.mediaTypes.video.skippable || 0, + linearity: dmx.mediaTypes.video.linearity || 1, + minduration: dmx.mediaTypes.video.minduration || 5, + maxduration: dmx.mediaTypes.video.maxduration || 60, + playbackmethod: getPlaybackmethod(dmx.mediaTypes.video.playback_method), + api: getApi(dmx.mediaTypes.video), + mimes: dmx.mediaTypes.video.mimes || ['video/mp4'], + protocols: getProtocols(dmx.mediaTypes.video), + w: dmx.mediaTypes.video.playerSize[0][0], + h: dmx.mediaTypes.video.playerSize[0][1], + format: dmx.mediaTypes.video.playerSize.map(s => { + return {w: s[0], h: s[1]}; + }).filter(obj => typeof obj.w === 'number' && typeof obj.h === 'number') + }; + } else { + obj.banner = { + topframe: 1, + w: cleanSizes(dmx.sizes, 'w'), + h: cleanSizes(dmx.sizes, 'h'), + format: cleanSizes(dmx.sizes).map(s => { + return {w: s[0], h: s[1]}; + }).filter(obj => typeof obj.w === 'number' && typeof obj.h === 'number') + }; + } return obj; }); @@ -169,6 +217,27 @@ export const spec = { } } +export function getFloor (bid) { + let floor = null; + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: bid.mediaTypes.video ? 'video' : 'banner', + size: bid.sizes.map(size => { + return { + w: size[0], + h: size[1] + } + }) + }); + if (typeof floorInfo === 'object' && + floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + floor = parseFloat(floorInfo.floor); + } + } + return floor !== null ? floor : bid.params.floor; +} + export function cleanSizes(sizes, value) { const supportedSize = [ { @@ -310,4 +379,65 @@ export function bindUserId(eids, value, source, atype) { }) } } + +export function getApi({protocols}) { + let defaultValue = [2]; + let listProtocols = [ + {key: 'VPAID_1_0', value: 1}, + {key: 'VPAID_2_0', value: 2}, + {key: 'MRAID_1', value: 3}, + {key: 'ORMMA', value: 4}, + {key: 'MRAID_2', value: 5}, + {key: 'MRAID_3', value: 6}, + ]; + if (protocols) { + return listProtocols.filter(p => { + return protocols.indexOf(p.key) !== -1; + }).map(p => p.value) + } else { + return defaultValue; + } +} +export function getPlaybackmethod(playback) { + if (Array.isArray(playback) && playback.length > 0) { + return playback.map(label => { + return VIDEO_MAPPING.playback_method[label] + }) + } + return [2] +} + +export function getProtocols({protocols}) { + let defaultValue = [2, 3, 5, 6, 7, 8]; + let listProtocols = [ + {key: 'VAST_1_0', value: 1}, + {key: 'VAST_2_0', value: 2}, + {key: 'VAST_3_0', value: 3}, + {key: 'VAST_1_0_WRAPPER', value: 4}, + {key: 'VAST_2_0_WRAPPER', value: 5}, + {key: 'VAST_3_0_WRAPPER', value: 6}, + {key: 'VAST_4_0', value: 7}, + {key: 'VAST_4_0_WRAPPER', value: 8} + ]; + if (protocols) { + return listProtocols.filter(p => { + return protocols.indexOf(p.key) !== -1 + }).map(p => p.value); + } else { + return defaultValue; + } +} + +export function cleanVast(str) { + const toberemove = /]*?src\s*=\s*['\"]([^'\"]*?)['\"][^>]*?>/ + const [img, url] = str.match(toberemove) + str = str.replace(toberemove, '') + if (img) { + if (url) { + const insrt = `` + str = str.replace('', `${insrt}`) + } + } + return str; +} registerBidder(spec); diff --git a/modules/districtmDmxBidAdapter.md b/modules/districtmDmxBidAdapter.md index 792cf2e7305..5d5dd2affe6 100644 --- a/modules/districtmDmxBidAdapter.md +++ b/modules/districtmDmxBidAdapter.md @@ -20,13 +20,14 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman ## Media Types * Banner - +* Video ## Bidder Parameters | Key | Scope | Type | Description | --- | --- | --- | --- | `dmxid` | Mandatory | Integer | Unique identifier of the placement, dmxid can be obtained in the district m Boost platform. | `memberid` | Mandatory | Integer | Unique identifier for your account, memberid can be obtained in the district m Boost platform. +| `floor` | Optional | float | Most placement can have floor set in our platform, but this can now be set on the request too. # Ad Unit Configuration Example @@ -48,6 +49,35 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman }]; ``` +# Ad Unit Configuration Example for video request + +```javascript + var videoAdUnit = { + code: 'video1', + sizes: [640,480], + mediaTypes: { video: {context: 'instream', //or 'outstream' + playerSize: [[640, 480]], + skipppable: true, + minduration: 5, + maxduration: 45, + playback_method: ['auto_play_sound_off', 'viewport_sound_off'], + mimes: ["application/javascript", + "video/mp4"], + + } }, + bids: [ + { + bidder: 'districtmDMX', + params: { + dmxid: '100001', + memberid: '100003', + } + } + + ] + }; +``` + # Ad Unit Configuration when COPPA is needed @@ -117,6 +147,35 @@ Our demand and adapter supports multiple sizes per placement, as such a single d }]; ``` +Our bidder only supports instream context at the moment and we strongly like to put the media types and setting in the ad unit settings. +If no value is set the default value will be applied. + +```javascript + var videoAdUnit = { + code: 'video1', + sizes: [640,480], + mediaTypes: { video: {context: 'instream', //or 'outstream' + playerSize: [[640, 480]], + skipppable: true, + minduration: 5, + maxduration: 45, + playback_method: ['auto_play_sound_off', 'viewport_sound_off'], + mimes: ["application/javascript", + "video/mp4"], + + } }, + bids: [ + { + bidder: 'districtmDMX', + params: { + dmxid: '250258', + memberid: '100600', + } + } + ] + }; +``` + ###### 4. Implementation Checking Once the bidder is live in your Prebid configuration you may confirm it is making requests to our end point by looking for requests to `https://dmx.districtm.io/b/v1`. diff --git a/test/spec/modules/districtmDmxBidAdapter_spec.js b/test/spec/modules/districtmDmxBidAdapter_spec.js index d8f0beb9a36..9b65ab855d8 100644 --- a/test/spec/modules/districtmDmxBidAdapter_spec.js +++ b/test/spec/modules/districtmDmxBidAdapter_spec.js @@ -1,6 +1,60 @@ import {expect} from 'chai'; import * as _ from 'lodash'; -import {spec, matchRequest, checkDeepArray, defaultSize, upto5, cleanSizes, shuffle} from '../../../modules/districtmDMXBidAdapter.js'; +import {spec, matchRequest, checkDeepArray, defaultSize, upto5, cleanSizes, shuffle, getApi, bindUserId, getPlaybackmethod, getProtocols, cleanVast} from '../../../modules/districtmDMXBidAdapter.js'; + +const sample_vast = ` + + + + + + + + + 00:00:15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` const supportedSize = [ { @@ -42,6 +96,28 @@ const bidRequest = [{ 'dmxid': 100001, 'memberid': 100003, }, + 'userId': { + idl_env: {}, + digitrustid: { + data: { + id: {} + } + }, + id5id: {}, + pubcid: {}, + tdid: {}, + criteoId: {}, + britepoolid: {}, + intentiqid: {}, + lotamePanoramaId: {}, + parrableId: {}, + netId: {}, + sharedid: {}, + lipb: { + lipbid: {} + }, + + }, 'adUnitCode': 'div-gpt-ad-12345678-1', 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', 'sizes': [ @@ -53,6 +129,31 @@ const bidRequest = [{ 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' }]; +const bidRequestVideo = [{ + 'bidder': 'districtmDMX', + 'params': { + 'dmxid': 100001, + 'memberid': 100003, + 'video': { + id: 123, + skipppable: true, + playback_method: ['auto_play_sound_off', 'viewport_sound_off'], + mimes: ['application/javascript', + 'video/mp4'], + } + }, + 'mediaTypes': { video: {context: 'instream', // or 'outstream' + playerSize: [[640, 480]]} }, + 'adUnitCode': 'div-gpt-ad-12345678-1', + 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', + 'sizes': [ + [300, 250], + [300, 600] + ], + 'bidId': '29a28a1bbc8a8d', + 'bidderRequestId': '124b579a136515', + 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' +}]; const bidRequestNoCoppa = [{ 'bidder': 'districtmDMX', 'params': { @@ -505,6 +606,51 @@ describe('DistrictM Adaptor', function () { expect(upto5([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], bidRequest, bidderRequest, 'https://google').length).to.be.equal(3) }) }) + + describe('test vast tag', function() { + it('img tag should not be present', function() { + expect(cleanVast(sample_vast).indexOf('img') !== -1).to.be.equal(false) + }) + }) + describe('Test getApi function', function() { + const data = { + protocols: ['VPAID_1_0'] + } + it('Will return 1 for vpaid version 1', function() { + expect(getApi(data)[0]).to.be.equal(1) + }) + it('Will return 2 for vpaid default', function() { + expect(getApi({})[0]).to.be.equal(2) + }) + }) + + describe('Test cleanSizes function', function() { + it('sequence will be respected', function() { + expect(cleanSizes(bidderRequest.bids[0].sizes).toString()).to.be.equal('300,250,300,600') + }) + it('sequence will be respected', function() { + expect(cleanSizes([[728, 90], [970, 90], [300, 600], [320, 50]]).toString()).to.be.equal('728,90,320,50,300,600,970,90') + }) + }) + + describe('Test getPlaybackmethod function', function() { + it('getPlaybackmethod will return 2', function() { + expect(getPlaybackmethod([])[0]).to.be.equal(2) + }) + it('getPlaybackmethod will return 6', function() { + expect(getPlaybackmethod(['viewport_sound_off'])[0]).to.be.equal(6) + }) + }) + + describe('Test getProtocols function', function() { + it('getProtocols will return 3', function() { + expect(getProtocols({protocols: ['VAST_3_0']})[0]).to.be.equal(3) + }) + it('getProtocols will return 6', function() { + expect(_.isEqual(getProtocols({}), [2, 3, 5, 6, 7, 8])).to.be.equal(true) + }) + }) + describe('All needed functions are available', function () { it(`isBidRequestValid is present and type function`, function () { expect(districtm.isBidRequestValid).to.exist.and.to.be.a('function') @@ -589,6 +735,12 @@ describe('DistrictM Adaptor', function () { }); }); + describe('bidRequest Video testing', function() { + const request = districtm.buildRequests(bidRequestVideo, bidRequestVideo); + const data = JSON.parse(request.data) + expect(data instanceof Object).to.be.equal(true) + }) + describe(`interpretResponse test usage`, function () { const responseResults = districtm.interpretResponse(responses, {bidderRequest}); const emptyResponseResults = districtm.interpretResponse(emptyResponse, {bidderRequest}); From 494015f9f3a09a6500de3a83b558fba8afb55a04 Mon Sep 17 00:00:00 2001 From: Karim Mourra Date: Thu, 20 Aug 2020 14:12:08 -0400 Subject: [PATCH 34/43] JW Player Real Time Data Provider (#5537) * [AD-469] Add player vendor. * ssets up targeting module * implements getTargeting * implements getPlayer * blocks bids until all targeting requests complete * makes getTarget more resilient * enables mdule hook * replaces triple dot notation * Revert "replaces triple dot notation" This reverts commit 7a76ea62e1eb210c61abdc8e74da8ee68d47590c. * Revert "Revert "replaces triple dot notation"" This reverts commit 130aa2adf5103013814537e8666e4ec49cd2d127. * checks current item only if mediaid is missing * adds unit tests * completes test cases * stores segments for current item * renames jwp targeting * refactors fetch tests * refactors get targeting tests * refactors blocking tests * renames module * cleans changes made to app nexus * removes setup and player utilities * renames onFetchCompletion * renames onFetchCOmpletion in unti tests * throws instead of early return * reduces timeout and introduces override * targeting timeout supersedes * renames feed fetch timeout * adds inline doc * uses find util * adds jwplayer rtd provider * implements targeting retrieval * ensures provider is found * implements init * jwTargeting is object * commits test file * adds file extension * adds tests * fixes test for proper structure * uses default clock mock * ends reqs before rtd module timeout * removes obsolete export * request counts updates in aggregate * cleans server mock after each test * deletes jwplayer targeting * includes content id * adds test for missing segment * getSegments is nullable * replaces condition with guard * updates doc * adds md file * adds example page Co-authored-by: vseventer Co-authored-by: karimJWP --- .../gpt/jwplayerRtdProvider_example.html | 98 +++++ modules/.submodules.json | 3 +- modules/jwplayerRtdProvider.js | 209 +++++++++ modules/jwplayerRtdProvider.md | 96 +++++ test/spec/modules/jwplayerRtdProvider_spec.js | 399 ++++++++++++++++++ 5 files changed, 804 insertions(+), 1 deletion(-) create mode 100644 integrationExamples/gpt/jwplayerRtdProvider_example.html create mode 100644 modules/jwplayerRtdProvider.js create mode 100644 modules/jwplayerRtdProvider.md create mode 100644 test/spec/modules/jwplayerRtdProvider_spec.js diff --git a/integrationExamples/gpt/jwplayerRtdProvider_example.html b/integrationExamples/gpt/jwplayerRtdProvider_example.html new file mode 100644 index 00000000000..3791ab42137 --- /dev/null +++ b/integrationExamples/gpt/jwplayerRtdProvider_example.html @@ -0,0 +1,98 @@ + + + + + + + JW Player RTD Provider Example + + + + + + + + +
Div-1
+
+ +
+ + diff --git a/modules/.submodules.json b/modules/.submodules.json index dd40557c35b..af7806616e1 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -18,6 +18,7 @@ "dfpAdServerVideo" ], "rtdModule": [ - "browsiRtdProvider" + "browsiRtdProvider", + "jwplayerRtdProvider" ] } diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js new file mode 100644 index 00000000000..b7c8879ed8e --- /dev/null +++ b/modules/jwplayerRtdProvider.js @@ -0,0 +1,209 @@ +/** + * This module adds the jwplayer provider to the Real Time Data module (rtdModule) + * The {@link module:modules/realTimeData} module is required + * The module will allow Ad Bidders to obtain JW Player's Video Ad Targeting information + * The module will fetch segments for the media ids present in the prebid config when the module loads. If any bid + * requests are made while the segments are being fetched, they will be blocked until all requests complete, or the + * timeout expires. + * @module modules/jwplayerRtdProvider + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js'; +import { config } from '../src/config.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import { logError } from '../src/utils.js'; +import find from 'core-js-pure/features/array/find.js'; + +const SUBMODULE_NAME = 'jwplayer'; +let requestCount = 0; +let requestTimeout = 150; +const segCache = {}; +let resumeBidRequest; + +/** @type {RtdSubmodule} */ +export const jwplayerSubmodule = { + /** + * used to link submodule with realTimeData + * @type {string} + */ + name: SUBMODULE_NAME, + /** + * get data and send back to realTimeData module + * @function + * @param {adUnit[]} adUnits + * @param {function} onDone + */ + getData: getSegments, + init +}; + +config.getConfig('realTimeData', ({realTimeData}) => { + const providers = realTimeData.dataProviders; + const jwplayerProvider = providers && find(providers, pr => pr.name && pr.name.toLowerCase() === SUBMODULE_NAME); + const params = jwplayerProvider && jwplayerProvider.params; + if (!params) { + return; + } + const rtdModuleTimeout = params.auctionDelay || params.timeout; + requestTimeout = rtdModuleTimeout === undefined ? requestTimeout : Math.max(rtdModuleTimeout - 1, 0); + fetchTargetingInformation(params); +}); + +submodule('realTimeData', jwplayerSubmodule); + +function init(config, gdpr, usp) { + return true; +} + +export function fetchTargetingInformation(jwTargeting) { + const mediaIDs = jwTargeting.mediaIDs; + if (!mediaIDs) { + return; + } + mediaIDs.forEach(mediaID => { + fetchTargetingForMediaId(mediaID); + }); +} + +export function fetchTargetingForMediaId(mediaId) { + const ajax = ajaxBuilder(requestTimeout); + requestCount++; + ajax(`https://cdn.jwplayer.com/v2/media/${mediaId}`, { + success: function (response) { + try { + const data = JSON.parse(response); + if (!data) { + throw ('Empty response'); + } + + const playlist = data.playlist; + if (!playlist || !playlist.length) { + throw ('Empty playlist'); + } + + const jwpseg = playlist[0].jwpseg; + if (jwpseg) { + segCache[mediaId] = jwpseg; + } + } catch (err) { + logError(err); + } + onRequestCompleted(); + }, + error: function () { + logError('failed to retrieve targeting information'); + onRequestCompleted(); + } + }); +} + +function onRequestCompleted() { + requestCount--; + if (requestCount > 0) { + return; + } + + if (resumeBidRequest) { + resumeBidRequest(); + resumeBidRequest = null; + } +} + +function getSegments(adUnits, onDone) { + executeAfterPrefetch(() => { + const realTimeData = adUnits.reduce((data, adUnit) => { + const code = adUnit.code; + const vat = code && getTargetingForBid(adUnit); + if (!vat) { + return data; + } + + const { segments, mediaID } = vat; + const jwTargeting = {}; + if (segments && segments.length) { + jwTargeting.segments = segments; + } + + if (mediaID) { + const id = 'jw_' + mediaID; + jwTargeting.content = { + id + } + } + + data[code] = { + jwTargeting + }; + return data; + }, {}); + onDone(realTimeData); + }); +} + +function executeAfterPrefetch(callback) { + if (requestCount > 0) { + resumeBidRequest = callback; + } else { + callback(); + } +} + +/** + * Retrieves the targeting information pertaining to a bid request. + * @param bidRequest {object} - the bid which is passed to a prebid adapter for use in `buildRequests`. It must contain + * a jwTargeting property. + * @returns targetingInformation {object} nullable - contains the media ID as well as the jwpseg targeting segments + * found for the given bidRequest information + */ +export function getTargetingForBid(bidRequest) { + const jwTargeting = bidRequest.jwTargeting; + if (!jwTargeting) { + return null; + } + const playerID = jwTargeting.playerID; + let mediaID = jwTargeting.mediaID; + let segments = segCache[mediaID]; + if (segments) { + return { + segments, + mediaID + }; + } + + const player = getPlayer(playerID); + if (!player) { + return null; + } + + const item = mediaID ? find(player.getPlaylist(), item => item.mediaid === mediaID) : player.getPlaylistItem(); + if (!item) { + return null; + } + + mediaID = mediaID || item.mediaid; + segments = item.jwpseg; + if (segments && mediaID) { + segCache[mediaID] = segments; + } + + return { + segments, + mediaID + }; +} + +function getPlayer(playerID) { + const jwplayer = window.jwplayer; + if (!jwplayer) { + logError('jwplayer.js was not found on page'); + return; + } + + const player = jwplayer(playerID); + if (!player || !player.getPlaylist) { + logError('player ID did not match any players'); + return; + } + return player; +} diff --git a/modules/jwplayerRtdProvider.md b/modules/jwplayerRtdProvider.md new file mode 100644 index 00000000000..06a7f69f497 --- /dev/null +++ b/modules/jwplayerRtdProvider.md @@ -0,0 +1,96 @@ +The purpose of this Real Time Data Provider is to allow publishers to target against their JW Player media without +having to integrate with the VPB product. This prebid module makes JW Player's video ad targeting information accessible +to Bid Adapters. + +**Usage for Publishers:** + +Compile the JW Player RTD Provider into your Prebid build: + +`gulp build --modules=jwplayerRtdProvider` + +Publishers must register JW Player as a real time data provider by setting up a Prebid Config conformant to the +following structure: + +```javascript +const jwplayerDataProvider = { + name: "jwplayer" +}; + +pbjs.setConfig({ + ..., + realTimeData: { + dataProviders: [ + jwplayerDataProvider + ] + } +}); +``` + +In order to prefetch targeting information for certain media, include the media IDs in the `jwplayerDataProvider` var: + +```javascript +const jwplayerDataProvider = { + name: "jwplayer", + params: { + mediaIDs: ['abc', 'def', 'ghi', 'jkl'] + } +}; +``` +Lastly, include the content's media ID and/or the player's ID in the matching AdUnit: + +```javascript +const adUnit = { + code: '/19968336/prebid_native_example_1', + ... + jwTargeting: { + playerID: 'abcd', + mediaID: '1234' + } +}; + +pbjs.que.push(function() { + pbjs.addAdUnits([adUnit]); + pbjs.requestBids({ + ... + }); +}); +``` + +**Usage for Bid Adapters:** + +Implement the `buildRequests` function. When it is called, the `bidRequests` param will be an array of bids. +Each bid for which targeting information was found will conform to the following object structure: + +```javascript +{ + adUnitCode: 'xyz', + bidId: 'abc', + ... + realTimeData: { + ..., + jwTargeting: { + segments: ['123', '456'], + content: { + id: 'jw_abc123' + } + } + } +} +``` + +where: +- `segments` is an array of jwpseg targeting segments, of type string. +- `content` is an object containing metadata for the media. It may contain the following information: + - `id` is a unique identifier for the specific media asset. + +**Example:** + +To view an example: + +- in your cli run: + +`gulp serve --modules=jwplayerRtdProvider` + +- in your browser, navigate to: + +`http://localhost:9999/integrationExamples/gpt/jwplayerRtdProvider_example.html` diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js new file mode 100644 index 00000000000..b5bacdc3694 --- /dev/null +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -0,0 +1,399 @@ +import { fetchTargetingForMediaId, getTargetingForBid, + fetchTargetingInformation, jwplayerSubmodule } from 'modules/jwplayerRtdProvider.js'; +import { server } from 'test/mocks/xhr.js'; + +describe('jwplayerRtdProvider', function() { + const testIdForSuccess = 'test_id_for_success'; + const testIdForFailure = 'test_id_for_failure'; + const validSegments = ['test_seg_1', 'test_seg_2']; + const responseHeader = {'Content-Type': 'application/json'}; + + describe('Fetch targeting for mediaID tests', function () { + let request; + + describe('Fetch succeeds', function () { + beforeEach(function () { + fetchTargetingForMediaId(testIdForSuccess); + request = server.requests[0]; + }); + + afterEach(function () { + server.respond(); + }); + + it('should reach out to media endpoint', function () { + expect(request.url).to.be.eq(`https://cdn.jwplayer.com/v2/media/${testIdForSuccess}`); + }); + + it('should write to cache when successful', function () { + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4', + jwpseg: validSegments + } + ] + }) + ); + + const targetingInfo = getTargetingForBid({ + jwTargeting: { + mediaID: testIdForSuccess + } + }); + + const validTargeting = { + segments: validSegments, + mediaID: testIdForSuccess + }; + + expect(targetingInfo).to.deep.equal(validTargeting); + }); + }); + + describe('Fetch fails', function () { + beforeEach(function () { + fetchTargetingForMediaId(testIdForFailure); + request = server.requests[0] + }); + + it('should not write to cache when response is malformed', function() { + request.respond('{]'); + const targetingInfo = getTargetingForBid({ + jwTargeting: { + mediaID: testIdForFailure + } + }); + expect(targetingInfo).to.be.null; + }); + + it('should not write to cache when playlist is absent', function() { + request.respond({}); + const targetingInfo = getTargetingForBid({ + jwTargeting: { + mediaID: testIdForFailure + } + }); + expect(targetingInfo).to.be.null; + }); + + it('should not write to cache when segments are absent', function() { + request.respond( + 200, + responseHeader, + JSON.stringify({ + playlist: [ + { + file: 'test.mp4' + } + ] + }) + ); + const targetingInfo = getTargetingForBid({ + jwTargeting: { + mediaID: testIdForFailure + } + }); + expect(targetingInfo).to.be.null; + }); + + it('should not write to cache when request errors', function() { + request.error(); + const targetingInfo = getTargetingForBid({ + jwTargeting: { + mediaID: testIdForFailure + } + }); + expect(targetingInfo).to.be.null; + }); + }); + }); + + describe('Get targeting for bid', function() { + const mediaIdWithSegment = 'media_ID_1'; + const mediaIdNoSegment = 'media_ID_2'; + const mediaIdForCurrentItem = 'media_ID_current'; + const mediaIdNotCached = 'media_test_ID'; + + const validPlayerID = 'player_test_ID_valid'; + const invalidPlayerID = 'player_test_ID_invalid'; + + it('returns null when targeting block is missing', function () { + const targeting = getTargetingForBid({}); + expect(targeting).to.be.null; + }); + + it('returns null when jwplayer.js is absent from page', function () { + const targeting = getTargetingForBid({ + jwTargeting: { + playerID: invalidPlayerID, + mediaID: mediaIdNotCached + } + }); + expect(targeting).to.be.null; + }); + + describe('When jwplayer.js is on page', function () { + const playlistItemWithSegmentMock = { + mediaid: mediaIdWithSegment, + jwpseg: validSegments + }; + + const targetingForMediaWithSegment = { + segments: validSegments, + mediaID: mediaIdWithSegment + }; + + const playlistItemNoSegmentMock = { + mediaid: mediaIdNoSegment + }; + + const currentItemSegments = ['test_seg_3', 'test_seg_4']; + const currentPlaylistItemMock = { + mediaid: mediaIdForCurrentItem, + jwpseg: currentItemSegments + }; + const targetingForCurrentItem = { + segments: currentItemSegments, + mediaID: mediaIdForCurrentItem + }; + + const playerInstanceMock = { + getPlaylist: function () { + return [playlistItemWithSegmentMock, playlistItemNoSegmentMock]; + }, + + getPlaylistItem: function () { + return currentPlaylistItemMock; + } + }; + + const jwplayerMock = function(playerID) { + if (playerID === validPlayerID) { + return playerInstanceMock; + } else { + return {}; + } + }; + + beforeEach(function () { + window.jwplayer = jwplayerMock; + }); + + it('returns null when player ID does not match player on page', function () { + const targeting = getTargetingForBid({ + jwTargeting: { + playerID: invalidPlayerID, + mediaID: mediaIdNotCached + } + }); + expect(targeting).to.be.null; + }); + + it('returns segments when media ID matches a playlist item with segments', function () { + const targeting = getTargetingForBid({ + jwTargeting: { + playerID: validPlayerID, + mediaID: mediaIdWithSegment + } + }); + expect(targeting).to.deep.equal(targetingForMediaWithSegment); + }); + + it('caches segments media ID matches a playist item with segments', function () { + getTargetingForBid({ + jwTargeting: { + playerID: validPlayerID, + mediaID: mediaIdWithSegment + } + }); + + window.jwplayer = null; + const targeting2 = getTargetingForBid({ + jwTargeting: { + playerID: invalidPlayerID, + mediaID: mediaIdWithSegment + } + }); + expect(targeting2).to.deep.equal(targetingForMediaWithSegment); + }); + + it('returns segments of current item when media ID is missing', function () { + const targeting = getTargetingForBid({ + jwTargeting: { + playerID: validPlayerID + } + }); + expect(targeting).to.deep.equal(targetingForCurrentItem); + }); + + it('caches segments from the current item', function () { + getTargetingForBid({ + jwTargeting: { + playerID: validPlayerID + } + }); + + window.jwplayer = null; + const targeting2 = getTargetingForBid({ + jwTargeting: { + playerID: invalidPlayerID, + mediaID: mediaIdForCurrentItem + } + }); + expect(targeting2).to.deep.equal(targetingForCurrentItem); + }); + + it('returns undefined segments when segments are absent', function () { + const targeting = getTargetingForBid({ + jwTargeting: { + playerID: validPlayerID, + mediaID: mediaIdNoSegment + } + }); + expect(targeting).to.deep.equal({ + mediaID: mediaIdNoSegment, + segments: undefined + }); + }); + }); + }); + + describe('jwplayerSubmodule', function () { + it('successfully instantiates', function () { + expect(jwplayerSubmodule.init()).to.equal(true); + }); + + describe('getData', function () { + const validMediaIDs = ['media_ID_1', 'media_ID_2', 'media_ID_3']; + let bidRequestSpy; + let fakeServer; + let clock; + + beforeEach(function () { + bidRequestSpy = sinon.spy(); + + fakeServer = sinon.createFakeServer(); + fakeServer.respondImmediately = false; + fakeServer.autoRespond = false; + + clock = sinon.useFakeTimers(); + }); + + afterEach(function () { + clock.restore(); + fakeServer.respond(); + }); + + it('executes callback immediately when no requests are pending', function () { + fetchTargetingInformation({ + mediaIDs: [] + }); + jwplayerSubmodule.getData([], bidRequestSpy); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('executes callback after requests complete', function() { + fetchTargetingInformation({ + mediaIDs: validMediaIDs + }); + jwplayerSubmodule.getData([], bidRequestSpy); + expect(bidRequestSpy.notCalled).to.be.true; + + const req1 = fakeServer.requests[0]; + const req2 = fakeServer.requests[1]; + const req3 = fakeServer.requests[2]; + + req1.respond(); + expect(bidRequestSpy.notCalled).to.be.true; + + req2.respond(); + expect(bidRequestSpy.notCalled).to.be.true; + + req3.respond(); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('executes callback after timeout', function () { + fetchTargetingInformation({ + mediaIDs: validMediaIDs + }); + jwplayerSubmodule.getData([], bidRequestSpy); + expect(bidRequestSpy.notCalled).to.be.true; + clock.tick(150); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('executes callback only once if requests succeed after timeout', function () { + fetchTargetingInformation({ + mediaIDs: validMediaIDs + }); + jwplayerSubmodule.getData([], bidRequestSpy); + expect(bidRequestSpy.notCalled).to.be.true; + clock.tick(150); + expect(bidRequestSpy.calledOnce).to.be.true; + + fakeServer.respond(); + expect(bidRequestSpy.calledOnce).to.be.true; + }); + + it('returns data in proper structure', function () { + const adUnitCode = 'test_ad_unit'; + const adUnitWithMediaId = { + code: adUnitCode, + jwTargeting: { + mediaID: testIdForSuccess + } + }; + const adUnitEmpty = { + code: 'test_ad_unit_empty' + }; + const expectedData = {}; + const expectedContentId = 'jw_' + testIdForSuccess; + expectedData[adUnitCode] = { + jwTargeting: { + segments: validSegments, + content: { + id: expectedContentId + } + } + }; + jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); + expect(bidRequestSpy.calledOnceWithExactly(expectedData)).to.be.true; + }); + + it('returns an empty object when media id is invalid', function () { + const adUnitCode = 'test_ad_unit'; + const adUnitWithMediaId = { + code: adUnitCode, + jwTargeting: { + mediaID: testIdForFailure + } + }; + const adUnitEmpty = { + code: 'test_ad_unit_empty' + }; + + jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); + expect(bidRequestSpy.calledOnceWithExactly({})).to.be.true; + }); + + it('returns an empty object when jwTargeting block is absent', function () { + const adUnitCode = 'test_ad_unit'; + const adUnitWithMediaId = { + code: adUnitCode, + mediaID: testIdForSuccess + }; + const adUnitEmpty = { + code: 'test_ad_unit_empty' + }; + + jwplayerSubmodule.getData([adUnitWithMediaId, adUnitEmpty], bidRequestSpy); + expect(bidRequestSpy.calledOnceWithExactly({})).to.be.true; + }); + }); + }); +}); From 1d1ffa3ded51654e6321c1ea8cef703225301047 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 20 Aug 2020 11:49:09 -0700 Subject: [PATCH 35/43] Prebid 4.4.0 Release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cb04dc6f8a..04016700633 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.4.0-pre", + "version": "4.4.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 8513f130330aa9d878a473af714d44d25ca22b09 Mon Sep 17 00:00:00 2001 From: robertrmartinez Date: Thu, 20 Aug 2020 12:07:56 -0700 Subject: [PATCH 36/43] Increment pre version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04016700633..af36566b95b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "4.4.0", + "version": "4.5.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { From 32066aa711d194d0ae6d95c6774f8b6c0eb33d26 Mon Sep 17 00:00:00 2001 From: bretg Date: Sat, 22 Aug 2020 11:49:03 -0400 Subject: [PATCH 37/43] module rule documentation updates (#5619) * module rule updates * Adding FPD.user --- CONTRIBUTING.md | 2 ++ PR_REVIEW.md | 23 +++++++++++------------ README.md | 6 ++---- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 016f4055216..962e057fbc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,6 +6,8 @@ master branch. Pull requests must have 80% code coverage before beign considered for merge. Additional details about the process can be found [here](./PR_REVIEW.md). +There are more details available if you'd like to contribute a [bid adapter](https://docs.prebid.org/dev-docs/bidder-adaptor.html) or [analytics adapter](https://docs.prebid.org/dev-docs/integrate-with-the-prebid-analytics-api.html). + ## Issues [prebid.org](http://prebid.org/) contains documentation that may help answer questions you have about using Prebid.js. If you can't find the answer there, try searching for a similar issue on the [issues page](https://github.com/prebid/Prebid.js/issues). diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 6402fcbbbaa..9a57539a0cd 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -14,7 +14,7 @@ For modules and core platform updates, the initial reviewer should request an ad - Review for obvious errors or bad coding practice / use best judgement here. - If the change is a new feature / change to core prebid.js - review the change with a Tech Lead on the project and make sure they agree with the nature of change. - 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. - - Below are some examples of bidder specific updates that should require docs update (in their dev-docs/bidders/bidder.md file): + - Below are some examples of bidder specific updates that should require docs update (in their dev-docs/bidders/BIDDER.md file): - If they support the GDPR consentManagement module and TCF1, add `gdpr_supported: true` - If they support the GDPR consentManagement module and TCF2, add `tcf2_supported: true` - If they support the US Privacy consentManagementUsp module, add `usp_supported: true` @@ -23,7 +23,7 @@ For modules and core platform updates, the initial reviewer should request an ad - If they support COPPA, add `coppa_supported: true` - If they support SChain, add `schain_supported: true` - If their bidder doesn't work well with safeframed creatives, add `safeframes_ok: false`. This will alert publishers to not use safeframed creatives when creating the ad server entries for their bidder. - - If they're a member of Prebid.org, add `prebid_member: true` + - If they're setting a deal ID in some scenarios, add `bidder_supports_deals: true` - If all above is good, add a `LGTM` comment and request 1 additional core member to review. - Once there is 2 `LGTM` on the PR, merge to master - Ask the submitter to add a PR for documentation if applicable. @@ -34,18 +34,17 @@ For modules and core platform updates, the initial reviewer should request an ad - Follow steps above for general review process. In addition, please verify the following: - Verify that bidder has submitted valid bid params and that bids are being received. - Verify that bidder is not manipulating the prebid.js auction in any way or doing things that go against the principles of the project. If unsure check with the Tech Lead. -- Verify that the bidder is being as efficient as possible, ideally not loading an external library, however if they do load a library it should be cached. - Verify that code re-use is being done properly and that changes introduced by a bidder don't impact other bidders. - If the adapter being submitted is an alias type, check with the bidder contact that is being aliased to make sure it's allowed. -- If the adapter is triggering any user syncs make sure they are using the user sync module in the Prebid.js core. -- Requests to the bidder should support HTTPS -- Responses from the bidder should be compressed (such as gzip, compress, deflate) -- Bid responses may not use JSONP: All requests must be AJAX with JSON responses -- Video openrtb params should be read from the ad unit when available; however bidder config can override the ad unit. -- All user-sync (aka pixel) activity must be registered via the provided functions -- Adapters may not use the $$PREBID_GLOBAL$$ variable -- All adapters must support the creation of multiple concurrent instances. This means, for example, that adapters cannot rely on mutable global variables. -- Adapters may not globally override or default the standard ad server targeting values: hb_adid, hb_bidder, hb_pb, hb_deal, or hb_size, hb_source, hb_format. +- 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. +- All bidder parameter conventions must be followed: + - Video params must be read from AdUnit.mediaTypes.video when available; however bidder config can override the ad unit. + - First party data must be read from [`fpd.context` and `fpd.user`](https://docs.prebid.org/dev-docs/publisher-api-reference.html#setConfig-fpd). + - Adapters that accept a floor parameter must also support the [floors module](https://docs.prebid.org/dev-docs/modules/floors.html) -- look for a call to the `getFloors()` function. + - Adapters cannot accept an schain parameter. Rather, they must look for the schain parameter at bidRequest.schain. + - The bidRequest page referrer must checked in addition to any bidder-specific parameter. + - If they're getting the COPPA flag, it must come from config.getConfig('coppa'); + - After a new adapter is approved, let the submitter know they may open a PR in the [headerbid-expert repository](https://github.com/prebid/headerbid-expert) to have their adapter recognized by the [Headerbid Expert extension](https://chrome.google.com/webstore/detail/headerbid-expert/cgfkddgbnfplidghapbbnngaogeldmop). The PR should be to the [bidder patterns file](https://github.com/prebid/headerbid-expert/blob/master/bidderPatterns.js), adding an entry with their adapter's name and the url the adapter uses to send and receive bid responses. ## Ticket Coordinator diff --git a/README.md b/README.md index b3f33b27aa5..44882570d89 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ As you make code changes, the bundles will be rebuilt and the page reloaded auto ## Contribute -Many SSPs, bidders, and publishers have contributed to this project. [60+ Bidders](https://github.com/prebid/Prebid.js/tree/master/src/adapters) are supported by Prebid.js. +Many SSPs, bidders, and publishers have contributed to this project. [Hundreds of bidders](https://github.com/prebid/Prebid.js/tree/master/src/adapters) are supported by Prebid.js. For guidelines, see [Contributing](./CONTRIBUTING.md). @@ -276,9 +276,7 @@ Our PR review process can be found [here](https://github.com/prebid/Prebid.js/tr ### Add a Bidder Adapter -To add a bidder adapter module, see the instructions in [How to add a bidder adaptor](http://prebid.org/dev-docs/bidder-adaptor.html). - -Please **do NOT load Prebid.js inside your adapter**. If you do this, we will reject or remove your adapter as appropriate. +To add a bidder adapter module, see the instructions in [How to add a bidder adapter](https://docs.prebid.org/dev-docs/bidder-adaptor.html). ### Code Quality From 29bd8de8b089721ed032df92fb87b79a21d1981a Mon Sep 17 00:00:00 2001 From: wojciech-bialy-wpm <67895844+wojciech-bialy-wpm@users.noreply.github.com> Date: Mon, 24 Aug 2020 18:51:47 +0200 Subject: [PATCH 38/43] Add sspBC adapter (#5531) 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 Co-authored-by: Wojciech Biały --- modules/sspBCAdapter.js | 319 +++++++++++++++++++++++ modules/sspBCAdapter.md | 40 +++ test/spec/modules/sspBCAdapter_spec.js | 334 +++++++++++++++++++++++++ 3 files changed, 693 insertions(+) create mode 100644 modules/sspBCAdapter.js create mode 100644 modules/sspBCAdapter.md create mode 100644 test/spec/modules/sspBCAdapter_spec.js diff --git a/modules/sspBCAdapter.js b/modules/sspBCAdapter.js new file mode 100644 index 00000000000..ef89fb08449 --- /dev/null +++ b/modules/sspBCAdapter.js @@ -0,0 +1,319 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'sspBC'; +const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; +const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; +const TMAX = 450; +const BIDDER_VERSION = '4.5'; +const W = window; +const { navigator } = W; + +const cookieSupport = () => { + const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); + const useCookies = navigator.cookieEnabled || !!document.cookie.length; + + return !isSafari && useCookies; +}; + +const applyClientHints = ortbRequest => { + const connection = navigator.connection || false; + const viewport = W.visualViewport || false; + const segments = []; + const hints = { + 'CH-Ect': connection.effectiveType, + 'CH-Rtt': connection.rtt, + 'CH-SaveData': connection.saveData, + 'CH-Downlink': connection.downlink, + 'CH-DeviceMemory': navigator.deviceMemory, + 'CH-Dpr': W.devicePixelRatio, + 'CH-ViewportWidth': viewport.width, + }; + + Object.keys(hints).forEach(key => { + const hint = hints[key]; + + if (hint) { + segments.push({ + name: key, + value: hint.toString(), + }); + } + }); + const data = [ + { + id: '12', + name: 'NetInfo', + segment: segments, + }]; + + ortbRequest.user = Object.assign(ortbRequest.user, { data }); +}; + +function applyGdpr(bidderRequest, ortbRequest) { + if (bidderRequest && bidderRequest.gdprConsent) { + ortbRequest.regs = Object.assign(ortbRequest.regs, { '[ortb_extensions.gdpr]': bidderRequest.gdprConsent.gdprApplies ? 1 : 0 }); + ortbRequest.user = Object.assign(ortbRequest.user, { '[ortb_extensions.consent]': bidderRequest.gdprConsent.consentString }); + } +} + +function setOnAny(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = utils.deepAccess(collection[i], key); + + if (result) { + return result; + } + } +} + +/** + * @param {object} slot Ad Unit Params by Prebid + * @returns {object} Banner by OpenRTB 2.5 §3.2.6 + */ +function mapBanner(slot) { + if (slot.mediaType === 'banner' || + utils.deepAccess(slot, 'mediaTypes.banner') || + (!slot.mediaType && !slot.mediaTypes)) { + const format = slot.sizes.map(size => ({ + w: size[0], + h: size[1], + })); + + // override - tylko 1szy wymiar + // format = format.slice(0, 1); + return { + format, + id: slot.bidId, + }; + } +} + +function mapImpression(slot) { + const imp = { + id: slot.params.id, + banner: mapBanner(slot), + /* native: mapNative(slot), */ + tagid: slot.params.id, + }; + + const bidfloor = parseFloat(slot.params.bidfloor); + + if (bidfloor) { + imp.bidfloor = bidfloor; + } + + return imp; +} + +function renderCreative(site, auctionId, bid, seat, request) { + let gam; + + const mcad = { + id: auctionId, + seat, + seatbid: [{ + bid: [bid], + }], + }; + + const mcbase = btoa(encodeURI(JSON.stringify(mcad))); + + if (bid.adm) { + // parse adm for gam config + try { + gam = JSON.parse(bid.adm).gam; + + if (!gam || !Object.keys(gam).length) { + gam = undefined; + } else { + gam.namedSizes = ['fluid']; + gam.div = 'div-gpt-ad-x01'; + gam.targeting = Object.assign(gam.targeting || {}, { + OAS_retarg: '0', + PREBID_ON: '1', + emptygaf: '0', + }); + } + + if (gam && !gam.targeting) { + gam.targeting = {}; + } + } catch (err) { + utils.logWarn('Could not parse adm data', bid.adm); + } + } + + let adcode = ` + + + + + + + +
+ + + `; + + return adcode; +} + +const spec = { + code: BIDDER_CODE, + aliases: [], + supportedMediaTypes: [BANNER], + isBidRequestValid(bid) { + if (bid.params && bid.params.siteId && bid.params.id) { + return true; + } + + return false; + }, + buildRequests(validBidRequests, bidderRequest) { + if ((!validBidRequests) || (validBidRequests.length < 1)) { + return false; + } + + const siteId = setOnAny(validBidRequests, 'params.siteId'); + const page = setOnAny(validBidRequests, 'params.page') || bidderRequest.refererInfo.referer; + const domain = setOnAny(validBidRequests, 'params.domain') || utils.parseUrl(page).hostname; + const tmax = setOnAny(validBidRequests, 'params.tmax') ? parseInt(setOnAny(validBidRequests, 'params.tmax'), 10) : TMAX; + const pbver = '$prebid.version$'; + const testMode = setOnAny(validBidRequests, 'params.test') ? 1 : undefined; + + let ref; + + try { + if (W.self === W.top && document.referrer) { ref = document.referrer; } + } catch (e) { + } + + const payload = { + id: bidderRequest.auctionId, + site: { id: siteId, page, domain, ref }, + imp: validBidRequests.map(slot => mapImpression(slot)), + tmax, + user: {}, + regs: {}, + test: testMode, + }; + + applyGdpr(bidderRequest, payload); + applyClientHints(payload); + + return { + method: 'POST', + url: BIDDER_URL + '?cs=' + cookieSupport() + '&bdver=' + BIDDER_VERSION + '&pbver=' + pbver + '&inver=0', + data: JSON.stringify(payload), + bidderRequest, + }; + }, + + interpretResponse(serverResponse, request) { + const response = serverResponse.body; + const bids = []; + let site = JSON.parse(request.data).site; // get page and referer data from request + site.sn = response.sn || 'mc_adapter'; // WPM site name (wp_sn) + let seat; + + if (response.seatbid !== undefined) { + response.seatbid.forEach(seatbid => { + seat = seatbid.seat; + seatbid.bid.forEach(serverBid => { + const bidRequest = request.bidderRequest.bids.filter(b => b.params.id === serverBid.impid)[0]; + + if (bidRequest) { + const bidFloor = bidRequest.params.bidFloor || 0; + const bidCpm = bidRequest.params.flatCpm; + + if (!serverBid.gam && bidRequest.params.gam) { + // build GAM config + serverBid.gam = JSON.stringify({ + placement: bidRequest.params.gam, + multiplier: 1, + floor: bidRequest.params.gamFloor, + ceil: 100, + namedSizes: ['fluid'], + div: 'div-gpt-ad-x01', + targeting: { + OAS_retarg: '0', + PREBID_ON: '1', + DFPHASH: '', + emptygaf: '0', + }, + }); + } + + const bid = { + requestId: bidRequest.bidId, + creativeId: serverBid.crid || 'mcad_' + request.bidderRequest.auctionId + '_' + request.bidderRequest.params.id, + cpm: bidCpm || serverBid.price, + currency: response.cur, + ttl: serverBid.exp || 300, + width: serverBid.w, + height: serverBid.h, + bidderCode: BIDDER_CODE, + mediaType: 'banner', + meta: { + advertiserDomains: serverBid.adomain, + }, + netRevenue: true, + ad: renderCreative(site, response.id, serverBid, seat, request.bidderRequest), + }; + + if (bid.cpm > 0) { + if (bid.cpm >= bidFloor) { + bids.push(bid); + } else { + utils.logWarn('Discarding bid due to bidFloor setting', bid.cpm, bidFloor); + } + } + } else { + utils.logWarn('Discarding response - no matching request', serverBid.impid); + } + }); + }); + } + + return bids; + }, + getUserSyncs(syncOptions) { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: SYNC_URL, + }]; + } + utils.logWarn('sspBC adapter requires iframe based user sync.'); + }, + onTimeout() { + }, +}; + +registerBidder(spec); + +export { + spec, +}; diff --git a/modules/sspBCAdapter.md b/modules/sspBCAdapter.md new file mode 100644 index 00000000000..645f41fcdc1 --- /dev/null +++ b/modules/sspBCAdapter.md @@ -0,0 +1,40 @@ +# Overview + +Module Name: sspBC Bidder Adapter +Module Type: Bidder Adapter +Maintainer: wojciech.bialy@grupawp.pl + +# Description + +Module that connects to Wirtualna Polska Media header bidding endpoint to fetch bids. +Only banner format is supported. +Supported currencies: USD, EUR, PLN + + +Required parameters: + + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'sspBC', + params: { + id: '006', // required + siteId: '235911', // required + domain: 'somesite.pl', // optional + page: 'somesite.pl/somepage.html', // optional + tmax: 250 // optional + } + }] + } +]; +``` diff --git a/test/spec/modules/sspBCAdapter_spec.js b/test/spec/modules/sspBCAdapter_spec.js new file mode 100644 index 00000000000..2cb0e8defa4 --- /dev/null +++ b/test/spec/modules/sspBCAdapter_spec.js @@ -0,0 +1,334 @@ +import { assert, expect } from 'chai'; +import { spec } from 'modules/sspBCAdapter.js'; +import * as utils from 'src/utils.js'; + +const BIDDER_CODE = 'sspBC'; +const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; +const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; + +describe('SSPBC adapter', function () { + function prepareTestData() { + const bidderRequestId = '1041bb47b0fafa'; + const auctionId = '8eda6d06-3d7c-4a94-9b35-74e42fbb3089'; + const transactionId = '50259989-b5c0-4edf-8f47-b1ef5fbedf39'; + const gdprConsent = { + consentString: 'BOtq-3dOtq-30BIABCPLC4-AAAAthr_7__7-_9_-_f__9uj3Or_v_f__30ccL59v_h_7v-_7fi_20nV4u_1vft9yfk1-5ctDztp505iakivHmqNeb9v_mz1_5pRP78k89r7337Ew_v8_v-b7JCON_Ig', + gdprApplies: true, + } + const bids = [{ + adUnitCode: 'test_wideboard', + bidder: BIDDER_CODE, + mediaTypes: { + banner: { + sizes: [ + [728, 90], + [750, 100], + [750, 200] + ] + } + }, + sizes: [ + [728, 90], + [750, 100], + [750, 200] + ], + params: { + id: '003', + siteId: '8816', + }, + auctionId, + bidderRequestId, + bidId: auctionId + '1', + transactionId, + }, + { + adUnitCode: 'test_rectangle', + bidder: BIDDER_CODE, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + sizes: [ + [300, 250] + ], + params: { + id: '005', + siteId: '8816', + }, + auctionId, + bidderRequestId, + bidId: auctionId + '1', + transactionId, + } + ]; + const bids_test = [{ + adUnitCode: 'test_wideboard', + bidder: BIDDER_CODE, + mediaTypes: { + banner: { + sizes: [ + [970, 300], + [750, 300], + [750, 200], + [750, 100], + [300, 250] + ] + } + }, + sizes: [ + [970, 300], + [750, 300], + [750, 200], + [750, 100], + [300, 250] + ], + params: { + id: '005', + siteId: '235911', + test: 1 + }, + auctionId, + bidderRequestId, + bidId: auctionId + '1', + transactionId, + }]; + const bidRequest = { + auctionId, + bidderCode: BIDDER_CODE, + bidderRequestId, + bids, + gdprConsent, + refererInfo: { + reachedTop: true, + referer: 'https://test.site.pl/', + stack: ['https://test.site.pl/'], + } + }; + const bidRequestSingle = { + auctionId, + bidderCode: BIDDER_CODE, + bidderRequestId, + bids: [bids[0]], + gdprConsent, + refererInfo: { + reachedTop: true, + referer: 'https://test.site.pl/', + stack: ['https://test.site.pl/'], + } + }; + const bidRequestTest = { + auctionId, + bidderCode: BIDDER_CODE, + bidderRequestId, + bids: bids_test, + gdprConsent, + refererInfo: { + reachedTop: true, + referer: 'https://test.site.pl/', + stack: ['https://test.site.pl/'], + } + }; + const bidRequestTestNoGDPR = { + auctionId, + bidderCode: BIDDER_CODE, + bidderRequestId, + bids: bids_test, + refererInfo: { + reachedTop: true, + referer: 'https://test.site.pl/', + stack: ['https://test.site.pl/'], + } + }; + const serverResponse = { + 'body': { + 'id': auctionId, + 'seatbid': [{ + 'bid': [{ + 'id': '3347324c-6889-46d2-a800-ae78a5214c06', + 'impid': '003', + 'price': 1, + 'adid': 'lxHWkB7OnZeso3QiN1N4', + 'nurl': '', + 'adm': 'AD CODE 1', + 'adomain': ['adomain.pl'], + 'cid': 'BZ4gAg21T5nNtxlUCDSW', + 'crid': 'lxHWkB7OnZeso3QiN1N4', + 'w': 728, + 'h': 90 + }], + 'seat': 'dsp1', + 'group': 0 + }, { + 'bid': [{ + 'id': '2d766853-ea07-4529-8299-5f0ebadc546a', + 'impid': '005', + 'price': 2, + 'adm': 'AD CODE 2', + 'cid': '57744', + 'crid': '858252', + 'w': 300, + 'h': 250 + }], + 'seat': 'dsp2', + 'group': 0 + }], + 'cur': 'PLN' + } + }; + const serverResponseSingle = { + 'body': { + 'id': auctionId, + 'seatbid': [{ + 'bid': [{ + 'id': '3347324c-6889-46d2-a800-ae78a5214c06', + 'impid': '003', + 'price': 1, + 'adid': 'lxHWkB7OnZeso3QiN1N4', + 'nurl': '', + 'adm': 'AD CODE 1', + 'adomain': ['adomain.pl'], + 'cid': 'BZ4gAg21T5nNtxlUCDSW', + 'crid': 'lxHWkB7OnZeso3QiN1N4', + 'w': 728, + 'h': 90 + }], + 'seat': 'dsp1', + 'group': 0 + }], + 'cur': 'PLN' + } + }; + const emptyResponse = { + 'body': { + 'id': auctionId, + } + } + return { + bids, + bids_test, + bidRequest, + bidRequestSingle, + bidRequestTest, + bidRequestTestNoGDPR, + serverResponse, + serverResponseSingle, + emptyResponse + }; + }; + + describe('dependencies', function () { + it('utils should contain required functions', function () { + expect(utils.parseUrl).to.be.a('function'); + expect(utils.deepAccess).to.be.a('function'); + expect(utils.logWarn).to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const { bids } = prepareTestData(); + let bid = bids[0]; + + it('should return true when required params found', function () { + assert(spec.isBidRequestValid(bid)); + }); + + it('should return false when required params are missing', function () { + bid.params.id = undefined; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + }); + + describe('buildRequests', function () { + const { bids, bidRequest, bidRequestSingle } = prepareTestData(); + const request = spec.buildRequests(bids, bidRequest); + const requestSingle = spec.buildRequests([bids[0]], bidRequestSingle); + const payload = request ? JSON.parse(request.data) : { site: false, imp: false }; + const payloadSingle = request ? JSON.parse(requestSingle.data) : { site: false, imp: false }; + + it('should send bid request to endpoint via POST', function () { + expect(request.url).to.contain(BIDDER_URL); + expect(request.method).to.equal('POST'); + }); + + it('should contain prebid and bidder versions', function () { + expect(request.url).to.contain('bdver'); + expect(request.url).to.contain('pbver=$prebid.version$'); + }); + + it('should create one imp object per bid', function () { + expect(payload.imp.length).to.equal(bids.length); + expect(payloadSingle.imp.length).to.equal(1); + }); + + it('should save bidder request data', function () { + expect(request.bidderRequest).to.deep.equal(bidRequest); + }); + + it('should send site Id from bidder params', function () { + expect(payload.site.id).to.equal(bids[0].params.siteId); + }); + + it('should send page url from refererInfo', function () { + expect(payload.site.page).to.equal(bidRequest.refererInfo.referer); + }); + + it('should send gdpr data', function () { + expect(payload.regs).to.be.an('object').and.to.have.property('[ortb_extensions.gdpr]', 1); + expect(payload.user).to.be.an('object').and.to.have.property('[ortb_extensions.consent]', bidRequest.gdprConsent.consentString); + }); + }); + + describe('interpretResponse', function () { + const { bids, emptyResponse, serverResponse, serverResponseSingle, bidRequest, bidRequestSingle } = prepareTestData(); + const request = spec.buildRequests(bids, bidRequest); + const requestSingle = spec.buildRequests([bids[0]], bidRequestSingle); + + it('should handle nobid responses', function () { + let result = spec.interpretResponse(emptyResponse, request); + expect(result.length).to.equal(0); + }); + + it('should create bids from non-empty responses', function () { + let result = spec.interpretResponse(serverResponse, request); + let resultSingle = spec.interpretResponse(serverResponseSingle, requestSingle); + + expect(result.length).to.equal(bids.length); + expect(resultSingle.length).to.equal(1); + expect(resultSingle[0]).to.have.keys('ad', 'cpm', 'width', 'height', 'bidderCode', 'mediaType', 'meta', 'requestId', 'creativeId', 'currency', 'netRevenue', 'ttl'); + }); + + it('should handle a partial response', function () { + let resultPartial = spec.interpretResponse(serverResponseSingle, request); + expect(resultPartial.length).to.equal(1); + }); + + it('banner ad code should contain required variables', function () { + let resultSingle = spec.interpretResponse(serverResponseSingle, requestSingle); + let adcode = resultSingle[0].ad; + expect(adcode).to.be.a('string'); + expect(adcode).to.contain('window.rekid'); + expect(adcode).to.contain('window.mcad'); + expect(adcode).to.contain('window.gdpr'); + expect(adcode).to.contain('window.page'); + }) + }); + + describe('getUserSyncs', function () { + let syncResultAll = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }); + let syncResultImage = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: true }); + let syncResultNone = spec.getUserSyncs({ iframeEnabled: false, pixelEnabled: false }); + + it('should provide correct url, if frame sync is allowed', function () { + expect(syncResultAll).to.have.length(1); + expect(syncResultAll[0].url).to.be.equal(SYNC_URL); + }); + + it('should send no syncs, if frame sync is not allowed', function () { + expect(syncResultImage).to.be.undefined; + expect(syncResultNone).to.be.undefined; + }); + }); +}); From b26bfe31485b12288eafc89639fe8e671c1ccf4e Mon Sep 17 00:00:00 2001 From: NemanjaRajkovic9 <45632924+NemanjaRajkovic9@users.noreply.github.com> Date: Mon, 24 Aug 2020 19:40:58 +0200 Subject: [PATCH 39/43] [IdentityLinkIdSystem] - pass tcfv2 consent string to envelope api (#5634) * [IdentityLinkIdSystem] - pass tcfv2 consent string to envelope api * [IdentityLinkIdSystem] - used deepAccess --- modules/identityLinkIdSystem.js | 22 ++++++++--- .../spec/modules/identityLinkIdSystem_spec.js | 38 ++++++++++++++++++- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index c516c06d11a..14c33329b2d 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -6,8 +6,8 @@ */ import * as utils from '../src/utils.js' -import {ajax} from '../src/ajax.js'; -import {submodule} from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; /** @type {Submodule} */ export const identityLinkSubmodule = { @@ -16,6 +16,11 @@ export const identityLinkSubmodule = { * @type {string} */ name: 'identityLink', + /** + * used to specify vendor id + * @type {number} + */ + gvlid: 97, /** * decode the stored id value for passing to bid requests * @function @@ -39,10 +44,15 @@ export const identityLinkSubmodule = { } const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; const gdprConsentString = hasGdpr ? consentData.consentString : ''; + const tcfPolicyV2 = utils.deepAccess(consentData, 'vendorData.tcfPolicyVersion') === 2; // use protocol relative urls for http or https - const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? '&ct=1&cv=' + gdprConsentString : ''}`; + if (hasGdpr && (!gdprConsentString || gdprConsentString === '')) { + utils.logInfo('Consent string is required to call envelope API.'); + return; + } + const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? (tcfPolicyV2 ? '&ct=4&cv=' : '&ct=1&cv=') + gdprConsentString : ''}`; let resp; - resp = function(callback) { + resp = function (callback) { // Check ats during callback so it has a chance to initialise. // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint if (window.ats) { @@ -60,7 +70,7 @@ export const identityLinkSubmodule = { } }; - return {callback: resp}; + return { callback: resp }; } }; // return envelope from third party endpoint @@ -83,7 +93,7 @@ function getEnvelope(url, callback) { callback(); } }; - ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true}); + ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true }); } submodule('userId', identityLinkSubmodule); diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index 0d539d5988c..9f36ba92558 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -41,7 +41,22 @@ describe('IdentityLinkId tests', function () { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the LiveRamp envelope endpoint with consent string', function () { + it('should NOT call the LiveRamp envelope endpoint if gdpr applies but consent string is empty string', function () { + let consentData = { + gdprApplies: true, + consentString: '' + }; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData); + expect(submoduleCallback).to.be.undefined; + }); + + it('should NOT call the LiveRamp envelope endpoint if gdpr applies but consent string is missing', function () { + let consentData = { gdprApplies: true }; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData); + expect(submoduleCallback).to.be.undefined; + }); + + it('should call the LiveRamp envelope endpoint with IAB consent string v1', function () { let callBackSpy = sinon.spy(); let consentData = { gdprApplies: true, @@ -59,6 +74,27 @@ describe('IdentityLinkId tests', function () { expect(callBackSpy.calledOnce).to.be.true; }); + it('should call the LiveRamp envelope endpoint with IAB consent string v2', function () { + let callBackSpy = sinon.spy(); + let consentData = { + gdprApplies: true, + consentString: 'CO4VThZO4VTiuADABBENAzCgAP_AAEOAAAAAAwwAgAEABhAAgAgAAA.YAAAAAAAAAA', + vendorData: { + tcfPolicyVersion: 2 + } + }; + let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams, consentData).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url).to.be.eq('https://api.rlcdn.com/api/identity/envelope?pid=14&ct=4&cv=CO4VThZO4VTiuADABBENAzCgAP_AAEOAAAAAAwwAgAEABhAAgAgAAA.YAAAAAAAAAA'); + request.respond( + 200, + responseHeader, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); + it('should not throw Uncaught TypeError when envelope endpoint returns empty response', function () { let callBackSpy = sinon.spy(); let submoduleCallback = identityLinkSubmodule.getId(defaultConfigParams).callback; From 01f4b287d4a284c95e569d97b22387a534b365c9 Mon Sep 17 00:00:00 2001 From: Robert Ray Martinez III Date: Mon, 24 Aug 2020 13:25:32 -0700 Subject: [PATCH 40/43] Fix bug and add tests to catch next time (#5656) --- modules/rubiconBidAdapter.js | 4 ++-- test/spec/modules/rubiconBidAdapter_spec.js | 23 ++++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 2768b41da21..9f5c2ec10d5 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -550,8 +550,8 @@ export const spec = { try { floorInfo = bidRequest.getFloor({ currency: 'USD', - mediaType: 'video', - size: parseSizes(bidRequest, 'video') + mediaType: 'banner', + size: '*' }); } catch (e) { utils.logError('Rubicon: getFloor threw an error: ', e); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 1d5769b9ac1..49a3f60bac7 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1,5 +1,4 @@ import {expect} from 'chai'; -import adapterManager from 'src/adapterManager.js'; import {spec, getPriceGranularity, masSizeOrdering, resetUserSync, hasVideoMediaType, FASTLANE_ENDPOINT} from 'modules/rubiconBidAdapter.js'; import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; @@ -419,7 +418,18 @@ describe('the rubicon adapter', function () { it('should correctly send hard floors when getFloor function is present and returns valid floor', function () { // default getFloor response is empty object so should not break and not send hard_floor bidderRequest.bids[0].getFloor = () => getFloorResponse; + sinon.spy(bidderRequest.bids[0], 'getFloor'); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // make sure banner bid called with right stuff + expect( + bidderRequest.bids[0].getFloor.calledWith({ + currency: 'USD', + mediaType: 'banner', + size: '*' + }) + ).to.be.true; + let data = parseQuery(request.data); expect(data.rp_hard_floor).to.be.undefined; @@ -1597,12 +1607,23 @@ describe('the rubicon adapter', function () { createVideoBidderRequest(); // default getFloor response is empty object so should not break and not send hard_floor bidderRequest.bids[0].getFloor = () => getFloorResponse; + sinon.spy(bidderRequest.bids[0], 'getFloor'); + sandbox.stub(Date, 'now').callsFake(() => bidderRequest.auctionStart + 100 ); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + // make sure banner bid called with right stuff + expect( + bidderRequest.bids[0].getFloor.calledWith({ + currency: 'USD', + mediaType: 'video', + size: [640, 480] + }) + ).to.be.true; + // not an object should work and not send expect(request.data.imp[0].bidfloor).to.be.undefined; From 7001a23635d7f6d5cbfbec082e9ceaf2e32f6f49 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Tue, 25 Aug 2020 11:02:48 -0400 Subject: [PATCH 41/43] Update vidazooBidAdapter_spec.js (#5639) * Update vidazooBidAdapter_spec.js * Update vidazooBidAdapter_spec.js --- test/spec/modules/vidazooBidAdapter_spec.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 57e7f4e8ae2..8b3a492d2e5 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -315,12 +315,7 @@ describe('VidazooBidAdapter', function () { describe('unique deal id', function () { const key = 'myKey'; let uniqueDealId; - - it('should get fresh unique deal id', function () { - const now = Date.now(); - uniqueDealId = getUniqueDealId(key); - expect(uniqueDealId).to.be.equal(`${key}_${now.toString()}`); - }); + uniqueDealId = getUniqueDealId(key); it('should get current unique deal id', function (done) { // waiting some time so `now` will become past From 71ff9344510439235f6528c5785cb6e1ccdda421 Mon Sep 17 00:00:00 2001 From: smartclip-adtech <65160328+smartclip-adtech@users.noreply.github.com> Date: Tue, 25 Aug 2020 18:10:21 +0200 Subject: [PATCH 42/43] Add smartxBidAdapter (#5275) * Add smartclipBidAdapter * Updated imported files * .js file * Beautify code * Revert "Beautify code" This reverts commit da2aea761e4187fd3136e9b8b4f4ab5857266114. * testing * partial tab fix * syntax fixes * further syntax fixes * Trailing spaces... * const parameters fpr smartInTxt Functions * const to var * // eslint-disable-next-line camelcase * new commit * Update Targeting * Update syntax * beautify code * remove trailing spaces * Update smartclipBidAdapter.md * Update smartclipBidAdapter.js * Update smartclipBidAdapter.js * rename files * added first unit tests, further adjustments * adjusted queries, ran gulp lint, etc * adjustments, further tests * extendet unit tests * naming for spec * adjusted bidfloor and bidfloorcur, removed old queries, adjustments in the markdown with more use cases * added more use cases within md file * layout fix * md layout * smartxBidAdapter.js - triple equals, removed context2 due to not used in params.ad_units. Adjusted query. smartxBidAdapter_spec.js - fixed typo Co-authored-by: smartclip AdTechnology Co-authored-by: Gino Cirlini --- modules/smartxBidAdapter.js | 400 ++++++++++++++++ modules/smartxBidAdapter.md | 159 +++++++ test/spec/modules/smartxBidAdapter_spec.js | 513 +++++++++++++++++++++ 3 files changed, 1072 insertions(+) create mode 100644 modules/smartxBidAdapter.js create mode 100644 modules/smartxBidAdapter.md create mode 100644 test/spec/modules/smartxBidAdapter_spec.js diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js new file mode 100644 index 00000000000..a745c54e39c --- /dev/null +++ b/modules/smartxBidAdapter.js @@ -0,0 +1,400 @@ +import * as utils from '../src/utils.js'; +import { + Renderer +} from '../src/Renderer.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; +import { + VIDEO +} from '../src/mediaTypes.js'; +const BIDDER_CODE = 'smartx'; +const URL = 'https://bid.sxp.smartclip.net/bid/1000'; +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO], + /** + * Determines whether or not the given bid request is valid. + * From Prebid.js: isBidRequestValid - Verify the the AdUnits.bids, respond with true (valid) or false (invalid). + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + if (bid && typeof bid.params !== 'object') { + utils.logError(BIDDER_CODE + ': params is not defined or is incorrect in the bidder settings.'); + return false; + } + if (!utils.deepAccess(bid, 'mediaTypes.video')) { + utils.logError(BIDDER_CODE + ': mediaTypes.video is not present in the bidder settings.'); + return false; + } + const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); + if (!playerSize || !utils.isArray(playerSize)) { + utils.logError(BIDDER_CODE + ': mediaTypes.video.playerSize is not defined in the bidder settings.'); + return false; + } + if (!utils.getBidIdParameter('tagId', bid.params)) { + utils.logError(BIDDER_CODE + ': tagId is not present in bidder params'); + return false; + } + if (!utils.getBidIdParameter('publisherId', bid.params)) { + utils.logError(BIDDER_CODE + ': publisherId is not present in bidder params'); + return false; + } + if (!utils.getBidIdParameter('siteId', bid.params)) { + utils.logError(BIDDER_CODE + ': siteId is not present in bidder params'); + return false; + } + if (!utils.getBidIdParameter('bidfloor', bid.params)) { + utils.logError(BIDDER_CODE + ': bidfloor is not present in bidder params'); + return false; + } + if (!utils.getBidIdParameter('bidfloorcur', bid.params)) { + utils.logError(BIDDER_CODE + ': bidfloorcur is not present in bidder params'); + return false; + } + if (utils.deepAccess(bid, 'mediaTypes.video.context') === 'outstream') { + if (!utils.getBidIdParameter('outstream_options', bid.params)) { + utils.logError(BIDDER_CODE + ': outstream_options parameter is not defined'); + return false; + } + if (!utils.getBidIdParameter('slot', bid.params.outstream_options)) { + utils.logError(BIDDER_CODE + ': slot parameter is not defined in outstream_options object in the configuration'); + return false; + } + if (!utils.getBidIdParameter('outstream_function', bid.params)) { + utils.logMessage(BIDDER_CODE + ': outstream_function parameter is not defined. The default outstream renderer will be injected in the header. You can override the default SmartX outstream rendering by defining your own Outstream function using field outstream_function.'); + return true; + } + } + + return true; + }, + /** + * Make a server request from the list of BidRequests. + * from Prebid.js: buildRequests - Takes an array of valid bid requests, all of which are guaranteed to have passed the isBidRequestValid() test. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (bidRequests, bidderRequest) { + const page = bidderRequest.refererInfo.referer; + const isPageSecure = !!page.match(/^https:/) + + const smartxRequests = bidRequests.map(function (bid) { + const tagId = utils.getBidIdParameter('tagId', bid.params); + const publisherId = utils.getBidIdParameter('publisherId', bid.params); + const bidfloor = utils.getBidIdParameter('bidfloor', bid.params); + const bidfloorcur = utils.getBidIdParameter('bidfloorcur', bid.params); + const siteId = utils.getBidIdParameter('siteId', bid.params); + const domain = utils.getBidIdParameter('domain', bid.params); + const cat = utils.getBidIdParameter('cat', bid.params); + let pubcid = null; + const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); + const contentWidth = playerSize[0][0]; + const contentHeight = playerSize[0][1]; + const secure = +(isPageSecure || (utils.getBidIdParameter('secure', bid.params) ? 1 : 0)); + const ext = { + sdk_name: 'Prebid 1+' + }; + const mimes = utils.getBidIdParameter('mimes', bid.params) || ['application/javascript', 'video/mp4', 'video/webm']; + const linearity = utils.getBidIdParameter('linearity', bid.params) || 1; + const minduration = utils.getBidIdParameter('minduration', bid.params) || 0; + const maxduration = utils.getBidIdParameter('maxduration', bid.params) || 500; + const startdelay = utils.getBidIdParameter('startdelay', bid.params) || 0; + const minbitrate = utils.getBidIdParameter('minbitrate', bid.params) || 0; + const maxbitrate = utils.getBidIdParameter('maxbitrate', bid.params) || 3500; + const delivery = utils.getBidIdParameter('delivery', bid.params) || [2]; + const pos = utils.getBidIdParameter('pos', bid.params) || 1; + const api = utils.getBidIdParameter('api', bid.params) || [2]; + const protocols = utils.getBidIdParameter('protocols', bid.params) || [2, 3, 5, 6]; + var contextcustom = utils.deepAccess(bid, 'mediaTypes.video.context'); + var placement = 1; + + if (contextcustom === 'outstream') { + placement = 3; + } + + let smartxReq = { + id: bid.bidId, + secure: secure, + bidfloor: bidfloor, + bidfloorcur: bidfloorcur, + video: { + w: contentWidth, + h: contentHeight, + mimes: mimes, + linearity: linearity, + minduration: minduration, + maxduration: maxduration, + startdelay: startdelay, + protocols: protocols, + minbitrate: minbitrate, + maxbitrate: maxbitrate, + delivery: delivery, + pos: pos, + placement: placement, + api: api, + ext: ext + }, + tagid: tagId, + ext: { + 'smart.bidpricetype': 1 + } + }; + + if (bid.crumbs && bid.crumbs.pubcid) { + pubcid = bid.crumbs.pubcid; + } + + const language = navigator.language ? 'language' : 'userLanguage'; + const device = { + h: screen.height, + w: screen.width, + dnt: utils.getDNT() ? 1 : 0, + language: navigator[language].split('-')[0], + make: navigator.vendor ? navigator.vendor : '', + ua: navigator.userAgent + }; + const at = utils.getBidIdParameter('at', bid.params) || 2; + const cur = utils.getBidIdParameter('cur', bid.params) || ['EUR']; + const requestPayload = { + id: utils.generateUUID(), + imp: smartxReq, + site: { + id: siteId, + page: page, + cat: cat, + content: 'content', + domain: domain, + publisher: { + id: publisherId + } + }, + device: device, + at: at, + cur: cur + }; + const userExt = {}; + + // Add GDPR flag and consent string + if (bidderRequest && bidderRequest.gdprConsent) { + userExt.consent = bidderRequest.gdprConsent.consentString; + if (typeof bidderRequest.gdprConsent.gdprApplies !== 'undefined') { + requestPayload.regs = { + ext: { + gdpr: (bidderRequest.gdprConsent.gdprApplies ? 1 : 0) + } + }; + } + } + + // Add common id if available + if (pubcid) { + userExt.fpc = pubcid; + } + + // Only add the user object if it's not empty + if (!utils.isEmpty(userExt)) { + requestPayload.user = { + ext: userExt + }; + } + + // Targeting + if (utils.getBidIdParameter('data', bid.params.user)) { + var targetingarr = []; + for (var i = 0; i < bid.params.user.data.length; i++) { + var isemq = (bid.params.user.data[i].name) || 'empty'; + if (isemq !== 'empty') { + var provider = bid.params.user.data[i].name; + var targetingstring = (bid.params.user.data[i].segment[0].value) || 'empty'; + targetingarr.push({ + id: provider, + name: provider, + segment: { + name: provider, + value: targetingstring, + } + }) + } + } + + requestPayload.user = { + ext: userExt, + data: targetingarr + } + } + + return { + method: 'POST', + url: URL, + data: requestPayload, + bidRequest: bidderRequest, + options: { + contentType: 'application/json', + customHeaders: { + 'x-openrtb-version': '2.3' + } + } + }; + }); + + return smartxRequests; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidderRequest) { + const bidResponses = []; + const serverResponseBody = serverResponse.body; + if (serverResponseBody && utils.isArray(serverResponseBody.seatbid)) { + utils._each(serverResponseBody.seatbid, function (bids) { + utils._each(bids.bid, function (smartxBid) { + let currentBidRequest = {}; + for (let i in bidderRequest.bidRequest.bids) { + if (smartxBid.impid == bidderRequest.bidRequest.bids[i].bidId) { + currentBidRequest = bidderRequest.bidRequest.bids[i]; + } + } + /** + * Make sure currency and price are the right ones + * TODO: what about the pre_market_bid partners sizes? + */ + utils._each(currentBidRequest.params.pre_market_bids, function (pmb) { + if (pmb.deal_id == smartxBid.id) { + smartxBid.price = pmb.price; + serverResponseBody.cur = pmb.currency; + } + }); + const bid = { + requestId: currentBidRequest.bidId, + currency: serverResponseBody.cur || 'USD', + cpm: smartxBid.price, + creativeId: smartxBid.crid || '', + ttl: 360, + netRevenue: true, + vastContent: smartxBid.adm, + vastXml: smartxBid.adm, + mediaType: VIDEO, + width: smartxBid.w, + height: smartxBid.h + }; + const context = utils.deepAccess(currentBidRequest, 'mediaTypes.video.context'); + if (context === 'outstream') { + const playersize = utils.deepAccess(currentBidRequest, 'mediaTypes.video.playerSize'); + const renderer = Renderer.install({ + id: 0, + url: '//', + config: { + adText: 'SmartX Outstream Video Ad via Prebid.js', + player_width: playersize[0][0], + player_height: playersize[0][1], + content_page_url: utils.deepAccess(bidderRequest, 'data.site.page'), + ad_mute: +!!utils.deepAccess(currentBidRequest, 'params.ad_mute'), + hide_skin: +!!utils.deepAccess(currentBidRequest, 'params.hide_skin'), + outstream_options: utils.deepAccess(currentBidRequest, 'params.outstream_options'), + outstream_function: utils.deepAccess(currentBidRequest, 'params.outstream_function') + } + }); + try { + renderer.setRender(outstreamRender); + renderer.setEventHandlers({ + impression: function impression() { + return utils.logMessage('SmartX outstream video impression event'); + }, + loaded: function loaded() { + return utils.logMessage('SmartX outstream video loaded event'); + }, + ended: function ended() { + utils.logMessage('SmartX outstream renderer video event'); + } + }); + } catch (err) { + utils.logWarn('Prebid Error calling setRender or setEventHandlers on renderer', err); + } + bid.renderer = renderer; + } + bidResponses.push(bid); + }) + }); + } + return bidResponses; + } +} + +function createOutstreamScript(bid) { + // const slot = utils.getBidIdParameter('slot', bid.renderer.config.outstream_options); + utils.logMessage('[SMARTX][renderer] Handle SmartX outstream renderer'); + const elementId = bid.adUnitCode; + // eslint-disable-next-line camelcase + var sc_smartIntxtStart; + // eslint-disable-next-line camelcase + var sc_smartIntxtNoad; + // eslint-disable-next-line camelcase + var sc_smartIntxtEnd; + var SmartPlay; + let smartPlayObj = { + minAdWidth: 290, + maxAdWidth: 900, + elementLocator: { + allowInViewport: false, + minimumElementWidth: 290, + scanPixelsBelowViewport: 800 + }, + onStartCallback: function (m, n) { + try { + sc_smartIntxtStart(n); + } catch (f) {} + }, + onCappedCallback: function (m, n) { + try { + sc_smartIntxtNoad(n); + } catch (f) {} + }, + onEndCallback: function (m, n) { + try { + sc_smartIntxtEnd(n); + } catch (f) {} + }, + debug: true + }; + smartPlayObj.adResponse = bid.vastContent; + const script = window.document.createElement('script'); + script.type = 'text/javascript'; + script.async = 'true'; + script.src = 'https://dco.smartclip.net/?plc=7777777'; + script.onload = script.onreadystatechange = function () { + var rs = this.readyState; + if (rs && rs != 'complete' && rs != 'loaded') return; + try { + SmartPlay(elementId, smartPlayObj); + } catch (e) { + utils.logError('error caught : ' + e); + } + }; + return script; +} + +function outstreamRender(bid) { + const script = createOutstreamScript(bid); + if (bid.renderer.config.outstream_function != null && typeof bid.renderer.config.outstream_function === 'function') { + bid.renderer.config.outstream_function(bid, script); + } else { + try { + const slot = utils.getBidIdParameter('slot', bid.renderer.config.outstream_options); + if (slot && window.document.getElementById(slot)) { + window.document.getElementById(slot).appendChild(script); + } else { + window.document.getElementsByTagName('head')[0].appendChild(script); + } + } catch (err) { + utils.logError('[SMARTX][renderer] Error:' + err.message) + } + } +} +registerBidder(spec); diff --git a/modules/smartxBidAdapter.md b/modules/smartxBidAdapter.md new file mode 100644 index 00000000000..a53af839e2b --- /dev/null +++ b/modules/smartxBidAdapter.md @@ -0,0 +1,159 @@ +# Overview + +``` +Module Name: smartclip Bidder Adapter +Module Type: Bidder Adapter +Maintainer: adtech@smartclip.tv +``` + +# Description + +Connect to smartx for bids. + +This adapter requires setup and approval from the smartclip team. + +# Test Parameters - Use case #1 - Out-Stream example and default rendering options +``` + var adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 360] + } + }, + bids: [{ + bidder: 'smartx', + params: { + tagId: 'Nu68JuOWAvrbzoyrOR9a7A', + publisherId: '11986', + siteId: '22860', + bidfloor: 0.3, + bidfloorcur: "EUR", + at: 2, + cur: ["EUR"], + outstream_options: { + slot: 'video1' + }, + } + }], + }]; +``` + +# Test Parameters - Use case #2 - Out-Stream with targeting example and default rendering options +``` + var adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 360] + } + }, + bids: [{ + bidder: 'smartx', + params: { + tagId: 'Nu68JuOWAvrbzoyrOR9a7A', + publisherId: '11986', + siteId: '22860', + bidfloor: 0.3, + bidfloorcur: "EUR", + at: 2, + cur: ["EUR"], + outstream_options: { + slot: 'video1' + }, + user: { + data: [{ + id: 'emq', + name: 'emq', + segment: [{ + id: 'emq', + name: 'emq', + value: 'e0:k14:e24' + }] + }, { + id: 'gs', + name: 'gs', + segment: [{ + id: 'gs', + name: 'gs', + value: 'tone_of_voice_dislike:tone_of_voice_negative:gs_health' + }] + }] + } + } + }] + }]; +``` + +# Test Parameters - Use case #3 - In-Stream example and default rendering options +``` + var adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 360] + } + }, + bids: [{ + bidder: 'smartx', + params: { + tagId: 'Nu68JuOWAvrbzoyrOR9a7A', + publisherId: '11986', + siteId: '22860', + bidfloor: 0.3, + bidfloorcur: "EUR", + at: 2, + cur: ["EUR"] + } + }], + }]; +``` + +# Test Parameters - Use case #4 - In-Stream with targeting example and default rendering options +``` + var adUnits = [{ + code: 'video1', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 360] + } + }, + bids: [{ + bidder: 'smartx', + params: { + tagId: 'Nu68JuOWAvrbzoyrOR9a7A', + publisherId: '11986', + siteId: '22860', + bidfloor: 0.3, + bidfloorcur: "EUR", + at: 2, + cur: ["EUR"], + user: { + data: [{ + id: 'emq', + name: 'emq', + segment: [{ + id: 'emq', + name: 'emq', + value: 'e0:k14:e24' + }] + }, + { + id: 'gs', + name: 'gs', + segment: [{ + id: 'gs', + name: 'gs', + value: 'tone_of_voice_dislike:tone_of_voice_negative:gs_health' + }] + } + ] + } + } + }], + }]; +``` \ No newline at end of file diff --git a/test/spec/modules/smartxBidAdapter_spec.js b/test/spec/modules/smartxBidAdapter_spec.js new file mode 100644 index 00000000000..efc6abcc5fa --- /dev/null +++ b/test/spec/modules/smartxBidAdapter_spec.js @@ -0,0 +1,513 @@ +import { + expect +} from 'chai'; +import { + spec +} from 'modules/smartxBidAdapter.js'; + +describe('The smartx adapter', function () { + function getValidBidObject() { + return { + bidId: 123, + mediaTypes: { + video: { + // context: 'outstream', + playerSize: [ + ['640', '360'] + ] + } + }, + params: { + tagId: 'Nu68JuOWAvrbzoyrOR9a7A', + publisherId: '__name__', + siteId: '__name__', + bidfloor: 0.3, + bidfloorcur: 'EUR', + // user: { + // data: '' + // } + // outstream_options: { + // slot: 'yourelementid' + // } + } + + }; + }; + + describe('isBidRequestValid', function () { + var bid; + + beforeEach(function () { + bid = getValidBidObject(); + }); + + it('should fail validation if the bid isn\'t defined or not an object', function () { + var result = spec.isBidRequestValid(); + + expect(result).to.equal(false); + + result = spec.isBidRequestValid('not an object'); + + expect(result).to.equal(false); + }); + + it('should succeed validation with all the right parameters', function () { + expect(spec.isBidRequestValid(getValidBidObject())).to.equal(true); + }); + + it('should succeed validation with mediaType and outstream_function or outstream_options', function () { + bid.mediaType = 'video'; + bid.params.outstream_function = 'outstream_func'; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + + delete bid.params.outstream_function; + bid.params.outstream_options = { + slot: 'yourelementid' + }; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should succeed with ad_unit outstream and outstream function set', function () { + bid.params.ad_unit = 'outstream'; + bid.params.outstream_function = function () {}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should succeed with mediaTypes_video_context outstream, options set for outstream and slot provided', function () { + bid.mediaTypes.video.context = 'outstream'; + bid.params.outstream_options = { + slot: 'yourelementid' + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should fail without video', function () { + delete bid.mediaTypes.video; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without playerSize', function () { + delete bid.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without tagId', function () { + delete bid.params.tagId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without publisherId', function () { + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without siteId', function () { + delete bid.params.siteId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without bidfloor', function () { + delete bid.params.bidfloor; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail without bidfloorcur', function () { + delete bid.params.bidfloorcur; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail with context outstream but no options set for outstream', function () { + bid.mediaTypes.video.context = 'outstream'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail with context outstream, options set for outstream but no slot provided', function () { + bid.mediaTypes.video.context = 'outstream'; + bid.params.outstream_options = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should succeed with context outstream, options set for outstream but no outstream_function is set', function () { + bid.mediaTypes.video.context = 'outstream'; + bid.params.outstream_options = { + slot: 'yourelementid' + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + var bid, + bidRequestObj; + + beforeEach(function () { + bid = getValidBidObject(); + bidRequestObj = { + refererInfo: { + referer: 'prebid.js' + } + }; + }); + + it('should build a very basic request', function () { + var request = spec.buildRequests([bid], bidRequestObj)[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://bid.sxp.smartclip.net/bid/1000'); + expect(request.bidRequest).to.equal(bidRequestObj); + expect(request.data.imp.id).to.match(/\d+/); + expect(request.data.imp.secure).to.equal(0); + + expect(request.data.imp.video).to.deep.equal({ + ext: { + sdk_name: 'Prebid 1+' + }, + h: '360', + w: '640', + mimes: [ + 'application/javascript', 'video/mp4', 'video/webm' + ], + api: [2], + delivery: [2], + linearity: 1, + maxbitrate: 3500, + maxduration: 500, + minbitrate: 0, + minduration: 0, + protocols: [ + 2, 3, 5, 6 + ], + startdelay: 0, + placement: 1, + pos: 1 + }); + + expect(request.data.site).to.deep.equal({ + content: 'content', + id: '__name__', + page: 'prebid.js', + cat: '', + domain: '', + publisher: { + id: '__name__' + } + }); + }); + + it('should change request parameters based on options sent', function () { + var request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.imp.video.ext).to.deep.equal({ + sdk_name: 'Prebid 1+' + }); + + expect(request.data.imp.video).to.contain({ + placement: 1 + }); + + bid.mediaTypes.video.context = 'outstream'; + + bid.params = { + outstream_options: { + foo: 'bar' + }, + outstream_function: '987', + mimes: 'foo', + linearity: 2, + minduration: 5, + maxduration: 10, + startdelay: 1, + minbitrate: 50, + maxbitrate: 500, + delivery: [1], + pos: 2, + api: [1], + protocols: [ + 2, 3, 5 + ], + bidfloor: 55, + bidfloorcur: 'foo', + at: 1, + cur: ['FOO'] + }; + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.imp.video.ext).to.deep.equal({ + sdk_name: 'Prebid 1+' + }); + + expect(request.data.imp.video).to.contain({ + minduration: 5, + maxduration: 10 + }); + + expect(request.data.imp.video.startdelay).to.equal(1); + + expect(request.data.imp.video).to.contain({ + placement: 3 + }); + + expect(request.data.imp.bidfloor).to.equal(55); + + expect(request.data.imp.bidfloorcur).to.equal('foo'); + + expect(request.data.imp.video.linearity).to.equal(2); + + expect(request.data.imp.video.minbitrate).to.equal(50); + + expect(request.data.imp.video.maxbitrate).to.equal(500); + }); + + it('should pass GDPR params', function () { + var request; + + bidRequestObj.gdprConsent = { + gdprApplies: true, + consentString: 'foo' + } + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.regs.ext.gdpr).to.equal(1); + expect(request.data.user.ext.consent).to.be.an('string'); + expect(request.data.user.ext.consent).to.equal('foo'); + }); + + it('should pass emq params', function () { + var request; + + bid.params.user = { + data: [{ + id: 'emq', + name: 'emq', + segment: [{ + name: 'emq', + value: 'foo' + }] + }] + } + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.user.data).to.deep.equal([{ + id: 'emq', + name: 'emq', + segment: { + name: 'emq', + value: 'foo' + } + }]); + }); + + it('should pass crumbs params', function () { + var request; + + bid.crumbs = { + pubcid: 'pubcid_1' + }; + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.user.ext).to.contain({ + fpc: 'pubcid_1' + }); + }); + + it('should pass linearity params', function () { + var request; + + bid.params.linearity = 3 + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.imp.video.linearity).to.equal(3); + }); + + it('should pass min and max duration params', function () { + var request; + + bid.params.minduration = 3 + bid.params.maxduration = 15 + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.imp.video.minduration).to.equal(3); + expect(request.data.imp.video.maxduration).to.equal(15); + }); + }); + + describe('interpretResponse', function () { + var serverResponse, bidderRequestObj; + + beforeEach(function () { + bidderRequestObj = { + bidRequest: { + bids: [{ + mediaTypes: { + video: { + playerSize: [ + ['400', '300'] + ] + } + }, + bidId: 123, + params: { + player_width: 400, + player_height: 300, + content_page_url: 'prebid.js', + ad_mute: 1, + outstream_options: { + foo: 'bar' + }, + outstream_function: 'function', + } + }, { + mediaTypes: { + video: { + playerSize: [ + ['200', '100'] + ] + } + }, + bidId: 124, + params: { + player_width: 200, + player_height: 100, + content_page_url: 'prebid.js', + ad_mute: 1, + outstream_options: { + foo: 'bar' + }, + outstream_function: 'function' + } + }] + } + }; + + serverResponse = { + body: { + id: 12345, + seatbid: [{ + bid: [{ + impid: 123, + cur: 'USD', + price: 12, + adomain: ['abc.com'], + crid: 321, + w: 400, + h: 300, + ext: { + slot: 'slot123' + } + }, { + impid: 124, + cur: 'USD', + price: 13, + adomain: ['def.com'], + crid: 654, + w: 200, + h: 100, + ext: { + slot: 'slot124' + } + }] + }] + } + }; + }); + + it('should return an array of bid responses', function () { + var responses = spec.interpretResponse(serverResponse, bidderRequestObj); + expect(responses).to.be.an('array').with.length(2); + expect(responses[0].requestId).to.equal(123); + expect(responses[0].currency).to.equal('USD'); + expect(responses[0].cpm).to.equal(12); + expect(responses[0].creativeId).to.equal(321); + expect(responses[0].ttl).to.equal(360); + expect(responses[0].netRevenue).to.equal(true); + expect(responses[0].mediaType).to.equal('video'); + expect(responses[0].width).to.equal(400); + expect(responses[0].height).to.equal(300); + expect(responses[1].requestId).to.equal(124); + expect(responses[1].currency).to.equal('USD'); + expect(responses[1].cpm).to.equal(13); + expect(responses[1].creativeId).to.equal(654); + expect(responses[1].ttl).to.equal(360); + expect(responses[1].netRevenue).to.equal(true); + expect(responses[1].mediaType).to.equal('video'); + expect(responses[1].width).to.equal(200); + expect(responses[1].height).to.equal(100); + }); + }); + + describe('oustreamRender', function () { + var serverResponse, bidderRequestObj; + + beforeEach(function () { + bidderRequestObj = { + bidRequest: { + bids: [{ + mediaTypes: { + video: { + context: 'outstream', + playerSize: [ + ['400', '300'] + ] + } + }, + bidId: 123, + params: { + player_width: 400, + player_height: 300, + content_page_url: 'prebid.js', + ad_mute: 1, + outstream_options: { + slot: 'slot123' + }, + outstream_function: 'function', + } + }] + } + }; + + serverResponse = { + body: { + id: 12345, + seatbid: [{ + bid: [{ + impid: 123, + cur: 'USD', + price: 12, + adomain: ['abc.com'], + crid: 321, + w: 400, + h: 300, + ext: { + slot: 'slot123' + } + }] + }] + } + }; + }); + + 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 + }) + }); + 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://dco.smartclip.net/?plc=7777777'); + + window.document.getElementById.restore(); + }); + }); +}) From e2c4e69ed6c1b81a89d0a8924a397d3abad7469f Mon Sep 17 00:00:00 2001 From: Mirko Feddern <3244291+mirkorean@users.noreply.github.com> Date: Wed, 26 Aug 2020 03:25:53 +0200 Subject: [PATCH 43/43] Add GDPR parameters to yieldlab delivery adtag (#5658) --- modules/yieldlabBidAdapter.js | 10 +++-- test/spec/modules/yieldlabBidAdapter_spec.js | 39 ++++++++++++++++++-- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index b2a9176e342..1d1636bda69 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -68,7 +68,8 @@ export const spec = { return { method: 'GET', url: `${ENDPOINT}/yp/${adslots}?${queryString}`, - validBidRequests: validBidRequests + validBidRequests: validBidRequests, + queryParams: query } }, @@ -80,6 +81,7 @@ export const spec = { interpretResponse: function (serverResponse, originalBidRequest) { const bidResponses = [] const timestamp = Date.now() + const reqParams = originalBidRequest.queryParams originalBidRequest.validBidRequests.forEach(function (bidRequest) { if (!serverResponse.body) { @@ -95,6 +97,8 @@ export const spec = { const customsize = bidRequest.params.adSize !== undefined ? parseSize(bidRequest.params.adSize) : primarysize const extId = bidRequest.params.extId !== undefined ? '&id=' + bidRequest.params.extId : '' const adType = matchedBid.adtype !== undefined ? matchedBid.adtype : '' + const gdprApplies = reqParams.gdpr ? '&gdpr=' + reqParams.gdpr : '' + const gdprConsent = reqParams.consent ? '&consent=' + reqParams.consent : '' const bidResponse = { requestId: bidRequest.bidId, @@ -107,7 +111,7 @@ export const spec = { netRevenue: false, ttl: BID_RESPONSE_TTL_SEC, referrer: '', - ad: `` + ad: `` } if (isVideo(bidRequest, adType)) { @@ -117,7 +121,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}` + bidResponse.vastUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/${customsize[0]}x${customsize[1]}?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}` if (isOutstream(bidRequest)) { const renderer = Renderer.install({ diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index 1e20343c1cd..e7a9285cb48 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -47,6 +47,16 @@ const VIDEO_RESPONSE = Object.assign({}, RESPONSE, { 'adtype': 'VIDEO' }) +const REQPARAMS = { + json: true, + ts: 1234567890 +} + +const REQPARAMS_GDPR = Object.assign({}, REQPARAMS, { + gdpr: true, + consent: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA' +}) + describe('yieldlabBidAdapter', function () { const adapter = newBidder(spec) @@ -131,7 +141,7 @@ describe('yieldlabBidAdapter', function () { }) it('should get correct bid response', function () { - const result = spec.interpretResponse({body: [RESPONSE]}, {validBidRequests: [REQUEST]}) + const result = spec.interpretResponse({body: [RESPONSE]}, {validBidRequests: [REQUEST], queryParams: REQPARAMS}) expect(result[0].requestId).to.equal('2d925f27f5079f') expect(result[0].cpm).to.equal(0.01) @@ -147,6 +157,13 @@ describe('yieldlabBidAdapter', function () { expect(result[0].ad).to.include('&id=abc') }) + it('should append gdpr parameters to adtag', function () { + const result = spec.interpretResponse({body: [RESPONSE]}, {validBidRequests: [REQUEST], queryParams: REQPARAMS_GDPR}) + + expect(result[0].ad).to.include('&gdpr=true') + expect(result[0].ad).to.include('&consent=BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA') + }) + it('should get correct bid response when passing more than one size', function () { const REQUEST2 = Object.assign({}, REQUEST, { 'sizes': [ @@ -155,7 +172,7 @@ describe('yieldlabBidAdapter', function () { [970, 90], ] }) - const result = spec.interpretResponse({body: [RESPONSE]}, {validBidRequests: [REQUEST2]}) + const result = spec.interpretResponse({body: [RESPONSE]}, {validBidRequests: [REQUEST2], queryParams: REQPARAMS}) expect(result[0].requestId).to.equal('2d925f27f5079f') expect(result[0].cpm).to.equal(0.01) @@ -179,7 +196,7 @@ describe('yieldlabBidAdapter', function () { } } }) - const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [VIDEO_REQUEST]}) + const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [VIDEO_REQUEST], queryParams: REQPARAMS}) expect(result[0].requestId).to.equal('2d925f27f5079f') expect(result[0].cpm).to.equal(0.01) @@ -188,6 +205,20 @@ describe('yieldlabBidAdapter', function () { expect(result[0].vastUrl).to.include('&id=abc') }) + it('should append gdpr parameters to vastUrl', function () { + const VIDEO_REQUEST = Object.assign({}, REQUEST, { + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + }) + const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [VIDEO_REQUEST], queryParams: REQPARAMS_GDPR}) + + expect(result[0].vastUrl).to.include('&gdpr=true') + expect(result[0].vastUrl).to.include('&consent=BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA') + }) + it('should add renderer if outstream context', function () { const OUTSTREAM_REQUEST = Object.assign({}, REQUEST, { 'mediaTypes': { @@ -197,7 +228,7 @@ describe('yieldlabBidAdapter', function () { } } }) - const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [OUTSTREAM_REQUEST]}) + const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [OUTSTREAM_REQUEST], queryParams: REQPARAMS}) expect(result[0].renderer.id).to.equal('2d925f27f5079f') expect(result[0].renderer.url).to.equal('https://ad2.movad.net/dynamic.ad?a=o193092&ma_loadEvent=ma-start-event')