From 247ea80e7f1da07d2847db64248f949de35a9ef4 Mon Sep 17 00:00:00 2001
From: jsnellbaker <31102355+jsnellbaker@users.noreply.github.com>
Date: Tue, 1 May 2018 10:07:26 -0400
Subject: [PATCH] GDPR consentManagement module (#2213)
* initial commit
* wip update 2
* wip update 3
* example
* clean up
* wip update 3
* hook setup for callBids
* wip update 4
* changed gdpr code to be async-like
* cleaned up the callback chain
* added iab cmp detection logic
* moved hook, reverted unit test changes, and restructed gdpr module
* renaming module from gdpr to consentManagement
* prebidserver adatper update, additional changes in module
* updated unit tests for all areas, updates to module logic and structure of consent data
* adding missing default value
* removing accidentally committed load time testing code
* changes to layout of consentManagement code and other items based on feedback
* moved unit test to different location
* finished incomplete unit test in appnexusBidAdapter_spec file
* altered CMP function call logic
* refactored consentManagement AN lookup function and added gdprDataHandler to help transfer data in auction
* some minor cleanup from previous commit
* change spacing to try to fix travis issue
* added scenario to support consentTimeout=0 skip setTimeout
* updated some comments
* refactored exit logic for module
* added support for consentRequired field in config
* remove internal consentRequired default
* minor comment fixes
* comment fixes that should be have part of last commit
* fix includes issue and added gdprConsent to getUserSyncs function
* renamed default CMP and config field to cmpApi
* wip - using postmessage to call cmp
* postMessage workflow added, removed CMP eventlistener check
* removed if statement
* cleanup; removed variable and unneeded comments
* add gdpr tests pages
* updates for 1.1 CMP spec
* remove rogue debugger in unit test
* restructured 1.1 CMP iframe code, renamed utils function, cleaned up unit tests
* GDPR support in adform adapter (#2396)
* bid response adId same as bidId
* test
* update adform bid adapter
* update unit tests
* Added adform adapter description file
* updated tests
* Another tests update
* Add auctionId
* Update adapter for auctionId
* add auctionId to adformBidAdapter
* Final updates to fit 1.0 version
* update docs and integration example
* Do not mutate original validBidRequests
* use atob and btoa instead of custom made module
* Renaming one query string parameter
* XDomainRequest.send exception fix (#1942)
* Added YIELDONE Bid Adapter for Prebid.js 1.0 (#1900)
* Added YIELDONE Bid Adapter for Prebid.js 1.0
* Update yieldoneBidAdapter.md
change placementId to 44082
* Changed to get size from bid.sizes
* fix sizes array
* Add user-facing docs reminder to PR template (#1956)
* allow non-mappable sizes to be passed and used in rubicon adapter (#1893)
* Typo correction of YIELDONE md file (#1954)
* Added YIELDONE Bid Adapter for Prebid.js 1.0
* Update yieldoneBidAdapter.md
change placementId to 44082
* Changed to get size from bid.sizes
* fix sizes array
* Fix a typo
* Serverbid bid adapter: update alias config (#1963)
* use auctionId instead of requestId (#1968)
* Add freewheel ssp bidder adapter for prebid 1.0 (#1793)
* add stickyadsTV bidder adapter
* init unit test file
* ad some unit tests
* fix unit test on ad format with parameters
* add some unit tests
* add unit tests on getBid method
* add some test cases in unit tests
* minor fix on component id tag.
* remove adapters-sticky.json test file
* use top most accessible window instead of window.top
* Pass in the bid request in the createBid call.
* use top most accessible window instead of window.top
* add unit tests
* update unit tests
* fix unit test.
* fix CI build
* add alias freewheel-ssp
* update unit tests on bidderCode value
* fix component id values and add unit tests
* allws to use any outstream format.
* fix ASLoader on futur outstream format versions
* minor: fix code format.
* update unit tests
* minor fix code format
* minor: add missing new line at eof
* replace StickyAdsTVAdapter by freewheel ssp bd adapter (for prebid 1.0)
* remove old stickyadstv unittest spec.
* fix server response parsing if sent as object with 'body' field
* use the vastXml field for video mediatype
* add user sync pixel in freewheel ssp adapter
* remove all console log calls (replaced using util helper)
* remove useless bidderCode (automatically added by the bidderFactory)
* Return the SYNC pixel to be added in the page by Prebid.js
* remove instance level properties to enable concurrent bids with the same adapter instance.
* fix the request apss through and corresponding unit tests
* fix 'freeheelssp' typo
* + fixed endpoint request data property names - width to w and height to h (#1955)
+ updated unit test for the adapter to comply with the property name changes
* Added iQM Bid Adapter for Prebid.js 1.0 (#1880)
* Added iQM Bid Adapter for Prebid.js 1.0
* Modified URL from http to https
* Removed geo function which was fetching user location.
* Remove stray console.log (#1975)
* Remove duplicate request id and fix empty response from getHighesCpmBids, getAdserverTargeting (#1970)
* Removed requestId and added auctionId
* Updated module fixtures to use auctionId and not requestId
* remove request id from external bid object and fix bug for empty result in public api
* use auctionId instead of requestId
* fixed lint errors
* [Add BidAdapter] rxrtb adapter for Perbid.js 1.0 (#1950)
* Add: rxrtb prebidAdapter
* Update: params for test
* Update: code format
* Update: code format
* Update: code format
* ServerBid Server BidAdapter (#1819)
* ServerBid Server BidAdapter
Allow S2S configuration with ServerBid.
* Updates to meet 1.0 callBids/config changes.
* Fix linting issues.
* added hb_source to default keys (#1969)
* added hb_source
* dropped function to add hb_source since it is now default key
* fixed lint error
* Prebid 1.1.0 Release
* Increment pre version
* S2s defaults fix in serverbidServerBidAdapter (#1986)
* removed s2s defaults
* start timestamp was missing on s2s requests
* remove hardcoded localhost port for tests (#1988)
* Fixes unit tests in browsers other than chrome (#1987)
* Fixes unit tests in browsers other than chrome
* fixed lint errors
* Prebid 1.1.1 Release
* Add note about docs needed before merge (#1959)
* Add note about docs needed before merge
* Update pr_review.md
* Update pr_review.md
* Update pr_review.md
* Adding optional width and height to display parameters (#1998)
* adding optional size
* no tabs
* TrustX adapter update (#1979)
* Add trustx adapter and tests for it
* update integration example
* Update trustx adapter
* Post-review fixes of Trustx adapter
* Code improvement for trustx adapter: changed default price type from gross to net
* Update TrustX adapter to support the 1.0 version
* Make requested changes for TrustX adapter
* Updated markdown file for TrustX adapter
* Fix TrustX adapter and spec file
* Update TrustX adapter: r parameter was added to ad request as cache buster
* Serverbid Bid Adapter: Add new ad sizes (#1983)
* Added dynamic ttl property for One Display and One Mobile. (#2004)
* pin gulp-connect at non-broken version (#2008)
* pin gulp-connect at non-broken version
* updated yarn.lock to specify pinned gulp-connect
* Gjirafa Bidder Adapter (#1944)
* Added Gjirafa adapter
* Add gjirafa adapter unit test
* adapter update
* New line
* Requested changes
* change hello_world.html to one bid
* change hello_world.html to one bid
* Dropping changes in gitignore and hello_world example
* hello_world changes
* Drop hello_world and gitignore
* multiformat size validation checks (#1964)
* initial commit for multiformat size validation checks
* adding unit tests and changes to checkBidRequestSizes function
* updates to appnexusBidAdapter
* Upgrade Admixer adapter for Prebid 1.0 (#1755)
* Migrating to Prebid 1.0
* Migrating to Prebid 1.0
* Fix spec
* Add NasmediaAdmixer adapter for Perbid.js 1.0 (#1937)
* add NasmediaAdmixer adapter for Perbid.js 1.0
* add NasmediaAdmixer adapter for Perbid.js 1.0
* add NasmediaAdmixer adapter for Perbid.js 1.0
* add NasmediaAdmixer adapter for Perbid.js 1.0
* add NasmediaAdmixer adapter for Perbid.js 1.0
* add NasmediaAdmixer adapter for Perbid.js 1.0
* Added gdpr to adform adapter
* Added unit tests
* Updated spacing
* Update gdprConsent object due to changes in spec
* Add gdpr support for PubMaticBidAdapter (#2469)
* GDPR support for AOL adapter (#2443)
* Added GDPR support for AOL adapter.
* Added unit tests for AOL GDPR changes.
* Added utils for resolving object type and undefined.
* Fixed issues caused by merge.
* Made changes in AOL adapter to support gdprApplies flag.
* Removed bid floor value from test bid config.
* removing iframe example pages
* comment updates
---
integrationExamples/gpt/gdpr_hello_world.html | 103 ++++++
modules/adformBidAdapter.js | 7 +-
modules/aolBidAdapter.js | 335 ++++++++++--------
modules/aolBidAdapter.md | 1 -
modules/appnexusBidAdapter.js | 9 +
modules/consentManagement.js | 278 +++++++++++++++
modules/pre1api.js | 2 +-
modules/prebidServerBidAdapter.js | 35 +-
modules/pubmaticBidAdapter.js | 42 ++-
src/adaptermanager.js | 16 +
src/adapters/bidderFactory.js | 6 +-
src/utils.js | 5 +
test/spec/modules/adformBidAdapter_spec.js | 9 +
test/spec/modules/aolBidAdapter_spec.js | 128 ++++++-
test/spec/modules/appnexusBidAdapter_spec.js | 26 +-
test/spec/modules/consentManagement_spec.js | 292 +++++++++++++++
.../modules/prebidServerBidAdapter_spec.js | 33 ++
test/spec/modules/pubmaticBidAdapter_spec.js | 63 +++-
test/spec/unit/core/adapterManager_spec.js | 260 ++++++++------
test/spec/utils_spec.js | 27 ++
20 files changed, 1390 insertions(+), 287 deletions(-)
create mode 100644 integrationExamples/gpt/gdpr_hello_world.html
create mode 100644 modules/consentManagement.js
create mode 100644 test/spec/modules/consentManagement_spec.js
diff --git a/integrationExamples/gpt/gdpr_hello_world.html b/integrationExamples/gpt/gdpr_hello_world.html
new file mode 100644
index 00000000000..9f6194edb16
--- /dev/null
+++ b/integrationExamples/gpt/gdpr_hello_world.html
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Prebid.js Test
+ Div-1
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/adformBidAdapter.js b/modules/adformBidAdapter.js
index 10a4696d755..a84def819c1 100644
--- a/modules/adformBidAdapter.js
+++ b/modules/adformBidAdapter.js
@@ -10,7 +10,7 @@ export const spec = {
isBidRequestValid: function (bid) {
return !!(bid.params.mid);
},
- buildRequests: function (validBidRequests) {
+ buildRequests: function (validBidRequests, bidderRequest) {
var i, l, j, k, bid, _key, _value, reqParams, netRevenue;
var request = [];
var globalParams = [ [ 'adxDomain', 'adx.adform.net' ], [ 'fd', 1 ], [ 'url', null ], [ 'tid', null ] ];
@@ -38,6 +38,11 @@ export const spec = {
request.push('pt=' + netRevenue);
request.push('stid=' + validBidRequests[0].auctionId);
+ if (bidderRequest && bidderRequest.gdprConsent) {
+ request.push('gdpr=' + bidderRequest.gdprConsent.gdprApplies);
+ request.push('gdpr_consent=' + bidderRequest.gdprConsent.consentString);
+ }
+
for (i = 1, l = globalParams.length; i < l; i++) {
_key = globalParams[i][0];
_value = globalParams[i][1];
diff --git a/modules/aolBidAdapter.js b/modules/aolBidAdapter.js
index 0fb5aa1a4d3..18d30685c56 100644
--- a/modules/aolBidAdapter.js
+++ b/modules/aolBidAdapter.js
@@ -2,6 +2,7 @@ import * as utils from 'src/utils';
import { registerBidder } from 'src/adapters/bidderFactory';
import { config } from 'src/config';
import { EVENTS } from 'src/constants.json';
+import { BANNER } from 'src/mediaTypes';
const AOL_BIDDERS_CODES = {
AOL: 'aol',
@@ -30,9 +31,9 @@ const SYNC_TYPES = {
}
};
-const pubapiTemplate = template`//${'host'}/pubapi/3.0/${'network'}/${'placement'}/${'pageid'}/${'sizeid'}/ADTECH;v=2;cmd=bid;cors=yes;alias=${'alias'}${'bidfloor'}${'keyValues'};misc=${'misc'}`;
+const pubapiTemplate = template`//${'host'}/pubapi/3.0/${'network'}/${'placement'}/${'pageid'}/${'sizeid'}/ADTECH;v=2;cmd=bid;cors=yes;alias=${'alias'};misc=${'misc'}${'bidfloor'}${'keyValues'}${'consentData'}`;
const nexageBaseApiTemplate = template`//${'host'}/bidRequest?`;
-const nexageGetApiTemplate = template`dcn=${'dcn'}&pos=${'pos'}&cmd=bid${'ext'}`;
+const nexageGetApiTemplate = template`dcn=${'dcn'}&pos=${'pos'}&cmd=bid${'dynamicParams'}`;
const MP_SERVER_MAP = {
us: 'adserver-us.adtech.advertising.com',
eu: 'adserver-eu.adtech.advertising.com',
@@ -46,10 +47,10 @@ $$PREBID_GLOBAL$$.aolGlobals = {
pixelsDropped: false
};
-let showCpmAdjustmentWarning = (function () {
+let showCpmAdjustmentWarning = (function() {
let showCpmWarning = true;
- return function () {
+ return function() {
let bidderSettings = $$PREBID_GLOBAL$$.bidderSettings;
if (showCpmWarning && bidderSettings && bidderSettings.aol &&
typeof bidderSettings.aol.bidCpmAdjustment === 'function') {
@@ -62,28 +63,18 @@ let showCpmAdjustmentWarning = (function () {
};
})();
-function isInteger(value) {
- return typeof value === 'number' &&
- isFinite(value) &&
- Math.floor(value) === value;
-}
-
function template(strings, ...keys) {
return function(...values) {
let dict = values[values.length - 1] || {};
let result = [strings[0]];
keys.forEach(function(key, i) {
- let value = isInteger(key) ? values[key] : dict[key];
+ let value = utils.isInteger(key) ? values[key] : dict[key];
result.push(value, strings[i + 1]);
});
return result.join('');
};
}
-function isSecureProtocol() {
- return document.location.protocol === 'https:';
-}
-
function parsePixelItems(pixels) {
let itemsRegExp = /(img|iframe)[\s\S]*?src\s*=\s*("|')(.*?)\2/gi;
let tagNameRegExp = /\w*(?=\s)/;
@@ -110,39 +101,6 @@ function parsePixelItems(pixels) {
return pixelsItems;
}
-function _buildMarketplaceUrl(bid) {
- const params = bid.params;
- const serverParam = params.server;
- let regionParam = params.region || 'us';
- let server;
-
- if (!MP_SERVER_MAP.hasOwnProperty(regionParam)) {
- utils.logWarn(`Unknown region '${regionParam}' for AOL bidder.`);
- regionParam = 'us'; // Default region.
- }
-
- if (serverParam) {
- server = serverParam;
- } else {
- server = MP_SERVER_MAP[regionParam];
- }
-
- // Set region param, used by AOL analytics.
- params.region = regionParam;
-
- return pubapiTemplate({
- host: server,
- network: params.network,
- placement: parseInt(params.placement),
- pageid: params.pageId || 0,
- sizeid: params.sizeId || 0,
- alias: params.alias || utils.getUniqueIdentifierStr(),
- bidfloor: formatMarketplaceBidFloor(params.bidFloor),
- keyValues: formatMarketplaceKeyValues(params.keyValues),
- misc: new Date().getTime() // cache busting
- });
-}
-
function formatMarketplaceBidFloor(bidFloor) {
return (typeof bidFloor !== 'undefined') ? `;bidfloor=${bidFloor.toString()}` : '';
}
@@ -157,39 +115,16 @@ function formatMarketplaceKeyValues(keyValues) {
return formattedKeyValues;
}
-function _buildOneMobileBaseUrl(bid) {
- return nexageBaseApiTemplate({
- host: bid.params.host || NEXAGE_SERVER
- });
-}
-
-function _buildOneMobileGetUrl(bid) {
- let {dcn, pos} = bid.params;
- let nexageApi = _buildOneMobileBaseUrl(bid);
- if (dcn && pos) {
- let ext = '';
- if (isSecureProtocol()) {
- bid.params.ext = bid.params.ext || {};
- bid.params.ext.secure = 1;
- }
- utils._each(bid.params.ext, (value, key) => {
- ext += `&${key}=${encodeURIComponent(value)}`;
- });
- nexageApi += nexageGetApiTemplate({dcn, pos, ext});
- }
- return nexageApi;
-}
-
function _isMarketplaceBidder(bidder) {
return bidder === AOL_BIDDERS_CODES.AOL || bidder === AOL_BIDDERS_CODES.ONEDISPLAY;
}
-function _isNexageBidder(bidder) {
- return bidder === AOL_BIDDERS_CODES.AOL || bidder === AOL_BIDDERS_CODES.ONEMOBILE;
+function _isOneMobileBidder(bidderCode) {
+ return bidderCode === AOL_BIDDERS_CODES.AOL || bidderCode === AOL_BIDDERS_CODES.ONEMOBILE;
}
function _isNexageRequestPost(bid) {
- if (_isNexageBidder(bid.bidder) && bid.params.id && bid.params.imp && bid.params.imp[0]) {
+ if (_isOneMobileBidder(bid.bidder) && bid.params.id && bid.params.imp && bid.params.imp[0]) {
let imp = bid.params.imp[0];
return imp.id && imp.tagid &&
((imp.banner && imp.banner.w && imp.banner.h) ||
@@ -198,7 +133,7 @@ function _isNexageRequestPost(bid) {
}
function _isNexageRequestGet(bid) {
- return _isNexageBidder(bid.bidder) && bid.params.dcn && bid.params.pos;
+ return _isOneMobileBidder(bid.bidder) && bid.params.dcn && bid.params.pos;
}
function isMarketplaceBid(bid) {
@@ -219,65 +154,25 @@ function resolveEndpointCode(bid) {
}
}
-function formatBidRequest(endpointCode, bid) {
- let bidRequest;
-
- switch (endpointCode) {
- case AOL_ENDPOINTS.DISPLAY.GET:
- bidRequest = {
- url: _buildMarketplaceUrl(bid),
- method: 'GET',
- ttl: ONE_DISPLAY_TTL
- };
- break;
-
- case AOL_ENDPOINTS.MOBILE.GET:
- bidRequest = {
- url: _buildOneMobileGetUrl(bid),
- method: 'GET',
- ttl: ONE_MOBILE_TTL
- };
- break;
-
- case AOL_ENDPOINTS.MOBILE.POST:
- bidRequest = {
- url: _buildOneMobileBaseUrl(bid),
- method: 'POST',
- ttl: ONE_MOBILE_TTL,
- data: bid.params,
- options: {
- contentType: 'application/json',
- customHeaders: {
- 'x-openrtb-version': '2.2'
- }
- }
- };
- break;
- }
-
- bidRequest.bidderCode = bid.bidder;
- bidRequest.bidId = bid.bidId;
- bidRequest.userSyncOn = bid.params.userSyncOn;
-
- return bidRequest;
-}
-
export const spec = {
code: AOL_BIDDERS_CODES.AOL,
aliases: [AOL_BIDDERS_CODES.ONEMOBILE, AOL_BIDDERS_CODES.ONEDISPLAY],
- isBidRequestValid: function(bid) {
+ supportedMediaTypes: [BANNER],
+ isBidRequestValid(bid) {
return isMarketplaceBid(bid) || isMobileBid(bid);
},
- buildRequests: function (bids) {
+ buildRequests(bids, bidderRequest) {
+ let consentData = bidderRequest ? bidderRequest.gdprConsent : null;
+
return bids.map(bid => {
const endpointCode = resolveEndpointCode(bid);
if (endpointCode) {
- return formatBidRequest(endpointCode, bid);
+ return this.formatBidRequest(endpointCode, bid, consentData);
}
});
},
- interpretResponse: function ({body}, bidRequest) {
+ interpretResponse({body}, bidRequest) {
showCpmAdjustmentWarning();
if (!body) {
@@ -290,17 +185,157 @@ export const spec = {
}
}
},
- _formatPixels: function (pixels) {
- let formattedPixels = pixels.replace(/<\/?script( type=('|")text\/javascript('|")|)?>/g, '');
+ getUserSyncs(options, bidResponses) {
+ let bidResponse = bidResponses[0];
- return '';
+ if (config.getConfig('aol.userSyncOn') === EVENTS.BID_RESPONSE) {
+ if (!$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped && bidResponse && bidResponse.ext && bidResponse.ext.pixels) {
+ $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = true;
+
+ return parsePixelItems(bidResponse.ext.pixels);
+ }
+ }
+
+ return [];
+ },
+
+ formatBidRequest(endpointCode, bid, consentData) {
+ let bidRequest;
+
+ switch (endpointCode) {
+ case AOL_ENDPOINTS.DISPLAY.GET:
+ bidRequest = {
+ url: this.buildMarketplaceUrl(bid, consentData),
+ method: 'GET',
+ ttl: ONE_DISPLAY_TTL
+ };
+ break;
+
+ case AOL_ENDPOINTS.MOBILE.GET:
+ bidRequest = {
+ url: this.buildOneMobileGetUrl(bid, consentData),
+ method: 'GET',
+ ttl: ONE_MOBILE_TTL
+ };
+ break;
+
+ case AOL_ENDPOINTS.MOBILE.POST:
+ bidRequest = {
+ url: this.buildOneMobileBaseUrl(bid),
+ method: 'POST',
+ ttl: ONE_MOBILE_TTL,
+ data: this.buildOpenRtbRequestData(bid, consentData),
+ options: {
+ contentType: 'application/json',
+ customHeaders: {
+ 'x-openrtb-version': '2.2'
+ }
+ }
+ };
+ break;
+ }
+
+ bidRequest.bidderCode = bid.bidder;
+ bidRequest.bidId = bid.bidId;
+ bidRequest.userSyncOn = bid.params.userSyncOn;
+
+ return bidRequest;
},
- _parseBidResponse: function (response, bidRequest) {
+ buildMarketplaceUrl(bid, consentData) {
+ const params = bid.params;
+ const serverParam = params.server;
+ let regionParam = params.region || 'us';
+ let server;
+
+ if (!MP_SERVER_MAP.hasOwnProperty(regionParam)) {
+ utils.logWarn(`Unknown region '${regionParam}' for AOL bidder.`);
+ regionParam = 'us'; // Default region.
+ }
+
+ if (serverParam) {
+ server = serverParam;
+ } else {
+ server = MP_SERVER_MAP[regionParam];
+ }
+
+ // Set region param, used by AOL analytics.
+ params.region = regionParam;
+
+ return pubapiTemplate({
+ host: server,
+ network: params.network,
+ placement: parseInt(params.placement),
+ pageid: params.pageId || 0,
+ sizeid: params.sizeId || 0,
+ alias: params.alias || utils.getUniqueIdentifierStr(),
+ misc: new Date().getTime(), // cache busting,
+ bidfloor: formatMarketplaceBidFloor(params.bidFloor),
+ keyValues: formatMarketplaceKeyValues(params.keyValues),
+ consentData: this.formatMarketplaceConsentData(consentData)
+ });
+ },
+ buildOneMobileGetUrl(bid, consentData) {
+ let {dcn, pos, ext} = bid.params;
+ let nexageApi = this.buildOneMobileBaseUrl(bid);
+ if (dcn && pos) {
+ let dynamicParams = this.formatOneMobileDynamicParams(ext, consentData);
+ nexageApi += nexageGetApiTemplate({dcn, pos, dynamicParams});
+ }
+ return nexageApi;
+ },
+ buildOneMobileBaseUrl(bid) {
+ return nexageBaseApiTemplate({
+ host: bid.params.host || NEXAGE_SERVER
+ });
+ },
+ formatOneMobileDynamicParams(params = {}, consentData) {
+ if (this.isSecureProtocol()) {
+ params.secure = 1;
+ }
+
+ if (this.isConsentRequired(consentData)) {
+ params.euconsent = consentData.consentString;
+ params.gdpr = 1;
+ }
+
+ let paramsFormatted = '';
+ utils._each(params, (value, key) => {
+ paramsFormatted += `&${key}=${encodeURIComponent(value)}`;
+ });
+
+ return paramsFormatted;
+ },
+ buildOpenRtbRequestData(bid, consentData) {
+ let openRtbObject = {
+ id: bid.params.id,
+ imp: bid.params.imp
+ };
+
+ if (this.isConsentRequired(consentData)) {
+ openRtbObject.user = {
+ ext: {
+ consent: consentData.consentString
+ }
+ };
+ openRtbObject.regs = {
+ ext: {
+ gdpr: 1
+ }
+ };
+ }
+
+ return openRtbObject;
+ },
+ isConsentRequired(consentData) {
+ return !!(consentData && consentData.consentString && consentData.gdprApplies);
+ },
+ formatMarketplaceConsentData(consentData) {
+ let consentRequired = this.isConsentRequired(consentData);
+
+ return consentRequired ? `;euconsent=${consentData.consentString};gdpr=1` : '';
+ },
+
+ _parseBidResponse(response, bidRequest) {
let bidData;
try {
@@ -322,17 +357,10 @@ export const spec = {
}
}
- let ad = bidData.adm;
- if (response.ext && response.ext.pixels) {
- if (config.getConfig('aol.userSyncOn') !== EVENTS.BID_RESPONSE) {
- ad += this._formatPixels(response.ext.pixels);
- }
- }
-
- return {
+ let bidResponse = {
bidderCode: bidRequest.bidderCode,
requestId: bidRequest.bidId,
- ad: ad,
+ ad: bidData.adm,
cpm: cpm,
width: bidData.w,
height: bidData.h,
@@ -343,19 +371,28 @@ export const spec = {
netRevenue: true,
ttl: bidRequest.ttl
};
- },
- getUserSyncs: function(options, bidResponses) {
- let bidResponse = bidResponses[0];
- if (config.getConfig('aol.userSyncOn') === EVENTS.BID_RESPONSE) {
- if (!$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped && bidResponse.ext && bidResponse.ext.pixels) {
- $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = true;
-
- return parsePixelItems(bidResponse.ext.pixels);
+ if (response.ext && response.ext.pixels) {
+ if (config.getConfig('aol.userSyncOn') !== EVENTS.BID_RESPONSE) {
+ bidResponse.ad += this.formatPixels(response.ext.pixels);
}
}
- return [];
+ return bidResponse;
+ },
+ formatPixels(pixels) {
+ let formattedPixels = pixels.replace(/<\/?script( type=('|")text\/javascript('|")|)?>/g, '');
+
+ return '';
+ },
+ isOneMobileBidder: _isOneMobileBidder,
+ isSecureProtocol() {
+ return document.location.protocol === 'https:';
}
};
diff --git a/modules/aolBidAdapter.md b/modules/aolBidAdapter.md
index a92e933bd36..8a9d1e3291d 100644
--- a/modules/aolBidAdapter.md
+++ b/modules/aolBidAdapter.md
@@ -22,7 +22,6 @@ Module that connects to AOL's demand sources
params: {
placement: '3611253',
network: '9599.1',
- bidFloor: '0.80',
keyValues: {
test: 'key'
}
diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js
index 75e48d1ee0b..82743974994 100644
--- a/modules/appnexusBidAdapter.js
+++ b/modules/appnexusBidAdapter.js
@@ -73,6 +73,15 @@ export const spec = {
if (member > 0) {
payload.member_id = member;
}
+
+ if (bidderRequest && bidderRequest.gdprConsent) {
+ // note - objects for impbus use underscore instead of camelCase
+ payload.gdpr_consent = {
+ consent_string: bidderRequest.gdprConsent.consentString,
+ consent_required: bidderRequest.gdprConsent.gdprApplies
+ };
+ }
+
const payloadString = JSON.stringify(payload);
return {
method: 'POST',
diff --git a/modules/consentManagement.js b/modules/consentManagement.js
new file mode 100644
index 00000000000..c7b6ac4df92
--- /dev/null
+++ b/modules/consentManagement.js
@@ -0,0 +1,278 @@
+/**
+ * This module adds GDPR consentManagement support to prebid.js. It interacts with
+ * supported CMPs (Consent Management Platforms) to grab the user's consent information
+ * and make it available for any GDPR supported adapters to read/pass this information to
+ * their system.
+ */
+import * as utils from 'src/utils';
+import { config } from 'src/config';
+import { gdprDataHandler } from 'src/adaptermanager';
+import includes from 'core-js/library/fn/array/includes';
+
+const DEFAULT_CMP = 'iab';
+const DEFAULT_CONSENT_TIMEOUT = 10000;
+const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true;
+
+export let userCMP;
+export let consentTimeout;
+export let allowAuction;
+
+let consentData;
+
+let context;
+let args;
+let nextFn;
+
+let timer;
+let haveExited;
+
+// add new CMPs here, with their dedicated lookup function
+const cmpCallMap = {
+ 'iab': lookupIabConsent
+};
+
+/**
+ * This function handles interacting with an IAB compliant CMP to obtain the consentObject value of the user.
+ * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function
+ * based on the appropriate result.
+ * @param {function(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP
+ * @param {function(string)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string)
+ */
+function lookupIabConsent(cmpSuccess, cmpError) {
+ let cmpCallbacks;
+
+ // check if the CMP is located on the same window level as the prebid code.
+ // if it's found, directly call the CMP via it's API and call the cmpSuccess callback.
+ // if it's not found, assume the prebid code may be inside an iframe and the CMP code is located in a higher parent window.
+ // in this case, use the IAB's iframe locator sample code (which is slightly cutomized) to try to find the CMP and use postMessage() to communicate with the CMP.
+ if (utils.isFn(window.__cmp)) {
+ window.__cmp('getVendorConsents', null, cmpSuccess);
+ } else {
+ callCmpWhileInIframe();
+ }
+
+ function callCmpWhileInIframe() {
+ /**
+ * START OF STOCK CODE FROM IAB 1.1 CMP SPEC
+ */
+
+ // find the CMP frame
+ let f = window;
+ let cmpFrame;
+ while (!cmpFrame) {
+ try {
+ if (f.frames['__cmpLocator']) cmpFrame = f;
+ } catch (e) {}
+ if (f === window.top) break;
+ f = f.parent;
+ }
+
+ cmpCallbacks = {};
+
+ /* Setup up a __cmp function to do the postMessage and stash the callback.
+ This function behaves (from the caller's perspective identicially to the in-frame __cmp call */
+ window.__cmp = function(cmd, arg, callback) {
+ if (!cmpFrame) {
+ removePostMessageListener();
+
+ let errmsg = 'CMP not found';
+ // small customization to properly return error
+ return cmpError(errmsg);
+ }
+ let callId = Math.random() + '';
+ let msg = {__cmpCall: {
+ command: cmd,
+ parameter: arg,
+ callId: callId
+ }};
+ cmpCallbacks[callId] = callback;
+ cmpFrame.postMessage(msg, '*');
+ }
+
+ /** when we get the return message, call the stashed callback */
+ // small customization to remove this eventListener later in module
+ window.addEventListener('message', readPostMessageResponse, false);
+
+ /**
+ * END OF STOCK CODE FROM IAB 1.1 CMP SPEC
+ */
+
+ // call CMP
+ window.__cmp('getVendorConsents', null, cmpIframeCallback);
+ }
+
+ function readPostMessageResponse(event) {
+ // small customization to prevent reading strings from other sources that aren't JSON.stringified
+ let json = (typeof event.data === 'string' && includes(event.data, 'cmpReturn')) ? JSON.parse(event.data) : event.data;
+ if (json.__cmpReturn) {
+ let i = json.__cmpReturn;
+ cmpCallbacks[i.callId](i.returnValue, i.success);
+ delete cmpCallbacks[i.callId];
+ }
+ }
+
+ function removePostMessageListener() {
+ window.removeEventListener('message', readPostMessageResponse, false);
+ }
+
+ function cmpIframeCallback(consentObject) {
+ removePostMessageListener();
+ cmpSuccess(consentObject);
+ }
+}
+
+/**
+ * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the
+ * user's encoded consent string from the supported CMP. Once obtained, the module will store this
+ * data as part of a gdprConsent object which gets transferred to adaptermanager's gdprDataHandler object.
+ * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system.
+ * @param {object} config required; This is the same param that's used in pbjs.requestBids.
+ * @param {function} fn required; The next function in the chain, used by hook.js
+ */
+export function requestBidsHook(config, fn) {
+ context = this;
+ args = arguments;
+ nextFn = fn;
+ haveExited = false;
+
+ // in case we already have consent (eg during bid refresh)
+ if (consentData) {
+ return exitModule();
+ }
+
+ if (!includes(Object.keys(cmpCallMap), userCMP)) {
+ utils.logWarn(`CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`);
+ return nextFn.apply(context, args);
+ }
+
+ cmpCallMap[userCMP].call(this, processCmpData, cmpFailed);
+
+ // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished)
+ if (!haveExited) {
+ if (consentTimeout === 0) {
+ processCmpData(undefined);
+ } else {
+ timer = setTimeout(cmpTimedOut, consentTimeout);
+ }
+ }
+}
+
+/**
+ * This function checks the consent data provided by CMP to ensure it's in an expected state.
+ * If it's bad, we exit the module depending on config settings.
+ * If it's good, then we store the value and exits the module.
+ * @param {object} consentObject required; object returned by CMP that contains user's consent choices
+ */
+function processCmpData(consentObject) {
+ if (!utils.isPlainObject(consentObject) || !utils.isStr(consentObject.metadata) || consentObject.metadata === '') {
+ cmpFailed(`CMP returned unexpected value during lookup process; returned value was (${consentObject}).`);
+ } else {
+ clearTimeout(timer);
+ storeConsentData(consentObject);
+
+ exitModule();
+ }
+}
+
+/**
+ * General timeout callback when interacting with CMP takes too long.
+ */
+function cmpTimedOut() {
+ cmpFailed('CMP workflow exceeded timeout threshold.');
+}
+
+/**
+ * This function contains the controlled steps to perform when there's a problem with CMP.
+ * @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened.
+*/
+function cmpFailed(errMsg) {
+ clearTimeout(timer);
+
+ // still set the consentData to undefined when there is a problem as per config options
+ if (allowAuction) {
+ storeConsentData(undefined);
+ }
+ exitModule(errMsg);
+}
+
+/**
+ * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanger.js for later in the auction
+ * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only)
+ */
+function storeConsentData(cmpConsentObject) {
+ consentData = {
+ consentString: (cmpConsentObject) ? cmpConsentObject.metadata : undefined,
+ vendorData: cmpConsentObject,
+ gdprApplies: (cmpConsentObject) ? cmpConsentObject.gdprApplies : undefined
+ };
+ gdprDataHandler.setConsentData(consentData);
+}
+
+/**
+ * This function handles the exit logic for the module.
+ * There are several paths in the module's logic to call this function and we only allow 1 of the 3 potential exits to happen before suppressing others.
+ *
+ * We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios.
+ * One scenario could be auction was canceled due to timeout with CMP being reached.
+ * While the timeout is the accepted exit and runs first, the CMP's callback still tries to process the user's data (which normally leads to a good exit).
+ * In this case, the good exit will be suppressed since we already decided to cancel the auction.
+ *
+ * Three exit paths are:
+ * 1. good exit where auction runs (CMP data is processed normally).
+ * 2. bad exit but auction still continues (warning message is logged, CMP data is undefined and still passed along).
+ * 3. bad exit with auction canceled (error message is logged).
+ * @param {string} errMsg optional; only to be used when there was a 'bad' exit. String is a descriptive message for the failure/issue encountered.
+ */
+function exitModule(errMsg) {
+ if (haveExited === false) {
+ haveExited = true;
+
+ if (errMsg) {
+ if (allowAuction) {
+ utils.logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.');
+ nextFn.apply(context, args);
+ } else {
+ utils.logError(errMsg + ' Canceling auction as per consentManagement config.');
+ }
+ } else {
+ nextFn.apply(context, args);
+ }
+ }
+}
+
+/**
+ * Simply resets the module's consentData variable back to undefined, mainly for testing purposes
+ */
+export function resetConsentData() {
+ consentData = undefined;
+}
+
+/**
+ * A configuration function that initializes some module variables, as well as add a hook into the requestBids function
+ * @param {object} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean)
+ */
+export function setConfig(config) {
+ if (utils.isStr(config.cmpApi)) {
+ userCMP = config.cmpApi;
+ } else {
+ userCMP = DEFAULT_CMP;
+ utils.logInfo(`consentManagement config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`);
+ }
+
+ if (utils.isNumber(config.timeout)) {
+ consentTimeout = config.timeout;
+ } else {
+ consentTimeout = DEFAULT_CONSENT_TIMEOUT;
+ utils.logInfo(`consentManagement config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`);
+ }
+
+ if (typeof config.allowAuctionWithoutConsent === 'boolean') {
+ allowAuction = config.allowAuctionWithoutConsent;
+ } else {
+ allowAuction = DEFAULT_ALLOW_AUCTION_WO_CONSENT;
+ utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent. Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`);
+ }
+
+ $$PREBID_GLOBAL$$.requestBids.addHook(requestBidsHook, 50);
+}
+config.getConfig('consentManagement', config => setConfig(config.consentManagement));
diff --git a/modules/pre1api.js b/modules/pre1api.js
index 707d10fbfd8..a8aa1f31e70 100644
--- a/modules/pre1api.js
+++ b/modules/pre1api.js
@@ -124,7 +124,7 @@ pbjs.requestBids.addHook((config, next = config) => {
} else {
logWarn(`${MODULE_NAME} module: concurrency has been disabled and "$$PREBID_GLOBAL$$.requestBids" call was queued`);
}
-}, 100);
+}, 5);
Object.keys(auctionPropMap).forEach(prop => {
if (prop === 'allBidsAvailable') {
diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js
index 22529def0a9..f499f5a0ae4 100644
--- a/modules/prebidServerBidAdapter.js
+++ b/modules/prebidServerBidAdapter.js
@@ -304,7 +304,7 @@ function transformHeightWidth(adUnit) {
*/
const LEGACY_PROTOCOL = {
- buildRequest(s2sBidRequest, adUnits) {
+ buildRequest(s2sBidRequest, bidRequests, adUnits) {
// pbs expects an ad_unit.video attribute if the imp is video
adUnits.forEach(adUnit => {
adUnit.sizes = transformHeightWidth(adUnit);
@@ -437,7 +437,7 @@ const OPEN_RTB_PROTOCOL = {
bidMap: {},
- buildRequest(s2sBidRequest, adUnits) {
+ buildRequest(s2sBidRequest, bidRequests, adUnits) {
let imps = [];
let aliases = {};
@@ -530,6 +530,35 @@ const OPEN_RTB_PROTOCOL = {
request.ext = { prebid: { aliases } };
}
+ if (bidRequests && bidRequests[0].gdprConsent) {
+ // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module
+ let gdprApplies;
+ if (typeof bidRequests[0].gdprConsent.gdprApplies === 'boolean') {
+ gdprApplies = bidRequests[0].gdprConsent.gdprApplies ? 1 : 0;
+ }
+
+ if (request.regs) {
+ if (request.regs.ext) {
+ request.regs.ext.gdpr = gdprApplies;
+ } else {
+ request.regs.ext = { gdpr: gdprApplies };
+ }
+ } else {
+ request.regs = { ext: { gdpr: gdprApplies } };
+ }
+
+ let consentString = bidRequests[0].gdprConsent.consentString;
+ if (request.user) {
+ if (request.user.ext) {
+ request.user.ext.consent = consentString;
+ } else {
+ request.user.ext = { consent: consentString };
+ }
+ } else {
+ request.user = { ext: { consent: consentString } };
+ }
+ }
+
return request;
},
@@ -637,7 +666,7 @@ export function PrebidServer() {
.reduce(utils.flatten)
.filter(utils.uniques);
- const request = protocolAdapter().buildRequest(s2sBidRequest, adUnitsWithSizes);
+ const request = protocolAdapter().buildRequest(s2sBidRequest, bidRequests, adUnitsWithSizes);
const requestJson = JSON.stringify(request);
ajax(
diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js
index dfcde047580..1f056bf0eff 100644
--- a/modules/pubmaticBidAdapter.js
+++ b/modules/pubmaticBidAdapter.js
@@ -19,6 +19,11 @@ const CUSTOM_PARAMS = {
'verId': '' // OpenWrap Legacy: version ID
};
const NET_REVENUE = false;
+const dealChannelValues = {
+ 1: 'PMP',
+ 5: 'PREF',
+ 6: 'PMPG'
+};
let publisherId = 0;
@@ -195,7 +200,7 @@ export const spec = {
* @param {validBidRequests[]} - an array of bids
* @return ServerRequest Info describing the request to the server.
*/
- buildRequests: validBidRequests => {
+ buildRequests: (validBidRequests, bidderRequest) => {
var conf = _initConf();
var payload = _createOrtbTemplate(conf);
validBidRequests.forEach(bid => {
@@ -217,14 +222,28 @@ export const spec = {
payload.site.publisher.id = conf.pubId.trim();
publisherId = conf.pubId.trim();
payload.ext.wrapper = {};
- payload.ext.wrapper.profile = conf.profId || UNDEFINED;
- payload.ext.wrapper.version = conf.verId || UNDEFINED;
+ payload.ext.wrapper.profile = parseInt(conf.profId) || UNDEFINED;
+ payload.ext.wrapper.version = parseInt(conf.verId) || UNDEFINED;
payload.ext.wrapper.wiid = conf.wiid || UNDEFINED;
payload.ext.wrapper.wv = constants.REPO_AND_VERSION;
payload.ext.wrapper.transactionId = conf.transactionId;
payload.ext.wrapper.wp = 'pbjs';
payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED);
payload.user.geo = {};
+
+ // Attaching GDPR Consent Params
+ if (bidderRequest && bidderRequest.gdprConsent) {
+ payload.user.ext = {
+ consent: bidderRequest.gdprConsent.consentString
+ };
+
+ payload.regs = {
+ ext: {
+ gdpr: (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)
+ }
+ };
+ }
+
payload.user.geo.lat = _parseSlotParam('lat', conf.lat);
payload.user.geo.lon = _parseSlotParam('lon', conf.lon);
payload.user.yob = _parseSlotParam('yob', conf.yob);
@@ -264,6 +283,11 @@ export const spec = {
referrer: utils.getTopWindowUrl(),
ad: bid.adm
};
+
+ if (bid.ext && bid.ext.deal_channel) {
+ newBid['dealChannel'] = dealChannelValues[bid.ext.deal_channel] || null;
+ }
+
bidResponses.push(newBid);
});
}
@@ -276,11 +300,19 @@ export const spec = {
/**
* Register User Sync.
*/
- getUserSyncs: syncOptions => {
+ getUserSyncs: (syncOptions, responses, gdprConsent) => {
+ let syncurl = USYNCURL + publisherId;
+
+ // Attaching GDPR Consent Params in UserSync url
+ if (gdprConsent) {
+ syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0);
+ syncurl += '&consent=' + encodeURIComponent(gdprConsent.consentString || '');
+ }
+
if (syncOptions.iframeEnabled) {
return [{
type: 'iframe',
- url: USYNCURL + publisherId
+ url: syncurl
}];
} else {
utils.logWarn('PubMatic: Please enable iframe based user sync.');
diff --git a/src/adaptermanager.js b/src/adaptermanager.js
index cef1635f100..98d9d5fb426 100644
--- a/src/adaptermanager.js
+++ b/src/adaptermanager.js
@@ -133,6 +133,16 @@ function getAdUnitCopyForClientAdapters(adUnits) {
return adUnitsClientCopy;
}
+exports.gdprDataHandler = {
+ consentData: null,
+ setConsentData: function(consentInfo) {
+ this.consentData = consentInfo;
+ },
+ getConsentData: function() {
+ return this.consentData;
+ }
+};
+
exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout, labels) {
let bidRequests = [];
@@ -197,6 +207,12 @@ exports.makeBidRequests = function(adUnits, auctionStart, auctionId, cbTimeout,
bidRequests.push(bidderRequest);
}
});
+
+ if (exports.gdprDataHandler.getConsentData()) {
+ bidRequests.forEach(bidRequest => {
+ bidRequest['gdprConsent'] = exports.gdprDataHandler.getConsentData();
+ });
+ }
return bidRequests;
};
diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js
index 7540a3a3398..958173a0965 100644
--- a/src/adapters/bidderFactory.js
+++ b/src/adapters/bidderFactory.js
@@ -191,7 +191,7 @@ export function newBidder(spec) {
// As soon as that is refactored, we can move this emit event where it should be, within the done function.
events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest);
- registerSyncs(responses);
+ registerSyncs(responses, bidderRequest.gdprConsent);
}
const validBidRequests = bidderRequest.bids.filter(filterAndWarn);
@@ -327,12 +327,12 @@ export function newBidder(spec) {
}
});
- function registerSyncs(responses) {
+ function registerSyncs(responses, gdprConsent) {
if (spec.getUserSyncs) {
let syncs = spec.getUserSyncs({
iframeEnabled: config.getConfig('userSync.iframeEnabled'),
pixelEnabled: config.getConfig('userSync.pixelEnabled'),
- }, responses);
+ }, responses, gdprConsent);
if (syncs) {
if (!Array.isArray(syncs)) {
syncs = [syncs];
diff --git a/src/utils.js b/src/utils.js
index 5b8508e52e4..169c578a356 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -11,6 +11,7 @@ var t_Arr = 'Array';
var t_Str = 'String';
var t_Fn = 'Function';
var t_Numb = 'Number';
+var t_Object = 'Object';
var toString = Object.prototype.toString;
let infoLogger = null;
try {
@@ -382,6 +383,10 @@ exports.isNumber = function(object) {
return this.isA(object, t_Numb);
};
+exports.isPlainObject = function(object) {
+ return this.isA(object, t_Object);
+}
+
/**
* Return if the object is "empty";
* this includes falsey, no keys, or no items at indices
diff --git a/test/spec/modules/adformBidAdapter_spec.js b/test/spec/modules/adformBidAdapter_spec.js
index d631234d6d5..21ff84bdad5 100644
--- a/test/spec/modules/adformBidAdapter_spec.js
+++ b/test/spec/modules/adformBidAdapter_spec.js
@@ -99,6 +99,15 @@ describe('Adform adapter', () => {
assert.deepEqual(resultBids, bids[0]);
});
+ it('should send GDPR Consent data to adform', () => {
+ var resultBids = JSON.parse(JSON.stringify(bids[0]));
+ let request = spec.buildRequests([bids[0]], {gdprConsent: {gdprApplies: 1, consentString: 'concentDataString'}});
+ let parsedUrl = parseUrl(request.url).query;
+
+ assert.equal(parsedUrl.gdpr, 1);
+ assert.equal(parsedUrl.gdpr_consent, 'concentDataString');
+ });
+
it('should set gross to the request, if there is any gross priceType', () => {
let request = spec.buildRequests([bids[5], bids[5]]);
let parsedUrl = parseUrl(request.url);
diff --git a/test/spec/modules/aolBidAdapter_spec.js b/test/spec/modules/aolBidAdapter_spec.js
index 38b36bbaf3d..d69b9e6e3d8 100644
--- a/test/spec/modules/aolBidAdapter_spec.js
+++ b/test/spec/modules/aolBidAdapter_spec.js
@@ -98,6 +98,7 @@ describe('AolAdapter', () => {
let bidRequest;
let logWarnSpy;
let formatPixelsStub;
+ let isOneMobileBidderStub;
beforeEach(() => {
bidderSettingsBackup = $$PREBID_GLOBAL$$.bidderSettings;
@@ -110,13 +111,15 @@ describe('AolAdapter', () => {
body: getDefaultBidResponse()
};
logWarnSpy = sinon.spy(utils, 'logWarn');
- formatPixelsStub = sinon.stub(spec, '_formatPixels');
+ formatPixelsStub = sinon.stub(spec, 'formatPixels');
+ isOneMobileBidderStub = sinon.stub(spec, 'isOneMobileBidder');
});
afterEach(() => {
$$PREBID_GLOBAL$$.bidderSettings = bidderSettingsBackup;
logWarnSpy.restore();
formatPixelsStub.restore();
+ isOneMobileBidderStub.restore();
});
it('should return formatted bid response with required properties', () => {
@@ -534,10 +537,10 @@ describe('AolAdapter', () => {
});
});
- describe('_formatPixels()', () => {
+ describe('formatPixels()', () => {
it('should return pixels wrapped for dropping them once and within nested frames ', () => {
let pixels = '';
- let formattedPixels = spec._formatPixels(pixels);
+ let formattedPixels = spec.formatPixels(pixels);
expect(formattedPixels).to.equal(
'');
});
- })
+ });
+
+ describe('isOneMobileBidder()', () => {
+ it('should return false when when bidderCode is not present', () => {
+ expect(spec.isOneMobileBidder(null)).to.be.false;
+ });
+
+ it('should return false for unknown bidder code', () => {
+ expect(spec.isOneMobileBidder('unknownBidder')).to.be.false;
+ });
+
+ it('should return true for aol bidder code', () => {
+ expect(spec.isOneMobileBidder('aol')).to.be.true;
+ });
+
+ it('should return true for one mobile bidder code', () => {
+ expect(spec.isOneMobileBidder('onemobile')).to.be.true;
+ });
+ });
+
+ describe('isConsentRequired()', () => {
+ it('should return false when consentData object is not present', () => {
+ expect(spec.isConsentRequired(null)).to.be.false;
+ });
+
+ it('should return false when gdprApplies equals true and consentString is not present', () => {
+ let consentData = {
+ consentString: null,
+ gdprApplies: true
+ };
+
+ expect(spec.isConsentRequired(consentData)).to.be.false;
+ });
+
+ it('should return false when consentString is present and gdprApplies equals false', () => {
+ let consentData = {
+ consentString: 'consent-string',
+ gdprApplies: false
+ };
+
+ expect(spec.isConsentRequired(consentData)).to.be.false;
+ });
+
+ it('should return true when consentString is present and gdprApplies equals true', () => {
+ let consentData = {
+ consentString: 'consent-string',
+ gdprApplies: true
+ };
+
+ expect(spec.isConsentRequired(consentData)).to.be.true;
+ });
+ });
+
+ describe('formatMarketplaceConsentData()', () => {
+ let consentRequiredStub;
+
+ beforeEach(() => {
+ consentRequiredStub = sinon.stub(spec, 'isConsentRequired');
+ });
+
+ afterEach(() => {
+ consentRequiredStub.restore();
+ });
+
+ it('should return empty string when consent is not required', () => {
+ consentRequiredStub.returns(false);
+ expect(spec.formatMarketplaceConsentData()).to.be.equal('');
+ });
+
+ it('should return formatted consent data when consent is required', () => {
+ consentRequiredStub.returns(true);
+ let formattedConsentData = spec.formatMarketplaceConsentData({
+ consentString: 'test-consent'
+ });
+ expect(formattedConsentData).to.be.equal(';euconsent=test-consent;gdpr=1');
+ });
+ });
+
+ describe('formatOneMobileDynamicParams()', () => {
+ let consentRequiredStub;
+ let secureProtocolStub;
+
+ beforeEach(() => {
+ consentRequiredStub = sinon.stub(spec, 'isConsentRequired');
+ secureProtocolStub = sinon.stub(spec, 'isSecureProtocol');
+ });
+
+ afterEach(() => {
+ consentRequiredStub.restore();
+ secureProtocolStub.restore();
+ });
+
+ it('should return empty string when params are not present', () => {
+ expect(spec.formatOneMobileDynamicParams()).to.be.equal('');
+ });
+
+ it('should return formatted params when params are present', () => {
+ let params = {
+ param1: 'val1',
+ param2: 'val2',
+ param3: 'val3'
+ };
+ expect(spec.formatOneMobileDynamicParams(params)).to.contain('¶m1=val1¶m2=val2¶m3=val3');
+ });
+
+ it('should return formatted gdpr params when isConsentRequired returns true', () => {
+ let consentData = {
+ consentString: 'test-consent'
+ };
+ consentRequiredStub.returns(true);
+ expect(spec.formatOneMobileDynamicParams({}, consentData)).to.be.equal('&euconsent=test-consent&gdpr=1');
+ });
+
+ it('should return formatted secure param when isSecureProtocol returns true', () => {
+ secureProtocolStub.returns(true);
+ expect(spec.formatOneMobileDynamicParams()).to.be.equal('&secure=1');
+ });
+ });
});
diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js
index 1ba4edfa4ea..53fbf390a6e 100644
--- a/test/spec/modules/appnexusBidAdapter_spec.js
+++ b/test/spec/modules/appnexusBidAdapter_spec.js
@@ -173,7 +173,7 @@ describe('AppNexusAdapter', () => {
});
});
- it('should attache native params to the request', () => {
+ it('should attach native params to the request', () => {
let bidRequest = Object.assign({},
bidRequests[0],
{
@@ -290,7 +290,7 @@ describe('AppNexusAdapter', () => {
}]);
});
- it('should should add payment rules to the request', () => {
+ it('should add payment rules to the request', () => {
let bidRequest = Object.assign({},
bidRequests[0],
{
@@ -306,6 +306,28 @@ describe('AppNexusAdapter', () => {
expect(payload.tags[0].use_pmt_rule).to.equal(true);
});
+
+ it('should add gdpr consent information to the request', () => {
+ let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==';
+ let bidderRequest = {
+ 'bidderCode': 'appnexus',
+ 'auctionId': '1d1a030790a475',
+ 'bidderRequestId': '22edbae2733bf6',
+ 'timeout': 3000,
+ 'gdprConsent': {
+ consentString: consentString,
+ gdprApplies: true
+ }
+ };
+ bidderRequest.bids = bidRequests;
+
+ const request = spec.buildRequests(bidRequests, bidderRequest);
+ const payload = JSON.parse(request.data);
+
+ expect(payload.gdpr_consent).to.exist;
+ expect(payload.gdpr_consent.consent_string).to.exist.and.to.equal(consentString);
+ expect(payload.gdpr_consent.consent_required).to.exist.and.to.be.true;
+ });
})
describe('interpretResponse', () => {
diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js
new file mode 100644
index 00000000000..5974ac79324
--- /dev/null
+++ b/test/spec/modules/consentManagement_spec.js
@@ -0,0 +1,292 @@
+import {setConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, allowAuction} from 'modules/consentManagement';
+import {gdprDataHandler} from 'src/adaptermanager';
+import * as utils from 'src/utils';
+import { config } from 'src/config';
+
+let assert = require('chai').assert;
+let expect = require('chai').expect;
+
+describe('consentManagement', function () {
+ describe('setConfig tests:', () => {
+ describe('empty setConfig value', () => {
+ beforeEach(() => {
+ sinon.stub(utils, 'logInfo');
+ });
+
+ afterEach(() => {
+ utils.logInfo.restore();
+ config.resetConfig();
+ });
+
+ it('should use system default values', () => {
+ setConfig({});
+ expect(userCMP).to.be.equal('iab');
+ expect(consentTimeout).to.be.equal(10000);
+ expect(allowAuction).to.be.true;
+ sinon.assert.callCount(utils.logInfo, 3);
+ });
+ });
+
+ describe('valid setConfig value', () => {
+ afterEach(() => {
+ config.resetConfig();
+ $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook);
+ });
+ it('results in all user settings overriding system defaults', () => {
+ let allConfig = {
+ cmpApi: 'iab',
+ timeout: 7500,
+ allowAuctionWithoutConsent: false
+ };
+
+ setConfig(allConfig);
+ expect(userCMP).to.be.equal('iab');
+ expect(consentTimeout).to.be.equal(7500);
+ expect(allowAuction).to.be.false;
+ });
+ });
+ });
+
+ describe('requestBidsHook tests:', () => {
+ let goodConfigWithCancelAuction = {
+ cmpApi: 'iab',
+ timeout: 7500,
+ allowAuctionWithoutConsent: false
+ };
+
+ let goodConfigWithAllowAuction = {
+ cmpApi: 'iab',
+ timeout: 7500,
+ allowAuctionWithoutConsent: true
+ };
+
+ let didHookReturn;
+
+ afterEach(() => {
+ gdprDataHandler.consentData = null;
+ resetConsentData();
+ });
+
+ describe('error checks:', () => {
+ describe('unknown CMP framework ID:', () => {
+ beforeEach(() => {
+ sinon.stub(utils, 'logWarn');
+ });
+
+ afterEach(() => {
+ utils.logWarn.restore();
+ config.resetConfig();
+ $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook);
+ gdprDataHandler.consentData = null;
+ });
+
+ it('should return Warning message and return to hooked function', () => {
+ let badCMPConfig = {
+ cmpApi: 'bad'
+ };
+ setConfig(badCMPConfig);
+ expect(userCMP).to.be.equal(badCMPConfig.cmpApi);
+
+ didHookReturn = false;
+
+ requestBidsHook({}, () => {
+ didHookReturn = true;
+ });
+ let consent = gdprDataHandler.getConsentData();
+ sinon.assert.calledOnce(utils.logWarn);
+ expect(didHookReturn).to.be.true;
+ expect(consent).to.be.null;
+ });
+ });
+ });
+
+ describe('already known consentData:', () => {
+ let cmpStub = sinon.stub();
+
+ beforeEach(() => {
+ didHookReturn = false;
+ window.__cmp = function() {};
+ });
+
+ afterEach(() => {
+ config.resetConfig();
+ $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook);
+ cmpStub.restore();
+ delete window.__cmp;
+ gdprDataHandler.consentData = null;
+ });
+
+ it('should bypass CMP and simply use previously stored consentData', () => {
+ let testConsentData = {
+ gdprApplies: true,
+ metadata: 'xyz'
+ };
+
+ cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
+ args[2](testConsentData);
+ });
+ setConfig(goodConfigWithAllowAuction);
+ requestBidsHook({}, () => {});
+ cmpStub.restore();
+
+ // reset the stub to ensure it wasn't called during the second round of calls
+ cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
+ args[2](testConsentData);
+ });
+
+ requestBidsHook({}, () => {
+ didHookReturn = true;
+ });
+ let consent = gdprDataHandler.getConsentData();
+
+ expect(didHookReturn).to.be.true;
+ expect(consent.consentString).to.equal(testConsentData.metadata);
+ expect(consent.gdprApplies).to.be.true;
+ sinon.assert.notCalled(cmpStub);
+ });
+ });
+
+ describe('CMP workflow for iframed page', () => {
+ let eventStub = sinon.stub();
+ let cmpStub = sinon.stub();
+
+ beforeEach(() => {
+ didHookReturn = false;
+ resetConsentData();
+ window.__cmp = function() {};
+ sinon.stub(utils, 'logError');
+ sinon.stub(utils, 'logWarn');
+ });
+
+ afterEach(() => {
+ config.resetConfig();
+ $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook);
+ eventStub.restore();
+ cmpStub.restore();
+ delete window.__cmp;
+ utils.logError.restore();
+ utils.logWarn.restore();
+ gdprDataHandler.consentData = null;
+ });
+
+ it('should return the consent string from a postmessage + addEventListener response', () => {
+ let testConsentData = {
+ data: {
+ __cmpReturn: {
+ returnValue: {
+ gdprApplies: true,
+ metadata: 'BOJy+UqOJy+UqABAB+AAAAAZ+A=='
+ }
+ }
+ }
+ };
+ eventStub = sinon.stub(window, 'addEventListener').callsFake((...args) => {
+ args[1](testConsentData);
+ });
+ cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
+ args[2]({
+ gdprApplies: true,
+ metadata: 'BOJy+UqOJy+UqABAB+AAAAAZ+A=='
+ });
+ });
+
+ setConfig(goodConfigWithAllowAuction);
+
+ requestBidsHook({}, () => {
+ didHookReturn = true;
+ });
+ let consent = gdprDataHandler.getConsentData();
+
+ sinon.assert.notCalled(utils.logWarn);
+ sinon.assert.notCalled(utils.logError);
+ expect(didHookReturn).to.be.true;
+ expect(consent.consentString).to.equal('BOJy+UqOJy+UqABAB+AAAAAZ+A==');
+ expect(consent.gdprApplies).to.be.true;
+ });
+ });
+
+ describe('CMP workflow for normal pages:', () => {
+ let cmpStub = sinon.stub();
+
+ beforeEach(() => {
+ didHookReturn = false;
+ resetConsentData();
+ sinon.stub(utils, 'logError');
+ sinon.stub(utils, 'logWarn');
+ window.__cmp = function() {};
+ });
+
+ afterEach(() => {
+ config.resetConfig();
+ $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook);
+ cmpStub.restore();
+ utils.logError.restore();
+ utils.logWarn.restore();
+ delete window.__cmp;
+ gdprDataHandler.consentData = null;
+ });
+
+ it('performs lookup check and stores consentData for a valid existing user', () => {
+ let testConsentData = {
+ gdprApplies: true,
+ metadata: 'BOJy+UqOJy+UqABAB+AAAAAZ+A=='
+ };
+ cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
+ args[2](testConsentData);
+ });
+
+ setConfig(goodConfigWithAllowAuction);
+
+ requestBidsHook({}, () => {
+ didHookReturn = true;
+ });
+ let consent = gdprDataHandler.getConsentData();
+
+ sinon.assert.notCalled(utils.logWarn);
+ sinon.assert.notCalled(utils.logError);
+ expect(didHookReturn).to.be.true;
+ expect(consent.consentString).to.equal(testConsentData.metadata);
+ expect(consent.gdprApplies).to.be.true;
+ });
+
+ it('throws an error when processCmpData check failed while config had allowAuction set to false', () => {
+ let testConsentData = null;
+
+ cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
+ args[2](testConsentData);
+ });
+
+ setConfig(goodConfigWithCancelAuction);
+
+ requestBidsHook({}, () => {
+ didHookReturn = true;
+ });
+ let consent = gdprDataHandler.getConsentData();
+
+ sinon.assert.calledOnce(utils.logError);
+ expect(didHookReturn).to.be.false;
+ expect(consent).to.be.null;
+ });
+
+ it('throws a warning + stores consentData + calls callback when processCmpData check failed while config had allowAuction set to true', () => {
+ let testConsentData = null;
+
+ cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => {
+ args[2](testConsentData);
+ });
+
+ setConfig(goodConfigWithAllowAuction);
+
+ requestBidsHook({}, () => {
+ didHookReturn = true;
+ });
+ let consent = gdprDataHandler.getConsentData();
+
+ sinon.assert.calledOnce(utils.logWarn);
+ expect(didHookReturn).to.be.true;
+ expect(consent.consentString).to.be.undefined;
+ expect(consent.gdprApplies).to.be.undefined;
+ });
+ });
+ });
+});
diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js
index eb14c300e33..cdb3113c205 100644
--- a/test/spec/modules/prebidServerBidAdapter_spec.js
+++ b/test/spec/modules/prebidServerBidAdapter_spec.js
@@ -6,6 +6,7 @@ import cookie from 'src/cookie';
import { userSync } from 'src/userSync';
import { ajax } from 'src/ajax';
import { config } from 'src/config';
+import { requestBidsHook } from 'modules/consentManagement';
let CONFIG = {
accountId: '1',
@@ -391,6 +392,38 @@ describe('S2S Adapter', () => {
expect(requestBid.ad_units[0].bids[0].params.member).to.exist.and.to.be.a('string');
});
+ it('adds gdpr consent information to ortb2 request depending on module use', () => {
+ let ortb2Config = utils.deepClone(CONFIG);
+ ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'
+
+ let consentConfig = { consentManagement: { cmp: 'iab' }, s2sConfig: ortb2Config };
+ config.setConfig(consentConfig);
+
+ let gdprBidRequest = utils.deepClone(BID_REQUESTS);
+ gdprBidRequest[0].gdprConsent = {
+ consentString: 'abc123',
+ gdprApplies: true
+ };
+
+ adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax);
+ let requestBid = JSON.parse(requests[0].requestBody);
+
+ expect(requestBid.regs.ext.gdpr).is.equal(1);
+ expect(requestBid.user.ext.consent).is.equal('abc123');
+
+ config.resetConfig();
+ config.setConfig({s2sConfig: CONFIG});
+
+ adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax);
+ requestBid = JSON.parse(requests[1].requestBody);
+
+ expect(requestBid.regs).to.not.exist;
+ expect(requestBid.user).to.not.exist;
+
+ config.resetConfig();
+ $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook);
+ });
+
it('sets invalid cacheMarkup value to 0', () => {
const s2sConfig = Object.assign({}, CONFIG, {
cacheMarkup: 999
diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js
index cbf17f9478a..7ea10315a4e 100644
--- a/test/spec/modules/pubmaticBidAdapter_spec.js
+++ b/test/spec/modules/pubmaticBidAdapter_spec.js
@@ -44,7 +44,10 @@ describe('PubMatic adapter', () => {
'price': 1.3,
'adm': 'image3.pubmatic.com Layer based creative',
'h': 250,
- 'w': 300
+ 'w': 300,
+ 'ext': {
+ 'deal_channel': 6
+ }
}]
}]
}
@@ -136,8 +139,44 @@ describe('PubMatic adapter', () => {
expect(data.ext.wrapper.wv).to.equal(constants.REPO_AND_VERSION); // Wrapper Version
expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].transactionId); // Prebid TransactionId
expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID
- expect(data.ext.wrapper.profile).to.equal(bidRequests[0].params.profId); // OpenWrap: Wrapper Profile ID
- expect(data.ext.wrapper.version).to.equal(bidRequests[0].params.verId); // OpenWrap: Wrapper Profile Version ID
+ expect(data.ext.wrapper.profile).to.equal(parseInt(bidRequests[0].params.profId)); // OpenWrap: Wrapper Profile ID
+ expect(data.ext.wrapper.version).to.equal(parseInt(bidRequests[0].params.verId)); // OpenWrap: Wrapper Profile Version ID
+
+ expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id
+ expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor
+ expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid
+ expect(data.imp[0].banner.w).to.equal(300); // width
+ expect(data.imp[0].banner.h).to.equal(250); // height
+ expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid
+ });
+
+ it('Request params check with GDPR Consent', () => {
+ let bidRequest = {
+ gdprConsent: {
+ consentString: 'kjfdniwjnifwenrif3',
+ gdprApplies: true
+ }
+ };
+ let request = spec.buildRequests(bidRequests, bidRequest);
+ let data = JSON.parse(request.data);
+ expect(data.user.ext.consent).to.equal('kjfdniwjnifwenrif3');
+ expect(data.regs.ext.gdpr).to.equal(1);
+ expect(data.at).to.equal(1); // auction type
+ expect(data.cur[0]).to.equal('USD'); // currency
+ expect(data.site.domain).to.be.a('string'); // domain should be set
+ expect(data.site.page).to.equal(bidRequests[0].params.kadpageurl); // forced pageURL
+ expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id
+ expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB
+ expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender
+ expect(data.device.geo.lat).to.equal(parseFloat(bidRequests[0].params.lat)); // Latitude
+ expect(data.device.geo.lon).to.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude
+ expect(data.user.geo.lat).to.equal(parseFloat(bidRequests[0].params.lat)); // Latitude
+ expect(data.user.geo.lon).to.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude
+ expect(data.ext.wrapper.wv).to.equal(constants.REPO_AND_VERSION); // Wrapper Version
+ expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].transactionId); // Prebid TransactionId
+ expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID
+ expect(data.ext.wrapper.profile).to.equal(parseInt(bidRequests[0].params.profId)); // OpenWrap: Wrapper Profile ID
+ expect(data.ext.wrapper.version).to.equal(parseInt(bidRequests[0].params.verId)); // OpenWrap: Wrapper Profile Version ID
expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id
expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor
@@ -175,6 +214,24 @@ describe('PubMatic adapter', () => {
expect(response[0].referrer).to.include(utils.getTopWindowUrl());
expect(response[0].ad).to.equal(bidResponses.body.seatbid[0].bid[0].adm);
});
+
+ it('should check for dealChannel value selection', () => {
+ let request = spec.buildRequests(bidRequests);
+ let response = spec.interpretResponse(bidResponses, request);
+ expect(response).to.be.an('array').with.length.above(0);
+ expect(response[0].dealChannel).to.equal('PMPG');
+ });
+
+ it('should check for unexpected dealChannel value selection', () => {
+ let request = spec.buildRequests(bidRequests);
+ let updateBiResponse = bidResponses;
+ updateBiResponse.body.seatbid[0].bid[0].ext.deal_channel = 11;
+
+ let response = spec.interpretResponse(updateBiResponse, request);
+
+ expect(response).to.be.an('array').with.length.above(0);
+ expect(response[0].dealChannel).to.equal(null);
+ });
});
});
});
diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js
index 39e468d4959..8b1c164a804 100644
--- a/test/spec/unit/core/adapterManager_spec.js
+++ b/test/spec/unit/core/adapterManager_spec.js
@@ -716,141 +716,171 @@ describe('adapterManager tests', () => {
expect(AdapterManager.videoAdapters).to.include(alias);
});
});
+ });
+
+ describe('makeBidRequests', () => {
+ let adUnits;
+ beforeEach(() => {
+ adUnits = utils.deepClone(getAdUnits()).map(adUnit => {
+ adUnit.bids = adUnit.bids.filter(bid => includes(['appnexus', 'rubicon'], bid.bidder));
+ return adUnit;
+ })
+ });
- describe('makeBidRequests', () => {
- let adUnits;
+ describe('setBidderSequence', () => {
beforeEach(() => {
- adUnits = utils.deepClone(getAdUnits()).map(adUnit => {
- adUnit.bids = adUnit.bids.filter(bid => includes(['appnexus', 'rubicon'], bid.bidder));
- return adUnit;
- })
+ sinon.spy(utils, 'shuffle');
});
- describe('setBidderSequence', () => {
- beforeEach(() => {
- sinon.spy(utils, 'shuffle');
- });
-
- afterEach(() => {
- config.resetConfig();
- utils.shuffle.restore();
- });
+ afterEach(() => {
+ config.resetConfig();
+ utils.shuffle.restore();
+ });
- it('setting to `random` uses shuffled order of adUnits', () => {
- config.setConfig({ bidderSequence: 'random' });
- let bidRequests = AdapterManager.makeBidRequests(
- adUnits,
- Date.now(),
- utils.getUniqueIdentifierStr(),
- function callback() {},
- []
- );
- sinon.assert.calledOnce(utils.shuffle);
- });
+ it('setting to `random` uses shuffled order of adUnits', () => {
+ config.setConfig({ bidderSequence: 'random' });
+ let bidRequests = AdapterManager.makeBidRequests(
+ adUnits,
+ Date.now(),
+ utils.getUniqueIdentifierStr(),
+ function callback() {},
+ []
+ );
+ sinon.assert.calledOnce(utils.shuffle);
});
+ });
- describe('sizeMapping', () => {
- beforeEach(() => {
- sinon.stub(window, 'matchMedia').callsFake(() => ({matches: true}));
- });
+ describe('sizeMapping', () => {
+ beforeEach(() => {
+ sinon.stub(window, 'matchMedia').callsFake(() => ({matches: true}));
+ });
- afterEach(() => {
- matchMedia.restore();
- setSizeConfig([]);
- });
+ afterEach(() => {
+ matchMedia.restore();
+ setSizeConfig([]);
+ });
- it('should not filter bids w/ no labels', () => {
- let bidRequests = AdapterManager.makeBidRequests(
- adUnits,
- Date.now(),
- utils.getUniqueIdentifierStr(),
- function callback() {},
- []
- );
-
- expect(bidRequests.length).to.equal(2);
- let rubiconBidRequests = find(bidRequests, bidRequest => bidRequest.bidderCode === 'rubicon');
- expect(rubiconBidRequests.bids.length).to.equal(1);
- expect(rubiconBidRequests.bids[0].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === rubiconBidRequests.bids[0].adUnitCode).sizes);
-
- let appnexusBidRequests = find(bidRequests, bidRequest => bidRequest.bidderCode === 'appnexus');
- expect(appnexusBidRequests.bids.length).to.equal(2);
- expect(appnexusBidRequests.bids[0].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === appnexusBidRequests.bids[0].adUnitCode).sizes);
- expect(appnexusBidRequests.bids[1].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === appnexusBidRequests.bids[1].adUnitCode).sizes);
- });
+ it('should not filter bids w/ no labels', () => {
+ let bidRequests = AdapterManager.makeBidRequests(
+ adUnits,
+ Date.now(),
+ utils.getUniqueIdentifierStr(),
+ function callback() {},
+ []
+ );
+
+ expect(bidRequests.length).to.equal(2);
+ let rubiconBidRequests = find(bidRequests, bidRequest => bidRequest.bidderCode === 'rubicon');
+ expect(rubiconBidRequests.bids.length).to.equal(1);
+ expect(rubiconBidRequests.bids[0].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === rubiconBidRequests.bids[0].adUnitCode).sizes);
+
+ let appnexusBidRequests = find(bidRequests, bidRequest => bidRequest.bidderCode === 'appnexus');
+ expect(appnexusBidRequests.bids.length).to.equal(2);
+ expect(appnexusBidRequests.bids[0].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === appnexusBidRequests.bids[0].adUnitCode).sizes);
+ expect(appnexusBidRequests.bids[1].sizes).to.deep.equal(find(adUnits, adUnit => adUnit.code === appnexusBidRequests.bids[1].adUnitCode).sizes);
+ });
- it('should filter sizes using size config', () => {
- let validSizes = [
- [728, 90],
- [300, 250]
- ];
-
- let validSizeMap = validSizes.map(size => size.toString()).reduce((map, size) => {
- map[size] = true;
- return map;
- }, {});
-
- setSizeConfig([{
- 'mediaQuery': '(min-width: 768px) and (max-width: 1199px)',
- 'sizesSupported': validSizes,
- 'labels': ['tablet', 'phone']
- }]);
-
- let bidRequests = AdapterManager.makeBidRequests(
- adUnits,
- Date.now(),
- utils.getUniqueIdentifierStr(),
- function callback() {},
- []
- );
+ it('should filter sizes using size config', () => {
+ let validSizes = [
+ [728, 90],
+ [300, 250]
+ ];
+
+ let validSizeMap = validSizes.map(size => size.toString()).reduce((map, size) => {
+ map[size] = true;
+ return map;
+ }, {});
+
+ setSizeConfig([{
+ 'mediaQuery': '(min-width: 768px) and (max-width: 1199px)',
+ 'sizesSupported': validSizes,
+ 'labels': ['tablet', 'phone']
+ }]);
+
+ let bidRequests = AdapterManager.makeBidRequests(
+ adUnits,
+ Date.now(),
+ utils.getUniqueIdentifierStr(),
+ function callback() {},
+ []
+ );
// only valid sizes as specified in size config should show up in bidRequests
- bidRequests.forEach(bidRequest => {
- bidRequest.bids.forEach(bid => {
- bid.sizes.forEach(size => {
- expect(validSizeMap[size]).to.equal(true);
- });
+ bidRequests.forEach(bidRequest => {
+ bidRequest.bids.forEach(bid => {
+ bid.sizes.forEach(size => {
+ expect(validSizeMap[size]).to.equal(true);
});
});
-
- setSizeConfig([{
- 'mediaQuery': '(min-width: 768px) and (max-width: 1199px)',
- 'sizesSupported': [],
- 'labels': ['tablet', 'phone']
- }]);
-
- bidRequests = AdapterManager.makeBidRequests(
- adUnits,
- Date.now(),
- utils.getUniqueIdentifierStr(),
- function callback() {},
- []
- );
-
- // if no valid sizes, all bidders should be filtered out
- expect(bidRequests.length).to.equal(0);
});
- it('should filter adUnits/bidders based on applied labels', () => {
- adUnits[0].labelAll = ['visitor-uk', 'mobile'];
- adUnits[1].labelAny = ['visitor-uk', 'desktop'];
- adUnits[1].bids[0].labelAny = ['mobile'];
- adUnits[1].bids[1].labelAll = ['desktop'];
+ setSizeConfig([{
+ 'mediaQuery': '(min-width: 768px) and (max-width: 1199px)',
+ 'sizesSupported': [],
+ 'labels': ['tablet', 'phone']
+ }]);
+
+ bidRequests = AdapterManager.makeBidRequests(
+ adUnits,
+ Date.now(),
+ utils.getUniqueIdentifierStr(),
+ function callback() {},
+ []
+ );
+
+ // if no valid sizes, all bidders should be filtered out
+ expect(bidRequests.length).to.equal(0);
+ });
+
+ it('should filter adUnits/bidders based on applied labels', () => {
+ adUnits[0].labelAll = ['visitor-uk', 'mobile'];
+ adUnits[1].labelAny = ['visitor-uk', 'desktop'];
+ adUnits[1].bids[0].labelAny = ['mobile'];
+ adUnits[1].bids[1].labelAll = ['desktop'];
- let bidRequests = AdapterManager.makeBidRequests(
- adUnits,
- Date.now(),
- utils.getUniqueIdentifierStr(),
- function callback() {},
- ['visitor-uk', 'desktop']
- );
+ let bidRequests = AdapterManager.makeBidRequests(
+ adUnits,
+ Date.now(),
+ utils.getUniqueIdentifierStr(),
+ function callback() {},
+ ['visitor-uk', 'desktop']
+ );
// 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);
- expect(bidRequests[0].bids[0].adUnitCode).to.equal(adUnits[1].code);
+ expect(bidRequests.length).to.equal(1);
+ expect(bidRequests[0].bidderCode).to.equal('rubicon');
+ expect(bidRequests[0].bids.length).to.equal(1);
+ expect(bidRequests[0].bids[0].adUnitCode).to.equal(adUnits[1].code);
+ });
+ });
+
+ describe('gdpr consent module', () => {
+ it('inserts gdprConsent object to bidRequest only when module was enabled', () => {
+ AdapterManager.gdprDataHandler.setConsentData({
+ consentString: 'abc123def456',
+ consentRequired: true
});
+
+ let bidRequests = AdapterManager.makeBidRequests(
+ adUnits,
+ Date.now(),
+ utils.getUniqueIdentifierStr(),
+ function callback() {},
+ []
+ );
+ expect(bidRequests[0].gdprConsent.consentString).to.equal('abc123def456');
+ expect(bidRequests[0].gdprConsent.consentRequired).to.be.true;
+
+ AdapterManager.gdprDataHandler.setConsentData(null);
+
+ bidRequests = AdapterManager.makeBidRequests(
+ adUnits,
+ Date.now(),
+ utils.getUniqueIdentifierStr(),
+ function callback() {},
+ []
+ );
+ expect(bidRequests[0].gdprConsent).to.be.undefined;
});
});
});
diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js
index f86840dbdba..9218409c46c 100755
--- a/test/spec/utils_spec.js
+++ b/test/spec/utils_spec.js
@@ -359,6 +359,33 @@ describe('Utils', function () {
});
});
+ describe('isPlainObject', function () {
+ it('should return false with input string', function () {
+ var output = utils.isPlainObject(obj_string);
+ assert.deepEqual(output, false);
+ });
+
+ it('should return false with input number', function () {
+ var output = utils.isPlainObject(obj_number);
+ assert.deepEqual(output, false);
+ });
+
+ it('should return true with input object', function () {
+ var output = utils.isPlainObject(obj_object);
+ assert.deepEqual(output, true);
+ });
+
+ it('should return false with input array', function () {
+ var output = utils.isPlainObject(obj_array);
+ assert.deepEqual(output, false);
+ });
+
+ it('should return false with input function', function () {
+ var output = utils.isPlainObject(obj_function);
+ assert.deepEqual(output, false);
+ });
+ });
+
describe('isEmpty', function () {
it('should return true with empty object', function () {
var output = utils.isEmpty(obj_object);