From fdbbec9621c7f9b53507833fe37c1eb7eb310236 Mon Sep 17 00:00:00 2001
From: Nitin Nimbalkar <96475150+nitin0610@users.noreply.github.com>
Date: Tue, 22 Mar 2022 23:42:26 +0530
Subject: [PATCH 01/16] =?UTF-8?q?userId=20Module:=20Added=20getEncryptedSi?=
=?UTF-8?q?gnalfromSources=20and=20registerSignalsources=20fun=E2=80=A6=20?=
=?UTF-8?q?(#8117)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ESP: Added getEncrptedSignalfromSources and registerSignalsources function to send eids to GAM
* Lint issue resolved
* ESP: Changes in code based on review comments
* ESP: Changes in code based on review comments
* ESP: Example HTML added
* ESP: Removed duplicate key from example.html
* ESP: GetUserEids by source name function added and change requested done
* ESP: SignalSources name changed to encryptedSignalSources under usersync object
* ESP: Added encryptedSignalConfig in userSync object having signal sources with customSources
* ESP: ESP example HTML modified
* ESP: removed unwanted arguments/parameters
* Test cases: Module added to test getUserIdsByEidsBySource function
* ESP: Added concat function instead of Object.assign
* Removed enableSingleRequest() calling from the code - testpage
* ESP: Lint issue solved
* ESP: LGTM alert issue fixed(This is always true)
* ESP: updated userid spec file and removed unwanted code
* ESP: Added check if registerDelay timeout is undefined and gtag check
* ESP: ESP configs updated and code clean up based on review comments
* ESP: Converted normal function to arrow functions
* ESP: Test cases position changed
* ESP: Handle undefined and null check
---
integrationExamples/gpt/esp_example.html | 177 +++++++++++++++++++++++
modules/userId/eids.js | 2 +-
modules/userId/index.js | 95 +++++++++++-
test/spec/modules/userId_spec.js | 92 ++++++++++++
4 files changed, 363 insertions(+), 3 deletions(-)
create mode 100644 integrationExamples/gpt/esp_example.html
diff --git a/integrationExamples/gpt/esp_example.html b/integrationExamples/gpt/esp_example.html
new file mode 100644
index 00000000000..c39a67243cc
--- /dev/null
+++ b/integrationExamples/gpt/esp_example.html
@@ -0,0 +1,177 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Basic Prebid.js Example
+
+ Div-1
+
+
+
+
+
+
+ Div-2
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/userId/eids.js b/modules/userId/eids.js
index 0f5feca4f67..4b390c0eac0 100644
--- a/modules/userId/eids.js
+++ b/modules/userId/eids.js
@@ -1,7 +1,7 @@
import { pick, isFn, isStr, isPlainObject, deepAccess } from '../../src/utils.js';
// Each user-id sub-module is expected to mention respective config here
-const USER_IDS_CONFIG = {
+export const USER_IDS_CONFIG = {
// key-name : {config}
diff --git a/modules/userId/index.js b/modules/userId/index.js
index a50e307ecf2..e656673befb 100644
--- a/modules/userId/index.js
+++ b/modules/userId/index.js
@@ -137,7 +137,7 @@ import {getGlobal} from '../../src/prebidGlobal.js';
import {gdprDataHandler} from '../../src/adapterManager.js';
import CONSTANTS from '../../src/constants.json';
import {hook, module} from '../../src/hook.js';
-import {buildEidPermissions, createEidsArray} from './eids.js';
+import {buildEidPermissions, createEidsArray, USER_IDS_CONFIG} from './eids.js';
import {getCoreStorageManager} from '../../src/storageManager.js';
import {
cyrb53Hash,
@@ -153,7 +153,8 @@ import {
logError,
logInfo,
logWarn,
- timestamp
+ timestamp,
+ isEmpty
} from '../../src/utils.js';
const MODULE_NAME = 'User ID';
@@ -462,6 +463,20 @@ function getCombinedSubmoduleIds(submodules) {
return combinedSubmoduleIds;
}
+/**
+ * This function will return a submodule ID object for particular source name
+ * @param {SubmoduleContainer[]} submodules
+ * @param {string} sourceName
+ */
+function getSubmoduleId(submodules, sourceName) {
+ if (!Array.isArray(submodules) || !submodules.length) {
+ return {};
+ }
+ const submodule = submodules.filter(sub => isPlainObject(sub.idObj) &&
+ Object.keys(sub.idObj).length && USER_IDS_CONFIG[Object.keys(sub.idObj)[0]].source === sourceName)
+ return !isEmpty(submodule) ? submodule[0].idObj : []
+}
+
/**
* This function will create a combined object for bidder with allowed subModule Ids
* @param {SubmoduleContainer[]} submodules
@@ -616,6 +631,79 @@ function getUserIdsAsEids() {
return createEidsArray(getCombinedSubmoduleIds(initializedSubmodules));
}
+/**
+ * This function will be exposed in global-name-space so that userIds stored by Prebid UserId module can be used by external codes as well.
+ * Simple use case will be passing these UserIds to A9 wrapper solution
+ */
+
+function getUserIdsAsEidBySource(sourceName) {
+ initializeSubmodulesAndExecuteCallbacks();
+ return createEidsArray(getSubmoduleId(initializedSubmodules, sourceName))[0];
+};
+
+/**
+ * This function will be exposed in global-name-space so that userIds for a source can be exposed
+ * Sample use case is exposing this function to ESP
+ */
+function getEncryptedEidsForSource(source, encrypt, customFunction) {
+ let eidsSignals = {};
+
+ if (isFn(customFunction)) {
+ logInfo(`${MODULE_NAME} - Getting encrypted signal from custom function : ${customFunction.name} & source : ${source} `);
+ // Publishers are expected to define a common function which will be proxy for signal function.
+ const customSignals = customFunction(source);
+ eidsSignals[source] = customSignals ? encryptSignals(customSignals) : null; // by default encrypt using base64 to avoid JSON errors
+ } else {
+ // initialize signal with eids by default
+ const eid = getUserIdsAsEidBySource(source);
+ logInfo(`${MODULE_NAME} - Getting encrypted signal for eids :${JSON.stringify(eid)}`);
+ if (!isEmpty(eid)) {
+ eidsSignals[eid.source] = encrypt === true ? encryptSignals(eid) : eid.uids[0].id; // If encryption is enabled append version (1||) and encrypt entire object
+ }
+ }
+ const promise = Promise.resolve(eidsSignals[source]);
+ logInfo(`${MODULE_NAME} - Fetching encrypted eids: ${eidsSignals[source]}`);
+ return promise;
+}
+
+function encryptSignals(signals, version = 1) {
+ let encryptedSig = '';
+ switch (version) {
+ case 1: // Base64 Encryption
+ encryptedSig = typeof signals === 'object' ? window.btoa(JSON.stringify(signals)) : window.btoa(signals); // Test encryption. To be replaced with better algo
+ break;
+ default:
+ break;
+ }
+ return `${version}||${encryptedSig}`;
+}
+
+/**
+* This function will be exposed in the global-name-space so that publisher can register the signals-ESP.
+*/
+function registerSignalSources() {
+ if (!isGptPubadsDefined()) {
+ return;
+ }
+ window.googletag.encryptedSignalProviders = window.googletag.encryptedSignalProviders || [];
+ const encryptedSignalSources = config.getConfig('userSync.encryptedSignalSources');
+ if (encryptedSignalSources) {
+ const registerDelay = encryptedSignalSources.registerDelay || 0;
+ setTimeout(() => {
+ encryptedSignalSources['sources'] && encryptedSignalSources['sources'].forEach(({ source, encrypt, customFunc }) => {
+ source.forEach((src) => {
+ window.googletag.encryptedSignalProviders.push({
+ id: src,
+ collectorFunction: () => getEncryptedEidsForSource(src, encrypt, customFunc)
+ });
+ });
+ })
+ }, registerDelay)
+ } else {
+ logWarn(`${MODULE_NAME} - ESP : encryptedSignalSources config not defined under userSync Object`);
+ }
+}
+
/**
* This function will be exposed in the global-name-space so that userIds can be refreshed after initialization.
* @param {RefreshUserIdsOptions} options
@@ -889,7 +977,10 @@ export function init(config) {
// exposing getUserIds function in global-name-space so that userIds stored in Prebid can be used by external codes.
(getGlobal()).getUserIds = getUserIds;
(getGlobal()).getUserIdsAsEids = getUserIdsAsEids;
+ (getGlobal()).getEncryptedEidsForSource = getEncryptedEidsForSource;
+ (getGlobal()).registerSignalSources = registerSignalSources;
(getGlobal()).refreshUserIds = refreshUserIds;
+ (getGlobal()).getUserIdsAsEidBySource = getUserIdsAsEidBySource;
}
// init config update listener to start the application
diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js
index aabd7176471..a567c6b5371 100644
--- a/test/spec/modules/userId_spec.js
+++ b/test/spec/modules/userId_spec.js
@@ -2653,5 +2653,97 @@ describe('User ID', function () {
});
});
});
+
+ describe('handles config with ESP configuration in user sync object', function() {
+ describe('Call registerSignalSources to register signal sources with gtag', function () {
+ it('pbjs.registerSignalSources should be defined', () => {
+ expect(typeof (getGlobal()).registerSignalSources).to.equal('function');
+ });
+ })
+
+ describe('Call getEncryptedEidsForSource to get encrypted Eids for source', function() {
+ const signalSources = ['pubcid.org'];
+
+ it('pbjs.getEncryptedEidsForSource should be defined', () => {
+ expect(typeof (getGlobal()).getEncryptedEidsForSource).to.equal('function');
+ });
+
+ it('pbjs.getEncryptedEidsForSource should return the string without encryption if encryption is false', (done) => {
+ setSubmoduleRegistry([sharedIdSystemSubmodule]);
+ init(config);
+ config.setConfig({
+ userSync: {
+ syncDelay: 0,
+ userIds: [
+ {
+ 'name': 'sharedId',
+ 'storage': {
+ 'type': 'cookie',
+ 'name': '_pubcid',
+ 'expires': 365
+ }
+ },
+ {
+ 'name': 'pubcid.org'
+ }
+ ]
+ },
+ });
+ const encrypt = false;
+ (getGlobal()).getEncryptedEidsForSource(signalSources[0], encrypt).then((data) => {
+ let users = (getGlobal()).getUserIdsAsEids();
+ expect(data).to.equal(users[0].uids[0].id);
+ done();
+ }).catch(done);
+ });
+
+ it('pbjs.getEncryptedEidsForSource should return the string base64 encryption if encryption is true', (done) => {
+ const encrypt = true;
+ (getGlobal()).getEncryptedEidsForSource(signalSources[0], encrypt).then((result) => {
+ expect(result.startsWith('1||')).to.true;
+ done();
+ }).catch(done);
+ });
+
+ it('pbjs.getEncryptedEidsForSource should return string if custom function is defined', () => {
+ const getCustomSignal = () => {
+ return '{"keywords":["tech","auto"]}';
+ }
+ const expectedString = '"1||{\"keywords\":[\"tech\",\"auto\"]}"';
+ const encrypt = false;
+ const source = 'pubmatic.com';
+ (getGlobal()).getEncryptedEidsForSource(source, encrypt, getCustomSignal).then((result) => {
+ expect(result).to.equal(expectedString);
+ done();
+ });
+ });
+
+ it('pbjs.getUserIdsAsEidBySource', () => {
+ const users = {
+ 'source': 'pubcid.org',
+ 'uids': [
+ {
+ 'id': '11111',
+ 'atype': 1
+ }
+ ]
+ }
+ setSubmoduleRegistry([sharedIdSystemSubmodule, amxIdSubmodule]);
+ init(config);
+ config.setConfig({
+ userSync: {
+ syncDelay: 0,
+ userIds: [{
+ name: 'pubCommonId', value: {'pubcid': '11111'}
+ }, {
+ name: 'amxId', value: {'amxId': 'amx-id-value-amx-id-value-amx-id-value'}
+ }]
+ }
+ });
+ expect(typeof (getGlobal()).getUserIdsAsEidBySource).to.equal('function');
+ expect((getGlobal()).getUserIdsAsEidBySource(signalSources[0])).to.deep.equal(users);
+ });
+ })
+ });
})
});
From dce0ac5678de86b8de2a111127cc4b0ce94f564a Mon Sep 17 00:00:00 2001
From: johnwier <49074029+johnwier@users.noreply.github.com>
Date: Wed, 23 Mar 2022 06:46:05 -0700
Subject: [PATCH 02/16] add support for the schain option to the conversant
adapter (#8203)
---
modules/conversantBidAdapter.js | 6 ++++++
test/spec/modules/conversantBidAdapter_spec.js | 15 +++++++++++++++
2 files changed, 21 insertions(+)
diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js
index f631ca2af3d..7ee8b1b7681 100644
--- a/modules/conversantBidAdapter.js
+++ b/modules/conversantBidAdapter.js
@@ -136,6 +136,12 @@ export const spec = {
let userExt = {};
+ // pass schain object if it is present
+ const schain = deepAccess(validBidRequests, '0.schain');
+ if (schain) {
+ deepSetValue(payload, 'source.ext.schain', schain);
+ }
+
if (bidderRequest) {
// Add GDPR flag and consent string
if (bidderRequest.gdprConsent) {
diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js
index bc5cbd4134b..53169326d3b 100644
--- a/test/spec/modules/conversantBidAdapter_spec.js
+++ b/test/spec/modules/conversantBidAdapter_spec.js
@@ -3,6 +3,7 @@ import {spec, storage} from 'modules/conversantBidAdapter.js';
import * as utils from 'src/utils.js';
import {createEidsArray} from 'modules/userId/eids.js';
import { config } from '../../../src/config.js';
+import {deepAccess} from 'src/utils';
describe('Conversant adapter tests', function() {
const siteId = '108060';
@@ -373,6 +374,20 @@ describe('Conversant adapter tests', function() {
config.resetConfig();
});
+ it('Verify supply chain data', () => {
+ const bidderRequest = {refererInfo: {referer: 'http://test.com?a=b&c=123'}};
+ const schain = {complete: 1, ver: '1.0', nodes: [{asi: 'bidderA.com', sid: '00001', hp: 1}]};
+ const bidsWithSchain = bidRequests.map((bid) => {
+ return Object.assign({
+ schain: schain
+ }, bid);
+ });
+ const request = spec.buildRequests(bidsWithSchain, bidderRequest);
+ const payload = request.data;
+ expect(deepAccess(payload, 'source.ext.schain.nodes')).to.exist;
+ expect(payload.source.ext.schain.nodes[0].asi).equals(schain.nodes[0].asi);
+ });
+
it('Verify override url', function() {
const testUrl = 'https://someurl?name=value';
const request = spec.buildRequests([{params: {white_label_url: testUrl}}]);
From 5df2004b6d6110ec50b047cc9be02368531cc6eb Mon Sep 17 00:00:00 2001
From: Patrick McCann
Date: Wed, 23 Mar 2022 14:43:47 -0400
Subject: [PATCH 03/16] Update kargoBidAdapter.js (#8205)
---
modules/kargoBidAdapter.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js
index 2d8d6ed62a0..098e38b2c43 100644
--- a/modules/kargoBidAdapter.js
+++ b/modules/kargoBidAdapter.js
@@ -87,7 +87,7 @@ export const spec = {
creativeId: adUnit.id,
dealId: adUnit.targetingCustom,
netRevenue: true,
- currency: (adUnit.currency && adUnit.currency) || bidRequest.currency,
+ currency: adUnit.currency || bidRequest.currency,
meta: meta
});
}
From 88cde714b520c816b1ab45ce0550ae768768aeda Mon Sep 17 00:00:00 2001
From: Jason Lydon <95770514+ftxmoJason@users.noreply.github.com>
Date: Wed, 23 Mar 2022 15:17:53 -0400
Subject: [PATCH 04/16] Flashtalking FTRACK User ID Submodule: initial release
(#8063)
* JDB-496: first commit, copying over files from prebid-js-ftrack-module PR-1.0.0 ftrack user ID submodule code
* Addressing the lgtm alert
* Addressing the remaining lgtm alerts
* Setting VENDOR_ID to null for now
* Pulled ftrack out and used a config.param property instead to load ftrack
* Cleaning up errors raised by linter
* Tweaking a comment
* JDB-496: cleaning up docs based on PR feedback
* PR fixes
Co-authored-by: Jason Lydon
---
modules/.submodules.json | 1 +
modules/ftrackIdSystem.js | 184 ++++++++++++++++++
modules/ftrackIdSystem.md | 72 +++++++
modules/userId/eids.js | 14 ++
modules/userId/eids.md | 7 +
modules/userId/userId.md | 11 ++
test/spec/modules/ftrackIdSystem_spec.js | 238 +++++++++++++++++++++++
7 files changed, 527 insertions(+)
create mode 100644 modules/ftrackIdSystem.js
create mode 100644 modules/ftrackIdSystem.md
create mode 100644 test/spec/modules/ftrackIdSystem_spec.js
diff --git a/modules/.submodules.json b/modules/.submodules.json
index 4177646ec38..85e4658cc61 100644
--- a/modules/.submodules.json
+++ b/modules/.submodules.json
@@ -15,6 +15,7 @@
"hadronIdSystem",
"haloIdSystem",
"id5IdSystem",
+ "ftrackIdSystem",
"identityLinkIdSystem",
"idxIdSystem",
"imuIdSystem",
diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js
new file mode 100644
index 00000000000..3f2182a8b98
--- /dev/null
+++ b/modules/ftrackIdSystem.js
@@ -0,0 +1,184 @@
+/**
+ * This module adds ftrack to the User ID module
+ * The {@link module:modules/userId} module is required
+ * @module modules/ftrack
+ * @requires module:modules/userId
+ */
+
+import * as utils from '../src/utils.js';
+import { submodule } from '../src/hook.js';
+import { getStorageManager } from '../src/storageManager.js';
+import { uspDataHandler } from '../src/adapterManager.js';
+
+const MODULE_NAME = 'ftrackId';
+const LOG_PREFIX = 'FTRACK - ';
+const LOCAL_STORAGE_EXP_DAYS = 30;
+const VENDOR_ID = null;
+const LOCAL_STORAGE = 'html5';
+const FTRACK_STORAGE_NAME = 'ftrackId';
+const FTRACK_PRIVACY_STORAGE_NAME = `${FTRACK_STORAGE_NAME}_privacy`;
+const FTRACK_URL = 'https://d9.flashtalking.com/d9core';
+const storage = getStorageManager(VENDOR_ID, MODULE_NAME);
+
+let consentInfo = {
+ gdpr: {
+ applies: 0,
+ consentString: null,
+ pd: null
+ },
+ usPrivacy: {
+ value: null
+ }
+};
+
+/** @type {Submodule} */
+export const ftrackIdSubmodule = {
+ /**
+ * used to link submodule with config
+ * @type {string}
+ */
+ name: `ftrack`,
+
+ /**
+ * Decodes the 'value'
+ * @function decode (required method)
+ * @param {(Object|string)} value
+ * @param {SubmoduleConfig|undefined} config
+ * @returns {(Object|undefined)} an object with the key being ideally camel case
+ * similar to the module name and ending in id or Id
+ */
+ decode (value, config) {
+ return {
+ ftrackId: value
+ };
+ },
+
+ /**
+ * performs action(s) to obtain ids from D9 and return the Device IDs
+ * should be the only method that gets a new ID (from ajax calls or a cookie/local storage)
+ * @function getId (required method)
+ * @param {SubmoduleConfig} config
+ * @param {ConsentData} consentData
+ * @param {(Object|undefined)} cacheIdObj
+ * @returns {IdResponse|undefined}
+ */
+ getId (config, consentData, cacheIdObj) {
+ if (this.isConfigOk(config) === false || this.isThereConsent(consentData) === false) return undefined;
+
+ return {
+ callback: function () {
+ window.D9v = {
+ UserID: '99999999999999',
+ CampID: '3175',
+ CCampID: '148556'
+ };
+ window.D9r = {
+ DeviceID: true,
+ SingleDeviceID: true,
+ callback: function(response) {
+ if (response) {
+ storage.setDataInLocalStorage(`${FTRACK_STORAGE_NAME}_exp`, (new Date(Date.now() + (1000 * 60 * 60 * 24 * LOCAL_STORAGE_EXP_DAYS))).toUTCString());
+ storage.setDataInLocalStorage(`${FTRACK_STORAGE_NAME}`, JSON.stringify(response));
+
+ storage.setDataInLocalStorage(`${FTRACK_PRIVACY_STORAGE_NAME}_exp`, (new Date(Date.now() + (1000 * 60 * 60 * 24 * LOCAL_STORAGE_EXP_DAYS))).toUTCString());
+ storage.setDataInLocalStorage(`${FTRACK_PRIVACY_STORAGE_NAME}`, JSON.stringify(consentInfo));
+ };
+
+ return response;
+ }
+ };
+
+ if (config.params && config.params.url && config.params.url === FTRACK_URL) {
+ var ftrackScript = document.createElement('script');
+ ftrackScript.setAttribute('src', config.params.url);
+ window.document.body.appendChild(ftrackScript);
+ }
+ }
+ };
+ },
+
+ /**
+ * Called when IDs are already in localStorage
+ * should just be adding additional data to the cacheIdObj object
+ * @function extendId (optional method)
+ * @param {SubmoduleConfig} config
+ * @param {ConsentData} consentData
+ * @param {(Object|undefined)} cacheIdObj
+ * @returns {IdResponse|undefined}
+ */
+ extendId (config, consentData, cacheIdObj) {
+ this.isConfigOk(config);
+ return cacheIdObj;
+ },
+
+ /*
+ * Validates the config, if it is not correct, then info cannot be saved in localstorage
+ * @function isConfigOk
+ * @param {SubmoduleConfig} config from HTML
+ * @returns {true|false}
+ */
+ isConfigOk: function(config) {
+ if (!config.storage || !config.storage.type || !config.storage.name) {
+ utils.logError(LOG_PREFIX + 'config.storage required to be set.');
+ return false;
+ }
+
+ // in a future release, we may return false if storage type or name are not set as required
+ if (config.storage.type !== LOCAL_STORAGE) {
+ utils.logWarn(LOG_PREFIX + 'config.storage.type recommended to be "' + LOCAL_STORAGE + '".');
+ }
+ // in a future release, we may return false if storage type or name are not set as required
+ if (config.storage.name !== FTRACK_STORAGE_NAME) {
+ utils.logWarn(LOG_PREFIX + 'config.storage.name recommended to be "' + FTRACK_STORAGE_NAME + '".');
+ }
+
+ if (!config.hasOwnProperty('params') || !config.params.hasOwnProperty('url') || config.params.url !== FTRACK_URL) {
+ utils.logWarn(LOG_PREFIX + 'config.params.url is required for ftrack to run. Url should be "' + FTRACK_URL + '".');
+ return false;
+ }
+
+ return true;
+ },
+
+ isThereConsent: function(consentData) {
+ let consentValue = true;
+
+ /*
+ * Scenario 1: GDPR
+ * if GDPR Applies is true|1, we do not have consent
+ * if GDPR Applies does not exist or is false|0, we do not NOT have consent
+ */
+ if (consentData && consentData.gdprApplies && (consentData.gdprApplies === true || consentData.gdprApplies === 1)) {
+ consentInfo.gdpr.applies = 1;
+ consentValue = false;
+ }
+ // If consentString exists, then we store it even though we are not using it
+ if (consentData && consentData.consentString !== 'undefined' && !utils.isEmpty(consentData.consentString) && !utils.isEmptyStr(consentData.consentString)) {
+ consentInfo.gdpr.consentString = consentData.consentString;
+ }
+
+ /*
+ * Scenario 2: CCPA/us_privacy
+ * if usp exists (assuming this check determines the location of the device to be within the California)
+ * parse the us_privacy string to see if we have consent
+ * for version 1 of us_privacy strings, if 'Opt-Out Sale' is 'Y' we do not track
+ */
+ const usp = uspDataHandler.getConsentData();
+ let usPrivacyVersion;
+ // let usPrivacyOptOut;
+ let usPrivacyOptOutSale;
+ // let usPrivacyLSPA;
+ if (typeof usp !== 'undefined' && !utils.isEmpty(usp) && !utils.isEmptyStr(usp)) {
+ consentInfo.usPrivacy.value = usp;
+ usPrivacyVersion = usp[0];
+ // usPrivacyOptOut = usp[1];
+ usPrivacyOptOutSale = usp[2];
+ // usPrivacyLSPA = usp[3];
+ }
+ if (usPrivacyVersion == 1 && usPrivacyOptOutSale === 'Y') consentValue = false;
+
+ return consentValue;
+ }
+};
+
+submodule('userId', ftrackIdSubmodule);
diff --git a/modules/ftrackIdSystem.md b/modules/ftrackIdSystem.md
new file mode 100644
index 00000000000..c5f255c2fc2
--- /dev/null
+++ b/modules/ftrackIdSystem.md
@@ -0,0 +1,72 @@
+# Flashtalking's FTrack Identity Framework User ID Module
+
+*The FTrack Identity Framework User ID Module allows publishers to take advantage of Flashtalking's FTrack ID during the bidding process.*
+
+### [FTrack](https://www.flashtalking.com/identity-framework#FTrack)
+
+Flashtalking’s cookieless tracking technology uses probabilistic device recognition to derive a privacy-friendly persistent ID for each device.
+
+**ANTI-FINGERPRINTING**
+FTrack operates in strict compliance with [Google’s definition of anti-fingerprinting](https://blog.google/products/ads-commerce/2021-01-privacy-sandbox/). FTrack does not access PII or sensitive information and provides consumers with notification and choice on every impression. We do not participate in the types of activities that most concern privacy advocates (profiling consumers, building audience segments, and/or monetizing consumer data).
+
+**GDPR COMPLIANT**
+Flashtalking is integrated with the IAB EU’s Transparency & Consent Framework (TCF) and operates on a Consent legal basis where required. As a Data Processor under GDPR, Flashtalking does not combine data across customers nor sell data to third parties.
+
+---
+
+### Support or Maintenance:
+
+Questions? Comments? Bugs? Praise? Please contact FlashTalking's Prebid Support at [prebid-support@flashtalking.com](mailto:prebid-support@flashtalking.com)
+
+---
+
+### FTrack User ID Configuration
+
+The following configuration parameters are available:
+
+```javascript
+pbjs.setConfig({
+ userSync: {
+ userIds: [{
+ name: 'FTrack',
+ params: {
+ url: 'https://d9.flashtalking.com/d9core' // required, if not populated ftrack will not run
+ },
+ storage: {
+ type: 'html5', // "html5" is the required storage type
+ name: 'FTrackId', // "FTrackId" is the required storage name
+ expires: 90, // storage lasts for 90 days
+ refreshInSeconds: 8*3600 // refresh ID every 8 hours to ensure it's fresh
+ }
+ }],
+ auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules
+ }
+});
+```
+
+| Param under userSync.userIds[] | Scope | Type | Description | Example |
+| :-- | :-- | :-- | :-- | :-- |
+| name | Required | String | The name of this module: `"FTrack"` | `"FTrack"` |
+| storage | Required | Object | Storage settings for how the User ID module will cache the FTrack ID locally | |
+| storage.type | Required | String | This is where the results of the user ID will be stored. FTrack **requires** `"html5"`. | `"html5"` |
+| storage.name | Required | String | The name of the local storage where the user ID will be stored. FTrack **requires** `"FTrackId"`. | `"FTrackId"` |
+| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. FTrack recommends `90`. | `90` |
+| storage.refreshInSeconds | Optional | Integer | How many seconds until the FTrack ID will be refreshed. FTrack strongly recommends 8 hours between refreshes | `8*3600` |
+
+---
+
+### Privacy Policies.
+
+Complete information available on the Flashtalking [privacy policy page](https://www.flashtalking.com/privacypolicy).
+
+#### OPTING OUT OF INTEREST-BASED ADVERTISING & COLLECTION OF PERSONAL INFORMATION
+
+Please visit our [Opt Out Page](https://www.flashtalking.com/optout).
+
+#### REQUEST REMOVAL OF YOUR PERSONAL DATA (WHERE APPLICABLE)
+
+You may request by emailing [mailto:privacy@flashtalking.com](privacy@flashtalking.com).
+
+#### GDPR
+
+In its current state, Flashtalking’s FTrack Identity Framework User ID Module does not create an ID if a user's consentData is "truthy" (true, 1). In other words, if GDPR applies in any way to a user, FTrack does not create an ID.
\ No newline at end of file
diff --git a/modules/userId/eids.js b/modules/userId/eids.js
index 4b390c0eac0..5d138795bd8 100644
--- a/modules/userId/eids.js
+++ b/modules/userId/eids.js
@@ -63,6 +63,20 @@ export const USER_IDS_CONFIG = {
}
},
+ // ftrack
+ 'ftrackId': {
+ source: 'flashtalking.com',
+ atype: 1,
+ getValue: function(data) {
+ return data.uid
+ },
+ getUidExt: function(data) {
+ if (data.ext) {
+ return data.ext;
+ }
+ }
+ },
+
// parrableId
'parrableId': {
source: 'parrable.com',
diff --git a/modules/userId/eids.md b/modules/userId/eids.md
index 4c516d5441c..a61ef66c56c 100644
--- a/modules/userId/eids.md
+++ b/modules/userId/eids.md
@@ -65,6 +65,13 @@ userIdAsEids = [
}]
},
+ {
+ source: 'flashtalking.com',
+ uids: [{
+ id: 'some-random-id-value',
+ atype: 1
+ },
+
{
source: 'parrable.com',
uids: [{
diff --git a/modules/userId/userId.md b/modules/userId/userId.md
index 93edc21bb66..ee49211e4cb 100644
--- a/modules/userId/userId.md
+++ b/modules/userId/userId.md
@@ -45,6 +45,17 @@ pbjs.setConfig({
expires: 90, // Expiration in days
refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires'
},
+ }, {
+ name: "ftrackId",
+ storage: {
+ type: "html5",
+ name: "ftrackId",
+ expires: 90,
+ refreshInSeconds: 8*3600
+ },
+ params: {
+ url: 'https://d9.flashtalking.com/d9core', // required, if not populated ftrack will not run
+ }
}, {
name: 'parrableId',
params: {
diff --git a/test/spec/modules/ftrackIdSystem_spec.js b/test/spec/modules/ftrackIdSystem_spec.js
new file mode 100644
index 00000000000..69d66d75bb1
--- /dev/null
+++ b/test/spec/modules/ftrackIdSystem_spec.js
@@ -0,0 +1,238 @@
+import { ftrackIdSubmodule } from 'modules/ftrackIdSystem.js';
+import * as utils from 'src/utils.js';
+import { uspDataHandler } from 'src/adapterManager.js';
+let expect = require('chai').expect;
+
+let server;
+
+let configMock = {
+ name: 'ftrack',
+ params: {
+ url: 'https://d9.flashtalking.com/d9core'
+ },
+ storage: {
+ name: 'ftrackId',
+ type: 'html5',
+ expires: 90,
+ refreshInSeconds: 8 * 3600
+ },
+ debug: true
+};
+
+let consentDataMock = {
+ gdprApplies: 0,
+ consentString: ''
+};
+
+describe('FTRACK ID System', () => {
+ describe(`Global Module Rules`, () => {
+ it(`should not use the "PREBID_GLOBAL" variable nor otherwise obtain a pointer to the global PBJS object`, () => {
+ expect((/PREBID_GLOBAL/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok;
+ });
+ });
+
+ describe('ftrackIdSubmodule.isConfigOk():', () => {
+ let logWarnStub;
+ let logErrorStub;
+
+ beforeEach(() => {
+ logWarnStub = sinon.stub(utils, 'logWarn');
+ logErrorStub = sinon.stub(utils, 'logError');
+ });
+
+ afterEach(() => {
+ logWarnStub.restore();
+ logErrorStub.restore();
+ });
+
+ it(`should be rejected if 'config.storage' property is missing`, () => {
+ let configMock1 = JSON.parse(JSON.stringify(configMock));
+ delete configMock1.storage;
+ delete configMock1.params;
+
+ ftrackIdSubmodule.isConfigOk(configMock1);
+ expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`);
+ });
+
+ it(`should be rejected if 'config.storage.name' property is missing`, () => {
+ let configMock1 = JSON.parse(JSON.stringify(configMock));
+ delete configMock1.storage.name;
+
+ ftrackIdSubmodule.isConfigOk(configMock1);
+ expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`);
+ });
+
+ it(`should be rejected if 'config.storage.name' is not 'ftrackId'`, () => {
+ let configMock1 = JSON.parse(JSON.stringify(configMock));
+ configMock1.storage.name = 'not-ftrack';
+
+ ftrackIdSubmodule.isConfigOk(configMock1);
+ expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.storage.name recommended to be "ftrackId".`);
+ });
+
+ it(`should be rejected if 'congig.storage.type' property is missing`, () => {
+ let configMock1 = JSON.parse(JSON.stringify(configMock));
+ delete configMock1.storage.type;
+
+ ftrackIdSubmodule.isConfigOk(configMock1);
+ expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`);
+ });
+
+ it(`should be rejected if 'config.storage.type' is not 'html5'`, () => {
+ let configMock1 = JSON.parse(JSON.stringify(configMock));
+ configMock1.storage.type = 'not-html5';
+
+ ftrackIdSubmodule.isConfigOk(configMock1);
+ expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.storage.type recommended to be "html5".`);
+ });
+
+ it(`should be rejected if 'config.params.url' does not exist`, () => {
+ let configMock1 = JSON.parse(JSON.stringify(configMock));
+ delete configMock1.params.url;
+
+ ftrackIdSubmodule.isConfigOk(configMock1);
+ expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.params.url is required for ftrack to run. Url should be "https://d9.flashtalking.com/d9core".`);
+ });
+
+ it(`should be rejected if 'storage.param.url' does not exist or is not 'https://d9.flashtalking.com/d9core'`, () => {
+ let configMock1 = JSON.parse(JSON.stringify(configMock));
+ configMock1.params.url = 'https://d9.NOT.flashtalking.com/d9core';
+
+ ftrackIdSubmodule.isConfigOk(configMock1);
+ expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.params.url is required for ftrack to run. Url should be "https://d9.flashtalking.com/d9core".`);
+ });
+ });
+
+ describe(`ftrackIdSubmodule.isThereConsent():`, () => {
+ let uspDataHandlerStub;
+ beforeEach(() => {
+ uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData');
+ });
+
+ afterEach(() => {
+ uspDataHandlerStub.restore();
+ });
+
+ describe(`returns 'false' if:`, () => {
+ it(`GDPR: if gdprApplies is truthy`, () => {
+ expect(ftrackIdSubmodule.isThereConsent({gdprApplies: 1})).to.not.be.ok;
+ expect(ftrackIdSubmodule.isThereConsent({gdprApplies: true})).to.not.be.ok;
+ });
+
+ it(`US_PRIVACY version 1: if 'Opt Out Sale' is 'Y'`, () => {
+ uspDataHandlerStub.returns('1YYY');
+ expect(ftrackIdSubmodule.isThereConsent({})).to.not.be.ok;
+ });
+ });
+
+ describe(`returns 'true' if`, () => {
+ it(`GDPR: if gdprApplies is undefined, false or 0`, () => {
+ expect(ftrackIdSubmodule.isThereConsent({gdprApplies: 0})).to.be.ok;
+ expect(ftrackIdSubmodule.isThereConsent({gdprApplies: false})).to.be.ok;
+ expect(ftrackIdSubmodule.isThereConsent({gdprApplies: null})).to.be.ok;
+ expect(ftrackIdSubmodule.isThereConsent({})).to.be.ok;
+ });
+
+ it(`US_PRIVACY version 1: if 'Opt Out Sale' is not 'Y' ('N','-')`, () => {
+ uspDataHandlerStub.returns('1NNN');
+ expect(ftrackIdSubmodule.isThereConsent(null)).to.be.ok;
+
+ uspDataHandlerStub.returns('1---');
+ expect(ftrackIdSubmodule.isThereConsent(null)).to.be.ok;
+ });
+ });
+ });
+
+ describe('getId() method', () => {
+ it(`should be using the StorageManager to set cookies or localstorage, as opposed to doing it directly`, () => {
+ expect((/localStorage/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok;
+ expect((/cookie/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok;
+ });
+
+ it(`should be the only method that gets a new ID aka hits the D9 endpoint`, () => {
+ let appendChildStub = sinon.stub(window.document.body, 'appendChild');
+
+ ftrackIdSubmodule.getId(configMock, null, null).callback();
+ expect(window.document.body.appendChild.called).to.be.ok;
+ let actualScriptTag = window.document.body.appendChild.args[0][0];
+ expect(actualScriptTag.tagName.toLowerCase()).to.equal('script');
+ expect(actualScriptTag.getAttribute('src')).to.equal('https://d9.flashtalking.com/d9core');
+ appendChildStub.resetHistory();
+
+ ftrackIdSubmodule.decode('value', configMock);
+ expect(window.document.body.appendChild.called).to.not.be.ok;
+ expect(window.document.body.appendChild.args).to.deep.equal([]);
+ appendChildStub.resetHistory();
+
+ ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}});
+ expect(window.document.body.appendChild.called).to.not.be.ok;
+ expect(window.document.body.appendChild.args).to.deep.equal([]);
+
+ appendChildStub.restore();
+ });
+
+ it(`should populate localstorage and return the IDS (end-to-end test)`, () => {
+ let ftrackId,
+ ftrackIdExp,
+ forceCallback = false;
+
+ // Confirm that our item is not in localStorage yet
+ expect(window.localStorage.getItem('ftrack-rtd')).to.not.be.ok;
+ expect(window.localStorage.getItem('ftrack-rtd_exp')).to.not.be.ok;
+
+ ftrackIdSubmodule.getId(configMock, consentDataMock, null).callback();
+ return new Promise(function(resolve, reject) {
+ window.testTimer = function () {
+ // Sinon fake server is NOT changing the readyState to 4, so instead
+ // we are forcing the callback to run and just passing in the expected Object
+ if (!forceCallback && window.hasOwnProperty('D9r')) {
+ window.D9r.callback({ 'DeviceID': [''], 'SingleDeviceID': [''] });
+ forceCallback = true;
+ }
+
+ ftrackId = window.localStorage.getItem('ftrackId');
+ ftrackIdExp = window.localStorage.getItem('ftrackId_exp');
+
+ if (!!ftrackId && !!ftrackIdExp) {
+ expect(window.localStorage.getItem('ftrackId')).to.be.ok;
+ expect(window.localStorage.getItem('ftrackId_exp')).to.be.ok;
+ resolve();
+ } else {
+ window.setTimeout(window.testTimer, 25);
+ }
+ };
+ window.testTimer();
+ });
+ });
+ });
+
+ describe(`decode() method`, () => {
+ it(`should respond with an object with the key 'ftrackId'`, () => {
+ expect(ftrackIdSubmodule.decode('value', configMock)).to.deep.equal({ftrackId: 'value'});
+ });
+
+ it(`should not be making requests to retrieve a new ID, it should just be decoding a response`, () => {
+ server = sinon.createFakeServer();
+ ftrackIdSubmodule.decode('value', configMock);
+
+ expect(server.requests).to.have.length(0);
+
+ server.restore();
+ })
+ });
+
+ describe(`extendId() method`, () => {
+ it(`should not be making requests to retrieve a new ID, it should just be adding additional data to the id object`, () => {
+ server = sinon.createFakeServer();
+ ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}});
+
+ expect(server.requests).to.have.length(0);
+
+ server.restore();
+ });
+
+ it(`should return cacheIdObj`, () => {
+ expect(ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}})).to.deep.equal({cache: {id: ''}});
+ });
+ });
+});
From 46306fee5b31c3bda9d62e01cc875b1fd6a9a916 Mon Sep 17 00:00:00 2001
From: Pavlo Kyrylenko
Date: Wed, 23 Mar 2022 21:21:27 +0200
Subject: [PATCH 05/16] Id ward RTD Module: initial release (#8076)
* added idWardRtdProvider module
* added idward_segments_example.html to test module
* improver description
* added 'Testing' section to readme
* added missing function description
* show sent FPD at the example html page
* review comments resolved
Co-authored-by: Pavlo
---
.../gpt/idward_segments_example.html | 112 +++++++++++++++++
modules/idWardRtdProvider.js | 105 ++++++++++++++++
modules/idWardRtdProvider.md | 44 +++++++
test/spec/modules/idWardRtdProvider_spec.js | 113 ++++++++++++++++++
4 files changed, 374 insertions(+)
create mode 100644 integrationExamples/gpt/idward_segments_example.html
create mode 100644 modules/idWardRtdProvider.js
create mode 100644 modules/idWardRtdProvider.md
create mode 100644 test/spec/modules/idWardRtdProvider_spec.js
diff --git a/integrationExamples/gpt/idward_segments_example.html b/integrationExamples/gpt/idward_segments_example.html
new file mode 100644
index 00000000000..9bc06124c77
--- /dev/null
+++ b/integrationExamples/gpt/idward_segments_example.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+Prebid.js Test
+Div-1
+
+
+
+First Party Data (ortb2) Sent to Bidding Adapter
+
+
+
diff --git a/modules/idWardRtdProvider.js b/modules/idWardRtdProvider.js
new file mode 100644
index 00000000000..454902a6b9a
--- /dev/null
+++ b/modules/idWardRtdProvider.js
@@ -0,0 +1,105 @@
+/**
+ * This module adds the ID Ward RTD provider to the real time data module
+ * The {@link module:modules/realTimeData} module is required
+ * The module will poulate real-time data from ID Ward
+ * @module modules/idWardRtdProvider
+ * @requires module:modules/realTimeData
+ */
+import {config} from '../src/config.js';
+import {getStorageManager} from '../src/storageManager.js';
+import {submodule} from '../src/hook.js';
+import {isPlainObject, mergeDeep, logMessage, logError} from '../src/utils.js';
+
+const MODULE_NAME = 'realTimeData';
+const SUBMODULE_NAME = 'idWard';
+
+export const storage = getStorageManager(null, SUBMODULE_NAME);
+
+/**
+ * Add real-time data & merge segments.
+ * @param {Object} rtd
+ */
+function addRealTimeData(rtd) {
+ if (isPlainObject(rtd.ortb2)) {
+ const ortb2 = config.getConfig('ortb2') || {};
+ logMessage('idWardRtdProvider: merging original: ', ortb2);
+ logMessage('idWardRtdProvider: merging in: ', rtd.ortb2);
+ config.setConfig({ortb2: mergeDeep(ortb2, rtd.ortb2)});
+ }
+}
+
+/**
+ * Try parsing stringified array of segment IDs.
+ * @param {String} data
+ */
+function tryParse(data) {
+ try {
+ return JSON.parse(data);
+ } catch (err) {
+ logError(`idWardRtdProvider: failed to parse json:`, data);
+ return null;
+ }
+}
+
+/**
+ * Real-time data retrieval from ID Ward
+ * @param {Object} reqBidsConfigObj
+ * @param {function} onDone
+ * @param {Object} rtdConfig
+ * @param {Object} userConsent
+ */
+export function getRealTimeData(reqBidsConfigObj, onDone, rtdConfig, userConsent) {
+ if (rtdConfig && isPlainObject(rtdConfig.params)) {
+ const jsonData = storage.getDataFromLocalStorage(rtdConfig.params.cohortStorageKey)
+
+ if (!jsonData) {
+ return;
+ }
+
+ const segments = tryParse(jsonData);
+
+ if (segments) {
+ const udSegment = {
+ name: 'id-ward.com',
+ ext: {
+ segtax: rtdConfig.params.segtax
+ },
+ segment: segments.map(x => ({id: x}))
+ }
+
+ logMessage('idWardRtdProvider: user.data.segment: ', udSegment);
+ const data = {
+ rtd: {
+ ortb2: {
+ user: {
+ data: [
+ udSegment
+ ]
+ }
+ }
+ }
+ };
+ addRealTimeData(data.rtd);
+ onDone();
+ }
+ }
+}
+
+/**
+ * Module init
+ * @param {Object} provider
+ * @param {Object} userConsent
+ * @return {boolean}
+ */
+function init(provider, userConsent) {
+ return true;
+}
+
+/** @type {RtdSubmodule} */
+export const idWardRtdSubmodule = {
+ name: SUBMODULE_NAME,
+ getBidRequestData: getRealTimeData,
+ init: init
+};
+
+submodule(MODULE_NAME, idWardRtdSubmodule);
diff --git a/modules/idWardRtdProvider.md b/modules/idWardRtdProvider.md
new file mode 100644
index 00000000000..5a44bfa49f3
--- /dev/null
+++ b/modules/idWardRtdProvider.md
@@ -0,0 +1,44 @@
+### Overview
+
+ID Ward is a data anonymization technology for privacy-preserving advertising. Publishers and advertisers are able to target and retarget custom audience segments covering 100% of consented audiences.
+ID Ward’s Real-time Data Provider automatically obtains segment IDs from the ID Ward on-domain script (via localStorage) and passes them to the bid-stream.
+
+### Integration
+
+ 1) Build the idWardRtd module into the Prebid.js package with:
+
+ ```
+ gulp build --modules=idWardRtdProvider,...
+ ```
+
+ 2) Use `setConfig` to instruct Prebid.js to initilaize the idWardRtdProvider module, as specified below.
+
+### Configuration
+
+```
+ pbjs.setConfig({
+ realTimeData: {
+ dataProviders: [
+ {
+ name: "idWard",
+ waitForIt: true,
+ params: {
+ cohortStorageKey: "cohort_ids",
+ segtax: ,
+ }
+ }
+ ]
+ }
+ });
+ ```
+
+Please note that idWardRtdProvider should be integrated into the publisher website along with the [ID Ward Pixel](https://publishers-web.id-ward.com/pixel-integration).
+Please reach out to Id Ward representative(support@id-ward.com) if you have any questions or need further help to integrate Prebid, idWardRtdProvider, and Id Ward Pixel
+
+### Testing
+To view an example of available segments returned by Id Ward:
+```
+‘gulp serve --modules=rtdModule,idWardRtdProvider,pubmaticBidAdapter
+```
+and then point your browser at:
+"http://localhost:9999/integrationExamples/gpt/idward_segments_example.html"
diff --git a/test/spec/modules/idWardRtdProvider_spec.js b/test/spec/modules/idWardRtdProvider_spec.js
new file mode 100644
index 00000000000..949365baec6
--- /dev/null
+++ b/test/spec/modules/idWardRtdProvider_spec.js
@@ -0,0 +1,113 @@
+import {config} from 'src/config.js';
+import {getRealTimeData, idWardRtdSubmodule, storage} from 'modules/idWardRtdProvider.js';
+
+describe('idWardRtdProvider', function() {
+ let getDataFromLocalStorageStub;
+
+ const testReqBidsConfigObj = {
+ adUnits: [
+ {
+ bids: ['bid1', 'bid2']
+ }
+ ]
+ };
+
+ const onDone = function() { return true };
+
+ const cmoduleConfig = {
+ 'name': 'idWard',
+ 'params': {
+ 'cohortStorageKey': 'cohort_ids'
+ }
+ }
+
+ beforeEach(function() {
+ config.resetConfig();
+ getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage')
+ });
+
+ afterEach(function () {
+ getDataFromLocalStorageStub.restore();
+ });
+
+ describe('idWardRtdSubmodule', function() {
+ it('successfully instantiates', function () {
+ expect(idWardRtdSubmodule.init()).to.equal(true);
+ });
+ });
+
+ describe('Get Real-Time Data', function() {
+ it('gets rtd from local storage', function() {
+ const rtdConfig = {
+ params: {
+ cohortStorageKey: 'cohort_ids',
+ segtax: 503
+ }
+ };
+
+ const bidConfig = {};
+
+ const rtdUserObj1 = {
+ name: 'id-ward.com',
+ ext: {
+ segtax: 503
+ },
+ segment: [
+ {
+ id: 'TCZPQOWPEJG3MJOTUQUF793A'
+ },
+ {
+ id: '93SUG3H540WBJMYNT03KX8N3'
+ }
+ ]
+ };
+
+ getDataFromLocalStorageStub.withArgs('cohort_ids')
+ .returns(JSON.stringify(['TCZPQOWPEJG3MJOTUQUF793A', '93SUG3H540WBJMYNT03KX8N3']));
+
+ expect(config.getConfig().ortb2).to.be.undefined;
+ getRealTimeData(bidConfig, () => {}, rtdConfig, {});
+ expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]);
+ });
+
+ it('do not set rtd if local storage empty', function() {
+ const rtdConfig = {
+ params: {
+ cohortStorageKey: 'cohort_ids',
+ segtax: 503
+ }
+ };
+
+ const bidConfig = {};
+
+ getDataFromLocalStorageStub.withArgs('cohort_ids')
+ .returns(null);
+
+ expect(config.getConfig().ortb2).to.be.undefined;
+ getRealTimeData(bidConfig, () => {}, rtdConfig, {});
+ expect(config.getConfig().ortb2).to.be.undefined;
+ });
+
+ it('do not set rtd if local storage has incorrect value', function() {
+ const rtdConfig = {
+ params: {
+ cohortStorageKey: 'cohort_ids',
+ segtax: 503
+ }
+ };
+
+ const bidConfig = {};
+
+ getDataFromLocalStorageStub.withArgs('cohort_ids')
+ .returns('wrong cohort ids value');
+
+ expect(config.getConfig().ortb2).to.be.undefined;
+ getRealTimeData(bidConfig, () => {}, rtdConfig, {});
+ expect(config.getConfig().ortb2).to.be.undefined;
+ });
+
+ it('should initalise and return with config', function () {
+ expect(getRealTimeData(testReqBidsConfigObj, onDone, cmoduleConfig)).to.equal(undefined)
+ });
+ });
+});
From af4b71c0f34ddecfb0a34ad6c505b4b7933cb3c5 Mon Sep 17 00:00:00 2001
From: Timothy Ace
Date: Wed, 23 Mar 2022 15:28:49 -0400
Subject: [PATCH 06/16] Synacormedia Bid Adapter: ttl and eid update (#8006)
* CAP-2474 Synacor Media Bid Adapter: Use OpenRTB parameters to populate bids' `ttl` values
* CAP-2474 Revised bid expiration/ttl code to conform to revised requirements w/ adserver tag expiration
* CAP-2474 Added tests and cleaned up tag expiration code
* feature/CAP-2516: Remove the filtering of eids for synacormediaBidAdapter
Co-authored-by: Andrew Fuchs
Co-authored-by: Timothy M. Ace
Co-authored-by: Tim Ace
---
modules/synacormediaBidAdapter.js | 34 ++++-----
.../modules/synacormediaBidAdapter_spec.js | 73 +++++++++++++++++--
2 files changed, 83 insertions(+), 24 deletions(-)
diff --git a/modules/synacormediaBidAdapter.js b/modules/synacormediaBidAdapter.js
index 5f0057276a7..4cc648a2e04 100644
--- a/modules/synacormediaBidAdapter.js
+++ b/modules/synacormediaBidAdapter.js
@@ -14,12 +14,7 @@ const BLOCKED_AD_SIZES = [
'1x1',
'1x2'
];
-const SUPPORTED_USER_ID_SOURCES = [
- 'liveramp.com', // Liveramp IdentityLink
- 'nextroll.com', // NextRoll XID
- 'verizonmedia.com', // Verizon Media ConnectID
- 'pubcid.org' // PubCommon ID
-];
+const DEFAULT_MAX_TTL = 420; // 7 minutes
export const spec = {
code: 'synacormedia',
supportedMediaTypes: [ BANNER, VIDEO ],
@@ -95,7 +90,7 @@ export const spec = {
// User ID
if (validBidReqs[0] && validBidReqs[0].userIdAsEids && Array.isArray(validBidReqs[0].userIdAsEids)) {
- const eids = this.processEids(validBidReqs[0].userIdAsEids);
+ const eids = validBidReqs[0].userIdAsEids;
if (eids.length) {
deepSetValue(openRtbBidRequest, 'user.ext.eids', eids);
}
@@ -114,16 +109,6 @@ export const spec = {
}
},
- processEids: function(userIdAsEids) {
- const eids = [];
- userIdAsEids.forEach(function(eid) {
- if (SUPPORTED_USER_ID_SOURCES.indexOf(eid.source) > -1) {
- eids.push(eid);
- }
- });
- return eids;
- },
-
buildBannerImpressions: function (adSizes, bid, tagIdOrPlacementId, pos, videoOrBannerKey) {
let format = [];
let imps = [];
@@ -248,6 +233,19 @@ export const spec = {
}
});
}
+
+ let maxTtl = DEFAULT_MAX_TTL;
+ if (bid.ext && bid.ext['imds.tv'] && bid.ext['imds.tv'].ttl) {
+ const bidTtlMax = parseInt(bid.ext['imds.tv'].ttl, 10);
+ maxTtl = !isNaN(bidTtlMax) && bidTtlMax > 0 ? bidTtlMax : DEFAULT_MAX_TTL;
+ }
+
+ let ttl = maxTtl;
+ if (bid.exp) {
+ const bidTtl = parseInt(bid.exp, 10);
+ ttl = !isNaN(bidTtl) && bidTtl > 0 ? Math.min(bidTtl, maxTtl) : maxTtl;
+ }
+
const bidObj = {
requestId: impid,
cpm: parseFloat(bid.price),
@@ -258,7 +256,7 @@ export const spec = {
netRevenue: true,
mediaType: isVideo ? VIDEO : BANNER,
ad: creative,
- ttl: 60
+ ttl,
};
if (bid.adomain != undefined || bid.adomain != null) {
diff --git a/test/spec/modules/synacormediaBidAdapter_spec.js b/test/spec/modules/synacormediaBidAdapter_spec.js
index c053771c296..b9a02799219 100644
--- a/test/spec/modules/synacormediaBidAdapter_spec.js
+++ b/test/spec/modules/synacormediaBidAdapter_spec.js
@@ -244,6 +244,13 @@ describe('synacormediaBidAdapter ', function () {
rtiPartner: 'TDID'
}
}]
+ },
+ {
+ source: 'neustar.biz',
+ uids: [{
+ id: 'neustar809-044-23njhwer3',
+ atype: 1
+ }]
}
];
@@ -989,7 +996,7 @@ describe('synacormediaBidAdapter ', function () {
netRevenue: true,
mediaType: 'video',
ad: '\n\n\n\nSynacor Media Ad Server - 9999\nhttps://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45\n\n\n',
- ttl: 60,
+ ttl: 420,
meta: { advertiserDomains: ['psacentral.org'] },
videoCacheKey: 'QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk',
vastUrl: 'https://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45'
@@ -1010,7 +1017,7 @@ describe('synacormediaBidAdapter ', function () {
netRevenue: true,
mediaType: BANNER,
ad: '',
- ttl: 60
+ ttl: 420
});
});
@@ -1032,7 +1039,7 @@ describe('synacormediaBidAdapter ', function () {
netRevenue: true,
mediaType: BANNER,
ad: '',
- ttl: 60
+ ttl: 420
});
expect(resp[1]).to.eql({
@@ -1045,7 +1052,7 @@ describe('synacormediaBidAdapter ', function () {
netRevenue: true,
mediaType: BANNER,
ad: '',
- ttl: 60
+ ttl: 420
});
});
@@ -1156,7 +1163,7 @@ describe('synacormediaBidAdapter ', function () {
netRevenue: true,
mediaType: 'video',
ad: '\n\n\n\nSynacor Media Ad Server - 9999\nhttps://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45\n\n\n',
- ttl: 60,
+ ttl: 420,
meta: { advertiserDomains: ['psacentral.org'] },
videoCacheKey: 'QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk',
vastUrl: 'https://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45'
@@ -1209,9 +1216,63 @@ describe('synacormediaBidAdapter ', function () {
netRevenue: true,
mediaType: BANNER,
ad: '',
- ttl: 60
+ ttl: 420
});
});
+
+ it('should return ttl equal to DEFAULT_TTL_MAX if bid.exp and bid.ext["imds.tv"].ttl are both undefined', function() {
+ const br = { ...bidResponse };
+ serverResponse.body.seatbid[0].bid.push(br);
+ const resp = spec.interpretResponse(serverResponse, bidRequest);
+ expect(resp).to.be.an('array').to.have.lengthOf(1);
+ expect(resp[0]).to.have.property('ttl');
+ expect(resp[0].ttl).to.equal(420);
+ });
+
+ it('should return ttl equal to bid.ext["imds.tv"].ttl if it is defined but bid.exp is undefined', function() {
+ let br = { ext: { 'imds.tv': { ttl: 4321 } }, ...bidResponse };
+ serverResponse.body.seatbid[0].bid.push(br);
+ let resp = spec.interpretResponse(serverResponse, bidRequest);
+ expect(resp).to.be.an('array').to.have.lengthOf(1);
+ expect(resp[0]).to.have.property('ttl');
+ expect(resp[0].ttl).to.equal(4321);
+ });
+
+ it('should return ttl equal to bid.exp if bid.exp is less than or equal to DEFAULT_TTL_MAX and bid.ext["imds.tv"].ttl is undefined', function() {
+ const br = { exp: 123, ...bidResponse };
+ serverResponse.body.seatbid[0].bid.push(br);
+ const resp = spec.interpretResponse(serverResponse, bidRequest);
+ expect(resp).to.be.an('array').to.have.lengthOf(1);
+ expect(resp[0]).to.have.property('ttl');
+ expect(resp[0].ttl).to.equal(123);
+ });
+
+ it('should return ttl equal to DEFAULT_TTL_MAX if bid.exp is greater than DEFAULT_TTL_MAX and bid.ext["imds.tv"].ttl is undefined', function() {
+ const br = { exp: 4321, ...bidResponse };
+ serverResponse.body.seatbid[0].bid.push(br);
+ const resp = spec.interpretResponse(serverResponse, bidRequest);
+ expect(resp).to.be.an('array').to.have.lengthOf(1);
+ expect(resp[0]).to.have.property('ttl');
+ expect(resp[0].ttl).to.equal(420);
+ });
+
+ it('should return ttl equal to bid.exp if bid.exp is less than or equal to bid.ext["imds.tv"].ttl', function() {
+ const br = { exp: 1234, ext: { 'imds.tv': { ttl: 4321 } }, ...bidResponse };
+ serverResponse.body.seatbid[0].bid.push(br);
+ const resp = spec.interpretResponse(serverResponse, bidRequest);
+ expect(resp).to.be.an('array').to.have.lengthOf(1);
+ expect(resp[0]).to.have.property('ttl');
+ expect(resp[0].ttl).to.equal(1234);
+ });
+
+ it('should return ttl equal to bid.ext["imds.tv"].ttl if bid.exp is greater than bid.ext["imds.tv"].ttl', function() {
+ const br = { exp: 4321, ext: { 'imds.tv': { ttl: 1234 } }, ...bidResponse };
+ serverResponse.body.seatbid[0].bid.push(br);
+ const resp = spec.interpretResponse(serverResponse, bidRequest);
+ expect(resp).to.be.an('array').to.have.lengthOf(1);
+ expect(resp[0]).to.have.property('ttl');
+ expect(resp[0].ttl).to.equal(1234);
+ });
});
describe('getUserSyncs', function () {
it('should return a usersync when iframes is enabled', function () {
From 6366b39c8c215720c20d17d2deeda1a228ba54ec Mon Sep 17 00:00:00 2001
From: Demetrio Girardi
Date: Wed, 23 Mar 2022 15:01:17 -0700
Subject: [PATCH 07/16] FTrackIdSystem & IDWardRtdProvider: fix calls to
`getStorageManager` (#8208)
---
modules/ftrackIdSystem.js | 2 +-
modules/idWardRtdProvider.js | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js
index 3f2182a8b98..c570e69e1d3 100644
--- a/modules/ftrackIdSystem.js
+++ b/modules/ftrackIdSystem.js
@@ -18,7 +18,7 @@ const LOCAL_STORAGE = 'html5';
const FTRACK_STORAGE_NAME = 'ftrackId';
const FTRACK_PRIVACY_STORAGE_NAME = `${FTRACK_STORAGE_NAME}_privacy`;
const FTRACK_URL = 'https://d9.flashtalking.com/d9core';
-const storage = getStorageManager(VENDOR_ID, MODULE_NAME);
+const storage = getStorageManager({gvlid: VENDOR_ID, moduleName: MODULE_NAME});
let consentInfo = {
gdpr: {
diff --git a/modules/idWardRtdProvider.js b/modules/idWardRtdProvider.js
index 454902a6b9a..a130d3cc8d2 100644
--- a/modules/idWardRtdProvider.js
+++ b/modules/idWardRtdProvider.js
@@ -13,8 +13,7 @@ import {isPlainObject, mergeDeep, logMessage, logError} from '../src/utils.js';
const MODULE_NAME = 'realTimeData';
const SUBMODULE_NAME = 'idWard';
-export const storage = getStorageManager(null, SUBMODULE_NAME);
-
+export const storage = getStorageManager({moduleName: SUBMODULE_NAME});
/**
* Add real-time data & merge segments.
* @param {Object} rtd
From 22f547ab6326ecc31863ef96e802a26d24d73f2e Mon Sep 17 00:00:00 2001
From: matthieularere-msq
<63732822+matthieularere-msq@users.noreply.github.com>
Date: Thu, 24 Mar 2022 11:25:30 +0100
Subject: [PATCH 08/16] Mediasquare Bid Adapter: add metrics to onBidWon Event
(#8209)
* remove old userSyncs method
* Update mediasquareBidAdapter.js
* Update mediasquareBidAdapter.js
* Update mediasquareBidAdapter_spec.js
* Mediasquare Bid Adapter: add floor module support
* Update mediasquareBidAdapter_spec.js
* Update mediasquareBidAdapter.js
---
modules/mediasquareBidAdapter.js | 23 ++++++++++++-------
.../modules/mediasquareBidAdapter_spec.js | 10 ++++++++
2 files changed, 25 insertions(+), 8 deletions(-)
diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js
index 0b6f763988f..427a16f1341 100644
--- a/modules/mediasquareBidAdapter.js
+++ b/modules/mediasquareBidAdapter.js
@@ -55,7 +55,8 @@ export const spec = {
});
const payload = {
codes: codes,
- referer: encodeURIComponent(bidderRequest.refererInfo.referer)
+ referer: encodeURIComponent(bidderRequest.refererInfo.referer),
+ pbjs: '$prebid.version$'
};
if (bidderRequest) { // modules informations (gdpr, ccpa, schain, userId)
if (bidderRequest.gdprConsent) {
@@ -116,6 +117,9 @@ export const spec = {
if ('match' in value) {
bidResponse['mediasquare']['match'] = value['match'];
}
+ if ('hasConsent' in value) {
+ bidResponse['mediasquare']['hasConsent'] = value['hasConsent'];
+ }
if ('native' in value) {
bidResponse['native'] = value['native'];
bidResponse['mediaType'] = 'native';
@@ -153,19 +157,22 @@ export const spec = {
*/
onBidWon: function(bid) {
// fires a pixel to confirm a winning bid
- let params = [];
+ let params = {'pbjs': '$prebid.version$'};
let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD;
let paramsToSearchFor = ['cpm', 'size', 'mediaType', 'currency', 'creativeId', 'adUnitCode', 'timeToRespond', 'requestId', 'auctionId']
if (bid.hasOwnProperty('mediasquare')) {
- if (bid['mediasquare'].hasOwnProperty('bidder')) { params.push('bidder=' + bid['mediasquare']['bidder']); }
- if (bid['mediasquare'].hasOwnProperty('code')) { params.push('code=' + bid['mediasquare']['code']); }
- if (bid['mediasquare'].hasOwnProperty('match')) { params.push('match=' + bid['mediasquare']['match']); }
+ if (bid['mediasquare'].hasOwnProperty('bidder')) { params['bidder'] = bid['mediasquare']['bidder']; }
+ if (bid['mediasquare'].hasOwnProperty('code')) { params['code'] = bid['mediasquare']['code']; }
+ if (bid['mediasquare'].hasOwnProperty('match')) { params['match'] = bid['mediasquare']['match']; }
+ if (bid['mediasquare'].hasOwnProperty('hasConsent')) { params['hasConsent'] = bid['mediasquare']['hasConsent']; }
};
for (let i = 0; i < paramsToSearchFor.length; i++) {
- if (bid.hasOwnProperty(paramsToSearchFor[i])) { params.push(paramsToSearchFor[i] + '=' + bid[paramsToSearchFor[i]]); }
+ if (bid.hasOwnProperty(paramsToSearchFor[i])) {
+ params[paramsToSearchFor[i]] = bid[paramsToSearchFor[i]];
+ if (typeof params[paramsToSearchFor[i]] == 'number') { params[paramsToSearchFor[i]] = params[paramsToSearchFor[i]].toString() }
+ }
}
- if (params.length > 0) { params = '?' + params.join('&'); }
- ajax(endpoint + BIDDER_ENDPOINT_WINNING + params, null, undefined, {method: 'GET', withCredentials: true});
+ ajax(endpoint + BIDDER_ENDPOINT_WINNING, null, JSON.stringify(params), {method: 'POST', withCredentials: true});
return true;
}
diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js
index 86ab6e1f611..40d0c44e4f9 100644
--- a/test/spec/modules/mediasquareBidAdapter_spec.js
+++ b/test/spec/modules/mediasquareBidAdapter_spec.js
@@ -172,6 +172,14 @@ describe('MediaSquare bid adapter tests', function () {
expect(bid.mediasquare.match).to.exist;
expect(bid.mediasquare.match).to.equal(true);
});
+ it('Verifies hasConsent', function () {
+ const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS);
+ BID_RESPONSE.body.responses[0].hasConsent = true;
+ const response = spec.interpretResponse(BID_RESPONSE, request);
+ const bid = response[0];
+ expect(bid.mediasquare.hasConsent).to.exist;
+ expect(bid.mediasquare.hasConsent).to.equal(true);
+ });
it('Verifies bidder code', function () {
expect(spec.code).to.equal('mediasquare');
});
@@ -185,6 +193,8 @@ describe('MediaSquare bid adapter tests', function () {
});
it('Verifies bid won', function () {
const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS);
+ BID_RESPONSE.body.responses[0].match = true
+ BID_RESPONSE.body.responses[0].hasConsent = true;
const response = spec.interpretResponse(BID_RESPONSE, request);
const won = spec.onBidWon(response[0]);
expect(won).to.equal(true);
From 49c0859399974afd52eead0a3aa8f074ffc7620d Mon Sep 17 00:00:00 2001
From: Robert Ray Martinez III
Date: Thu, 24 Mar 2022 03:28:47 -0700
Subject: [PATCH 09/16] Rubicon Analytics, pass along gpid (#8210)
---
modules/rubiconAnalyticsAdapter.js | 4 ++-
.../modules/rubiconAnalyticsAdapter_spec.js | 29 +++++++++++++++++++
2 files changed, 32 insertions(+), 1 deletion(-)
diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js
index 55877a43d38..34e4a04aac2 100644
--- a/modules/rubiconAnalyticsAdapter.js
+++ b/modules/rubiconAnalyticsAdapter.js
@@ -232,6 +232,7 @@ function sendMessage(auctionId, bidWonId, trigger) {
'adserverTargeting', () => !isEmpty(cache.targeting[bid.adUnit.adUnitCode]) ? stringProperties(cache.targeting[bid.adUnit.adUnitCode]) : undefined,
'gam', gam => !isEmpty(gam) ? gam : undefined,
'pbAdSlot',
+ 'gpid',
'pattern'
]);
adUnit.bids = [];
@@ -770,7 +771,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, {
}
},
'pbAdSlot', () => deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'),
- 'pattern', () => deepAccess(bid, 'ortb2Imp.ext.data.aupname')
+ 'pattern', () => deepAccess(bid, 'ortb2Imp.ext.data.aupname'),
+ 'gpid', () => deepAccess(bid, 'ortb2Imp.ext.gpid')
])
]);
return memo;
diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js
index 397388c8237..354fbb53027 100644
--- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js
+++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js
@@ -2098,6 +2098,35 @@ describe('rubicon analytics adapter', function () {
expect(message.auctions[0].adUnits[1].pattern).to.equal('1234/mycoolsite/*&gpt_skyscraper&deviceType=mobile');
});
+ it('should pass gpid if defined', function () {
+ let bidRequest = utils.deepClone(MOCK.BID_REQUESTED);
+ bidRequest.bids[0].ortb2Imp = {
+ ext: {
+ gpid: '1234/mycoolsite/lowerbox'
+ }
+ };
+ bidRequest.bids[1].ortb2Imp = {
+ ext: {
+ gpid: '1234/mycoolsite/leaderboard'
+ }
+ };
+ events.emit(AUCTION_INIT, MOCK.AUCTION_INIT);
+ events.emit(BID_REQUESTED, bidRequest);
+ events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]);
+ events.emit(BIDDER_DONE, MOCK.BIDDER_DONE);
+ events.emit(AUCTION_END, MOCK.AUCTION_END);
+ events.emit(SET_TARGETING, MOCK.SET_TARGETING);
+
+ clock.tick(SEND_TIMEOUT + 1000);
+
+ expect(server.requests.length).to.equal(1);
+
+ let message = JSON.parse(server.requests[0].requestBody);
+ validate(message);
+ expect(message.auctions[0].adUnits[0].gpid).to.equal('1234/mycoolsite/lowerbox');
+ expect(message.auctions[0].adUnits[1].gpid).to.equal('1234/mycoolsite/leaderboard');
+ });
+
it('should pass bidderDetail for multibid auctions', function () {
let bidResponse = utils.deepClone(MOCK.BID_RESPONSE[1]);
bidResponse.targetingBidder = 'rubi2';
From 4de3a60ab6c79692990d3a95858ddbb5cbd654b1 Mon Sep 17 00:00:00 2001
From: pm-azhar-mulla <75726247+pm-azhar-mulla@users.noreply.github.com>
Date: Thu, 24 Mar 2022 16:48:17 +0530
Subject: [PATCH 10/16] PubMatic Bid Adapter: Added multibid support for GroupM
(#8193)
* Changed net revenue to True
* Added miltibid support for GroupM
* Added marketplace array to check for values
* Added marketplace check while requesting too
Co-authored-by: Azhar
---
modules/pubmaticBidAdapter.js | 14 ++++++++
test/spec/modules/pubmaticBidAdapter_spec.js | 36 ++++++++++++++++++++
2 files changed, 50 insertions(+)
diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js
index 93afe96af78..0667ac0fc74 100644
--- a/modules/pubmaticBidAdapter.js
+++ b/modules/pubmaticBidAdapter.js
@@ -12,6 +12,7 @@ const USER_SYNC_URL_IMAGE = 'https://image8.pubmatic.com/AdServer/ImgSync?p=';
const DEFAULT_CURRENCY = 'USD';
const AUCTION_TYPE = 1;
const GROUPM_ALIAS = {code: 'groupm', gvlid: 98};
+const MARKETPLACE_PARTNERS = ['groupm']
const UNDEFINED = undefined;
const DEFAULT_WIDTH = 0;
const DEFAULT_HEIGHT = 0;
@@ -1065,6 +1066,12 @@ export const spec = {
* @return ServerRequest Info describing the request to the server.
*/
buildRequests: (validBidRequests, bidderRequest) => {
+ if (bidderRequest && MARKETPLACE_PARTNERS.includes(bidderRequest.bidderCode)) {
+ // We have got the buildRequests function call for Marketplace Partners
+ logInfo('For all publishers using ' + bidderRequest.bidderCode + ' bidder, the PubMatic bidder will also be enabled so PubMatic server will respond back with the bids that needs to be submitted for PubMatic and ' + bidderRequest.bidderCode + ' in the network call sent by PubMatic bidder. Hence we do not want to create a network call for ' + bidderRequest.bidderCode + '. This way we are trying to save a network call from browser.');
+ return;
+ }
+
var refererInfo;
if (bidderRequest && bidderRequest.refererInfo) {
refererInfo = bidderRequest.refererInfo;
@@ -1287,6 +1294,13 @@ export const spec = {
};
}
+ // if from the server-response the bid.ext.marketplace is set then
+ // submit the bid to Prebid as marketplace name
+ if (bid.ext && !!bid.ext.marketplace && MARKETPLACE_PARTNERS.includes(bid.ext.marketplace)) {
+ newBid.bidderCode = bid.ext.marketplace;
+ newBid.bidder = bid.ext.marketplace;
+ }
+
bidResponses.push(newBid);
});
});
diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js
index 6261586f451..64e95460321 100644
--- a/test/spec/modules/pubmaticBidAdapter_spec.js
+++ b/test/spec/modules/pubmaticBidAdapter_spec.js
@@ -3901,4 +3901,40 @@ describe('PubMatic adapter', function () {
expect(data.imp[0]['video']['battr']).to.equal(undefined);
});
});
+
+ describe('GroupM params', function() {
+ let sandbox, utilsMock, newBidRequests, newBidResponses;
+ beforeEach(() => {
+ utilsMock = sinon.mock(utils);
+ sandbox = sinon.sandbox.create();
+ sandbox.spy(utils, 'logInfo');
+ newBidRequests = utils.deepClone(bidRequests)
+ newBidRequests[0].bidder = 'groupm';
+ newBidResponses = utils.deepClone(bidResponses);
+ newBidResponses.body.seatbid[0].bid[0].ext.marketplace = 'groupm'
+ });
+
+ afterEach(() => {
+ utilsMock.restore();
+ sandbox.restore();
+ })
+
+ it('Should log info when bidder is groupm and return', function () {
+ let request = spec.buildRequests(newBidRequests, {bidderCode: 'groupm',
+ auctionId: 'new-auction-id'
+ });
+ sinon.assert.calledOnce(utils.logInfo);
+ expect(request).to.equal(undefined);
+ });
+
+ it('Should add bidder code & bidder as groupm for marketplace groupm response', function () {
+ let request = spec.buildRequests(newBidRequests, {
+ auctionId: 'new-auction-id'
+ });
+ let response = spec.interpretResponse(newBidResponses, request);
+ expect(response).to.be.an('array').with.length.above(0);
+ expect(response[0].bidderCode).to.equal('groupm');
+ expect(response[0].bidder).to.equal('groupm');
+ });
+ });
});
From 37f02a91b56811261b5b7156c61c79b351da3086 Mon Sep 17 00:00:00 2001
From: readpeaktuomo <66239046+readpeaktuomo@users.noreply.github.com>
Date: Thu, 24 Mar 2022 13:21:16 +0200
Subject: [PATCH 11/16] Add banner support to readpeak bid adapter (#8179)
---
modules/readpeakBidAdapter.js | 41 +-
modules/readpeakBidAdapter.md | 55 +-
test/spec/modules/readpeakBidAdapter_spec.js | 536 +++++++++++++------
3 files changed, 459 insertions(+), 173 deletions(-)
diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js
index 31e430d79f9..099e1fb6332 100644
--- a/modules/readpeakBidAdapter.js
+++ b/modules/readpeakBidAdapter.js
@@ -1,7 +1,7 @@
import { logError, replaceAuctionPrice, parseUrl } from '../src/utils.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { config } from '../src/config.js';
-import { NATIVE } from '../src/mediaTypes.js';
+import { NATIVE, BANNER } from '../src/mediaTypes.js';
export const ENDPOINT = 'https://app.readpeak.com/header/prebid';
@@ -19,10 +19,9 @@ const BIDDER_CODE = 'readpeak';
export const spec = {
code: BIDDER_CODE,
- supportedMediaTypes: [NATIVE],
+ supportedMediaTypes: [NATIVE, BANNER],
- isBidRequestValid: bid =>
- !!(bid && bid.params && bid.params.publisherId && bid.nativeParams),
+ isBidRequestValid: bid => !!(bid && bid.params && bid.params.publisherId),
buildRequests: (bidRequests, bidderRequest) => {
const currencyObj = config.getConfig('currency');
@@ -31,8 +30,7 @@ export const spec = {
const request = {
id: bidRequests[0].bidderRequestId,
imp: bidRequests
- .map(slot => impression(slot))
- .filter(imp => imp.native != null),
+ .map(slot => impression(slot)),
site: site(bidRequests, bidderRequest),
app: app(bidRequests),
device: device(),
@@ -96,10 +94,16 @@ function bidResponseAvailable(bidRequest, bidResponse) {
creativeId: idToBidMap[id].crid,
ttl: 300,
netRevenue: true,
- mediaType: NATIVE,
- currency: bidResponse.cur,
- native: nativeResponse(idToImpMap[id], idToBidMap[id])
+ mediaType: idToImpMap[id].native ? NATIVE : BANNER,
+ currency: bidResponse.cur
};
+ if (idToImpMap[id].native) {
+ bid.native = nativeResponse(idToImpMap[id], idToBidMap[id]);
+ } else if (idToImpMap[id].banner) {
+ bid.ad = idToBidMap[id].adm
+ bid.width = idToBidMap[id].w
+ bid.height = idToBidMap[id].h
+ }
if (idToBidMap[id].adomain) {
bid.meta = {
advertiserDomains: idToBidMap[id].adomain
@@ -121,13 +125,19 @@ function impression(slot) {
});
bidFloorFromModule = floorInfo.currency === 'USD' ? floorInfo.floor : undefined;
}
- return {
+ const imp = {
id: slot.bidId,
- native: nativeImpression(slot),
bidfloor: bidFloorFromModule || slot.params.bidfloor || 0,
bidfloorcur: (bidFloorFromModule && 'USD') || slot.params.bidfloorcur || 'USD',
tagId: slot.params.tagId || '0'
};
+
+ if (slot.mediaTypes.native) {
+ imp.native = nativeImpression(slot);
+ } else if (slot.mediaTypes.banner) {
+ imp.banner = bannerImpression(slot);
+ }
+ return imp
}
function nativeImpression(slot) {
@@ -218,6 +228,15 @@ function dataAsset(id, params, type, defaultLen) {
: null;
}
+function bannerImpression(slot) {
+ var sizes = slot.mediaTypes.banner.sizes || slot.sizes;
+ return {
+ format: sizes.map((s) => ({ w: s[0], h: s[1] })),
+ w: sizes[0][0],
+ h: sizes[0][1],
+ }
+}
+
function site(bidRequests, bidderRequest) {
const url =
config.getConfig('pageUrl') ||
diff --git a/modules/readpeakBidAdapter.md b/modules/readpeakBidAdapter.md
index da250e7f77a..8f8e7369ea5 100644
--- a/modules/readpeakBidAdapter.md
+++ b/modules/readpeakBidAdapter.md
@@ -15,17 +15,48 @@ Please reach out to your account team or hello@readpeak.com for more information
# Test Parameters
```javascript
- var adUnits = [{
- code: '/19968336/prebid_native_example_2',
- mediaTypes: { native: { type: 'image' } },
- bids: [{
- bidder: 'readpeak',
- params: {
- bidfloor: 5.00,
- publisherId: 'test',
- siteId: 'test',
- tagId: 'test-tag-1'
+ var adUnits = [
+ {
+ code: '/19968336/prebid_native_example_2',
+ mediaTypes: {
+ native: {
+ title: {
+ required: true
+ },
+ image: {
+ required: true
+ },
+ body: {
+ required: true
+ },
+ }
},
- }]
- }];
+ bids: [{
+ bidder: 'readpeak',
+ params: {
+ bidfloor: 5.00,
+ publisherId: 'test',
+ siteId: 'test',
+ tagId: 'test-tag-1'
+ },
+ }]
+ },
+ {
+ code: '/19968336/prebid_banner_example_2',
+ mediaTypes: {
+ banner: {
+ sizes: [[640, 320], [300, 600]],
+ }
+ },
+ bids: [{
+ bidder: 'readpeak',
+ params: {
+ bidfloor: 5.00,
+ publisherId: 'test',
+ siteId: 'test',
+ tagId: 'test-tag-2'
+ },
+ }]
+ }
+ ];
```
diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js
index eefd7792a7c..04358fad52b 100644
--- a/test/spec/modules/readpeakBidAdapter_spec.js
+++ b/test/spec/modules/readpeakBidAdapter_spec.js
@@ -4,9 +4,13 @@ import { config } from 'src/config.js';
import { parseUrl } from 'src/utils.js';
describe('ReadPeakAdapter', function() {
- let bidRequest;
- let serverResponse;
- let serverRequest;
+ let baseBidRequest;
+ let bannerBidRequest;
+ let nativeBidRequest;
+ let nativeServerResponse;
+ let nativeServerRequest;
+ let bannerServerResponse;
+ let bannerServerRequest;
let bidderRequest;
beforeEach(function() {
@@ -16,15 +20,8 @@ describe('ReadPeakAdapter', function() {
}
};
- bidRequest = {
+ baseBidRequest = {
bidder: 'readpeak',
- nativeParams: {
- title: { required: true, len: 200 },
- image: { wmin: 100 },
- sponsoredBy: {},
- body: { required: false },
- cta: { required: false }
- },
params: {
bidfloor: 5.0,
publisherId: '11bc5dd5-7421-4dd8-c926-40fa653bec76',
@@ -34,17 +31,46 @@ describe('ReadPeakAdapter', function() {
bidId: '2ffb201a808da7',
bidderRequestId: '178e34bad3658f',
auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a',
- transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b'
+ transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b',
+ };
+
+ nativeBidRequest = {
+ ...baseBidRequest,
+ nativeParams: {
+ title: { required: true, len: 200 },
+ image: { wmin: 100 },
+ sponsoredBy: {},
+ body: { required: false },
+ cta: { required: false }
+ },
+ mediaTypes: {
+ native: {
+ title: { required: true, len: 200 },
+ image: { wmin: 100 },
+ sponsoredBy: {},
+ body: { required: false },
+ cta: { required: false }
+ },
+ }
};
- serverResponse = {
- id: bidRequest.bidderRequestId,
+ bannerBidRequest = {
+ ...baseBidRequest,
+ mediaTypes: {
+ banner: {
+ sizes: [[640, 320], [300, 600]],
+ }
+ },
+ sizes: [[640, 320], [300, 600]],
+ }
+ nativeServerResponse = {
+ id: baseBidRequest.bidderRequestId,
cur: 'USD',
seatbid: [
{
bid: [
{
- id: 'bidRequest.bidId',
- impid: bidRequest.bidId,
+ id: 'baseBidRequest.bidId',
+ impid: baseBidRequest.bidId,
price: 0.12,
cid: '12',
crid: '123',
@@ -91,7 +117,30 @@ describe('ReadPeakAdapter', function() {
}
]
};
- serverRequest = {
+ bannerServerResponse = {
+ id: baseBidRequest.bidderRequestId,
+ cur: 'USD',
+ seatbid: [
+ {
+ bid: [
+ {
+ id: 'baseBidRequest.bidId',
+ impid: baseBidRequest.bidId,
+ price: 0.12,
+ cid: '12',
+ crid: '123',
+ adomain: ['readpeak.com'],
+ adm: '',
+ burl: 'https://localhost:8081/url/b?d=0O95O4326I528Ie4d39f94-533d-4577-a579-585fd4c02b0aI0I352e303232363639333139393939393939&c=USD&p=${AUCTION_PRICE}&bad=0-0-95O0O0OdO640360&gc=0',
+ nurl: 'https://localhost:8081/url/n?d=0O95O4326I528Ie4d39f94-533d-4577-a579-585fd4c02b0aI0I352e303232363639333139393939393939&gc=0',
+ w: 640,
+ h: 360,
+ }
+ ]
+ }
+ ]
+ };
+ nativeServerRequest = {
method: 'POST',
url: 'http://localhost:60080/header/prebid',
data: JSON.stringify({
@@ -101,7 +150,7 @@ describe('ReadPeakAdapter', function() {
id: '2ffb201a808da7',
native: {
request:
- '{"assets":[{"id":1,"required":1,"title":{"len":200}},{"id":2,"required":0,"data":{"type":1,"len":50}},{"id":3,"required":0,"img":{"type":3,"wmin":100,"hmin":150}}]}',
+ '{\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":70}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":150,\"hmin\":150}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}',
ver: '1.1'
},
bidfloor: 5,
@@ -127,175 +176,362 @@ describe('ReadPeakAdapter', function() {
isPrebid: true
})
};
+ bannerServerRequest = {
+ method: 'POST',
+ url: 'http://localhost:60080/header/prebid',
+ data: JSON.stringify({
+ id: '178e34bad3658f',
+ imp: [
+ {
+ id: '2ffb201a808da7',
+ bidfloor: 5,
+ bidfloorcur: 'USD',
+ tagId: 'test-tag-1',
+ banner: {
+ w: 640,
+ h: 360,
+ format: [
+ { w: 640, h: 360 },
+ { w: 320, h: 320 },
+ ]
+ }
+ }
+ ],
+ site: {
+ publisher: {
+ id: '11bc5dd5-7421-4dd8-c926-40fa653bec76'
+ },
+ id: '11bc5dd5-7421-4dd8-c926-40fa653bec77',
+ ref: '',
+ page: 'http://localhost',
+ domain: 'localhost'
+ },
+ app: null,
+ device: {
+ ua:
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/61.0.3163.100 Safari/537.36',
+ language: 'en-US'
+ },
+ isPrebid: true
+ })
+ };
});
- describe('spec.isBidRequestValid', function() {
- it('should return true when the required params are passed', function() {
- expect(spec.isBidRequestValid(bidRequest)).to.equal(true);
- });
+ describe('Native', function() {
+ describe('spec.isBidRequestValid', function() {
+ it('should return true when the required params are passed', function() {
+ expect(spec.isBidRequestValid(nativeBidRequest)).to.equal(true);
+ });
- it('should return false when the native params are missing', function() {
- bidRequest.nativeParams = undefined;
- expect(spec.isBidRequestValid(bidRequest)).to.equal(false);
- });
+ it('should return false when the "publisherId" param is missing', function() {
+ nativeBidRequest.params = {
+ bidfloor: 5.0
+ };
+ expect(spec.isBidRequestValid(nativeBidRequest)).to.equal(false);
+ });
+
+ it('should return false when no bid params are passed', function() {
+ nativeBidRequest.params = {};
+ expect(spec.isBidRequestValid(nativeBidRequest)).to.equal(false);
+ });
- it('should return false when the "publisherId" param is missing', function() {
- bidRequest.params = {
- bidfloor: 5.0
- };
- expect(spec.isBidRequestValid(bidRequest)).to.equal(false);
+ it('should return false when a bid request is not passed', function() {
+ expect(spec.isBidRequestValid()).to.equal(false);
+ expect(spec.isBidRequestValid({})).to.equal(false);
+ });
});
- it('should return false when no bid params are passed', function() {
- bidRequest.params = {};
- expect(spec.isBidRequestValid(bidRequest)).to.equal(false);
+ describe('spec.buildRequests', function() {
+ it('should create a POST request for every bid', function() {
+ const request = spec.buildRequests([nativeBidRequest], bidderRequest);
+ expect(request.method).to.equal('POST');
+ expect(request.url).to.equal(ENDPOINT);
+ });
+
+ it('should attach request data', function() {
+ config.setConfig({
+ currency: {
+ adServerCurrency: 'EUR'
+ }
+ });
+
+ const request = spec.buildRequests([nativeBidRequest], bidderRequest);
+
+ const data = JSON.parse(request.data);
+
+ expect(data.source.ext.prebid).to.equal('$prebid.version$');
+ expect(data.id).to.equal(nativeBidRequest.bidderRequestId);
+ expect(data.imp[0].bidfloor).to.equal(nativeBidRequest.params.bidfloor);
+ expect(data.imp[0].bidfloorcur).to.equal('USD');
+ expect(data.imp[0].tagId).to.equal('test-tag-1');
+ expect(data.site.publisher.id).to.equal(nativeBidRequest.params.publisherId);
+ expect(data.site.id).to.equal(nativeBidRequest.params.siteId);
+ expect(data.site.page).to.equal(bidderRequest.refererInfo.referer);
+ expect(data.site.domain).to.equal(parseUrl(bidderRequest.refererInfo.referer).hostname);
+ expect(data.device).to.deep.contain({
+ ua: navigator.userAgent,
+ language: navigator.language
+ });
+ expect(data.cur).to.deep.equal(['EUR']);
+ expect(data.user).to.be.undefined;
+ expect(data.regs).to.be.undefined;
+ });
+
+ it('should get bid floor from module', function() {
+ const floorModuleData = {
+ currency: 'USD',
+ floor: 3.2,
+ }
+ nativeBidRequest.getFloor = function () {
+ return floorModuleData
+ }
+ const request = spec.buildRequests([nativeBidRequest], bidderRequest);
+
+ const data = JSON.parse(request.data);
+
+ expect(data.source.ext.prebid).to.equal('$prebid.version$');
+ expect(data.id).to.equal(nativeBidRequest.bidderRequestId);
+ expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor);
+ expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency);
+ });
+
+ it('should send gdpr data when gdpr does not apply', function() {
+ const gdprData = {
+ gdprConsent: {
+ gdprApplies: false,
+ consentString: undefined,
+ }
+ }
+ const request = spec.buildRequests([nativeBidRequest], {...bidderRequest, ...gdprData});
+
+ const data = JSON.parse(request.data);
+
+ expect(data.user).to.deep.equal({
+ ext: {
+ consent: ''
+ }
+ });
+ expect(data.regs).to.deep.equal({
+ ext: {
+ gdpr: false
+ }
+ });
+ });
+
+ it('should send gdpr data when gdpr applies', function() {
+ const tcString = 'sometcstring';
+ const gdprData = {
+ gdprConsent: {
+ gdprApplies: true,
+ consentString: tcString
+ }
+ }
+ const request = spec.buildRequests([nativeBidRequest], {...bidderRequest, ...gdprData});
+
+ const data = JSON.parse(request.data);
+
+ expect(data.user).to.deep.equal({
+ ext: {
+ consent: tcString
+ }
+ });
+ expect(data.regs).to.deep.equal({
+ ext: {
+ gdpr: true
+ }
+ });
+ });
});
- it('should return false when a bid request is not passed', function() {
- expect(spec.isBidRequestValid()).to.equal(false);
- expect(spec.isBidRequestValid({})).to.equal(false);
+ describe('spec.interpretResponse', function() {
+ it('should return no bids if the response is not valid', function() {
+ const bidResponse = spec.interpretResponse({ body: null }, nativeServerRequest);
+ expect(bidResponse.length).to.equal(0);
+ });
+
+ it('should return a valid bid response', function() {
+ const bidResponse = spec.interpretResponse(
+ { body: nativeServerResponse },
+ nativeServerRequest
+ )[0];
+ expect(bidResponse).to.contain({
+ requestId: nativeBidRequest.bidId,
+ cpm: nativeServerResponse.seatbid[0].bid[0].price,
+ creativeId: nativeServerResponse.seatbid[0].bid[0].crid,
+ ttl: 300,
+ netRevenue: true,
+ mediaType: 'native',
+ currency: nativeServerResponse.cur
+ });
+
+ expect(bidResponse.meta).to.deep.equal({
+ advertiserDomains: ['readpeak.com'],
+ })
+ expect(bidResponse.native.title).to.equal('Title');
+ expect(bidResponse.native.body).to.equal('Description');
+ expect(bidResponse.native.image).to.deep.equal({
+ url: 'http://url.to/image',
+ width: 750,
+ height: 500
+ });
+ expect(bidResponse.native.clickUrl).to.equal(
+ 'http%3A%2F%2Furl.to%2Ftarget'
+ );
+ expect(bidResponse.native.impressionTrackers).to.contain(
+ 'http://url.to/pixeltracker'
+ );
+ });
});
});
- describe('spec.buildRequests', function() {
- it('should create a POST request for every bid', function() {
- const request = spec.buildRequests([bidRequest], bidderRequest);
- expect(request.method).to.equal('POST');
- expect(request.url).to.equal(ENDPOINT);
- });
+ describe('Banner', function() {
+ describe('spec.isBidRequestValid', function() {
+ it('should return true when the required params are passed', function() {
+ expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(true);
+ });
- it('should attach request data', function() {
- config.setConfig({
- currency: {
- adServerCurrency: 'EUR'
- }
+ it('should return false when the "publisherId" param is missing', function() {
+ bannerBidRequest.params = {
+ bidfloor: 5.0
+ };
+ expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(false);
});
- const request = spec.buildRequests([bidRequest], bidderRequest);
-
- const data = JSON.parse(request.data);
-
- expect(data.source.ext.prebid).to.equal('$prebid.version$');
- expect(data.id).to.equal(bidRequest.bidderRequestId);
- expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor);
- expect(data.imp[0].bidfloorcur).to.equal('USD');
- expect(data.imp[0].tagId).to.equal('test-tag-1');
- expect(data.site.publisher.id).to.equal(bidRequest.params.publisherId);
- expect(data.site.id).to.equal(bidRequest.params.siteId);
- expect(data.site.page).to.equal(bidderRequest.refererInfo.referer);
- expect(data.site.domain).to.equal(parseUrl(bidderRequest.refererInfo.referer).hostname);
- expect(data.device).to.deep.contain({
- ua: navigator.userAgent,
- language: navigator.language
+ it('should return false when no bid params are passed', function() {
+ bannerBidRequest.params = {};
+ expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(false);
});
- expect(data.cur).to.deep.equal(['EUR']);
- expect(data.user).to.be.undefined;
- expect(data.regs).to.be.undefined;
});
- it('should get bid floor from module', function() {
- const floorModuleData = {
- currency: 'USD',
- floor: 3.2,
- }
- bidRequest.getFloor = function () {
- return floorModuleData
- }
- const request = spec.buildRequests([bidRequest], bidderRequest);
+ describe('spec.buildRequests', function() {
+ it('should create a POST request for every bid', function() {
+ const request = spec.buildRequests([bannerBidRequest], bidderRequest);
+ expect(request.method).to.equal('POST');
+ expect(request.url).to.equal(ENDPOINT);
+ });
- const data = JSON.parse(request.data);
+ it('should attach request data', function() {
+ config.setConfig({
+ currency: {
+ adServerCurrency: 'EUR'
+ }
+ });
- expect(data.source.ext.prebid).to.equal('$prebid.version$');
- expect(data.id).to.equal(bidRequest.bidderRequestId);
- expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor);
- expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency);
- });
+ const request = spec.buildRequests([bannerBidRequest], bidderRequest);
- it('should send gdpr data when gdpr does not apply', function() {
- const gdprData = {
- gdprConsent: {
- gdprApplies: false,
- consentString: undefined,
- }
- }
- const request = spec.buildRequests([bidRequest], {...bidderRequest, ...gdprData});
+ const data = JSON.parse(request.data);
- const data = JSON.parse(request.data);
+ expect(data.source.ext.prebid).to.equal('$prebid.version$');
+ expect(data.id).to.equal(bannerBidRequest.bidderRequestId);
+ expect(data.imp[0].bidfloor).to.equal(bannerBidRequest.params.bidfloor);
+ expect(data.imp[0].bidfloorcur).to.equal('USD');
+ expect(data.imp[0].tagId).to.equal('test-tag-1');
+ expect(data.site.publisher.id).to.equal(bannerBidRequest.params.publisherId);
+ expect(data.site.id).to.equal(bannerBidRequest.params.siteId);
+ expect(data.site.page).to.equal(bidderRequest.refererInfo.referer);
+ expect(data.site.domain).to.equal(parseUrl(bidderRequest.refererInfo.referer).hostname);
+ expect(data.device).to.deep.contain({
+ ua: navigator.userAgent,
+ language: navigator.language
+ });
+ expect(data.cur).to.deep.equal(['EUR']);
+ expect(data.user).to.be.undefined;
+ expect(data.regs).to.be.undefined;
+ });
- expect(data.user).to.deep.equal({
- ext: {
- consent: ''
+ it('should get bid floor from module', function() {
+ const floorModuleData = {
+ currency: 'USD',
+ floor: 3.2,
}
- });
- expect(data.regs).to.deep.equal({
- ext: {
- gdpr: false
+ bannerBidRequest.getFloor = function () {
+ return floorModuleData
}
+ const request = spec.buildRequests([bannerBidRequest], bidderRequest);
+
+ const data = JSON.parse(request.data);
+
+ expect(data.source.ext.prebid).to.equal('$prebid.version$');
+ expect(data.id).to.equal(bannerBidRequest.bidderRequestId);
+ expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor);
+ expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency);
});
- });
- it('should send gdpr data when gdpr applies', function() {
- const tcString = 'sometcstring';
- const gdprData = {
- gdprConsent: {
- gdprApplies: true,
- consentString: tcString
+ it('should send gdpr data when gdpr does not apply', function() {
+ const gdprData = {
+ gdprConsent: {
+ gdprApplies: false,
+ consentString: undefined,
+ }
}
- }
- const request = spec.buildRequests([bidRequest], {...bidderRequest, ...gdprData});
+ const request = spec.buildRequests([bannerBidRequest], {...bidderRequest, ...gdprData});
- const data = JSON.parse(request.data);
+ const data = JSON.parse(request.data);
- expect(data.user).to.deep.equal({
- ext: {
- consent: tcString
- }
+ expect(data.user).to.deep.equal({
+ ext: {
+ consent: ''
+ }
+ });
+ expect(data.regs).to.deep.equal({
+ ext: {
+ gdpr: false
+ }
+ });
});
- expect(data.regs).to.deep.equal({
- ext: {
- gdpr: true
+
+ it('should send gdpr data when gdpr applies', function() {
+ const tcString = 'sometcstring';
+ const gdprData = {
+ gdprConsent: {
+ gdprApplies: true,
+ consentString: tcString
+ }
}
- });
- });
- });
+ const request = spec.buildRequests([bannerBidRequest], {...bidderRequest, ...gdprData});
+
+ const data = JSON.parse(request.data);
- describe('spec.interpretResponse', function() {
- it('should return no bids if the response is not valid', function() {
- const bidResponse = spec.interpretResponse({ body: null }, serverRequest);
- expect(bidResponse.length).to.equal(0);
+ expect(data.user).to.deep.equal({
+ ext: {
+ consent: tcString
+ }
+ });
+ expect(data.regs).to.deep.equal({
+ ext: {
+ gdpr: true
+ }
+ });
+ });
});
- it('should return a valid bid response', function() {
- const bidResponse = spec.interpretResponse(
- { body: serverResponse },
- serverRequest
- )[0];
- expect(bidResponse).to.contain({
- requestId: bidRequest.bidId,
- cpm: serverResponse.seatbid[0].bid[0].price,
- creativeId: serverResponse.seatbid[0].bid[0].crid,
- ttl: 300,
- netRevenue: true,
- mediaType: 'native',
- currency: serverResponse.cur
+ describe('spec.interpretResponse', function() {
+ it('should return no bids if the response is not valid', function() {
+ const bidResponse = spec.interpretResponse({ body: null }, bannerServerRequest);
+ expect(bidResponse.length).to.equal(0);
});
- expect(bidResponse.meta).to.deep.equal({
- advertiserDomains: ['readpeak.com'],
- })
- expect(bidResponse.native.title).to.equal('Title');
- expect(bidResponse.native.body).to.equal('Description');
- expect(bidResponse.native.image).to.deep.equal({
- url: 'http://url.to/image',
- width: 750,
- height: 500
+ it('should return a valid bid response', function() {
+ const bidResponse = spec.interpretResponse(
+ { body: bannerServerResponse },
+ bannerServerRequest
+ )[0];
+ expect(bidResponse).to.contain({
+ requestId: bannerBidRequest.bidId,
+ cpm: bannerServerResponse.seatbid[0].bid[0].price,
+ creativeId: bannerServerResponse.seatbid[0].bid[0].crid,
+ ttl: 300,
+ netRevenue: true,
+ mediaType: 'banner',
+ currency: bannerServerResponse.cur,
+ ad: bannerServerResponse.seatbid[0].bid[0].adm,
+ width: bannerServerResponse.seatbid[0].bid[0].w,
+ height: bannerServerResponse.seatbid[0].bid[0].h,
+ });
+ expect(bidResponse.meta).to.deep.equal({
+ advertiserDomains: ['readpeak.com'],
+ });
});
- expect(bidResponse.native.clickUrl).to.equal(
- 'http%3A%2F%2Furl.to%2Ftarget'
- );
- expect(bidResponse.native.impressionTrackers).to.contain(
- 'http://url.to/pixeltracker'
- );
});
});
});
From c8c326f33980f451c33577848f5ac975e73a65e7 Mon Sep 17 00:00:00 2001
From: Demetrio Girardi
Date: Thu, 24 Mar 2022 07:23:04 -0700
Subject: [PATCH 12/16] Prebid core & PBS adapter: better support for
server-side stored impressions (#8154)
* Update ad unit validation to allow no bids if ortb2Imp is specified
* Move s2sTesting logic to the s2sTesting module
* Accept adUnit.storedAuctionResponse in lieu of adUnit.bids; update PBS adapter to work with no-bid adUnits
* Do not accept 'storedAuctionResponse' as an alternative to 'ortb2Imp'
* Fix short-circuit in building out imp[].ext
* Make `s2sConfig.bidders` optional
---
modules/prebidServerBidAdapter/index.js | 17 +-
modules/s2sTesting.js | 77 +++++++--
modules/sizeMappingV2.js | 13 +-
src/adapterManager.js | 160 ++++++++----------
src/prebid.js | 41 +++--
.../modules/prebidServerBidAdapter_spec.js | 38 +++--
test/spec/modules/sizeMappingV2_spec.js | 54 ++----
test/spec/unit/core/adapterManager_spec.js | 124 ++++++++++++--
test/spec/unit/pbjs_api_spec.js | 19 ++-
9 files changed, 340 insertions(+), 203 deletions(-)
diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js
index ec9ba99dade..d30fd5bf810 100644
--- a/modules/prebidServerBidAdapter/index.js
+++ b/modules/prebidServerBidAdapter/index.js
@@ -99,6 +99,7 @@ let eidPermissions;
* @type {S2SDefaultConfig}
*/
const s2sDefaultConfig = {
+ bidders: Object.freeze([]),
timeout: 1000,
syncTimeout: 1000,
maxBids: 1,
@@ -143,7 +144,7 @@ function updateConfigDefaultVendor(option) {
*/
function validateConfigRequiredProps(option) {
const keys = Object.keys(option);
- if (['accountId', 'bidders', 'endpoint'].filter(key => {
+ if (['accountId', 'endpoint'].filter(key => {
if (!includes(keys, key)) {
logError(key + ' missing in server to server config');
return true;
@@ -706,6 +707,7 @@ Object.assign(ORTB2.prototype, {
// get bidder params in form { : {...params} }
// initialize reduce function with the user defined `ext` properties on the ad unit
const ext = adUnit.bids.reduce((acc, bid) => {
+ if (bid.bidder == null) return acc;
const adapter = adapterManager.bidderRegistry[bid.bidder];
if (adapter && adapter.getSpec().transformBidParams) {
bid.params = adapter.getSpec().transformBidParams(bid.params, true, adUnit, bidRequests);
@@ -914,10 +916,15 @@ Object.assign(ORTB2.prototype, {
// a seatbid object contains a `bid` array and a `seat` string
response.seatbid.forEach(seatbid => {
(seatbid.bid || []).forEach(bid => {
- const bidRequest = this.getBidRequest(bid.impid, seatbid.seat);
- if (bidRequest == null && !s2sConfig.allowUnknownBidderCodes) {
- logWarn(`PBS adapter received bid from unknown bidder (${seatbid.seat}), but 's2sConfig.allowUnknownBidderCodes' is not set. Ignoring bid.`);
- return;
+ let bidRequest = this.getBidRequest(bid.impid, seatbid.seat);
+ if (bidRequest == null) {
+ if (!s2sConfig.allowUnknownBidderCodes) {
+ logWarn(`PBS adapter received bid from unknown bidder (${seatbid.seat}), but 's2sConfig.allowUnknownBidderCodes' is not set. Ignoring bid.`);
+ return;
+ }
+ // for stored impression, a request was made with bidder code `null`. Pick it up here so that NO_BID, BID_WON, etc events
+ // can work as expected (otherwise, the original request will always result in NO_BID).
+ bidRequest = this.getBidRequest(bid.impid, null);
}
const cpm = bid.price;
diff --git a/modules/s2sTesting.js b/modules/s2sTesting.js
index 1f2bb473174..8e9628c8810 100644
--- a/modules/s2sTesting.js
+++ b/modules/s2sTesting.js
@@ -1,12 +1,12 @@
-import { setS2STestingModule } from '../src/adapterManager.js';
+import {PARTITIONS, partitionBidders, filterBidsForAdUnit, getS2SBidderSet} from '../src/adapterManager.js';
+import {find} from '../src/polyfill.js';
+import {getBidderCodes, logWarn} from '../src/utils.js';
-let s2sTesting = {};
-
-const SERVER = 'server';
-const CLIENT = 'client';
-
-s2sTesting.SERVER = SERVER;
-s2sTesting.CLIENT = CLIENT;
+const {CLIENT, SERVER} = PARTITIONS;
+export const s2sTesting = {
+ ...PARTITIONS,
+ clientTestBidders: new Set()
+};
s2sTesting.bidSource = {}; // store bidder sources determined from s2sConfig bidderControl
s2sTesting.globalRand = Math.random(); // if 10% of bidderA and 10% of bidderB should be server-side, make it the same 10%
@@ -40,7 +40,7 @@ s2sTesting.getSourceBidderMap = function(adUnits = [], allS2SBidders = []) {
[SERVER]: Object.keys(sourceBidders[SERVER]),
[CLIENT]: Object.keys(sourceBidders[CLIENT])
};
-};
+}
/**
* @function calculateBidSources determines the source for each s2s bidder based on bidderControl weightings. these can be overridden at the adUnit level
@@ -53,7 +53,7 @@ s2sTesting.calculateBidSources = function(s2sConfig = {}) {
(s2sConfig.bidders || []).forEach((bidder) => {
s2sTesting.bidSource[bidder] = s2sTesting.getSource(bidderControl[bidder] && bidderControl[bidder].bidSource) || SERVER; // default to server
});
-};
+}
/**
* @function getSource() gets a random source based on the given sourceWeights (export just for testing)
@@ -76,10 +76,59 @@ s2sTesting.getSource = function(sourceWeights = {}, bidSources = [SERVER, CLIENT
// choose the first source with an incremental weight > random weight
if (rndWeight < srcIncWeight[source]) return source;
}
-};
+}
+
+function doingS2STesting(s2sConfig) {
+ return s2sConfig && s2sConfig.enabled && s2sConfig.testing;
+}
-// inject the s2sTesting module into the adapterManager rather than importing it
-// importing it causes the packager to include it even when it's not explicitly included in the build
-setS2STestingModule(s2sTesting);
+function isTestingServerOnly(s2sConfig) {
+ return Boolean(doingS2STesting(s2sConfig) && s2sConfig.testServerOnly);
+}
+
+const adUnitsContainServerRequests = (adUnits, s2sConfig) => Boolean(
+ find(adUnits, adUnit => find(adUnit.bids, bid => (
+ bid.bidSource ||
+ (s2sConfig.bidderControl && s2sConfig.bidderControl[bid.bidder])
+ ) && bid.finalSource === SERVER))
+);
+
+partitionBidders.before(function (next, adUnits, s2sConfigs) {
+ const serverBidders = getS2SBidderSet(s2sConfigs);
+ let serverOnly = false;
+
+ s2sConfigs.forEach((s2sConfig) => {
+ if (doingS2STesting(s2sConfig)) {
+ s2sTesting.calculateBidSources(s2sConfig);
+ const bidderMap = s2sTesting.getSourceBidderMap(adUnits, [...serverBidders]);
+ // get all adapters doing client testing
+ bidderMap[CLIENT].forEach((bidder) => s2sTesting.clientTestBidders.add(bidder))
+ }
+ if (isTestingServerOnly(s2sConfig) && adUnitsContainServerRequests(adUnits, s2sConfig)) {
+ logWarn('testServerOnly: True. All client requests will be suppressed.');
+ serverOnly = true;
+ }
+ });
+
+ next.bail(getBidderCodes(adUnits).reduce((memo, bidder) => {
+ if (serverBidders.has(bidder)) {
+ memo[SERVER].push(bidder);
+ }
+ if (!serverOnly && (!serverBidders.has(bidder) || s2sTesting.clientTestBidders.has(bidder))) {
+ memo[CLIENT].push(bidder);
+ }
+ return memo;
+ }, {[CLIENT]: [], [SERVER]: []}));
+});
+
+filterBidsForAdUnit.before(function(next, bids, s2sConfig) {
+ if (s2sConfig == null) {
+ next.bail(bids.filter((bid) => !s2sTesting.clientTestBidders.size || bid.finalSource !== SERVER));
+ } else {
+ const serverBidders = getS2SBidderSet(s2sConfig);
+ next.bail(bids.filter((bid) => serverBidders.has(bid.bidder) &&
+ (!doingS2STesting(s2sConfig) || bid.finalSource !== CLIENT)));
+ }
+});
export default s2sTesting;
diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js
index a6a024c0387..405799813eb 100644
--- a/modules/sizeMappingV2.js
+++ b/modules/sizeMappingV2.js
@@ -147,19 +147,12 @@ export function checkAdUnitSetupHook(adUnits) {
}
const validatedAdUnits = [];
adUnits.forEach(adUnit => {
- const bids = adUnit.bids;
+ adUnit = adUnitSetupChecks.validateAdUnit(adUnit);
+ if (adUnit == null) return;
+
const mediaTypes = adUnit.mediaTypes;
let validatedBanner, validatedVideo, validatedNative;
- if (!bids || !isArray(bids)) {
- logError(`Detected adUnit.code '${adUnit.code}' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.`);
- return;
- }
-
- if (!mediaTypes || Object.keys(mediaTypes).length === 0) {
- logError(`Detected adUnit.code '${adUnit.code}' did not have a 'mediaTypes' object defined. This is a required field for the auction, so this adUnit has been removed.`);
- return;
- }
if (mediaTypes.banner) {
if (mediaTypes.banner.sizes) {
// Ad unit is using 'mediaTypes.banner.sizes' instead of the new property 'sizeConfig'. Apply the old checks!
diff --git a/src/adapterManager.js b/src/adapterManager.js
index edba4b8ca0f..93eeba51cde 100644
--- a/src/adapterManager.js
+++ b/src/adapterManager.js
@@ -32,9 +32,13 @@ import { adunitCounter } from './adUnits.js';
import { getRefererInfo } from './refererDetection.js';
import {GdprConsentHandler, UspConsentHandler} from './consentHandler.js';
+export const PARTITIONS = {
+ CLIENT: 'client',
+ SERVER: 'server'
+}
+
var CONSTANTS = require('./constants.json');
var events = require('./events.js');
-let s2sTestingModule; // store s2sTesting module if it's loaded
let adapterManager = {};
@@ -102,19 +106,33 @@ function getBids({bidderCode, auctionId, bidderRequestId, adUnits, src}) {
const hookedGetBids = hook('sync', getBids, 'getBids');
+/**
+ * Filter an adUnit's bids for building client and/or server requests
+ *
+ * @param bids an array of bids as defined in an adUnit
+ * @param s2sConfig null if the adUnit is being routed to a client adapter; otherwise the s2s adapter's config
+ * @returns the subset of `bids` that are pertinent for the given `s2sConfig`
+ */
+export function _filterBidsForAdUnit(bids, s2sConfig, {getS2SBidders = getS2SBidderSet} = {}) {
+ if (s2sConfig == null) {
+ return bids;
+ } else {
+ const serverBidders = getS2SBidders(s2sConfig);
+ return bids.filter((bid) => serverBidders.has(bid.bidder))
+ }
+}
+export const filterBidsForAdUnit = hook('sync', _filterBidsForAdUnit, 'filterBidsForAdUnit');
+
function getAdUnitCopyForPrebidServer(adUnits, s2sConfig) {
- let adaptersServerSide = s2sConfig.bidders;
let adUnitsCopy = deepClone(adUnits);
adUnitsCopy.forEach((adUnit) => {
// filter out client side bids
- adUnit.bids = adUnit.bids.filter((bid) => {
- return includes(adaptersServerSide, bid.bidder) &&
- (!doingS2STesting(s2sConfig) || bid.finalSource !== s2sTestingModule.CLIENT);
- }).map((bid) => {
- bid.bid_id = getUniqueIdentifierStr();
- return bid;
- });
+ adUnit.bids = filterBidsForAdUnit(adUnit.bids, s2sConfig)
+ .map((bid) => {
+ bid.bid_id = getUniqueIdentifierStr();
+ return bid;
+ });
});
// don't send empty requests
@@ -126,11 +144,8 @@ function getAdUnitCopyForPrebidServer(adUnits, s2sConfig) {
function getAdUnitCopyForClientAdapters(adUnits) {
let adUnitsClientCopy = deepClone(adUnits);
- // filter out s2s bids
adUnitsClientCopy.forEach((adUnit) => {
- adUnit.bids = adUnit.bids.filter((bid) => {
- return !clientTestAdapters.length || bid.finalSource !== s2sTestingModule.SERVER;
- })
+ adUnit.bids = filterBidsForAdUnit(adUnit.bids, null);
});
// don't send empty requests
@@ -150,21 +165,6 @@ export let coppaDataHandler = {
}
};
-// export for testing
-export let clientTestAdapters = [];
-export const allS2SBidders = [];
-
-export function getAllS2SBidders() {
- adapterManager.s2STestingEnabled = false;
- _s2sConfigs.forEach(s2sConfig => {
- if (s2sConfig && s2sConfig.enabled) {
- if (s2sConfig.bidders && s2sConfig.bidders.length) {
- allS2SBidders.push(...s2sConfig.bidders);
- }
- }
- })
-}
-
/**
* Filter and/or modify media types for ad units based on the given labels.
*
@@ -176,6 +176,37 @@ export const setupAdUnitMediaTypes = hook('sync', (adUnits, labels) => {
return processAdUnitsForLabels(adUnits, labels);
}, 'setupAdUnitMediaTypes')
+/**
+ * @param {{}|Array<{}>} s2sConfigs
+ * @returns {Set} a set of all the bidder codes that should be routed through the S2S adapter(s)
+ * as defined in `s2sConfigs`
+ */
+export function getS2SBidderSet(s2sConfigs) {
+ if (!isArray(s2sConfigs)) s2sConfigs = [s2sConfigs];
+ // `null` represents the "no bid bidder" - when an ad unit is meant only for S2S adapters, like stored impressions
+ const serverBidders = new Set([null]);
+ s2sConfigs.filter((s2s) => s2s && s2s.enabled)
+ .flatMap((s2s) => s2s.bidders)
+ .forEach((bidder) => serverBidders.add(bidder));
+ return serverBidders;
+}
+
+/**
+ * @returns {{[PARTITIONS.CLIENT]: Array, [PARTITIONS.SERVER]: Array}}
+ * All the bidder codes in the given `adUnits`, divided in two arrays -
+ * those that should be routed to client, and server adapters (according to the configuration in `s2sConfigs`).
+ */
+export function _partitionBidders (adUnits, s2sConfigs, {getS2SBidders = getS2SBidderSet} = {}) {
+ const serverBidders = getS2SBidders(s2sConfigs);
+ return getBidderCodes(adUnits).reduce((memo, bidder) => {
+ const partition = serverBidders.has(bidder) ? PARTITIONS.SERVER : PARTITIONS.CLIENT;
+ memo[partition].push(bidder);
+ return memo;
+ }, {[PARTITIONS.CLIENT]: [], [PARTITIONS.SERVER]: []})
+}
+
+export const partitionBidders = hook('sync', _partitionBidders, 'partitionBidders');
+
adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, auctionId, cbTimeout, labels) {
/**
* emit and pass adunits for external modification
@@ -185,62 +216,22 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a
decorateAdUnitsWithNativeParams(adUnits);
adUnits = setupAdUnitMediaTypes(adUnits, labels);
- let bidderCodes = getBidderCodes(adUnits);
+ let {[PARTITIONS.CLIENT]: clientBidders, [PARTITIONS.SERVER]: serverBidders} = partitionBidders(adUnits, _s2sConfigs);
+
if (config.getConfig('bidderSequence') === RANDOM) {
- bidderCodes = shuffle(bidderCodes);
+ clientBidders = shuffle(clientBidders);
}
const refererInfo = getRefererInfo();
- let clientBidderCodes = bidderCodes;
-
let bidRequests = [];
- if (allS2SBidders.length === 0) {
- getAllS2SBidders();
- }
-
_s2sConfigs.forEach(s2sConfig => {
if (s2sConfig && s2sConfig.enabled) {
- if (doingS2STesting(s2sConfig)) {
- s2sTestingModule.calculateBidSources(s2sConfig);
- const bidderMap = s2sTestingModule.getSourceBidderMap(adUnits, allS2SBidders);
- // get all adapters doing client testing
- bidderMap[s2sTestingModule.CLIENT].forEach(bidder => {
- if (!includes(clientTestAdapters, bidder)) {
- clientTestAdapters.push(bidder);
- }
- })
- }
- }
- })
-
- // don't call these client side (unless client request is needed for testing)
- clientBidderCodes = bidderCodes.filter(bidderCode => {
- return !includes(allS2SBidders, bidderCode) || includes(clientTestAdapters, bidderCode)
- });
-
- // these are called on the s2s adapter
- let adaptersServerSide = allS2SBidders;
-
- const adUnitsContainServerRequests = (adUnits, s2sConfig) => Boolean(
- find(adUnits, adUnit => find(adUnit.bids, bid => (
- bid.bidSource ||
- (s2sConfig.bidderControl && s2sConfig.bidderControl[bid.bidder])
- ) && bid.finalSource === s2sTestingModule.SERVER))
- );
-
- _s2sConfigs.forEach(s2sConfig => {
- if (s2sConfig && s2sConfig.enabled) {
- if ((isTestingServerOnly(s2sConfig) && adUnitsContainServerRequests(adUnits, s2sConfig))) {
- logWarn('testServerOnly: True. All client requests will be suppressed.');
- clientBidderCodes.length = 0;
- }
-
let adUnitsS2SCopy = getAdUnitCopyForPrebidServer(adUnits, s2sConfig);
// uniquePbsTid is so we know which server to send which bids to during the callBids function
let uniquePbsTid = generateUUID();
- adaptersServerSide.forEach(bidderCode => {
+ serverBidders.forEach(bidderCode => {
const bidderRequestId = getUniqueIdentifierStr();
const bidderRequest = {
bidderCode,
@@ -277,7 +268,7 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a
// client adapters
let adUnitsClientCopy = getAdUnitCopyForClientAdapters(adUnits);
- clientBidderCodes.forEach(bidderCode => {
+ clientBidders.forEach(bidderCode => {
const bidderRequestId = getUniqueIdentifierStr();
const bidderRequest = {
bidderCode,
@@ -342,7 +333,7 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request
// $.source.tid MUST be a unique UUID and also THE SAME between all PBS Requests for a given Auction
const sourceTid = generateUUID();
_s2sConfigs.forEach((s2sConfig) => {
- if (s2sConfig && uniqueServerBidRequests[counter] && includes(s2sConfig.bidders, uniqueServerBidRequests[counter].bidderCode)) {
+ if (s2sConfig && uniqueServerBidRequests[counter] && getS2SBidderSet(s2sConfig).has(uniqueServerBidRequests[counter].bidderCode)) {
// s2s should get the same client side timeout as other client side requests.
const s2sAjax = ajaxBuilder(requestBidsTimeout, requestCallbacks ? {
request: requestCallbacks.request.bind(null, 's2s'),
@@ -363,11 +354,8 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request
return doneCb.bind(bidRequest);
});
- // only log adapters that actually have adUnit bids
- let allBidders = s2sBidRequest.ad_units.reduce((adapters, adUnit) => {
- return adapters.concat((adUnit.bids || []).reduce((adapters, bid) => adapters.concat(bid.bidder), []));
- }, []);
- logMessage(`CALLING S2S HEADER BIDDERS ==== ${adaptersServerSide.filter(adapter => includes(allBidders, adapter)).join(',')}`);
+ const bidders = getBidderCodes(s2sBidRequest.ad_units).filter((bidder) => adaptersServerSide.includes(bidder));
+ logMessage(`CALLING S2S HEADER BIDDERS ==== ${bidders.length > 0 ? bidders.join(', ') : 'No bidder specified, using "ortb2Imp" definition(s) only'}`);
// fire BID_REQUESTED event for each s2s bidRequest
uniqueServerRequests.forEach(bidRequest => {
@@ -426,14 +414,6 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request
});
};
-function doingS2STesting(s2sConfig) {
- return s2sConfig && s2sConfig.enabled && s2sConfig.testing && s2sTestingModule;
-}
-
-function isTestingServerOnly(s2sConfig) {
- return Boolean(doingS2STesting(s2sConfig) && s2sConfig.testServerOnly);
-};
-
function getSupportedMediaTypes(bidderCode) {
let supportedMediaTypes = [];
if (includes(adapterManager.videoAdapters, bidderCode)) supportedMediaTypes.push('video');
@@ -548,12 +528,6 @@ 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) {
- s2sTestingModule = module;
-}
-
function tryCallBidderMethod(bidder, method, param) {
try {
const adapter = _bidderRegistry[bidder];
diff --git a/src/prebid.js b/src/prebid.js
index ca97558fc69..d3f5ec989f3 100644
--- a/src/prebid.js
+++ b/src/prebid.js
@@ -159,7 +159,34 @@ function validateAdUnitPos(adUnit, mediaType) {
return adUnit
}
+function validateAdUnit(adUnit) {
+ const msg = (msg) => `adUnit.code '${adUnit.code}' ${msg}`;
+
+ const mediaTypes = adUnit.mediaTypes;
+ const bids = adUnit.bids;
+
+ if (bids != null && !isArray(bids)) {
+ logError(msg(`defines 'adUnit.bids' that is not an array. Removing adUnit from auction`));
+ return null;
+ }
+ if (bids == null && adUnit.ortb2Imp == null) {
+ logError(msg(`has no 'adUnit.bids' and no 'adUnit.ortb2Imp'. Removing adUnit from auction`));
+ return null;
+ }
+ if (!mediaTypes || Object.keys(mediaTypes).length === 0) {
+ logError(msg(`does not define a 'mediaTypes' object. This is a required field for the auction, so this adUnit has been removed.`));
+ return null;
+ }
+ if (adUnit.ortb2Imp != null && (bids == null || bids.length === 0)) {
+ adUnit.bids = [{bidder: null}]; // the 'null' bidder is treated as an s2s-only placeholder by adapterManager
+ logMessage(msg(`defines 'adUnit.ortb2Imp' with no 'adUnit.bids'; it will be seen only by S2S adapters`));
+ }
+
+ return adUnit;
+}
+
export const adUnitSetupChecks = {
+ validateAdUnit,
validateBannerMediaType,
validateVideoMediaType,
validateNativeMediaType,
@@ -170,20 +197,12 @@ export const checkAdUnitSetup = hook('sync', function (adUnits) {
const validatedAdUnits = [];
adUnits.forEach(adUnit => {
+ adUnit = validateAdUnit(adUnit);
+ if (adUnit == null) return;
+
const mediaTypes = adUnit.mediaTypes;
- const bids = adUnit.bids;
let validatedBanner, validatedVideo, validatedNative;
- if (!bids || !isArray(bids)) {
- logError(`Detected adUnit.code '${adUnit.code}' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.`);
- return;
- }
-
- if (!mediaTypes || Object.keys(mediaTypes).length === 0) {
- logError(`Detected adUnit.code '${adUnit.code}' did not have a 'mediaTypes' object defined. This is a required field for the auction, so this adUnit has been removed.`);
- return;
- }
-
if (mediaTypes.banner) {
validatedBanner = validateBannerMediaType(adUnit);
if (mediaTypes.banner.hasOwnProperty('pos')) validatedBanner = validateAdUnitPos(validatedBanner, 'banner');
diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js
index 0c80036eeed..7b297aa4c5a 100644
--- a/test/spec/modules/prebidServerBidAdapter_spec.js
+++ b/test/spec/modules/prebidServerBidAdapter_spec.js
@@ -2471,6 +2471,29 @@ describe('S2S Adapter', function () {
expect(addBidResponse.calledWith(sinon.match.any, sinon.match({bidderCode: 'unknown'}))).to.be.true;
});
+ it('uses "null" request\'s ID for all responses, when a null request is present', function () {
+ const cfg = {...CONFIG, allowUnknownBidderCodes: true};
+ config.setConfig({s2sConfig: cfg});
+ const req = {...REQUEST, s2sConfig: cfg, ad_units: [{...REQUEST.ad_units[0], bids: [{bidder: null, bid_id: 'testId'}]}]};
+ const bidReq = {...BID_REQUESTS[0], bidderCode: null, bids: [{...BID_REQUESTS[0].bids[0], bidder: null, bidId: 'testId'}]}
+ adapter.callBids(req, [bidReq], addBidResponse, done, ajax);
+ const response = deepClone(RESPONSE_OPENRTB);
+ response.seatbid[0].seat = 'storedImpression';
+ server.requests[0].respond(200, {}, JSON.stringify(response));
+ sinon.assert.calledWith(addBidResponse, sinon.match.any, sinon.match({bidderCode: 'storedImpression', requestId: 'testId'}))
+ });
+
+ it('copies ortb2Imp to response when there is only a null bid', () => {
+ const cfg = {...CONFIG};
+ config.setConfig({s2sConfig: cfg});
+ const ortb2Imp = {ext: {prebid: {storedrequest: 'value'}}};
+ const req = {...REQUEST, s2sConfig: cfg, ad_units: [{...REQUEST.ad_units[0], bids: [{bidder: null, bid_id: 'testId'}], ortb2Imp}]};
+ const bidReq = {...BID_REQUESTS[0], bidderCode: null, bids: [{...BID_REQUESTS[0].bids[0], bidder: null, bidId: 'testId'}]}
+ adapter.callBids(req, [bidReq], addBidResponse, done, ajax);
+ const actual = JSON.parse(server.requests[0].requestBody);
+ sinon.assert.match(actual.imp[0], sinon.match(ortb2Imp));
+ });
+
describe('on sync requested with no cookie', () => {
let cfg, req, csRes;
@@ -2623,21 +2646,6 @@ describe('S2S Adapter', function () {
sinon.assert.calledOnce(logErrorSpy);
});
- it('should log an error when bidders is missing', function () {
- const options = {
- accountId: '1',
- enabled: true,
- timeout: 1000,
- adapter: 's2s',
- endpoint: {
- p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'
- }
- };
-
- config.setConfig({ s2sConfig: options });
- sinon.assert.calledOnce(logErrorSpy);
- });
-
it('should log an error when endpoint is missing', function () {
const options = {
accountId: '1',
diff --git a/test/spec/modules/sizeMappingV2_spec.js b/test/spec/modules/sizeMappingV2_spec.js
index c8002b4bacc..9bbd472c7e0 100644
--- a/test/spec/modules/sizeMappingV2_spec.js
+++ b/test/spec/modules/sizeMappingV2_spec.js
@@ -194,49 +194,23 @@ describe('sizeMappingV2', function () {
utils.logError.restore();
});
- it('should filter out adUnit if it does not contain the required property mediaTypes', function () {
- let adUnits = utils.deepClone(AD_UNITS);
- delete adUnits[0].mediaTypes;
- // before checkAdUnitSetupHook is called, the length of adUnits should be '2'
- expect(adUnits.length).to.equal(2);
- adUnits = checkAdUnitSetupHook(adUnits);
-
- // after checkAdUnitSetupHook is called, the length of adUnits should be '1'
- expect(adUnits.length).to.equal(1);
- expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1');
- });
-
- it('should filter out adUnit if it does not contain the required property "bids"', function() {
- let adUnits = utils.deepClone(AD_UNITS);
- delete adUnits[0].mediaTypes;
- // before checkAdUnitSetupHook is called, the length of the adUnits should equal '2'
- expect(adUnits.length).to.equal(2);
- adUnits = checkAdUnitSetupHook(adUnits);
-
- // after checkAdUnitSetupHook is called, the length of the adUnits should be '1'
- expect(adUnits.length).to.equal(1);
- expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1');
- });
+ describe('basic validation', () => {
+ let validateAdUnit;
- it('should filter out adUnit if it has declared property mediaTypes with an empty object', function () {
- let adUnits = utils.deepClone(AD_UNITS);
- adUnits[0].mediaTypes = {};
- // before checkAdUnitSetupHook is called, the length of adUnits should be '2'
- expect(adUnits.length).to.equal(2);
- adUnits = checkAdUnitSetupHook(adUnits);
-
- // after checkAdUnitSetupHook is called, the length of adUnits should be '1'
- expect(adUnits.length).to.equal(1);
- expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1');
- });
+ beforeEach(() => {
+ validateAdUnit = sinon.stub(adUnitSetupChecks, 'validateAdUnit');
+ });
- it('should log an error message if Ad Unit does not contain the required property "mediaTypes"', function () {
- let adUnits = utils.deepClone(AD_UNITS);
- delete adUnits[0].mediaTypes;
+ afterEach(() => {
+ validateAdUnit.restore();
+ });
- checkAdUnitSetupHook(adUnits);
- sinon.assert.callCount(utils.logError, 1);
- sinon.assert.calledWith(utils.logError, 'Detected adUnit.code \'div-gpt-ad-1460505748561-0\' did not have a \'mediaTypes\' object defined. This is a required field for the auction, so this adUnit has been removed.');
+ it('should filter out adUnits that do not pass adUnitSetupChecks.validateAdUnit', () => {
+ validateAdUnit.returns(null);
+ const adUnits = checkAdUnitSetupHook(utils.deepClone(AD_UNITS));
+ AD_UNITS.forEach((u) => sinon.assert.calledWith(validateAdUnit, u));
+ expect(adUnits.length).to.equal(0);
+ });
});
describe('banner mediaTypes checks', function () {
diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js
index 41eaa140517..88beaa88a67 100644
--- a/test/spec/unit/core/adapterManager_spec.js
+++ b/test/spec/unit/core/adapterManager_spec.js
@@ -1,5 +1,11 @@
import { expect } from 'chai';
-import adapterManager, { allS2SBidders, clientTestAdapters, gdprDataHandler, coppaDataHandler } from 'src/adapterManager.js';
+import adapterManager, {
+ gdprDataHandler,
+ coppaDataHandler,
+ _partitionBidders,
+ PARTITIONS,
+ getS2SBidderSet, _filterBidsForAdUnit
+} from 'src/adapterManager.js';
import {
getAdUnits,
getServerTestingConfig,
@@ -87,6 +93,10 @@ describe('adapterManager tests', function () {
config.setConfig({s2sConfig: { enabled: false }});
});
+ afterEach(() => {
+ s2sTesting.clientTestBidders.clear();
+ });
+
describe('callBids', function () {
before(function () {
config.setConfig({s2sConfig: { enabled: false }});
@@ -701,10 +711,6 @@ describe('adapterManager tests', function () {
prebidServerAdapterMock.callBids.reset();
});
- afterEach(function () {
- allS2SBidders.length = 0;
- });
-
const bidRequests = [{
'bidderCode': 'appnexus',
'auctionId': '1863e370099523',
@@ -1305,9 +1311,6 @@ describe('adapterManager tests', function () {
}
beforeEach(function () {
- allS2SBidders.length = 0;
- clientTestAdapters.length = 0
-
adapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock;
adapterManager.bidderRegistry['adequant'] = adequantAdapterMock;
adapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock;
@@ -1662,7 +1665,6 @@ describe('adapterManager tests', function () {
describe('makeBidRequests', function () {
let adUnits;
beforeEach(function () {
- allS2SBidders.length = 0
adUnits = utils.deepClone(getAdUnits()).map(adUnit => {
adUnit.bids = adUnit.bids.filter(bid => includes(['appnexus', 'rubicon'], bid.bidder));
return adUnit;
@@ -1736,8 +1738,6 @@ describe('adapterManager tests', function () {
let sandbox;
beforeEach(function () {
sandbox = sinon.sandbox.create();
- allS2SBidders.length = 0;
- clientTestAdapters.length = 0;
// always have matchMedia return true for us
sandbox.stub(utils.getWindowTop(), 'matchMedia').callsFake(() => ({matches: true}));
});
@@ -1913,7 +1913,7 @@ describe('adapterManager tests', function () {
['visitor-uk', 'desktop']
);
- // only one adUnit and one bid from that adUnit should make it through the applied labels above
+ // only one adUnit and one bid from that adUnit should make it through the applied labels above
expect(bidRequests.length).to.equal(1);
expect(bidRequests[0].bidderCode).to.equal('rubicon');
expect(bidRequests[0].bids.length).to.equal(1);
@@ -1997,7 +1997,6 @@ describe('adapterManager tests', function () {
describe('s2sTesting - testServerOnly', () => {
beforeEach(() => {
config.setConfig({ s2sConfig: getServerTestingConfig(CONFIG) });
- allS2SBidders.length = 0
s2sTesting.bidSource = {};
});
@@ -2128,7 +2127,6 @@ describe('adapterManager tests', function () {
afterEach(() => {
config.resetConfig()
- allS2SBidders.length = 0;
s2sTesting.bidSource = {};
});
@@ -2308,4 +2306,102 @@ describe('adapterManager tests', function () {
);
});
});
+
+ describe('getS2SBidderSet', () => {
+ it('should always return the "null" bidder', () => {
+ expect([...getS2SBidderSet({bidders: []})]).to.eql([null]);
+ });
+
+ it('should not consider disabled s2s adapters', () => {
+ const actual = getS2SBidderSet([{enabled: false, bidders: ['A', 'B']}, {enabled: true, bidders: ['C']}]);
+ expect([...actual]).to.include.members(['C']);
+ expect([...actual]).not.to.include.members(['A', 'B']);
+ });
+
+ it('should accept both single config objects and an array of them', () => {
+ const conf = {enabled: true, bidders: ['A', 'B']};
+ expect(getS2SBidderSet(conf)).to.eql(getS2SBidderSet([conf]));
+ });
+ });
+
+ describe('separation of client and server bidders', () => {
+ let s2sBidders, getS2SBidders;
+ beforeEach(() => {
+ s2sBidders = null;
+ getS2SBidders = sinon.stub();
+ getS2SBidders.callsFake(() => s2sBidders);
+ })
+
+ describe('partitionBidders', () => {
+ let adUnits;
+
+ beforeEach(() => {
+ adUnits = [{
+ bids: [{
+ bidder: 'A'
+ }, {
+ bidder: 'B'
+ }]
+ }, {
+ bids: [{
+ bidder: 'A',
+ }, {
+ bidder: 'C'
+ }]
+ }];
+ });
+
+ function partition(adUnits, s2sConfigs) {
+ return _partitionBidders(adUnits, s2sConfigs, {getS2SBidders})
+ }
+
+ Object.entries({
+ 'all client': {
+ s2s: [],
+ expected: {
+ [PARTITIONS.CLIENT]: ['A', 'B', 'C'],
+ [PARTITIONS.SERVER]: []
+ }
+ },
+ 'all server': {
+ s2s: ['A', 'B', 'C'],
+ expected: {
+ [PARTITIONS.CLIENT]: [],
+ [PARTITIONS.SERVER]: ['A', 'B', 'C']
+ }
+ },
+ 'mixed': {
+ s2s: ['B', 'C'],
+ expected: {
+ [PARTITIONS.CLIENT]: ['A'],
+ [PARTITIONS.SERVER]: ['B', 'C']
+ }
+ }
+ }).forEach(([test, {s2s, expected}]) => {
+ it(`should partition ${test} requests`, () => {
+ s2sBidders = new Set(s2s);
+ const s2sConfig = {};
+ expect(partition(adUnits, s2sConfig)).to.eql(expected);
+ sinon.assert.calledWith(getS2SBidders, sinon.match.same(s2sConfig));
+ });
+ });
+ });
+
+ describe('filterBidsForAdUnit', () => {
+ function filterBids(bids, s2sConfig) {
+ return _filterBidsForAdUnit(bids, s2sConfig, {getS2SBidders});
+ }
+ it('should not filter any bids when s2sConfig == null', () => {
+ const bids = ['untouched', 'data'];
+ expect(filterBids(bids)).to.eql(bids);
+ });
+
+ it('should remove bids that have bidder not present in s2sConfig', () => {
+ s2sBidders = new Set('A', 'B');
+ const s2sConfig = {};
+ expect(filterBids(['A', 'C', 'D'].map((code) => ({bidder: code})), s2sConfig)).to.eql([{bidder: 'A'}]);
+ sinon.assert.calledWith(getS2SBidders, sinon.match.same(s2sConfig));
+ })
+ });
+ });
});
diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js
index ccd25be1048..5302b3b8c74 100644
--- a/test/spec/unit/pbjs_api_spec.js
+++ b/test/spec/unit/pbjs_api_spec.js
@@ -1626,6 +1626,7 @@ describe('Unit: Prebid Module', function () {
let auctionArgs;
beforeEach(function () {
+ auctionArgs = null;
adUnitsBackup = auction.getAdUnits
auctionManagerStub = sinon.stub(auctionManager, 'createAuction').callsFake(function() {
auctionArgs = arguments[0];
@@ -1898,6 +1899,22 @@ describe('Unit: Prebid Module', function () {
expect(auctionArgs.adUnits[0].mediaTypes.banner.pos).to.equal(2);
expect(auctionArgs.adUnits[1].mediaTypes.banner.pos).to.equal(0);
});
+
+ it(`should allow no bids if 'ortb2Imp' is specified`, () => {
+ const adUnit = {
+ code: 'test',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250]]
+ }
+ },
+ ortb2Imp: {}
+ };
+ $$PREBID_GLOBAL$$.requestBids({
+ adUnits: [adUnit]
+ });
+ sinon.assert.match(auctionArgs.adUnits[0], adUnit);
+ });
});
describe('negative tests for validating adUnits', function() {
@@ -2033,7 +2050,7 @@ describe('Unit: Prebid Module', function () {
});
expect(auctionArgs.adUnits.length).to.equal(1);
expect(auctionArgs.adUnits[1]).to.not.exist;
- assert.ok(logErrorSpy.calledWith("Detected adUnit.code 'bad-ad-unit-2' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction."));
+ assert.ok(logErrorSpy.calledWith("adUnit.code 'bad-ad-unit-2' has no 'adUnit.bids' and no 'adUnit.ortb2Imp'. Removing adUnit from auction"));
});
});
});
From 4d2e77c25f53e6a8ae1dcf164eb4cc57473ee7fe Mon Sep 17 00:00:00 2001
From: Nitin Nimbalkar <96475150+nitin0610@users.noreply.github.com>
Date: Thu, 24 Mar 2022 21:02:57 +0530
Subject: [PATCH 13/16] UserID module: fix esp unit test (#8212)
* ESP:Assetion Issue solved
---
test/spec/modules/userId_spec.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js
index a567c6b5371..63107aa5107 100644
--- a/test/spec/modules/userId_spec.js
+++ b/test/spec/modules/userId_spec.js
@@ -2705,17 +2705,17 @@ describe('User ID', function () {
}).catch(done);
});
- it('pbjs.getEncryptedEidsForSource should return string if custom function is defined', () => {
+ it('pbjs.getEncryptedEidsForSource should return string if custom function is defined', (done) => {
const getCustomSignal = () => {
return '{"keywords":["tech","auto"]}';
}
- const expectedString = '"1||{\"keywords\":[\"tech\",\"auto\"]}"';
+ const expectedString = '1||eyJrZXl3b3JkcyI6WyJ0ZWNoIiwiYXV0byJdfQ==';
const encrypt = false;
const source = 'pubmatic.com';
(getGlobal()).getEncryptedEidsForSource(source, encrypt, getCustomSignal).then((result) => {
expect(result).to.equal(expectedString);
done();
- });
+ }).catch(done);
});
it('pbjs.getUserIdsAsEidBySource', () => {
From e07025247a81bf8c04501de8c66d817143cc30d1 Mon Sep 17 00:00:00 2001
From: "Prebid.js automated release"
Date: Thu, 24 Mar 2022 15:50:20 +0000
Subject: [PATCH 14/16] Prebid 6.17.0 release
---
package-lock.json | 2 +-
package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index db070474a86..8ee913f472e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "prebid.js",
- "version": "6.17.0-pre",
+ "version": "6.17.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
diff --git a/package.json b/package.json
index 9c55f8d6783..1e860caddfb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "prebid.js",
- "version": "6.17.0-pre",
+ "version": "6.17.0",
"description": "Header Bidding Management Library",
"main": "src/prebid.js",
"scripts": {
From 3fbea52a9807cb3509475ffe1b11439f0883fa92 Mon Sep 17 00:00:00 2001
From: "Prebid.js automated release"
Date: Thu, 24 Mar 2022 15:50:20 +0000
Subject: [PATCH 15/16] Increment version to 6.18.0-pre
---
package-lock.json | 2 +-
package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 8ee913f472e..9fbd4aea6a8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "prebid.js",
- "version": "6.17.0",
+ "version": "6.18.0-pre",
"lockfileVersion": 2,
"requires": true,
"packages": {
diff --git a/package.json b/package.json
index 1e860caddfb..c812bc123f0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "prebid.js",
- "version": "6.17.0",
+ "version": "6.18.0-pre",
"description": "Header Bidding Management Library",
"main": "src/prebid.js",
"scripts": {
From 4ec2cbbdc860246d8766cd2c1b2600598fc49de5 Mon Sep 17 00:00:00 2001
From: Damyan
Date: Fri, 25 Mar 2022 15:08:25 +0200
Subject: [PATCH 16/16] AdHash Bid Adapter: add brand safety (#8167)
* AdHash Bidder Adapter: minor changes
We're operating on a com TLD now.
Added publisher in URL for easier routing.
* Implemented brand safety
Implemented brand safety checks
* Fix for GDPR consent
Removing the extra information as request data becomes too big and is sometimes truncated
* Ad fraud prevention formula changed
Ad fraud prevention formula changed to support negative values as well as linear distribution of article length
---
modules/adhashBidAdapter.js | 82 ++++++++++++++++++++-
test/spec/modules/adhashBidAdapter_spec.js | 84 +++++++++++++++++++---
2 files changed, 153 insertions(+), 13 deletions(-)
diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js
index 679a9052cd3..7f5af047993 100644
--- a/modules/adhashBidAdapter.js
+++ b/modules/adhashBidAdapter.js
@@ -3,6 +3,79 @@ import {includes} from '../src/polyfill.js';
import {BANNER} from '../src/mediaTypes.js';
const VERSION = '1.0';
+const BAD_WORD_STEP = 0.1;
+const BAD_WORD_MIN = 0.2;
+
+/**
+ * Function that checks the page where the ads are being served for brand safety.
+ * If unsafe words are found the scoring of that page increases.
+ * If it becomes greater than the maximum allowed score false is returned.
+ * The rules may vary based on the website language or the publisher.
+ * The AdHash bidder will not bid on unsafe pages (according to 4A's).
+ * @param badWords list of scoring rules to chech against
+ * @param maxScore maximum allowed score for that bidding
+ * @returns boolean flag is the page safe
+ */
+function brandSafety(badWords, maxScore) {
+ /**
+ * Performs the ROT13 encoding on the string argument and returns the resulting string.
+ * The Adhash bidder uses ROT13 so that the response is not blocked by:
+ * - ad blocking software
+ * - parental control software
+ * - corporate firewalls
+ * due to the bad words contained in the response.
+ * @param value The input string.
+ * @returns string Returns the ROT13 version of the given string.
+ */
+ const rot13 = value => {
+ const input = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+ const output = 'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm';
+ const index = x => input.indexOf(x);
+ const translate = x => index(x) > -1 ? output[index(x)] : x;
+ return value.split('').map(translate).join('');
+ };
+
+ /**
+ * Calculates the scoring for each bad word with dimishing returns
+ * @param {integer} points points that this word costs
+ * @param {integer} occurances number of occurances
+ * @returns {float} final score
+ */
+ const scoreCalculator = (points, occurances) => {
+ let positive = true;
+ if (points < 0) {
+ points *= -1;
+ positive = false;
+ }
+ let result = 0;
+ for (let i = 0; i < occurances; i++) {
+ result += Math.max(points - i * BAD_WORD_STEP, BAD_WORD_MIN);
+ }
+ return positive ? result : -result;
+ };
+
+ // Default parameters if the bidder is unable to send some of them
+ badWords = badWords || [];
+ maxScore = parseInt(maxScore) || 10;
+
+ try {
+ let score = 0;
+ const content = window.top.document.body.innerText.toLowerCase();
+ const words = content.trim().split(/\s+/).length;
+ for (const [word, rule, points] of badWords) {
+ if (rule === 'full' && new RegExp('\\b' + rot13(word) + '\\b', 'i').test(content)) {
+ const occurances = content.match(new RegExp('\\b' + rot13(word) + '\\b', 'g')).length;
+ score += scoreCalculator(points, occurances);
+ } else if (rule === 'partial' && content.indexOf(rot13(word.toLowerCase())) > -1) {
+ const occurances = content.match(new RegExp(rot13(word), 'g')).length;
+ score += scoreCalculator(points, occurances);
+ }
+ }
+ return score < maxScore * words / 500;
+ } catch (e) {
+ return true;
+ }
+}
export const spec = {
code: 'adhash',
@@ -59,7 +132,8 @@ export const spec = {
blockedCreatives: [],
currentTimestamp: new Date().getTime(),
recentAds: [],
- GDPR: gdprConsent
+ GDPRApplies: gdprConsent ? gdprConsent.gdprApplies : null,
+ GDPR: gdprConsent ? gdprConsent.consentString : null
},
options: {
withCredentials: false,
@@ -73,7 +147,11 @@ export const spec = {
interpretResponse: (serverResponse, request) => {
const responseBody = serverResponse ? serverResponse.body : {};
- if (!responseBody.creatives || responseBody.creatives.length === 0) {
+ if (
+ !responseBody.creatives ||
+ responseBody.creatives.length === 0 ||
+ !brandSafety(responseBody.badWords, responseBody.maxScore)
+ ) {
return [];
}
diff --git a/test/spec/modules/adhashBidAdapter_spec.js b/test/spec/modules/adhashBidAdapter_spec.js
index 6c214b84928..40bf354c4d9 100644
--- a/test/spec/modules/adhashBidAdapter_spec.js
+++ b/test/spec/modules/adhashBidAdapter_spec.js
@@ -7,7 +7,7 @@ describe('adhashBidAdapter', function () {
bidder: 'adhash',
params: {
publisherId: '0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb',
- platformURL: 'https://adhash.org/p/struma/'
+ platformURL: 'https://adhash.com/p/struma/'
},
mediaTypes: {
banner: {
@@ -73,7 +73,7 @@ describe('adhashBidAdapter', function () {
it('should build the request correctly', function () {
const result = spec.buildRequests(
[ bidRequest ],
- { gdprConsent: true, refererInfo: { referer: 'http://example.com/' } }
+ { gdprConsent: { gdprApplies: true, consentString: 'example' }, refererInfo: { referer: 'http://example.com/' } }
);
expect(result.length).to.equal(1);
expect(result[0].method).to.equal('POST');
@@ -90,7 +90,7 @@ describe('adhashBidAdapter', function () {
expect(result[0].data).to.have.property('recentAds');
});
it('should build the request correctly without referer', function () {
- const result = spec.buildRequests([ bidRequest ], { gdprConsent: true });
+ const result = spec.buildRequests([ bidRequest ], { gdprConsent: { gdprApplies: true, consentString: 'example' } });
expect(result.length).to.equal(1);
expect(result[0].method).to.equal('POST');
expect(result[0].url).to.equal('https://bidder.adhash.com/rtb?version=1.0&prebid=true&publisher=0xc3b09b27e9c6ef73957901aa729b9e69e5bbfbfb');
@@ -115,18 +115,31 @@ describe('adhashBidAdapter', function () {
adUnitCode: 'adunit-code',
sizes: [[300, 250]],
params: {
- platformURL: 'https://adhash.org/p/struma/'
+ platformURL: 'https://adhash.com/p/struma/'
}
}
};
+ let bodyStub;
+
+ const serverResponse = {
+ body: {
+ creatives: [{ costEUR: 1.234 }],
+ advertiserDomains: 'adhash.com',
+ badWords: [
+ ['onqjbeq1', 'full', 1],
+ ['onqjbeq2', 'partial', 1],
+ ['tbbqjbeq', 'full', -1],
+ ],
+ maxScore: 2
+ }
+ };
+
+ afterEach(function() {
+ bodyStub && bodyStub.restore();
+ });
+
it('should interpret the response correctly', function () {
- const serverResponse = {
- body: {
- creatives: [{ costEUR: 1.234 }],
- advertiserDomains: 'adhash.org'
- }
- };
const result = spec.interpretResponse(serverResponse, request);
expect(result.length).to.equal(1);
expect(result[0].requestId).to.equal('12345678901234');
@@ -137,7 +150,56 @@ describe('adhashBidAdapter', function () {
expect(result[0].netRevenue).to.equal(true);
expect(result[0].currency).to.equal('EUR');
expect(result[0].ttl).to.equal(60);
- expect(result[0].meta.advertiserDomains).to.eql(['adhash.org']);
+ expect(result[0].meta.advertiserDomains).to.eql(['adhash.com']);
+ });
+
+ it('should return empty array when there are bad words (full)', function () {
+ bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() {
+ return 'example text badWord1 badWord1 example badWord1 text' + ' word'.repeat(493);
+ });
+ expect(spec.interpretResponse(serverResponse, request).length).to.equal(0);
+ });
+
+ it('should return empty array when there are bad words (partial)', function () {
+ bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() {
+ return 'example text partialBadWord2 badword2 example BadWord2text' + ' word'.repeat(494);
+ });
+ expect(spec.interpretResponse(serverResponse, request).length).to.equal(0);
+ });
+
+ it('should return non-empty array when there are not enough bad words (full)', function () {
+ bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() {
+ return 'example text badWord1 badWord1 example text' + ' word'.repeat(494);
+ });
+ expect(spec.interpretResponse(serverResponse, request).length).to.equal(1);
+ });
+
+ it('should return non-empty array when there are not enough bad words (partial)', function () {
+ bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() {
+ return 'example text partialBadWord2 example' + ' word'.repeat(496);
+ });
+ expect(spec.interpretResponse(serverResponse, request).length).to.equal(1);
+ });
+
+ it('should return non-empty array when there are no-bad word matches', function () {
+ bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() {
+ return 'example text partialBadWord1 example text' + ' word'.repeat(495);
+ });
+ expect(spec.interpretResponse(serverResponse, request).length).to.equal(1);
+ });
+
+ it('should return non-empty array when there are bad words and good words', function () {
+ bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() {
+ return 'example text badWord1 badWord1 example badWord1 goodWord goodWord ' + ' word'.repeat(492);
+ });
+ expect(spec.interpretResponse(serverResponse, request).length).to.equal(1);
+ });
+
+ it('should return non-empty array when there is a problem with the brand-safety', function () {
+ bodyStub = sinon.stub(window.top.document.body, 'innerText').get(function() {
+ return null;
+ });
+ expect(spec.interpretResponse(serverResponse, request).length).to.equal(1);
});
it('should return empty array when there are no creatives returned', function () {