diff --git a/README.md b/README.md index be07a27ddc1..0e07521ac3b 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ module.exports = { // override the regular exclusion from above (for being inside node_modules). { test: /.js$/, - include: new RegExp(`\\${path.sep}prebid\.js`), + include: new RegExp(`\\${path.sep}prebid\\.js`), use: { loader: 'babel-loader', // presets and plugins for Prebid.js must be manually specified separate from your other babel rule. diff --git a/allowedModules.js b/allowedModules.js index 0fee2cfdedd..4f8b8039d97 100644 --- a/allowedModules.js +++ b/allowedModules.js @@ -1,11 +1,11 @@ const sharedWhiteList = [ - "core-js/library/fn/array/find", // no ie11 - "core-js/library/fn/array/includes", // no ie11 - "core-js/library/fn/set", // ie11 supports Set but not Set#values - "core-js/library/fn/string/includes", // no ie11 - "core-js/library/fn/number/is-integer", // no ie11, - "core-js/library/fn/array/from" // no ie11 + 'core-js/library/fn/array/find', // no ie11 + 'core-js/library/fn/array/includes', // no ie11 + 'core-js/library/fn/set', // ie11 supports Set but not Set#values + 'core-js/library/fn/string/includes', // no ie11 + 'core-js/library/fn/number/is-integer', // no ie11, + 'core-js/library/fn/array/from' // no ie11 ]; module.exports = { @@ -21,6 +21,7 @@ module.exports = { 'fun-hooks/no-eval', 'just-clone', 'dlv', - 'dset' + 'dset', + 'deep-equal' ] }; diff --git a/gulpfile.js b/gulpfile.js index 111bd9dc3bb..1b5cd85abd6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -249,13 +249,13 @@ function test(done) { execa(wdioCmd, wdioOpts, { stdio: 'inherit' }) .then(stdout => { - //kill mock server + // kill mock server mockServer.kill('SIGINT'); done(); process.exit(0); }) .catch(err => { - //kill mock server + // kill mock server mockServer.kill('SIGINT'); done(new Error(`Tests failed with error: ${err}`)); process.exit(1); diff --git a/integrationExamples/gpt/digitrust_Full.html b/integrationExamples/gpt/digitrust_Full.html index 7ec268a619a..fc7704776f4 100644 --- a/integrationExamples/gpt/digitrust_Full.html +++ b/integrationExamples/gpt/digitrust_Full.html @@ -70,7 +70,7 @@ }()); var t = document.createElement('script'); t.async = false; - t.src = 'http://acdn.adnxs.com/cmp/cmp.bundle.js'; + t.src = 'https://acdn.adnxs.com/cmp/cmp.bundle.js'; var tag = document.getElementsByTagName('head')[0]; tag.appendChild(t); } diff --git a/integrationExamples/gpt/digitrust_Simple.html b/integrationExamples/gpt/digitrust_Simple.html index 91eca591898..2581c6ce7cc 100644 --- a/integrationExamples/gpt/digitrust_Simple.html +++ b/integrationExamples/gpt/digitrust_Simple.html @@ -71,7 +71,7 @@ }()); var t = document.createElement('script'); t.async = false; - t.src = 'http://acdn.adnxs.com/cmp/cmp.bundle.js'; + t.src = 'https://acdn.adnxs.com/cmp/cmp.bundle.js'; var tag = document.getElementsByTagName('head')[0]; tag.appendChild(t); } @@ -148,6 +148,9 @@ } else { console.error('Digitrust init failed'); + if(digiTrustResult.err){ + console.error(digiTrustResult.err); + } } } }, @@ -157,6 +160,12 @@ expires: 60 } }] + }, + userIdTargeting: { + "GAM": true, + "GAM_KEYS": { + "tdid": "TTD_ID" // send tdid as TTD_ID + } } }); pbjs.addAdUnits(adUnits); diff --git a/integrationExamples/gpt/digitrust_cmp_test.html b/integrationExamples/gpt/digitrust_cmp_test.html index f5fc0aace63..6f0a70188f3 100644 --- a/integrationExamples/gpt/digitrust_cmp_test.html +++ b/integrationExamples/gpt/digitrust_cmp_test.html @@ -187,6 +187,6 @@

DigiTrust Prebid Sample - No Framework

googletag.cmd.push(function () { googletag.display('test-div'); }); - + diff --git a/integrationExamples/gpt/hello_world.html b/integrationExamples/gpt/hello_world.html index d68e65011be..47ba5b8f18a 100755 --- a/integrationExamples/gpt/hello_world.html +++ b/integrationExamples/gpt/hello_world.html @@ -88,4 +88,4 @@
Div-1
- + \ No newline at end of file diff --git a/modules/aardvarkBidAdapter.js b/modules/aardvarkBidAdapter.js index 848a6672022..0b864286868 100644 --- a/modules/aardvarkBidAdapter.js +++ b/modules/aardvarkBidAdapter.js @@ -15,6 +15,7 @@ export function resetUserSync() { export const spec = { code: BIDDER_CODE, + gvlid: 52, aliases: ['adsparc', 'safereach'], isBidRequestValid: function(bid) { diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index d24350542a4..a21915e00a9 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -4,14 +4,17 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { loadExternalScript } from '../src/adloader.js' import JSEncrypt from 'jsencrypt/bin/jsencrypt.js'; import sha256 from 'crypto-js/sha256.js'; +import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'adagio'; -const VERSION = '2.1.0'; +const VERSION = '2.2.1'; const FEATURES_VERSION = '1'; const ENDPOINT = 'https://mp.4dex.io/prebid'; const SUPPORTED_MEDIA_TYPES = ['banner']; const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js'; const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript'; +const GVLID = 617; +const storage = getStorageManager(GVLID, 'adagio'); export const ADAGIO_PUBKEY = `-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9el0+OEn6fvEh1RdVHQu4cnT0 @@ -20,10 +23,8 @@ t0b0lsHN+W4n9kitS/DZ/xnxWK/9vxhv0ZtL1LL/rwR5Mup7rmJbNtDoNBw4TIGj pV6EP3MTLosuUEpLaQIDAQAB -----END PUBLIC KEY-----`; -export function getAdagioScript() { +export function adagioScriptFromLocalStorageCb(ls) { try { - const ls = utils.getDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); - if (!ls) { utils.logWarn('Adagio Script not found'); return; @@ -33,7 +34,7 @@ export function getAdagioScript() { if (!hashRgx.test(ls)) { utils.logWarn('No hash found in Adagio script'); - utils.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); + storage.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); } else { const r = ls.match(hashRgx); const hash = r[2]; @@ -47,7 +48,7 @@ export function getAdagioScript() { Function(ls)(); // eslint-disable-line no-new-func } else { utils.logWarn('Invalid Adagio script found'); - utils.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); + storage.removeDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY); } } } catch (err) { @@ -55,6 +56,12 @@ export function getAdagioScript() { } } +export function getAdagioScript() { + storage.getDataFromLocalStorage(ADAGIO_LOCALSTORAGE_KEY, (ls) => { + adagioScriptFromLocalStorageCb(ls) + }); +} + function canAccessTopWindow() { try { if (utils.getWindowTop().location.href) { @@ -335,13 +342,22 @@ function _getGdprConsent(bidderRequest) { if (bidderRequest.gdprConsent.allowAuctionWithoutConsent !== undefined) { consent.allowAuctionWithoutConsent = bidderRequest.gdprConsent.allowAuctionWithoutConsent ? 1 : 0; } + if (bidderRequest.gdprConsent.apiVersion !== undefined) { + consent.apiVersion = bidderRequest.gdprConsent.apiVersion; + } } return consent; } +function _getSchain(bidRequest) { + if (utils.deepAccess(bidRequest, 'schain')) { + return bidRequest.schain; + } +} + export const spec = { code: BIDDER_CODE, - + gvlid: GVLID, supportedMediaType: SUPPORTED_MEDIA_TYPES, isBidRequestValid: function (bid) { @@ -391,6 +407,7 @@ export const spec = { const site = _getSite(); const pageviewId = _getPageviewId(); const gdprConsent = _getGdprConsent(bidderRequest); + const schain = _getSchain(validBidRequests[0]); const adUnits = utils._map(validBidRequests, (bidRequest) => { bidRequest.features = _getFeatures(bidRequest); return bidRequest; @@ -419,6 +436,7 @@ export const spec = { pageviewId: pageviewId, adUnits: groupedAdUnits[organizationId], gdpr: gdprConsent, + schain: schain, prebidVersion: '$prebid.version$', adapterVersion: VERSION, featuresVersion: FEATURES_VERSION diff --git a/modules/adfinityBidAdapter.js b/modules/adfinityBidAdapter.js index 34407e015db..ebf1198fba2 100644 --- a/modules/adfinityBidAdapter.js +++ b/modules/adfinityBidAdapter.js @@ -64,6 +64,13 @@ export const spec = { 'placements': placements }; + if (bidderRequest) { + if (bidderRequest.gdprConsent) { + request.gdpr_consent = bidderRequest.gdprConsent.consentString || 'ALL' + request.gdpr_require = bidderRequest.gdprConsent.gdprApplies ? 1 : 0 + } + } + for (let i = 0; i < validBidRequests.length; i++) { let bid = validBidRequests[i]; let traff = bid.params.traffic || BANNER diff --git a/modules/adgenerationBidAdapter.md b/modules/adgenerationBidAdapter.md index 7dfc301e657..7f66732ff38 100644 --- a/modules/adgenerationBidAdapter.md +++ b/modules/adgenerationBidAdapter.md @@ -15,57 +15,61 @@ AdGeneration bid adapter supports Banner and Native. # Test Parameters ``` var adUnits = [ - // Banner adUnit - { - code: 'banner-div', // banner - sizes: [[300, 250]], - bids: [ - { - bidder: 'adg', - params: { - id: '58278', // banner - } - }, - ] - }, - // Native adUnit - { - code: 'native-div', - sizes: [[1,1]], - mediaTypes: { - native: { - image: { + // Banner adUnit + { + code: 'banner-div', // banner + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'adg', + params: { + id: '58278', // banner + } + }, + ] + }, + // Native adUnit + { + code: 'native-div', + sizes: [[1,1]], + mediaTypes: { + native: { + image: { required: true - }, - title: { + }, + title: { required: true, len: 80 - }, - sponsoredBy: { + }, + sponsoredBy: { required: true - }, - clickUrl: { + }, + clickUrl: { required: true - }, - body: { + }, + body: { required: true - }, - icon: { + }, + icon: { required: true - }, - privacyLink: { + }, + privacyLink: { required: true - }, }, - }, - bids: [ - { - bidder: 'adg', - params: { + }, + }, + bids: [ + { + bidder: 'adg', + params: { id: '58279', //native - } - }, - ] + } + }, + ] }, ]; ``` diff --git a/modules/adkernelAdnAnalyticsAdapter.js b/modules/adkernelAdnAnalyticsAdapter.js index 43cda60b0ec..b6db6fc22de 100644 --- a/modules/adkernelAdnAnalyticsAdapter.js +++ b/modules/adkernelAdnAnalyticsAdapter.js @@ -1,13 +1,14 @@ import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import {parse} from '../src/url.js'; import * as utils from '../src/utils.js'; import {ajax} from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; const ANALYTICS_VERSION = '1.0.1'; const DEFAULT_QUEUE_TIMEOUT = 4000; const DEFAULT_HOST = 'tag.adkernel.com'; +const storageObj = getStorageManager(); const ADK_HB_EVENTS = { AUCTION_INIT: 'auctionInit', @@ -175,10 +176,10 @@ const ORGANIC = '(organic)'; export let storage = { getItem: (name) => { - return utils.getDataFromLocalStorage(name); + return storageObj.getDataFromLocalStorage(name); }, setItem: (name, value) => { - utils.setDataInLocalStorage(name, value); + storageObj.setDataInLocalStorage(name, value); } }; @@ -209,7 +210,7 @@ export function getUmtSource(pageUrl, referrer) { if (se) { return asUtm(se, ORGANIC, ORGANIC); } - let parsedUrl = parse(pageUrl); + let parsedUrl = utils.parseUrl(pageUrl); let [refHost, refPath] = getReferrer(referrer); if (refHost && refHost !== parsedUrl.hostname) { return asUtm(refHost, REFERRAL, REFERRAL, '', refPath); @@ -236,12 +237,12 @@ export function getUmtSource(pageUrl, referrer) { } function getReferrer(referrer) { - let ref = parse(referrer); + let ref = utils.parseUrl(referrer); return [ref.hostname, ref.pathname]; } function getUTM(pageUrl) { - let urlParameters = parse(pageUrl).search; + let urlParameters = utils.parseUrl(pageUrl).search; if (!urlParameters['utm_campaign'] || !urlParameters['utm_source']) { return; } diff --git a/modules/adkernelAdnBidAdapter.js b/modules/adkernelAdnBidAdapter.js index 25a7f2f4abc..483d6de52b9 100644 --- a/modules/adkernelAdnBidAdapter.js +++ b/modules/adkernelAdnBidAdapter.js @@ -1,7 +1,6 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {parse as parseUrl} from '../src/url.js'; const DEFAULT_ADKERNEL_DSP_DOMAIN = 'tag.adkernel.com'; const DEFAULT_MIMES = ['video/mp4', 'video/webm', 'application/x-shockwave-flash', 'application/javascript']; @@ -71,7 +70,7 @@ function buildRequestParams(tags, auctionId, transactionId, gdprConsent, uspCons } function buildSite(refInfo) { - let loc = parseUrl(refInfo.referer); + let loc = utils.parseUrl(refInfo.referer); let result = { page: `${loc.protocol}://${loc.hostname}${loc.pathname}`, secure: ~~(loc.protocol === 'https') diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 18c60c3e438..1a16801ba67 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -1,9 +1,8 @@ import * as utils from '../src/utils.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import find from 'core-js/library/fn/array/find.js'; import includes from 'core-js/library/fn/array/includes.js'; -import {parse as parseUrl} from '../src/url.js'; /* * In case you're AdKernel whitelable platform's client who needs branded adapter to @@ -15,7 +14,30 @@ import {parse as parseUrl} from '../src/url.js'; const VIDEO_TARGETING = ['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'linearity', 'boxingallowed', 'playbackmethod', 'delivery', 'pos', 'api', 'ext']; -const VERSION = '1.3'; +const VERSION = '1.4'; + +const NATIVE_MODEL = [ + {name: 'title', assetType: 'title'}, + {name: 'icon', assetType: 'img', type: 1}, + {name: 'image', assetType: 'img', type: 3}, + {name: 'body', assetType: 'data', type: 2}, + {name: 'body2', assetType: 'data', type: 10}, + {name: 'sponsoredBy', assetType: 'data', type: 1}, + {name: 'phone', assetType: 'data', type: 8}, + {name: 'address', assetType: 'data', type: 9}, + {name: 'price', assetType: 'data', type: 6}, + {name: 'salePrice', assetType: 'data', type: 7}, + {name: 'cta', assetType: 'data', type: 12}, + {name: 'rating', assetType: 'data', type: 3}, + {name: 'downloads', assetType: 'data', type: 5}, + {name: 'likes', assetType: 'data', type: 4}, + {name: 'displayUrl', assetType: 'data', type: 11} +]; + +const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => { + acc[val.name] = {id: idx, ...val}; + return acc; +}, {}); /** * Adapter for requesting bids from AdKernel white-label display platform @@ -24,7 +46,7 @@ export const spec = { code: 'adkernel', aliases: ['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon'], - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: function(bidRequest) { return 'params' in bidRequest && typeof bidRequest.params.host !== 'undefined' && @@ -32,7 +54,7 @@ export const spec = { !isNaN(Number(bidRequest.params.zoneId)) && bidRequest.params.zoneId > 0 && bidRequest.mediaTypes && - (bidRequest.mediaTypes.banner || bidRequest.mediaTypes.video); + (bidRequest.mediaTypes.banner || bidRequest.mediaTypes.video || (bidRequest.mediaTypes.native && validateNativeAdUnit(bidRequest.mediaTypes.native))); }, buildRequests: function(bidRequests, bidderRequest) { let impDispatch = dispatchImps(bidRequests, bidderRequest.refererInfo); @@ -76,12 +98,14 @@ export const spec = { prBid.width = rtbBid.w; prBid.height = rtbBid.h; prBid.ad = formatAdMarkup(rtbBid); - } - if ('video' in imp) { + } else if ('video' in imp) { prBid.mediaType = VIDEO; prBid.vastUrl = rtbBid.nurl; prBid.width = imp.video.w; prBid.height = imp.video.h; + } else if ('native' in imp) { + prBid.mediaType = NATIVE; + prBid.native = buildNativeAd(JSON.parse(rtbBid.adm)); } return prBid; }); @@ -138,6 +162,12 @@ function buildImp(bidRequest, secure) { .filter(key => includes(VIDEO_TARGETING, key)) .forEach(key => imp.video[key] = bidRequest.params.video[key]); } + } else if (utils.deepAccess(bidRequest, 'mediaTypes.native')) { + let nativeRequest = buildNativeRequest(bidRequest.mediaTypes.native); + imp.native = { + ver: '1.1', + request: JSON.stringify(nativeRequest) + } } if (secure) { imp.secure = 1; @@ -145,6 +175,51 @@ function buildImp(bidRequest, secure) { return imp; } +/** + * Builds native request from native adunit + */ +function buildNativeRequest(nativeReq) { + let request = {ver: '1.1', assets: []}; + for (let k of Object.keys(nativeReq)) { + let v = nativeReq[k]; + let desc = NATIVE_INDEX[k]; + if (desc === undefined) { + continue; + } + let assetRoot = { + id: desc.id, + required: ~~v.required, + }; + if (desc.assetType === 'img') { + assetRoot[desc.assetType] = buildImageAsset(desc, v); + } else if (desc.assetType === 'data') { + assetRoot.data = utils.cleanObj({type: desc.type, len: v.len}); + } else if (desc.assetType === 'title') { + assetRoot.title = {len: v.len || 90}; + } else { + return; + } + request.assets.push(assetRoot); + } + return request; +} + +/** + * Builds image asset request + */ +function buildImageAsset(desc, val) { + let img = { + type: desc.type + }; + if (val.sizes) { + [img.w, img.h] = val.sizes; + } else if (val.aspect_ratios) { + img.wmin = val.aspect_ratios[0].min_width; + img.hmin = val.aspect_ratios[0].min_height; + } + return utils.cleanObj(img); +} + /** * Builds complete rtb request * @param imps collection of impressions @@ -198,7 +273,7 @@ function getLanguage() { * Creates site description object */ function createSite(refInfo) { - let url = parseUrl(refInfo.referer); + let url = utils.parseUrl(refInfo.referer); let site = { 'domain': url.hostname, 'page': `${url.protocol}://${url.hostname}${url.pathname}` @@ -224,3 +299,51 @@ function formatAdMarkup(bid) { } return adm; } + +/** + * Basic validates to comply with platform requirements + */ +function validateNativeAdUnit(adUnit) { + return validateNativeImageSize(adUnit.image) && validateNativeImageSize(adUnit.icon) && + !utils.deepAccess(adUnit, 'privacyLink.required') && // not supported yet + !utils.deepAccess(adUnit, 'privacyIcon.required'); // not supported yet +} + +/** + * Validates image asset size definition + */ +function validateNativeImageSize(img) { + if (!img) { + return true; + } + if (img.sizes) { + return utils.isArrayOfNums(img.sizes, 2); + } + if (utils.isArray(img.aspect_ratios)) { + return img.aspect_ratios.length > 0 && img.aspect_ratios[0].min_height && img.aspect_ratios[0].min_width; + } + return true; +} + +/** + * Creates native ad for native 1.1 response + */ +function buildNativeAd(nativeResp) { + const {assets, link, imptrackers, jstracker, privacy} = nativeResp.native; + let nativeAd = { + clickUrl: link.url, + impressionTrackers: imptrackers, + javascriptTrackers: jstracker ? [jstracker] : undefined, + privacyLink: privacy, + }; + utils._each(assets, asset => { + let assetName = NATIVE_MODEL[asset.id].name; + let assetType = NATIVE_MODEL[asset.id].assetType; + nativeAd[assetName] = asset[assetType].text || asset[assetType].value || utils.cleanObj({ + url: asset[assetType].url, + width: asset[assetType].w, + height: asset[assetType].h + }); + }); + return utils.cleanObj(nativeAd); +} diff --git a/modules/adkernelBidAdapter.md b/modules/adkernelBidAdapter.md index f89fa5a26df..b3854d721ad 100644 --- a/modules/adkernelBidAdapter.md +++ b/modules/adkernelBidAdapter.md @@ -9,43 +9,71 @@ Maintainer: prebid-dev@adkernel.com # Description Connects to AdKernel whitelabel platform. -Banner and video formats are supported. +Banner, video and native formats are supported. # Test Parameters ``` - var adUnits = [{ - code: 'banner-ad-div', - mediaTypes: { - banner: { - sizes: [[300, 250]], // banner size - } - }, - bids: [ - { - bidder: 'adkernel', - params: { - zoneId: '30164', //required parameter - host: 'cpm.metaadserving.com' //required parameter - } - } - ] - }, { - code: 'video-ad-player', - mediaTypes: { - video: { - context: 'instream', // or 'outstream' - playerSize: [640, 480] // video player size - } - }, - bids: [ - { - bidder: 'adkernel', - params: { - zoneId: '30164', //required parameter - host: 'cpm.metaadserving.com' //required parameter - } - } - ] - }]; +var adUnits = [{ + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], // banner size + } + }, + bids: [{ + bidder: 'adkernel', + params: { + zoneId: '30164', //required parameter + host: 'cpm.metaadserving.com' //required parameter + } + }] +}, { + code: 'video-ad-player', + mediaTypes: { + video: { + context: 'instream', // or 'outstream' + playerSize: [640, 480] // video player size + } + }, + bids: [{ + bidder: 'adkernel', + params: { + zoneId: '30164', //required parameter + host: 'cpm.metaadserving.com' //required parameter + } + }] +}, { + code: 'native-ad', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + displayUrl: { + required: false + } + } + }, + bids: [{ + bidder: 'adkernel', + params: { + host: 'cpm.metaadserving.com', + zoneId: 30164 + } + }] +}]; ``` diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js new file mode 100644 index 00000000000..16594b3453c --- /dev/null +++ b/modules/adnuntiusBidAdapter.js @@ -0,0 +1,68 @@ + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +const BIDDER_CODE = 'adnuntius'; +const ENDPOINT_URL = 'https://delivery.adnuntius.com/i?tzo=-60&format=json'; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function (bid) { + return !!(bid.params.auId || (bid.params.member && bid.params.invCode)); + }, + + buildRequests: function (validBidRequests) { + const networks = {}; + const bidRequests = {}; + const requests = []; + + for (var i = 0; i < validBidRequests.length; i++) { + const bid = validBidRequests[i] + const network = bid.params.network || 'network'; + bidRequests[network] = bidRequests[network] || []; + bidRequests[network].push(bid); + + networks[network] = networks[network] || {}; + networks[network].adUnits = networks[network].adUnits || []; + networks[network].adUnits.push({ ...bid.params.targeting, auId: bid.params.auId }); + } + + const networkKeys = Object.keys(networks) + for (var j = 0; j < networkKeys.length; j++) { + const network = networkKeys[j]; + requests.push({ + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(networks[network]), + bid: bidRequests[network] + }); + } + + return requests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + const serverBody = serverResponse.body; + + for (var k = 0; k < serverBody.adUnits.length; k++) { + const adUnit = serverBody.adUnits[k] + if (adUnit.matchedAdCount > 0) { + const bid = adUnit.ads[0]; + bidResponses.push({ + requestId: bidRequest.bid[k].bidId, + cpm: (bid.cpm) ? bid.cpm.amount : 0, + width: Number(bid.creativeWidth), + height: Number(bid.creativeHeight), + creativeId: bid.creativeId, + currency: (bid.cpm) ? bid.cpm.currency : 'EUR', + netRevenue: false, + ttl: 360, + ad: adUnit.html + }); + } + } + return bidResponses; + }, + +} +registerBidder(spec); diff --git a/modules/adnuntiusBidAdapter.md b/modules/adnuntiusBidAdapter.md new file mode 100644 index 00000000000..aed12079856 --- /dev/null +++ b/modules/adnuntiusBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: Adnuntius Bidder Adapter +Module Type: Bidder Adapter +Maintainer: info@adnuntius.com +``` + +# Description + +Adnuntius Bidder Adapter for Prebid.js. +Only Banner format is supported. + +# Test Parameters +``` + var adUnits = [ + { + code: "test-div", + mediaTypes: { + banner: { + sizes: [[980, 360], [980, 300], [980, 240], [980, 120]] + } + }, + bids: [ + { + bidder: "adnuntius", + params: { + auId: "8b6bc", + network: "adnuntius", + } + }, + ] + }, + + ]; +``` diff --git a/modules/adotBidAdapter.js b/modules/adotBidAdapter.js new file mode 100644 index 00000000000..94672b20c0c --- /dev/null +++ b/modules/adotBidAdapter.js @@ -0,0 +1,607 @@ +import {Renderer} from '../src/Renderer.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {isStr, isArray, isNumber, isPlainObject, isBoolean, logError} from '../src/utils.js'; +import find from 'core-js/library/fn/array/find.js'; + +const ADAPTER_VERSION = 'v1.0.0'; +const BID_METHOD = 'POST'; +const BIDDER_URL = 'https://dsp.adotmob.com/headerbidding/bidrequest'; +const SUPPORTED_VIDEO_CONTEXTS = ['instream', 'outstream']; +const SUPPORTED_INSTREAM_CONTEXTS = ['pre-roll', 'mid-roll', 'post-roll']; +const NATIVE_PLACEMENTS = { + title: {id: 1, name: 'title'}, + icon: {id: 2, type: 1, name: 'img'}, + image: {id: 3, type: 3, name: 'img'}, + sponsoredBy: {id: 4, name: 'data', type: 1}, + body: {id: 5, name: 'data', type: 2}, + cta: {id: 6, type: 12, name: 'data'} +}; +const NATIVE_ID_MAPPING = {1: 'title', 2: 'icon', 3: 'image', 4: 'sponsoredBy', 5: 'body', 6: 'cta'}; +const SUPPORTED_VIDEO_MIMES = ['video/mp4']; +const DOMAIN_REGEX = new RegExp('//([^/]*)'); +const FIRST_PRICE = 1; +const BID_SUPPORTED_MEDIA_TYPES = ['banner', 'video', 'native']; +const TTL = 10; +const NET_REVENUE = true; +// eslint-disable-next-line no-template-curly-in-string +const AUCTION_PRICE = '${AUCTION_PRICE}'; +const OUTSTREAM_VIDEO_PLAYER_URL = 'https://adserver.adotmob.com/video/player.min.js'; + +function isNone(value) { + return (value === null) || (value === undefined); +} + +function groupBy(values, key) { + const groups = values.reduce((acc, value) => { + const groupId = value[key]; + + if (!acc[groupId]) acc[groupId] = []; + acc[groupId].push(value); + + return acc; + }, {}); + + return Object + .keys(groups) + .map(id => ({id, key, values: groups[id]})); +} + +function validateMediaTypes(mediaTypes, allowedMediaTypes) { + if (!isPlainObject(mediaTypes)) return false; + if (!allowedMediaTypes.some(mediaType => mediaType in mediaTypes)) return false; + + if (isBanner(mediaTypes)) { + if (!validateBanner(mediaTypes.banner)) return false; + } + + if (isVideo(mediaTypes)) { + if (!validateVideo(mediaTypes.video)) return false; + } + + return true; +} + +function isBanner(mediaTypes) { + return isPlainObject(mediaTypes) && isPlainObject(mediaTypes.banner); +} + +function isVideo(mediaTypes) { + return isPlainObject(mediaTypes) && isPlainObject(mediaTypes.video); +} + +function validateBanner(banner) { + return isPlainObject(banner) && + isArray(banner.sizes) && + (banner.sizes.length > 0) && + banner.sizes.every(validateMediaSizes); +} + +function validateVideo(video) { + if (!isPlainObject(video)) return false; + if (!isStr(video.context)) return false; + if (SUPPORTED_VIDEO_CONTEXTS.indexOf(video.context) === -1) return false; + + if (!video.playerSize) return true; + if (!isArray(video.playerSize)) return false; + + return video.playerSize.every(validateMediaSizes); +} + +function validateMediaSizes(mediaSize) { + return isArray(mediaSize) && + (mediaSize.length === 2) && + mediaSize.every(size => (isNumber(size) && size >= 0)); +} + +function validateParameters(parameters, adUnit) { + if (isVideo(adUnit.mediaTypes)) { + if (!isPlainObject(parameters)) return false; + if (!validateVideoParameters(parameters.video, adUnit)) return false; + } + + return true; +} + +function validateVideoParameters(video, adUnit) { + if (!video) return false; + + if (!isArray(video.mimes)) return false; + if (video.mimes.length === 0) return false; + if (!video.mimes.every(isStr)) return false; + + if (video.minDuration && !isNumber(video.minDuration)) return false; + if (video.maxDuration && !isNumber(video.maxDuration)) return false; + + if (!isArray(video.protocols)) return false; + if (video.protocols.length === 0) return false; + if (!video.protocols.every(isNumber)) return false; + + if (isInstream(adUnit.mediaTypes.video)) { + if (!video.instreamContext) return false; + if (SUPPORTED_INSTREAM_CONTEXTS.indexOf(video.instreamContext) === -1) return false; + } + + return true; +} + +function validateServerRequest(serverRequest) { + return isPlainObject(serverRequest) && + isPlainObject(serverRequest.data) && + isArray(serverRequest.data.imp) && + isPlainObject(serverRequest._adot_internal) && + isArray(serverRequest._adot_internal.impressions) +} + +function createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) { + return { + method: BID_METHOD, + url: BIDDER_URL, + data: generateBidRequestsFromAdUnits(adUnits, bidRequestId, adUnitContext), + _adot_internal: generateAdotInternal(adUnits) + } +} + +function generateAdotInternal(adUnits) { + const impressions = adUnits.reduce((acc, adUnit) => { + const {bidId, mediaTypes, adUnitCode, params} = adUnit; + const base = {bidId, adUnitCode, container: params.video && params.video.container}; + + const imps = Object + .keys(mediaTypes) + .reduce((acc, mediaType, index) => { + const data = mediaTypes[mediaType]; + const impressionId = `${bidId}_${index}`; + + if (mediaType !== 'banner') return acc.concat({...base, impressionId}); + + const bannerImps = data.sizes.map((item, i) => ({...base, impressionId: `${impressionId}_${i}`})); + + return acc.concat(bannerImps); + }, []); + + return acc.concat(imps); + }, []); + + return {impressions}; +} + +function generateBidRequestsFromAdUnits(adUnits, bidRequestId, adUnitContext) { + return { + id: bidRequestId, + imp: adUnits.reduce(generateImpressionsFromAdUnit, []), + site: generateSiteFromAdUnitContext(adUnitContext), + device: getDeviceInfo(), + user: getUserInfoFromAdUnitContext(adUnitContext), + regs: getRegulationFromAdUnitContext(adUnitContext), + at: FIRST_PRICE, + ext: generateBidRequestExtension() + }; +} + +function generateImpressionsFromAdUnit(acc, adUnit) { + const {bidId, mediaTypes, params} = adUnit; + const {placementId} = params; + const pmp = {}; + + if (placementId) pmp.deals = [{id: placementId}] + + const imps = Object + .keys(mediaTypes) + .reduce((acc, mediaType, index) => { + const data = mediaTypes[mediaType]; + const impId = `${bidId}_${index}`; + + if (mediaType === 'banner') return acc.concat(generateBannerFromAdUnit(impId, data, params)); + if (mediaType === 'video') return acc.concat({id: impId, video: generateVideoFromAdUnit(data, params), pmp}); + if (mediaType === 'native') return acc.concat({id: impId, native: generateNativeFromAdUnit(data, params), pmp}); + }, []); + + return acc.concat(imps); +} + +function isImpressionAVideo(impression) { + return isPlainObject(impression) && isPlainObject(impression.video); +} + +function generateBannerFromAdUnit(impId, data, params) { + const {position, placementId} = params; + const pos = position || 0; + const pmp = {}; + + if (placementId) pmp.deals = [{id: placementId}] + + return data.sizes.map(([w, h], index) => ({id: `${impId}_${index}`, banner: {format: [{w, h}], w, h, pos}, pmp})); +} + +function generateVideoFromAdUnit(data, params) { + const {playerSize} = data; + const hasPlayerSize = isArray(playerSize) && playerSize.length > 0; + const {position, video = {}} = params; + const {minDuration, maxDuration, protocols} = video; + + const size = {width: hasPlayerSize ? playerSize[0][0] : null, height: hasPlayerSize ? playerSize[0][1] : null}; + const duration = {min: isNumber(minDuration) ? minDuration : null, max: isNumber(maxDuration) ? maxDuration : null}; + + return { + mimes: SUPPORTED_VIDEO_MIMES, + w: size.width, + h: size.height, + startdelay: computeStartDelay(data, params), + minduration: duration.min, + maxduration: duration.max, + protocols, + pos: position || 0 + }; +} + +function isInstream(video) { + return isPlainObject(video) && (video.context === 'instream'); +} + +function isOutstream(video) { + return isPlainObject(video) && (video.startdelay === null) +} + +function computeStartDelay(data, params) { + if (isInstream(data)) { + if (params.video.instreamContext === 'pre-roll') return 0; + if (params.video.instreamContext === 'mid-roll') return -1; + if (params.video.instreamContext === 'post-roll') return -2; + } + + return null; +} + +function generateNativeFromAdUnit(data, params) { + const placements = NATIVE_PLACEMENTS; + const assets = Object + .keys(data) + .reduce((acc, placement) => { + const placementData = data[placement]; + const assetInfo = placements[placement]; + + if (!assetInfo) return acc; + + const {id, name, type} = assetInfo; + const {required, len, sizes} = placementData; + const wmin = sizes && sizes[0]; + const hmin = sizes && sizes[1]; + const content = {}; + + if (type) content.type = type; + if (len) content.len = len; + if (wmin) content.wmin = wmin; + if (hmin) content.hmin = hmin; + + acc.push({id, required, [name]: content}); + + return acc; + }, []); + + return { + request: JSON.stringify({assets}) + }; +} + +function generateSiteFromAdUnitContext(adUnitContext) { + if (!adUnitContext || !adUnitContext.refererInfo) return null; + + const domain = extractSiteDomainFromURL(adUnitContext.refererInfo.referer); + + if (!domain) return null; + + return { + page: adUnitContext.refererInfo.referer, + domain: domain, + name: domain + }; +} + +function extractSiteDomainFromURL(url) { + if (!url || !isStr(url)) return null; + + const domain = url.match(DOMAIN_REGEX); + + if (isArray(domain) && domain.length === 2) return domain[1]; + + return null; +} + +function getDeviceInfo() { + return {ua: navigator.userAgent, language: navigator.language}; +} + +function getUserInfoFromAdUnitContext(adUnitContext) { + if (!adUnitContext || !adUnitContext.gdprConsent) return null; + if (!isStr(adUnitContext.gdprConsent.consentString)) return null; + + return { + ext: { + consent: adUnitContext.gdprConsent.consentString + } + }; +} + +function getRegulationFromAdUnitContext(adUnitContext) { + if (!adUnitContext || !adUnitContext.gdprConsent) return null; + if (!isBoolean(adUnitContext.gdprConsent.gdprApplies)) return null; + + return { + ext: { + gdpr: adUnitContext.gdprConsent.gdprApplies + } + }; +} + +function generateBidRequestExtension() { + return { + adot: {adapter_version: ADAPTER_VERSION}, + should_use_gzip: true + }; +} + +function validateServerResponse(serverResponse) { + return isPlainObject(serverResponse) && + isPlainObject(serverResponse.body) && + isStr(serverResponse.body.cur) && + isArray(serverResponse.body.seatbid); +} + +function seatBidsToAds(seatBid, bidResponse, serverRequest) { + return seatBid.bid + .filter(bid => validateBids(bid, serverRequest)) + .map(bid => generateAdFromBid(bid, bidResponse, serverRequest)); +} + +function validateBids(bid, serverRequest) { + if (!isPlainObject(bid)) return false; + if (!isStr(bid.impid)) return false; + if (!isStr(bid.crid)) return false; + if (!isNumber(bid.price)) return false; + + if (!isPlainObject(bid.ext)) return false; + if (!isPlainObject(bid.ext.adot)) return false; + if (!isStr(bid.ext.adot.media_type)) return false; + if (BID_SUPPORTED_MEDIA_TYPES.indexOf(bid.ext.adot.media_type) === -1) return false; + + if (!bid.adm && !bid.nurl) return false; + if (bid.adm) { + if (!isStr(bid.adm)) return false; + if (bid.adm.indexOf(AUCTION_PRICE) === -1) return false; + } + if (bid.nurl) { + if (!isStr(bid.nurl)) return false; + if (bid.nurl.indexOf(AUCTION_PRICE) === -1) return false; + } + + if (isBidABanner(bid)) { + if (!isNumber(bid.h)) return false; + if (!isNumber(bid.w)) return false; + } + if (isBidAVideo(bid)) { + if (!(isNone(bid.h) || isNumber(bid.h))) return false; + if (!(isNone(bid.w) || isNumber(bid.w))) return false; + } + + const impression = getImpressionData(serverRequest, bid.impid); + + if (!isPlainObject(impression.openRTB)) return false; + if (!isPlainObject(impression.internal)) return false; + if (!isStr(impression.internal.adUnitCode)) return false; + + if (isBidABanner(bid)) { + if (!isPlainObject(impression.openRTB.banner)) return false; + } + if (isBidAVideo(bid)) { + if (!isPlainObject(impression.openRTB.video)) return false; + } + if (isBidANative(bid)) { + if (!isPlainObject(impression.openRTB.native) || !tryParse(bid.adm)) return false; + } + + return true; +} + +function isBidABanner(bid) { + return isPlainObject(bid) && + isPlainObject(bid.ext) && + isPlainObject(bid.ext.adot) && + bid.ext.adot.media_type === 'banner'; +} + +function isBidAVideo(bid) { + return isPlainObject(bid) && + isPlainObject(bid.ext) && + isPlainObject(bid.ext.adot) && + bid.ext.adot.media_type === 'video'; +} + +function isBidANative(bid) { + return isPlainObject(bid) && + isPlainObject(bid.ext) && + isPlainObject(bid.ext.adot) && + bid.ext.adot.media_type === 'native'; +} + +function getImpressionData(serverRequest, impressionId) { + const openRTBImpression = find(serverRequest.data.imp, imp => imp.id === impressionId); + const internalImpression = find(serverRequest._adot_internal.impressions, imp => imp.impressionId === impressionId); + + return { + id: impressionId, + openRTB: openRTBImpression || null, + internal: internalImpression || null + }; +} + +function generateAdFromBid(bid, bidResponse, serverRequest) { + const impressionData = getImpressionData(serverRequest, bid.impid); + const isVideo = isBidAVideo(bid); + const base = { + requestId: impressionData.internal.bidId, + cpm: bid.price, + currency: bidResponse.cur, + ttl: TTL, + creativeId: bid.crid, + netRevenue: NET_REVENUE, + mediaType: bid.ext.adot.media_type, + }; + + if (isBidANative(bid)) return {...base, native: formatNativeData(bid.adm)}; + + const size = getSizeFromBid(bid, impressionData); + const creative = getCreativeFromBid(bid, impressionData); + + return { + ...base, + height: size.height, + width: size.width, + ad: creative.markup, + adUrl: creative.markupUrl, + vastXml: isVideo && !isStr(creative.markupUrl) ? creative.markup : null, + vastUrl: isVideo && isStr(creative.markupUrl) ? creative.markupUrl : null, + renderer: creative.renderer + }; +} + +function formatNativeData(adm) { + const parsedAdm = tryParse(adm); + const {assets, link: {url, clicktrackers}, imptrackers, jstracker} = parsedAdm.native; + const placements = NATIVE_PLACEMENTS; + const placementIds = NATIVE_ID_MAPPING; + + return assets.reduce((acc, asset) => { + const placementName = placementIds[asset.id]; + const content = placementName && asset[placements[placementName].name]; + if (!content) return acc; + acc[placementName] = content.text || content.value || {url: content.url, width: content.w, height: content.h}; + return acc; + }, { + clickUrl: url, + clickTrackers: clicktrackers, + impressionTrackers: imptrackers, + javascriptTrackers: jstracker && [jstracker] + }); +} + +function getSizeFromBid(bid, impressionData) { + if (isNumber(bid.w) && isNumber(bid.h)) { + return { width: bid.w, height: bid.h }; + } + + if (isImpressionAVideo(impressionData.openRTB)) { + const { video } = impressionData.openRTB; + + if (isNumber(video.w) && isNumber(video.h)) { + return { width: video.w, height: video.h }; + } + } + + return { width: null, height: null }; +} + +function getCreativeFromBid(bid, impressionData) { + const shouldUseAdMarkup = !!bid.adm; + + return { + markup: shouldUseAdMarkup ? bid.adm : null, + markupUrl: !shouldUseAdMarkup ? bid.nurl : null, + renderer: getRendererFromBid(bid, impressionData) + }; +} + +function getRendererFromBid(bid, impressionData) { + const isOutstreamImpression = isBidAVideo(bid) && + isImpressionAVideo(impressionData.openRTB) && + isOutstream(impressionData.openRTB.video); + + return isOutstreamImpression + ? buildOutstreamRenderer(impressionData) + : null; +} + +function buildOutstreamRenderer(impressionData) { + const renderer = Renderer.install({ + url: OUTSTREAM_VIDEO_PLAYER_URL, + loaded: false, + adUnitCode: impressionData.internal.adUnitCode + }); + + renderer.setRender((ad) => { + ad.renderer.push(() => { + const container = impressionData.internal.container + ? document.querySelector(impressionData.internal.container) + : document.getElementById(impressionData.internal.adUnitCode); + + const player = new window.VASTPlayer(container); + + player.on('ready', () => { + player.adVolume = 0; + player.startAd(); + }); + + try { + isStr(ad.adUrl) + ? player.load(ad.adUrl) + : player.loadXml(ad.ad) + } catch (err) { + logError(err); + } + }); + }); + + return renderer; +} + +function tryParse(data) { + try { + return JSON.parse(data); + } catch (err) { + logError(err); + return null; + } +} + +const adotBidderSpec = { + code: 'adot', + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + isBidRequestValid(adUnit) { + const allowedBidderCodes = [this.code]; + + return isPlainObject(adUnit) && + allowedBidderCodes.indexOf(adUnit.bidder) !== -1 && + isStr(adUnit.adUnitCode) && + isStr(adUnit.bidderRequestId) && + isStr(adUnit.bidId) && + validateMediaTypes(adUnit.mediaTypes, this.supportedMediaTypes) && + validateParameters(adUnit.params, adUnit); + }, + buildRequests(adUnits, adUnitContext) { + if (!adUnits) return null; + + return groupBy(adUnits, 'bidderRequestId').map(group => { + const bidRequestId = group.id; + const adUnits = groupBy(group.values, 'bidId').map((group) => { + const length = group.values.length; + return length > 0 && group.values[length - 1] + }); + + return createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) + }); + }, + interpretResponse(serverResponse, serverRequest) { + if (!validateServerRequest(serverRequest)) return []; + if (!validateServerResponse(serverResponse)) return []; + + const bidResponse = serverResponse.body; + + return bidResponse.seatbid + .filter(seatBid => isPlainObject(seatBid) && isArray(seatBid.bid)) + .reduce((acc, seatBid) => acc.concat(seatBidsToAds(seatBid, bidResponse, serverRequest)), []); + } +}; + +registerBidder(adotBidderSpec); + +export {adotBidderSpec as spec}; diff --git a/modules/adotBidAdapter.md b/modules/adotBidAdapter.md new file mode 100644 index 00000000000..88c8fb0b936 --- /dev/null +++ b/modules/adotBidAdapter.md @@ -0,0 +1,218 @@ +# Adot Bidder Adapter + +Adot Bidder Adapter is a module that enables the communication between the Prebid.js library and Adot's DSP. + +## Overview + +- Module name: Adot Bidder Adapter +- Module type: Bidder Adapter +- Maintainer: `maxime.lequain@we-are-adot.com` +- Supported media types: `banner`, `video`, `native` + +## Example ad units + +### Banner ad unit + +Adot Bidder Adapter accepts banner ad units using the following ad unit format: + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + banner: { + // Dimensions supported by the banner ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'adot', + params: {} + }] +} +``` + +### Video ad unit + +#### Outstream video ad unit + +Adot Bidder Adapter accepts outstream video ad units using the following ad unit format: + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + video: { + // Video context. Must be 'outstream'. + context: 'outstream', + // Video dimensions supported by the video ad unit. + // Each ad unit size is formatted as follows: [width, height]. + playerSize: [[300, 250]] + } + }, + bids: [{ + bidder: 'adot', + params: { + video: { + // Content MIME types supported by the ad unit. + mimes: ['video/mp4'], + // Minimum accepted video ad duration (in seconds). + minDuration: 5, + // Maximum accepted video ad duration (in seconds). + maxDuration: 35, + // Video protocols supported by the ad unit (see the OpenRTB 2.5 specifications, + // section 5.8). + protocols: [2, 3] + } + } + }] +} +``` + +#### Instream video ad unit + +Adot Bidder Adapter accepts instream video ad units using the following ad unit format: + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + video: { + // Video context. Must be 'instream'. + context: 'instream', + // Video dimensions supported by the video ad unit. + // Each ad unit size is formatted as follows: [width, height]. + playerSize: [[300, 250]] + } + }, + bids: [{ + bidder: 'adot', + params: { + video: { + // Content MIME types supported by the ad unit. + mimes: ['video/mp4'], + // Minimum accepted video ad duration (in seconds). + minDuration: 5, + // Maximum accepted video ad duration (in seconds). + maxDuration: 35, + // Video protocols supported by the ad unit (see the OpenRTB 2.5 specifications, + // section 5.8). + protocols: [2, 3], + // Instream video context. Must be either 'pre-roll', 'mid-roll' or 'post-roll'. + instreamContext: 'pre-roll' + } + } + }] +} +``` +### Native ad unit + +Adot Bidder Adapter accepts native ad units using the following ad unit format: + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + native: { + native: { + image: { + // Field required status + required: false, + // Image dimensions supported by the native ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [100, 50] + }, + title: { + // Field required status + required: false, + // Maximum length of the title + len: 140 + }, + sponsoredBy: { + // Field required status + required: false + }, + clickUrl: { + // Field required status + required: false + }, + body: { + // Field required status + required: false + }, + icon: { + // Field required status + required: false, + // Icon dimensions supported by the native ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [50, 50] + } + } + } + }, + bids: [{ + bidder: 'adot', + params: {} + }] +} +``` + +## Bidder parameters + +### Position + +You can use the `position` to specify the position of the ad as a relative measure of visibility or prominence. + +#### Accepted values + +|Value|Description | +|-----|----------------| +|0 | Unknown | +|1 | Above the fold | +|3 | Below the fold | + +Note that the position will default to `0` if the field is missing. + +#### Example + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + banner: { + // Dimensions supported by the banner ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'adot', + params: { + position: 0 + } + }] +} +``` + +### PlacementId + +#### Example + +```javascript +const adUnit = { + code: 'test-div', + mediaTypes: { + banner: { + // Dimensions supported by the banner ad unit. + // Each ad unit size is formatted as follows: [width, height]. + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'adot', + params: { + placementId: 'eae245d' + } + }] +} +``` diff --git a/modules/adspendBidAdapter.js b/modules/adspendBidAdapter.js index 3342a1934cc..9fe70885eeb 100644 --- a/modules/adspendBidAdapter.js +++ b/modules/adspendBidAdapter.js @@ -3,7 +3,9 @@ import { ajax } from '../src/ajax.js' import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; +const storage = getStorageManager(); const BIDDER_CODE = 'adspend'; const BID_URL = 'https://rtb.com.ru/headerbidding-bid'; const SYNC_URL = 'https://rtb.com.ru/headerbidding-sync?uid={UUID}'; @@ -40,7 +42,7 @@ export const spec = { bid.params.bidfloor && bid.crumbs.pubcid && utils.checkCookieSupport() && - utils.cookiesAreEnabled() + storage.cookiesAreEnabled() ); }, @@ -145,11 +147,11 @@ export const spec = { } const getUserID = () => { - const i = utils.getCookie(COOKIE_NAME); + const i = storage.getCookie(COOKIE_NAME); if (i === null) { const uuid = utils.generateUUID(); - utils.setCookie(COOKIE_NAME, uuid); + storage.setCookie(COOKIE_NAME, uuid); return uuid; } return i; diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index 5749866b65b..82601117017 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -13,6 +13,7 @@ const syncsCache = {}; export const spec = { code: BIDDER_CODE, + gvlid: 410, aliases: ['onefiftytwomedia', 'selectmedia'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { diff --git a/modules/advangelistsBidAdapter.js b/modules/advangelistsBidAdapter.js old mode 100644 new mode 100755 index b3064568d3a..64e4c58653b --- a/modules/advangelistsBidAdapter.js +++ b/modules/advangelistsBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js'; -import { parse as parseUrl } from '../src/url.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; @@ -293,7 +292,7 @@ function createVideoRequestData(bid, bidderRequest) { function getTopWindowLocation(bidderRequest) { let url = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer; - return parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); + return utils.parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); } function createBannerRequestData(bid, bidderRequest) { diff --git a/modules/advangelistsBidAdapter.md b/modules/advangelistsBidAdapter.md old mode 100644 new mode 100755 diff --git a/modules/adxcgAnalyticsAdapter.js b/modules/adxcgAnalyticsAdapter.js index 01baa0ebd46..9f514c545a1 100644 --- a/modules/adxcgAnalyticsAdapter.js +++ b/modules/adxcgAnalyticsAdapter.js @@ -2,7 +2,6 @@ import { ajax } from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import * as url from '../src/url.js'; import * as utils from '../src/utils.js'; /** @@ -113,7 +112,7 @@ function mapBidWon (bidResponse) { } function send (data) { - let adxcgAnalyticsRequestUrl = url.format({ + let adxcgAnalyticsRequestUrl = utils.buildUrl({ protocol: 'https', hostname: adxcgAnalyticsAdapter.context.host, pathname: '/pbrx/v2', diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index 37690c66d58..cc76731836b 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -1,6 +1,5 @@ import { config } from '../src/config.js' import * as utils from '../src/utils.js' -import * as url from '../src/url.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js' import includes from 'core-js/library/fn/array/includes.js' @@ -179,7 +178,7 @@ export const spec = { beaconParams.idl_env = validBidRequests[0].userId.idl_env; } - let adxcgRequestUrl = url.format({ + let adxcgRequestUrl = utils.buildUrl({ protocol: 'https', hostname: 'hbps.adxcg.net', pathname: '/get/adi', diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 7e591b3901c..9391a3fb94f 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,6 +1,4 @@ import * as utils from '../src/utils.js'; -import { format } from '../src/url.js'; -// import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import find from 'core-js/library/fn/array/find.js'; @@ -145,7 +143,7 @@ function getPageRefreshed() { /* Create endpoint url */ function createEndpoint(bidRequests, bidderRequest) { let host = getHostname(bidRequests); - return format({ + return utils.buildUrl({ protocol: 'https', host: `${DEFAULT_DC}${host}.omnitagjs.com`, pathname: '/hb-api/prebid/v1', diff --git a/modules/aolBidAdapter.js b/modules/aolBidAdapter.js index d5fb2970628..d7ff7453870 100644 --- a/modules/aolBidAdapter.js +++ b/modules/aolBidAdapter.js @@ -100,6 +100,7 @@ function resolveEndpointCode(bid) { export const spec = { code: AOL_BIDDERS_CODES.AOL, + gvlid: 25, aliases: [AOL_BIDDERS_CODES.ONEMOBILE, AOL_BIDDERS_CODES.ONEDISPLAY], supportedMediaTypes: [BANNER], isBidRequestValid(bid) { diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 9f107bf54ec..53cfc85389d 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -7,6 +7,7 @@ import { auctionManager } from '../src/auctionManager.js'; import find from 'core-js/library/fn/array/find.js'; import includes from 'core-js/library/fn/array/includes.js'; import { OUTSTREAM, INSTREAM } from '../src/video.js'; +import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'appnexus'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -38,10 +39,13 @@ const mappingFileUrl = 'https://acdn.adnxs.com/prebid/appnexus-mapping/mappings. const SCRIPT_TAG_START = ' includes(USER_PARAMS, param)) - .forEach(param => userObj[param] = userObjBid.params.user[param]); + .forEach((param) => { + let uparam = utils.convertCamelToUnderscore(param); + userObj[uparam] = userObjBid.params.user[param] + }); } const appDeviceObjBid = find(bidRequests, hasAppDeviceInfo); @@ -93,7 +100,7 @@ export const spec = { let debugObj = {}; let debugObjParams = {}; const debugCookieName = 'apn_prebid_debug'; - const debugCookie = utils.getCookie(debugCookieName) || null; + const debugCookie = storage.getCookie(debugCookieName) || null; if (debugCookie) { try { @@ -404,8 +411,24 @@ function getViewabilityScriptUrlFromPayload(viewJsPayload) { return jsTrackerSrc; } +function hasPurpose1Consent(bidderRequest) { + let result = true; + if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { + result = !!(utils.deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); + } + } + return result; +} + function formatRequest(payload, bidderRequest) { let request = []; + let options = {}; + if (!hasPurpose1Consent(bidderRequest)) { + options = { + withCredentials: false + } + } if (payload.tags.length > MAX_IMPS_PER_REQUEST) { const clonedPayload = utils.deepClone(payload); @@ -417,7 +440,8 @@ function formatRequest(payload, bidderRequest) { method: 'POST', url: URL, data: payloadString, - bidderRequest + bidderRequest, + options }); }); } else { @@ -426,7 +450,8 @@ function formatRequest(payload, bidderRequest) { method: 'POST', url: URL, data: payloadString, - bidderRequest + bidderRequest, + options }; } @@ -502,7 +527,7 @@ function newBid(serverBid, rtbBid, bidderRequest) { case ADPOD: const iabSubCatId = getIabSubCategory(bidRequest.bidder, rtbBid.brand_category_id); bid.meta = Object.assign({}, bid.meta, { iabSubCatId }); - const dealTier = rtbBid.rtb.dealPriority; + const dealTier = rtbBid.deal_priority; bid.video = { context: ADPOD, durationSeconds: Math.floor(rtbBid.rtb.video.duration_ms / 1000), @@ -659,6 +684,11 @@ function bidToTag(bid) { const videoMediaType = utils.deepAccess(bid, `mediaTypes.${VIDEO}`); const context = utils.deepAccess(bid, 'mediaTypes.video.context'); + if (videoMediaType && context === 'adpod') { + tag.hb_source = 7; + } else { + tag.hb_source = 1; + } if (bid.mediaType === VIDEO || videoMediaType) { tag.ad_types.push(VIDEO); } @@ -836,8 +866,20 @@ function buildNativeRequest(params) { return request; } +/** + * This function hides google div container for outstream bids to remove unwanted space on page. Appnexus renderer creates a new iframe outside of google iframe to render the outstream creative. + * @param {string} elementId element id + */ +function hidedfpContainer(elementId) { + var el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']"); + if (el[0]) { + el[0].style.setProperty('display', 'none'); + } +} + function outstreamRender(bid) { // push to render queue because ANOutstreamVideo may not be loaded yet + hidedfpContainer(bid.adUnitCode); bid.renderer.push(() => { window.ANOutstreamVideo.renderAd({ tagId: bid.adResponse.tag_id, diff --git a/modules/atsAnalyticsAdapter.js b/modules/atsAnalyticsAdapter.js index f15e5aa2b1f..7bf2ca75c17 100644 --- a/modules/atsAnalyticsAdapter.js +++ b/modules/atsAnalyticsAdapter.js @@ -21,7 +21,7 @@ function bidRequestedHandler(args) { user_browser: (browserIsFirefox() || browserIsEdge() || browserIsChrome() || browserIsSafari()), user_platform: navigator.platform, auction_start: new Date(args.auctionStart).toJSON(), - domain: args.refererInfo.referer, + domain: window.location.hostname, pid: atsAnalyticsAdapter.context.pid, }; }); diff --git a/modules/audienceNetworkBidAdapter.js b/modules/audienceNetworkBidAdapter.js index 54a50af4269..2d7b7eae0dc 100644 --- a/modules/audienceNetworkBidAdapter.js +++ b/modules/audienceNetworkBidAdapter.js @@ -2,8 +2,7 @@ * @file AudienceNetwork adapter. */ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { formatQS } from '../src/url.js'; -import { generateUUID, deepAccess, convertTypes } from '../src/utils.js'; +import { generateUUID, deepAccess, convertTypes, formatQS } from '../src/utils.js'; import findIndex from 'core-js/library/fn/array/find-index.js'; import includes from 'core-js/library/fn/array/includes.js'; diff --git a/modules/audigentRtdProvider.js b/modules/audigentRtdProvider.js index 8b45287bddc..0f32c84962f 100644 --- a/modules/audigentRtdProvider.js +++ b/modules/audigentRtdProvider.js @@ -20,6 +20,9 @@ import {getGlobal} from '../src/prebidGlobal.js'; import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajax} from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -33,12 +36,12 @@ let _moduleParams = {}; */ export function setData(data) { - utils.setDataInLocalStorage('__adgntseg', JSON.stringify(data)); + storage.setDataInLocalStorage('__adgntseg', JSON.stringify(data)); } function getSegments(adUnits, onDone) { try { - let jsonData = utils.getDataFromLocalStorage('__adgntseg'); + let jsonData = storage.getDataFromLocalStorage('__adgntseg'); if (jsonData) { let data = JSON.parse(jsonData); if (data.audigent_segments) { diff --git a/modules/automatadBidAdapter.js b/modules/automatadBidAdapter.js new file mode 100644 index 00000000000..4a9e6b6a1e4 --- /dev/null +++ b/modules/automatadBidAdapter.js @@ -0,0 +1,123 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js' +import * as utils from '../src/utils.js' +import {BANNER} from '../src/mediaTypes.js' +import {ajax} from '../src/ajax.js' + +const BIDDER = 'automatad' + +const ENDPOINT_URL = 'https://rtb2.automatad.com/ortb2' + +const DEFAULT_BID_TTL = 30 +const DEFAULT_CURRENCY = 'USD' +const DEFAULT_NET_REVENUE = true + +export const spec = { + code: BIDDER, + aliases: ['atd'], + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + // will receive request bid. check if have necessary params for bidding + return (bid && bid.hasOwnProperty('params') && bid.params.hasOwnProperty('siteId') && bid.params.hasOwnProperty('placementId') && bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty('banner')) + }, + + buildRequests: function (validBidRequests, bidderRequest) { + if (!validBidRequests || !bidderRequest) { + return + } + + const siteId = validBidRequests[0].params.siteId + const placementId = validBidRequests[0].params.placementId + + const impressions = validBidRequests.map(bidRequest => ({ + id: bidRequest.bidId, + banner: { + format: bidRequest.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1], + })) + }, + })) + + // params from bid request + const openrtbRequest = { + id: validBidRequests[0].auctionId, + imp: impressions, + site: { + id: siteId, + placement: placementId, + domain: window.location.hostname, + page: window.location.href, + ref: bidderRequest.refererInfo ? bidderRequest.refererInfo.referer || null : null, + }, + } + + const payloadString = JSON.stringify(openrtbRequest) + return { + method: 'POST', + url: ENDPOINT_URL + '/resp', + data: payloadString, + options: { + contentType: 'application/json', + withCredentials: false, + crossOrigin: true, + }, + } + }, + + interpretResponse: function (serverResponse, request) { + const bidResponses = [] + const response = (serverResponse || {}).body + + if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) { + response.seatbid[0].bid.forEach(bid => { + bidResponses.push({ + requestId: bid.impid, + cpm: bid.price, + ad: bid.adm, + adDomain: bid.adomain[0], + currency: DEFAULT_CURRENCY, + ttl: DEFAULT_BID_TTL, + creativeId: bid.crid, + width: bid.w, + height: bid.h, + netRevenue: DEFAULT_NET_REVENUE, + nurl: bid.nurl, + }) + }) + } else { + utils.logInfo('automatad :: no valid responses to interpret') + } + + return bidResponses + }, + getUserSyncs: function(syncOptions, serverResponse) { + return [{ + type: 'iframe', + url: 'https://rtb2.automatad.com/ortb2/async_usersync' + }] + }, + onBidWon: function(bid) { + if (!bid.nurl) { return } + const winUrl = bid.nurl.replace( + /\$\{AUCTION_PRICE\}/, + bid.cpm + ).replace( + /\$\{AUCTION_IMP_ID\}/, + bid.requestId + ).replace( + /\$\{AUCTION_CURRENCY\}/, + bid.currency + ).replace( + /\$\{AUCTION_ID\}/, + bid.auctionId + ) + spec.ajaxCall(winUrl, null) + return true + }, + ajaxCall: function(endpoint, data) { + ajax(endpoint, data) + }, + +} +registerBidder(spec) diff --git a/modules/automatadBidAdapter.md b/modules/automatadBidAdapter.md new file mode 100644 index 00000000000..56a4b53c067 --- /dev/null +++ b/modules/automatadBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: Automatad Bid Adapter +Module Type: Bidder Adapter +Maintainer: tech@automatad.com +``` + +# Description + +Connects to automatad exchange for bids. + +automatad bid adapter supports Banner ads. + +# Test Parameters +``` +var adUnits = [ + { + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'automatad', + params: { + siteId: 'someValue', + placementId: 'someValue' + } + }] + } +]; +``` diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index bbb4ae35729..9b6c431fdd7 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js'; -import { parse as parseUrl } from '../src/url.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; @@ -247,7 +246,7 @@ function isBannerBidValid(bid) { function getTopWindowLocation(bidderRequest) { let url = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer; - return parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); + return utils.parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); } function getTopWindowReferrer() { diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index 5ad78a6bdc6..521c6cb6f98 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -1,5 +1,5 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; - +import { getAdUnitSizes, parseSizesInput } from '../src/utils.js'; const BIDDER_CODE = 'between'; export const spec = { @@ -13,7 +13,7 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function(bid) { - return !!(bid.params.w && bid.params.h && bid.params.s); + return Boolean(bid.params.s); }, /** * Make a server request from the list of BidRequests. @@ -27,13 +27,12 @@ export const spec = { validBidRequests.forEach(i => { let params = { + sizes: parseSizesInput(getAdUnitSizes(i)), jst: 'hb', ord: Math.random() * 10000000000000000, tz: getTz(), fl: getFl(), rr: getRr(), - w: i.params.w, - h: i.params.h, s: i.params.s, bidid: i.bidId, transactionid: i.transactionId, diff --git a/modules/betweenBidAdapter.md b/modules/betweenBidAdapter.md index 426d0aa2ed7..4aadd31d3a3 100644 --- a/modules/betweenBidAdapter.md +++ b/modules/betweenBidAdapter.md @@ -15,14 +15,17 @@ About us : http://betweendigital.com ```javascript var adUnits = [ { - code: 'test-div', + code: 'ad_slot', + mediaTypes: { + banner: { + sizes: [[970, 250], [240, 400], [728, 90]] + } + }, bids: [ { bidder: "between", params: { - w: 200, - h: 400, - s: 111 + s: 122938 } } ] @@ -45,14 +48,16 @@ Where: var PREBID_TIMEOUT = 700; var adUnits = [{ - code: 'example', - sizes: [[300, 250], [200,400]], + code: 'example', + mediaTypes: { + banner: { + sizes: [[970, 250], [240, 400], [728, 90]] + } + }, bids: [{ bidder: 'between', params: { - w: 240, - h: 400, - s: 8 + s: 809832 } }] diff --git a/modules/bidfluenceBidAdapter.js b/modules/bidfluenceBidAdapter.js index 6857a0f833f..f8a1f9ac92f 100644 --- a/modules/bidfluenceBidAdapter.js +++ b/modules/bidfluenceBidAdapter.js @@ -1,5 +1,8 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'bidfluence'; function stdTimezoneOffset(t) { @@ -46,7 +49,7 @@ export const spec = { var payload = { v: '2.0', azr: true, - ck: utils.cookiesAreEnabled(), + ck: storage.cookiesAreEnabled(), re: refInfo ? refInfo.referer : '', st: refInfo ? refInfo.stack : [], tz: getBdfTz(new Date()), diff --git a/modules/bidlabBidAdapter.js b/modules/bidlabBidAdapter.js new file mode 100644 index 00000000000..8f501505a6d --- /dev/null +++ b/modules/bidlabBidAdapter.js @@ -0,0 +1,112 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const BIDDER_CODE = 'bidlab'; +const AD_URL = 'https://service.bidlab.ai/?c=o&m=multi'; +const URL_SYNC = 'https://service.bidlab.ai/?c=o&m=sync'; +const NO_SYNC = true; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency) { + return false; + } + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl); + case NATIVE: + return Boolean(bid.native && bid.native.title && bid.native.image && bid.native.impressionTrackers); + default: + return false; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + noSync: NO_SYNC, + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(parseInt(bid.params.placementId))); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + let winTop = window; + let location; + try { + location = new URL(bidderRequest.refererInfo.referer) + winTop = window.top; + } catch (e) { + location = winTop.location; + utils.logMessage(e); + }; + let placements = []; + let request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + 'secure': 1, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent + } + } + const len = validBidRequests.length; + + for (let i = 0; i < len; i++) { + let bid = validBidRequests[i]; + let traff = bid.params.traffic || BANNER + + placements.push({ + placementId: bid.params.placementId, + bidId: bid.bidId, + sizes: bid.mediaTypes && bid.mediaTypes[traff] && bid.mediaTypes[traff].sizes ? bid.mediaTypes[traff].sizes : [], + traffic: traff + }); + if (bid.schain) { + placements.schain = bid.schain; + } + } + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses) => { + if (NO_SYNC) { + return false + } else { + return [{ + type: 'image', + url: URL_SYNC + }]; + } + } + +}; + +registerBidder(spec); diff --git a/modules/bidlabBidAdapter.md b/modules/bidlabBidAdapter.md new file mode 100644 index 00000000000..3e5fe3128ed --- /dev/null +++ b/modules/bidlabBidAdapter.md @@ -0,0 +1,53 @@ +# Overview + +``` +Module Name: bidlab Bidder Adapter +Module Type: bidlab Bidder Adapter +``` + +# Description + +Module that connects to bidlab demand sources + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'placementId_0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'bidlab', + params: { + placementId: 0, + traffic: 'banner' + } + } + ] + }, + // Will return test vast xml. All video params are stored under placement in publishers UI + { + code: 'placementId_0', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'bidlab', + params: { + placementId: 0, + traffic: 'video' + } + } + ] + } + ]; +``` diff --git a/modules/brightcomBidAdapter.js b/modules/brightcomBidAdapter.js index 0a418406d4f..a4b013a2fe2 100644 --- a/modules/brightcomBidAdapter.js +++ b/modules/brightcomBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js'; -import * as url from '../src/url.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; @@ -57,7 +56,7 @@ function buildRequests(bidReqs, bidderRequest) { id: utils.getUniqueIdentifierStr(), imp: brightcomImps, site: { - domain: url.parse(referrer).host, + domain: utils.parseUrl(referrer).host, page: referrer, publisher: { id: publisherId diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index d3fe3176cda..3765b6603af 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -22,6 +22,9 @@ import * as utils from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajaxBuilder} from '../src/ajax.js'; import {loadExternalScript} from '../src/adloader.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -62,7 +65,7 @@ function collectData() { const doc = win.document; let browsiData = null; try { - browsiData = utils.getDataFromLocalStorage('__brtd'); + browsiData = storage.getDataFromLocalStorage('__brtd'); } catch (e) { utils.logError('unable to parse __brtd'); } diff --git a/modules/categoryTranslation.js b/modules/categoryTranslation.js index 722fc59c921..5342220d13a 100644 --- a/modules/categoryTranslation.js +++ b/modules/categoryTranslation.js @@ -14,9 +14,11 @@ import { config } from '../src/config.js'; import { setupBeforeHookFnOnce, hook } from '../src/hook.js'; import { ajax } from '../src/ajax.js'; -import { timestamp, logError, setDataInLocalStorage, getDataFromLocalStorage } from '../src/utils.js'; +import { timestamp, logError } from '../src/utils.js'; import { addBidResponse } from '../src/auction.js'; +import { getCoreStorageManager } from '../src/storageManager.js'; +export const storage = getCoreStorageManager('categoryTranslation'); const DEFAULT_TRANSLATION_FILE_URL = 'https://cdn.jsdelivr.net/gh/prebid/category-mapping-file@1/freewheel-mapping.json'; const DEFAULT_IAB_TO_FW_MAPPING_KEY = 'iabToFwMappingkey'; const DEFAULT_IAB_TO_FW_MAPPING_KEY_PUB = 'iabToFwMappingkeyPub'; @@ -43,7 +45,7 @@ export function getAdserverCategoryHook(fn, adUnitCode, bid) { let localStorageKey = (config.getConfig('brandCategoryTranslation.translationFile')) ? DEFAULT_IAB_TO_FW_MAPPING_KEY_PUB : DEFAULT_IAB_TO_FW_MAPPING_KEY; if (bid.meta && !bid.meta.adServerCatId) { - let mapping = getDataFromLocalStorage(localStorageKey); + let mapping = storage.getDataFromLocalStorage(localStorageKey); if (mapping) { try { mapping = JSON.parse(mapping); @@ -65,7 +67,7 @@ export function getAdserverCategoryHook(fn, adUnitCode, bid) { export function initTranslation(url, localStorageKey) { setupBeforeHookFnOnce(addBidResponse, getAdserverCategoryHook, 50); - let mappingData = getDataFromLocalStorage(localStorageKey); + let mappingData = storage.getDataFromLocalStorage(localStorageKey); if (!mappingData || timestamp() < mappingData.lastUpdated + refreshInDays * 24 * 60 * 60 * 1000) { ajax(url, { @@ -73,7 +75,7 @@ export function initTranslation(url, localStorageKey) { try { response = JSON.parse(response); response['lastUpdated'] = timestamp(); - setDataInLocalStorage(localStorageKey, JSON.stringify(response)); + storage.setDataInLocalStorage(localStorageKey, JSON.stringify(response)); } catch (error) { logError('Failed to parse translation mapping file'); } diff --git a/modules/ccxBidAdapter.js b/modules/ccxBidAdapter.js index 5532ec6ca1e..ee15d6bb3ec 100644 --- a/modules/ccxBidAdapter.js +++ b/modules/ccxBidAdapter.js @@ -1,6 +1,9 @@ import * as utils from '../src/utils.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { config } from '../src/config.js' +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'ccx' const BID_URL = 'https://delivery.clickonometrics.pl/ortb/prebid/bid' const SUPPORTED_VIDEO_PROTOCOLS = [2, 3, 5, 6] @@ -170,7 +173,7 @@ export const spec = { requestBody.site = _getSiteObj(bidderRequest) requestBody.device = _getDeviceObj() requestBody.id = bidderRequest.bids[0].auctionId - requestBody.ext = {'ce': (utils.cookiesAreEnabled() ? 1 : 0)} + requestBody.ext = {'ce': (storage.cookiesAreEnabled() ? 1 : 0)} // Attaching GDPR Consent Params if (bidderRequest && bidderRequest.gdprConsent) { diff --git a/modules/cedatoBidAdapter.js b/modules/cedatoBidAdapter.js index 72377b2dfdb..ab381698f01 100644 --- a/modules/cedatoBidAdapter.js +++ b/modules/cedatoBidAdapter.js @@ -1,13 +1,15 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'cedato'; const BID_URL = 'https://h.cedatoplayer.com/hb'; const SYNC_URL = 'https://h.cedatoplayer.com/hb_usync'; const TTL = 10000; const CURRENCY = 'USD'; -const FIRST_PRICE = 1; const NET_REVENUE = true; export const spec = { @@ -20,16 +22,13 @@ export const spec = { bid.params && bid.params.player_id && utils.checkCookieSupport() && - utils.cookiesAreEnabled() + storage.cookiesAreEnabled() ); }, buildRequests: function(bidRequests, bidderRequest) { - const req = bidRequests[Math.floor(Math.random() * bidRequests.length)]; - const params = req.params; - const at = FIRST_PRICE; - const site = { id: params.player_id, domain: document.domain }; - const device = { ua: navigator.userAgent }; + const site = { domain: document.domain }; + const device = { ua: navigator.userAgent, w: screen.width, h: screen.height }; const currency = CURRENCY; const tmax = bidderRequest.timeout; const auctionId = bidderRequest.auctionId; @@ -39,7 +38,7 @@ export const spec = { const imp = bidRequests.map(req => { const banner = getMediaType(req, 'banner'); const video = getMediaType(req, 'video'); - const bidfloor = params.bidfloor; + const params = req.params; const bidId = req.bidId; const adUnitCode = req.adUnitCode; const bidRequestsCount = req.bidRequestsCount; @@ -51,16 +50,15 @@ export const spec = { banner, video, adUnitCode, - bidfloor, bidRequestsCount, bidderWinsCount, - transactionId + transactionId, + params }; }); const payload = { version: '$prebid.version$', - at, site, device, imp, @@ -83,12 +81,7 @@ export const spec = { } } - return { - method: 'POST', - url: params.bid_url || BID_URL, - data: JSON.stringify(payload), - bidderRequest - }; + return formatRequest(payload, bidderRequest); }, interpretResponse: function(resp, {bidderRequest}) { @@ -186,6 +179,33 @@ function newBid(serverBid, bidderRequest) { return bid; } +function formatRequest(payload, bidderRequest) { + const payloadByUrl = {}; + const requests = []; + + payload.imp.forEach(imp => { + const url = imp.params.bid_url || BID_URL; + if (!payloadByUrl[url]) { + payloadByUrl[url] = { + ...payload, + imp: [] + }; + } + payloadByUrl[url].imp.push(imp); + }); + + for (const url in payloadByUrl) { + requests.push({ + url, + method: 'POST', + data: JSON.stringify(payloadByUrl[url]), + bidderRequest + }); + } + + return requests; +} + const getSync = (type, gdprConsent, uspConsent = '') => { const syncUrl = SYNC_URL; let params = '?type=' + type + '&us_privacy=' + uspConsent; diff --git a/modules/clickforceBidAdapter.js b/modules/clickforceBidAdapter.js index eb73033e2d0..20408fe9177 100644 --- a/modules/clickforceBidAdapter.js +++ b/modules/clickforceBidAdapter.js @@ -2,7 +2,7 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; const BIDDER_CODE = 'clickforce'; -const ENDPOINT_URL = 'https://ad.doublemax.net/adserver/prebid.json?cb=' + new Date().getTime() + '&hb=1&ver=1.21'; +const ENDPOINT_URL = 'https://ad.holmesmind.com/adserver/prebid.json?cb=' + new Date().getTime() + '&hb=1&ver=1.21'; export const spec = { code: BIDDER_CODE, @@ -112,12 +112,12 @@ export const spec = { if (syncOptions.iframeEnabled) { return [{ type: 'iframe', - url: 'https://cdn.doublemax.net/js/capmapping.htm' + url: 'https://cdn.holmesmind.com/js/capmapping.htm' }] } else if (syncOptions.pixelEnabled) { return [{ type: 'image', - url: 'https://c.doublemax.net/cm' + url: 'https://c.holmesmind.com/cm' }] } } diff --git a/modules/colombiaBidAdapter.js b/modules/colombiaBidAdapter.js new file mode 100644 index 00000000000..59257babdbe --- /dev/null +++ b/modules/colombiaBidAdapter.js @@ -0,0 +1,82 @@ +import * as utils from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +const BIDDER_CODE = 'colombia'; +const ENDPOINT_URL = 'https://ade.clmbtech.com/cde/prebid.htm'; +const HOST_NAME = document.location.protocol + '//' + window.location.host; + +export const spec = { + code: BIDDER_CODE, + aliases: ['clmb'], + supportedMediaTypes: [BANNER], + isBidRequestValid: function(bid) { + return !!(bid.params.placementId); + }, + buildRequests: function(validBidRequests, bidderRequest) { + return validBidRequests.map(bidRequest => { + const params = bidRequest.params; + const sizes = utils.parseSizesInput(bidRequest.sizes)[0]; + const width = sizes.split('x')[0]; + const height = sizes.split('x')[1]; + const placementId = params.placementId; + const cb = Math.floor(Math.random() * 99999999999); + const bidId = bidRequest.bidId; + const referrer = (bidderRequest && bidderRequest.refererInfo) ? bidderRequest.refererInfo.referer : ''; + const payload = { + v: 'hb1', + p: placementId, + w: width, + h: height, + cb: cb, + r: referrer, + uid: bidId, + t: 'i', + d: HOST_NAME, + }; + return { + method: 'POST', + url: ENDPOINT_URL, + data: payload, + } + }); + }, + interpretResponse: function(serverResponse, bidRequest) { + const bidResponses = []; + const response = serverResponse.body; + const crid = response.creativeId || 0; + const width = response.width || 0; + const height = response.height || 0; + let cpm = response.cpm || 0; + if (width == 300 && height == 250) { + cpm = cpm * 0.2; + } + if (width == 320 && height == 50) { + cpm = cpm * 0.55; + } + if (cpm < 1) { + return bidResponses; + } + if (width !== 0 && height !== 0 && cpm !== 0 && crid !== 0) { + const dealId = response.dealid || ''; + const currency = response.currency || 'USD'; + const netRevenue = (response.netRevenue === undefined) ? true : response.netRevenue; + const bidResponse = { + requestId: bidRequest.data.uid, + cpm: cpm, + width: response.width, + height: response.height, + creativeId: crid, + dealId: dealId, + currency: currency, + netRevenue: netRevenue, + ttl: config.getConfig('_bidderTimeout'), + referrer: bidRequest.data.r, + ad: response.ad + }; + bidResponses.push(bidResponse); + } + return bidResponses; + } +} +registerBidder(spec); diff --git a/modules/colombiaBidAdapter.md b/modules/colombiaBidAdapter.md index 2131fcb4c5a..c754e49771d 100644 --- a/modules/colombiaBidAdapter.md +++ b/modules/colombiaBidAdapter.md @@ -10,13 +10,17 @@ Maintainer: colombiaonline@timesinteret.in Connect to COLOMBIA for bids. -THE COLOMBIA adapter requires setup and approval from the COLOMBIA team. Please reach out to your account team or colombiaonline@timesinteret.in for more information. +COLOMBIA adapter requires setup and approval from the COLOMBIA team. Please reach out to your account team or colombiaonline@timesinteret.in for more information. # Test Parameters ``` var adUnits = [{ code: 'test-ad-div', - sizes: [[300, 250]], + mediaTypes: { + banner: { + sizes: [[300, 250],[728,90],[320,50]] + } + }, bids: [{ bidder: 'colombia', params: { diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index cf4d306e686..baa60a76a0d 100644 --- a/modules/colossussspBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -23,6 +23,19 @@ function isBidResponseValid(bid) { } } +function getUserId(eids, id, source, uidExt) { + if (id) { + var uid = { id }; + if (uidExt) { + uid.ext = uidExt; + } + eids.push({ + source, + uids: [ uid ] + }); + } +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -60,13 +73,17 @@ export const spec = { 'secure': location.protocol === 'https:' ? 1 : 0, 'host': location.host, 'page': location.pathname, - 'placements': placements + 'placements': placements, }; if (bidderRequest) { if (bidderRequest.uspConsent) { request.ccpa = bidderRequest.uspConsent; } + if (bidderRequest.gdprConsent) { + request.gdpr_consent = bidderRequest.gdprConsent.consentString || 'ALL' + request.gdpr_require = bidderRequest.gdprConsent.gdprApplies ? 1 : 0 + } } for (let i = 0; i < validBidRequests.length; i++) { @@ -76,11 +93,20 @@ export const spec = { placementId: bid.params.placement_id, bidId: bid.bidId, sizes: bid.mediaTypes[traff].sizes, - traffic: traff + traffic: traff, + eids: [] }; if (bid.schain) { placement.schain = bid.schain; } + if (bid.userId) { + getUserId(placement.eids, bid.userId.britepoolid, 'britepool.com'); + getUserId(placement.eids, bid.userId.idl_env, 'identityLink'); + getUserId(placement.eids, bid.userId.id5id, 'id5-sync.com') + getUserId(placement.eids, bid.userId.tdid, 'adserver.org', { + rtiPartner: 'TDID' + }); + } placements.push(placement); } return { diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js new file mode 100644 index 00000000000..3dcb8da9838 --- /dev/null +++ b/modules/connectadBidAdapter.js @@ -0,0 +1,299 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js' +import {config} from '../src/config.js'; + +const BIDDER_CODE = 'connectad'; +const BIDDER_CODE_ALIAS = 'connectadrealtime'; +const ENDPOINT_URL = 'https://i.connectad.io/api/v2'; +const SUPPORTED_MEDIA_TYPES = [BANNER]; + +export const spec = { + code: BIDDER_CODE, + aliases: [ BIDDER_CODE_ALIAS ], + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + + isBidRequestValid: function(bid) { + return !!(bid.params.networkId && bid.params.siteId); + }, + + buildRequests: function(validBidRequests, bidderRequest) { + let digitrust; + + let ret = { + method: 'POST', + url: '', + data: '', + bidRequest: [] + }; + + if (validBidRequests.length < 1) { + return ret; + } + + const data = Object.assign({ + placements: [], + time: Date.now(), + user: {}, + url: (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) ? bidderRequest.refererInfo.referer : window.location.href, + referrer: window.document.referrer, + referrer_info: bidderRequest.refererInfo, + screensize: getScreenSize(), + dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, + language: navigator.language, + ua: navigator.userAgent + }); + + // coppa compliance + if (config.getConfig('coppa') === true) { + utils.deepSetValue(data, 'user.coppa', 1); + } + + // adding schain object + if (validBidRequests[0].schain) { + utils.deepSetValue(data, 'source.ext.schain', validBidRequests[0].schain); + } + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + let gdprApplies; + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + gdprApplies = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + utils.deepSetValue(data, 'user.ext.gdpr', gdprApplies); + utils.deepSetValue(data, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + } + + // CCPA + if (bidderRequest.uspConsent) { + utils.deepSetValue(data, 'user.ext.us_privacy', bidderRequest.uspConsent); + } + + // Digitrust Support + const bidRequestDigitrust = utils.deepAccess(validBidRequests[0], 'userId.digitrustid.data'); + if (bidRequestDigitrust && (!bidRequestDigitrust.privacy || !bidRequestDigitrust.privacy.optout)) { + digitrust = { + id: bidRequestDigitrust.id, + keyv: bidRequestDigitrust.keyv + } + } + + if (digitrust) { + utils.deepSetValue(data, 'user.ext.digitrust', { + id: digitrust.id, + keyv: digitrust.keyv + }) + } + + if (validBidRequests[0].userId && typeof validBidRequests[0].userId === 'object' && (validBidRequests[0].userId.tdid || validBidRequests[0].userId.pubcid || validBidRequests[0].userId.lipb || validBidRequests[0].userId.id5id || validBidRequests[0].userId.parrableid)) { + utils.deepSetValue(data, 'user.ext.eids', []); + + if (validBidRequests[0].userId.tdid) { + data.user.ext.eids.push({ + source: 'adserver.org', + uids: [{ + id: validBidRequests[0].userId.tdid, + ext: { + rtiPartner: 'TDID' + } + }] + }); + } + + if (validBidRequests[0].userId.pubcid) { + data.user.ext.eids.push({ + source: 'pubcommon', + uids: [{ + id: validBidRequests[0].userId.pubcid, + }] + }); + } + + if (validBidRequests[0].userId.id5id) { + data.user.ext.eids.push({ + source: 'id5-sync.com', + uids: [{ + id: validBidRequests[0].userId.id5id, + }] + }); + } + + if (validBidRequests[0].userId.parrableid) { + data.user.ext.eids.push({ + source: 'parrable.com', + uids: [{ + id: validBidRequests[0].userId.parrableid, + }] + }); + } + + if (validBidRequests[0].userId.lipb && validBidRequests[0].userId.lipb.lipbid) { + data.user.ext.eids.push({ + source: 'liveintent.com', + uids: [{ + id: validBidRequests[0].userId.lipb.lipbid + }] + }); + } + } + + validBidRequests.map(bid => { + const placement = Object.assign({ + id: bid.transactionId, + divName: bid.bidId, + sizes: bid.mediaTypes.banner.sizes, + adTypes: getSize(bid.mediaTypes.banner.sizes || bid.sizes) + }, bid.params); + + if (placement.networkId && placement.siteId) { + data.placements.push(placement); + } + }); + + ret.data = JSON.stringify(data); + ret.bidRequest = validBidRequests; + ret.url = ENDPOINT_URL; + + return ret; + }, + + interpretResponse: function(serverResponse, bidRequest, bidderRequest) { + let bid; + let bids; + let bidId; + let bidObj; + let bidResponses = []; + + bids = bidRequest.bidRequest; + + serverResponse = (serverResponse || {}).body; + for (let i = 0; i < bids.length; i++) { + bid = {}; + bidObj = bids[i]; + bidId = bidObj.bidId; + + if (serverResponse) { + const decision = serverResponse.decisions && serverResponse.decisions[bidId]; + const price = decision && decision.pricing && decision.pricing.clearPrice; + + if (decision && price) { + bid.requestId = bidId; + bid.cpm = price; + bid.width = decision.width; + bid.height = decision.height; + bid.dealid = decision.dealid || null; + bid.ad = retrieveAd(decision); + bid.currency = 'USD'; + bid.creativeId = decision.adId; + bid.ttl = 360; + bid.netRevenue = true; + bidResponses.push(bid); + } + } + } + + return bidResponses; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncEndpoint = 'https://cdn.connectad.io/connectmyusers.php?'; + + if (gdprConsent) { + syncEndpoint = utils.tryAppendQueryString(syncEndpoint, 'gdpr', (gdprConsent.gdprApplies ? 1 : 0)); + } + + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + syncEndpoint = utils.tryAppendQueryString(syncEndpoint, 'gdpr_consent', gdprConsent.consentString); + } + + if (uspConsent) { + syncEndpoint = utils.tryAppendQueryString(syncEndpoint, 'us_privacy', uspConsent); + } + + if (config.getConfig('coppa') === true) { + syncEndpoint = utils.tryAppendQueryString(syncEndpoint, 'coppa', 1); + } + + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: syncEndpoint + }]; + } else { + utils.logWarn('Bidder ConnectAd: Please activate iFrame Sync'); + } + } +}; + +const sizeMap = [ + null, + '120x90', + '200x200', + '468x60', + '728x90', + '300x250', + '160x600', + '120x600', + '300x100', + '180x150', + '336x280', + '240x400', + '234x60', + '88x31', + '120x60', + '120x240', + '125x125', + '220x250', + '250x250', + '250x90', + '0x0', + '200x90', + '300x50', + '320x50', + '320x480', + '185x185', + '620x45', + '300x125', + '800x250', + '980x120', + '980x150', + '320x150', + '300x300', + '200x600', + '320x500', + '320x320' +]; + +sizeMap[77] = '970x90'; +sizeMap[123] = '970x250'; +sizeMap[43] = '300x600'; +sizeMap[286] = '970x66'; +sizeMap[3230] = '970x280'; +sizeMap[429] = '486x60'; +sizeMap[374] = '700x500'; +sizeMap[934] = '300x1050'; +sizeMap[1578] = '320x100'; +sizeMap[331] = '320x250'; +sizeMap[3301] = '320x267'; +sizeMap[2730] = '728x250'; + +function getSize(sizes) { + const result = []; + sizes.forEach(function(size) { + const index = sizeMap.indexOf(size[0] + 'x' + size[1]); + if (index >= 0) { + result.push(index); + } + }); + return result; +} + +function retrieveAd(decision) { + return decision.contents && decision.contents[0] && decision.contents[0].body; +} + +function getScreenSize() { + return [window.screen.width, window.screen.height].join('x'); +} + +registerBidder(spec); diff --git a/modules/connectadBidAdapter.md b/modules/connectadBidAdapter.md new file mode 100644 index 00000000000..e63494e1add --- /dev/null +++ b/modules/connectadBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: ConnectAd PreBid Adapter +Module Type: Bidder Adapter +Maintainer: support@connectad.io +``` + +# Description + +ConnectAd bid adapter supports only Banner at present. Video and Mobile will follow Q2/2020 + +# Sample Ad Unit: For Publishers +``` +var adUnits = [ +{ + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'connectad', + params: { + siteId: 123456, + networkId: 123456, + bidfloor: 0.20 // Optional: Requested Bidfloor + } + }] +} + +# ## Configuration +ConnectAd recommends the UserSync configuration below otherwise we will not be able to performe user syncs. + +```javascript +pbjs.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: ['connectad'], + filter: 'include' + } + } + } +}); \ No newline at end of file diff --git a/modules/consentManagement.js b/modules/consentManagement.js index f27bab70192..4a1fa9ba8d0 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -17,6 +17,7 @@ const DEFAULT_ALLOW_AUCTION_WO_CONSENT = true; export let userCMP; export let consentTimeout; export let allowAuction; +export let gdprScope; export let staticConsentData; let cmpVersion = 0; @@ -98,7 +99,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { if (success) { if (tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { cmpSuccess(tcfData, hookConfig); - } else if (tcfData.eventStatus === 'cmpuishown' && tcfData.tcString.length > 0 && tcfData.purposeOneTreatment === true) { + } else if (tcfData.eventStatus === 'cmpuishown' && tcfData.tcString && tcfData.purposeOneTreatment === true) { cmpSuccess(tcfData, hookConfig); } } else { @@ -365,17 +366,16 @@ function storeConsentData(cmpConsentObject) { consentData = { consentString: (cmpConsentObject) ? cmpConsentObject.getConsentData.consentData : undefined, vendorData: (cmpConsentObject) ? cmpConsentObject.getVendorConsents : undefined, - gdprApplies: (cmpConsentObject) ? cmpConsentObject.getConsentData.gdprApplies : undefined + gdprApplies: (cmpConsentObject) ? cmpConsentObject.getConsentData.gdprApplies : gdprScope }; } else { consentData = { consentString: (cmpConsentObject) ? cmpConsentObject.tcString : undefined, vendorData: (cmpConsentObject) || undefined, - gdprApplies: (cmpConsentObject) ? cmpConsentObject.gdprApplies : undefined + gdprApplies: (cmpConsentObject) ? cmpConsentObject.gdprApplies : gdprScope }; } consentData.apiVersion = cmpVersion; - gdprDataHandler.setConsentData(consentData); } @@ -434,7 +434,7 @@ export function resetConsentData() { /** * 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) + * @param {{cmp:string, timeout:number, allowAuctionWithoutConsent:boolean, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ export function setConsentConfig(config) { // if `config.gdpr` or `config.usp` exist, assume new config format. @@ -465,6 +465,9 @@ export function setConsentConfig(config) { utils.logInfo(`consentManagement config did not specify allowAuctionWithoutConsent. Using system default setting (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); } + // if true, then gdprApplies should be set to true + gdprScope = config.defaultGdprScope === true; + utils.logInfo('consentManagement module has been activated...'); if (userCMP === 'static') { diff --git a/modules/convergeBidAdapter.js b/modules/convergeBidAdapter.js new file mode 100644 index 00000000000..bea3b6cb1ab --- /dev/null +++ b/modules/convergeBidAdapter.js @@ -0,0 +1,313 @@ +import * as utils from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { Renderer } from '../src/Renderer.js'; +import { VIDEO, BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'converge'; +const ENDPOINT_URL = 'https://tech.convergd.com/hb'; +const TIME_TO_LIVE = 360; +const SYNC_URL = 'https://tech.convergd.com/push_sync'; +const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + +let hasSynced = false; + +const LOG_ERROR_MESS = { + noAuid: 'Bid from response has no auid parameter - ', + noAdm: 'Bid from response has no adm parameter - ', + noBid: 'Array of bid objects is empty', + noPlacementCode: "Can't find in requested bids the bid with auid - ", + emptyUids: 'Uids should be not empty', + emptySeatbid: 'Seatbid array from response has empty item', + emptyResponse: 'Response is empty', + hasEmptySeatbidArray: 'Response has empty seatbid array', + hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' +}; +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [ BANNER, VIDEO ], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!bid.params.uid; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - an array of bids + * @param {bidderRequest} bidderRequest - bidder request object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const auids = []; + const bidsMap = {}; + const slotsMapByUid = {}; + const sizeMap = {}; + const bids = validBidRequests || []; + let priceType = 'net'; + let pageKeywords; + let reqId; + + bids.forEach(bid => { + if (bid.params.priceType === 'gross') { + priceType = 'gross'; + } + reqId = bid.bidderRequestId; + const {params: {uid}, adUnitCode} = bid; + auids.push(uid); + const sizesId = utils.parseSizesInput(bid.sizes); + + if (!pageKeywords && !utils.isEmpty(bid.params.keywords)) { + const keywords = utils.transformBidderParamKeywords(bid.params.keywords); + + if (keywords.length > 0) { + keywords.forEach(deleteValues); + } + pageKeywords = keywords; + } + + if (!slotsMapByUid[uid]) { + slotsMapByUid[uid] = {}; + } + const slotsMap = slotsMapByUid[uid]; + if (!slotsMap[adUnitCode]) { + slotsMap[adUnitCode] = {adUnitCode, bids: [bid], parents: []}; + } else { + slotsMap[adUnitCode].bids.push(bid); + } + const slot = slotsMap[adUnitCode]; + + sizesId.forEach((sizeId) => { + sizeMap[sizeId] = true; + if (!bidsMap[uid]) { + bidsMap[uid] = {}; + } + + if (!bidsMap[uid][sizeId]) { + bidsMap[uid][sizeId] = [slot]; + } else { + bidsMap[uid][sizeId].push(slot); + } + slot.parents.push({parent: bidsMap[uid], key: sizeId, uid}); + }); + }); + + const payload = { + pt: priceType, + auids: auids.join(','), + sizes: utils.getKeys(sizeMap).join(','), + r: reqId, + wrapperType: 'Prebid_js', + wrapperVersion: '$prebid.version$' + }; + + if (pageKeywords) { + payload.keywords = JSON.stringify(pageKeywords); + } + + if (bidderRequest) { + if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { + payload.u = bidderRequest.refererInfo.referer; + } + if (bidderRequest.timeout) { + payload.wtimeout = bidderRequest.timeout; + } + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + payload.gdpr_applies = + (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') + ? Number(bidderRequest.gdprConsent.gdprApplies) : 1; + } + if (bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } + } + + return { + method: 'GET', + url: ENDPOINT_URL, + data: utils.parseQueryStringParameters(payload).replace(/\&$/, ''), + bidsMap: bidsMap, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @param {*} bidRequest + * @param {Renderer} RendererConst + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest, RendererConst = Renderer) { + serverResponse = serverResponse && serverResponse.body; + const bidResponses = []; + const bidsMap = bidRequest.bidsMap; + const priceType = bidRequest.data.pt; + + let errorMessage; + + if (!serverResponse) errorMessage = LOG_ERROR_MESS.emptyResponse; + else if (serverResponse.seatbid && !serverResponse.seatbid.length) { + errorMessage = LOG_ERROR_MESS.hasEmptySeatbidArray; + } + + if (!errorMessage && serverResponse.seatbid) { + serverResponse.seatbid.forEach(respItem => { + _addBidResponse(_getBidFromResponse(respItem), bidsMap, priceType, bidResponses, RendererConst); + }); + } + if (errorMessage) utils.logError(errorMessage); + return bidResponses; + }, + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + if (!hasSynced && syncOptions.pixelEnabled) { + let params = ''; + + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + params += `&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent) { + params += `&us_privacy=${uspConsent}`; + } + + hasSynced = true; + return { + type: 'image', + url: SYNC_URL + params + }; + } + } +}; + +function isPopulatedArray(arr) { + return !!(utils.isArray(arr) && arr.length > 0); +} + +function deleteValues(keyPairObj) { + if (isPopulatedArray(keyPairObj.value) && keyPairObj.value[0] === '') { + delete keyPairObj.value; + } +} + +function _getBidFromResponse(respItem) { + if (!respItem) { + utils.logError(LOG_ERROR_MESS.emptySeatbid); + } else if (!respItem.bid) { + utils.logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); + } else if (!respItem.bid[0]) { + utils.logError(LOG_ERROR_MESS.noBid); + } + return respItem && respItem.bid && respItem.bid[0]; +} + +function _addBidResponse(serverBid, bidsMap, priceType, bidResponses, RendererConst) { + if (!serverBid) return; + let errorMessage; + if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid); + if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); + else { + const awaitingBids = bidsMap[serverBid.auid]; + if (awaitingBids) { + const sizeId = `${serverBid.w}x${serverBid.h}`; + if (awaitingBids[sizeId]) { + const slot = awaitingBids[sizeId][0]; + + const bid = slot.bids.shift(); + const bidResponse = { + requestId: bid.bidId, // bid.bidderRequestId, + bidderCode: spec.code, + cpm: serverBid.price, + width: serverBid.w, + height: serverBid.h, + creativeId: serverBid.auid, // bid.bidId, + currency: 'EUR', + netRevenue: priceType !== 'gross', + ttl: TIME_TO_LIVE, + dealId: serverBid.dealid + }; + if (serverBid.content_type === 'video' || (!serverBid.content_type && bid.mediaTypes && bid.mediaTypes.video)) { + bidResponse.vastXml = serverBid.adm; + bidResponse.mediaType = VIDEO; + bidResponse.adResponse = { + content: bidResponse.vastXml + }; + if (!bid.renderer && (!bid.mediaTypes || !bid.mediaTypes.video || bid.mediaTypes.video.context === 'outstream')) { + bidResponse.renderer = createRenderer(bidResponse, { + id: bid.bidId, + url: RENDERER_URL + }, RendererConst); + } + } else { + bidResponse.ad = serverBid.adm; + bidResponse.mediaType = BANNER; + } + + bidResponses.push(bidResponse); + + if (!slot.bids.length) { + slot.parents.forEach(({parent, key, uid}) => { + const index = parent[key].indexOf(slot); + if (index > -1) { + parent[key].splice(index, 1); + } + if (!parent[key].length) { + delete parent[key]; + if (!utils.getKeys(parent).length) { + delete bidsMap[uid]; + } + } + }); + } + } + } else { + errorMessage = LOG_ERROR_MESS.noPlacementCode + serverBid.auid; + } + } + if (errorMessage) { + utils.logError(errorMessage); + } +} + +function outstreamRender (bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: bid.adResponse + }); + }); +} + +function createRenderer (bid, rendererParams, RendererConst) { + const rendererInst = RendererConst.install({ + id: rendererParams.id, + url: rendererParams.url, + loaded: false + }); + + try { + rendererInst.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + return rendererInst; +} + +export function resetUserSync() { + hasSynced = false; +} + +export function getSyncUrl() { + return SYNC_URL; +} + +registerBidder(spec); diff --git a/modules/convergeBidAdapter.md b/modules/convergeBidAdapter.md new file mode 100644 index 00000000000..ab916a8b3b6 --- /dev/null +++ b/modules/convergeBidAdapter.md @@ -0,0 +1,57 @@ +# Overview + +Module Name: Converge Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@converge-digital.com + +# Description + +Module that connects to Converge demand source to fetch bids. +Converge Bid Adapter supports Banner and Video (instream and outstream). + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "converge", + params: { + uid: '59', + priceType: 'gross' // by default is 'net' + } + } + ] + },{ + code: 'test-div', + sizes: [[728, 90]], + bids: [ + { + bidder: "converge", + params: { + uid: 1, + priceType: 'gross', + keywords: { + brandsafety: ['disaster'], + topic: ['stress', 'fear'] + } + } + } + ] + },{ + code: 'test-div', + sizes: [[640, 360]], + mediaTypes: { video: {} }, + bids: [ + { + bidder: "converge", + params: { + uid: 60 + } + } + ] + } + ]; +``` diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index b1f3a1a0a06..2ecdb2b7e98 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -1,12 +1,17 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const GVLID = 24; +export const storage = getStorageManager(GVLID); const BIDDER_CODE = 'conversant'; const URL = 'https://web.hb.ad.cpe.dotomi.com/s2s/header/24'; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, aliases: ['cnvr'], // short code supportedMediaTypes: [BANNER, VIDEO], @@ -53,6 +58,7 @@ export const spec = { let requestId = ''; let pubcid = null; let pubcidName = '_pubcid'; + let bidurl = URL; const conversantImps = validBidRequests.map(function(bid) { const bidfloor = utils.getBidIdParameter('bidfloor', bid.params); @@ -104,6 +110,9 @@ export const spec = { } else if (bid.crumbs && bid.crumbs.pubcid) { pubcid = bid.crumbs.pubcid; } + if (bid.params.white_label_url) { + bidurl = bid.params.white_label_url; + } return imp; }); @@ -159,7 +168,7 @@ export const spec = { return { method: 'POST', - url: URL, + url: bidurl, data: payload, }; }, @@ -343,13 +352,13 @@ function readStoredValue(key) { let storedValue; try { // check cookies first - storedValue = utils.getCookie(key); + storedValue = storage.getCookie(key); if (!storedValue) { // check expiration time before reading local storage - const storedValueExp = utils.getDataFromLocalStorage(`${key}_exp`); + const storedValueExp = storage.getDataFromLocalStorage(`${key}_exp`); if (storedValueExp === '' || (storedValueExp && (new Date(storedValueExp)).getTime() - Date.now() > 0)) { - storedValue = utils.getDataFromLocalStorage(key); + storedValue = storage.getDataFromLocalStorage(key); storedValue = storedValue ? decodeURIComponent(storedValue) : storedValue; } } diff --git a/modules/conversantBidAdapter.md b/modules/conversantBidAdapter.md index 5aba5653043..fba793adad2 100644 --- a/modules/conversantBidAdapter.md +++ b/modules/conversantBidAdapter.md @@ -38,6 +38,7 @@ var adUnits = [ site_id: '108060', api: [2], protocols: [1, 2], + white_label_url: 'https://web.hb.ad.cpe.dotomi.com/s2s/header/24', mimes: ['video/mp4'] } }] diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index caebb12c717..5d137c994fe 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -2,17 +2,19 @@ import { loadExternalScript } from '../src/adloader.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { parse } from '../src/url.js'; import * as utils from '../src/utils.js'; import find from 'core-js/library/fn/array/find.js'; import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; +import { getStorageManager } from '../src/storageManager.js'; -export const ADAPTER_VERSION = 26; +const GVLID = 91; +export const ADAPTER_VERSION = 29; const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; -const CRITEO_VENDOR_ID = 91; const PROFILE_ID_INLINE = 207; export const PROFILE_ID_PUBLISHERTAG = 185; +const storage = getStorageManager(GVLID); +const LOG_PREFIX = 'Criteo: '; // Unminified source code can be found in: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js const PUBLISHER_TAG_URL = 'https://static.criteo.net/js/ld/publishertag.prebid.js'; @@ -23,6 +25,7 @@ const FAST_BID_PUBKEY_N = 'ztQYwCE5BU7T9CDM5he6rKoabstXRmkzx54zFPZkWbK530dwtLBDe /** @type {BidderSpec} */ export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [ BANNER, VIDEO, NATIVE ], /** @@ -54,7 +57,11 @@ export const spec = { let url; let data; - Object.assign(bidderRequest, { ceh: config.getConfig('criteo.ceh') }); + Object.assign(bidderRequest, { + publisherExt: config.getConfig('fpd.context'), + userExt: config.getConfig('fpd.user'), + ceh: config.getConfig('criteo.ceh') + }); // If publisher tag not already loaded try to get it from fast bid if (!publisherTagAvailable()) { @@ -196,7 +203,7 @@ function buildContext(bidRequests, bidderRequest) { if (bidderRequest && bidderRequest.refererInfo) { referrer = bidderRequest.refererInfo.referer; } - const queryString = parse(referrer).search; + const queryString = utils.parseUrl(referrer).search; const context = { url: referrer, @@ -238,9 +245,20 @@ function buildCdbUrl(context) { return url; } +function checkNativeSendId(bidRequest) { + return !(bidRequest.nativeParams && + ((bidRequest.nativeParams.image && bidRequest.nativeParams.image.sendId !== true) || + (bidRequest.nativeParams.icon && bidRequest.nativeParams.icon.sendId !== true) || + (bidRequest.nativeParams.clickUrl && bidRequest.nativeParams.clickUrl.sendId !== true) || + (bidRequest.nativeParams.displayUrl && bidRequest.nativeParams.displayUrl.sendId !== true) || + (bidRequest.nativeParams.privacyLink && bidRequest.nativeParams.privacyLink.sendId !== true) || + (bidRequest.nativeParams.privacyIcon && bidRequest.nativeParams.privacyIcon.sendId !== true))); +} + /** * @param {CriteoContext} context * @param {BidRequest[]} bidRequests + * @param bidderRequest * @return {*} */ function buildCdbRequest(context, bidRequests, bidderRequest) { @@ -248,6 +266,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { const request = { publisher: { url: context.url, + ext: bidderRequest.publisherExt }, slots: bidRequests.map(bidRequest => { networkId = bidRequest.params.networkId || networkId; @@ -260,11 +279,20 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (bidRequest.params.zoneId) { slot.zoneid = bidRequest.params.zoneId; } + if (bidRequest.fpd && bidRequest.fpd.context) { + slot.ext = bidRequest.fpd.context; + } + if (bidRequest.params.ext) { + slot.ext = Object.assign({}, slot.ext, bidRequest.params.ext); + } if (bidRequest.params.publisherSubId) { slot.publishersubid = bidRequest.params.publisherSubId; } if (bidRequest.params.nativeCallback || utils.deepAccess(bidRequest, `mediaTypes.${NATIVE}`)) { slot.native = true; + if (!checkNativeSendId(bidRequest)) { + utils.logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); + } } if (hasVideoMediaType(bidRequest)) { const video = { @@ -273,7 +301,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { protocols: bidRequest.mediaTypes.video.protocols, maxduration: bidRequest.mediaTypes.video.maxduration, api: bidRequest.mediaTypes.video.api - } + }; video.skip = bidRequest.params.video.skip; video.placement = bidRequest.params.video.placement; @@ -289,7 +317,9 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (networkId) { request.publisher.networkid = networkId; } - request.user = {}; + request.user = { + ext: bidderRequest.userExt + }; if (bidderRequest && bidderRequest.ceh) { request.user.ceh = bidderRequest.ceh; } @@ -298,10 +328,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (typeof bidderRequest.gdprConsent.gdprApplies !== 'undefined') { request.gdprConsent.gdprApplies = !!(bidderRequest.gdprConsent.gdprApplies); } - if (bidderRequest.gdprConsent.vendorData && bidderRequest.gdprConsent.vendorData.vendorConsents && - typeof bidderRequest.gdprConsent.vendorData.vendorConsents[ CRITEO_VENDOR_ID.toString(10) ] !== 'undefined') { - request.gdprConsent.consentGiven = !!(bidderRequest.gdprConsent.vendorData.vendorConsents[ CRITEO_VENDOR_ID.toString(10) ]); - } + request.gdprConsent.version = bidderRequest.gdprConsent.apiVersion; if (typeof bidderRequest.gdprConsent.consentString !== 'undefined') { request.gdprConsent.consentData = bidderRequest.gdprConsent.consentString; } @@ -423,7 +450,7 @@ export function tryGetCriteoFastBid() { try { const fastBidStorageKey = 'criteo_fast_bid'; const hashPrefix = '// Hash: '; - const fastBidFromStorage = utils.getDataFromLocalStorage(fastBidStorageKey); + const fastBidFromStorage = storage.getDataFromLocalStorage(fastBidStorageKey); if (fastBidFromStorage !== null) { // The value stored must contain the file's encrypted hash as first line @@ -432,7 +459,7 @@ export function tryGetCriteoFastBid() { if (firstLine.substr(0, hashPrefix.length) !== hashPrefix) { utils.logWarn('No hash found in FastBid'); - utils.removeDataFromLocalStorage(fastBidStorageKey); + storage.removeDataFromLocalStorage(fastBidStorageKey); } else { // Remove the hash part from the locally stored value const publisherTagHash = firstLine.substr(hashPrefix.length); @@ -446,7 +473,7 @@ export function tryGetCriteoFastBid() { utils.insertElement(script); } else { utils.logWarn('Invalid Criteo FastBid found'); - utils.removeDataFromLocalStorage(fastBidStorageKey); + storage.removeDataFromLocalStorage(fastBidStorageKey); } } } diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index 469532e4aa7..c44f0c843ae 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -7,9 +7,11 @@ import * as utils from '../src/utils.js' import * as ajax from '../src/ajax.js' -import * as urlLib from '../src/url.js' import { getRefererInfo } from '../src/refererDetection.js' import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); const bididStorageKey = 'cto_bidid'; const bundleStorageKey = 'cto_bundle'; @@ -20,33 +22,33 @@ const pastDateString = new Date(0).toString(); const expirationString = new Date(utils.timestamp() + cookiesMaxAge).toString(); function areCookiesWriteable() { - utils.setCookie(cookieWriteableKey, '1'); - const canWrite = utils.getCookie(cookieWriteableKey) === '1'; - utils.setCookie(cookieWriteableKey, '', pastDateString); + storage.setCookie(cookieWriteableKey, '1'); + const canWrite = storage.getCookie(cookieWriteableKey) === '1'; + storage.setCookie(cookieWriteableKey, '', pastDateString); return canWrite; } function extractProtocolHost (url, returnOnlyHost = false) { - const parsedUrl = urlLib.parse(url) + const parsedUrl = utils.parseUrl(url) return returnOnlyHost ? `${parsedUrl.hostname}` : `${parsedUrl.protocol}://${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}/`; } function getFromAllStorages(key) { - return utils.getCookie(key) || utils.getDataFromLocalStorage(key); + return storage.getCookie(key) || storage.getDataFromLocalStorage(key); } function saveOnAllStorages(key, value) { if (key && value) { - utils.setCookie(key, value, expirationString); - utils.setDataInLocalStorage(key, value); + storage.setCookie(key, value, expirationString); + storage.setDataInLocalStorage(key, value); } } function deleteFromAllStorages(key) { - utils.setCookie(key, '', pastDateString); - utils.removeDataFromLocalStorage(key); + storage.setCookie(key, '', pastDateString); + storage.removeDataFromLocalStorage(key); } function getCriteoDataFromAllStorages() { @@ -56,18 +58,19 @@ function getCriteoDataFromAllStorages() { } } -function buildCriteoUsersyncUrl(topUrl, domain, bundle, areCookiesWriteable, isPublishertagPresent) { +function buildCriteoUsersyncUrl(topUrl, domain, bundle, areCookiesWriteable, isPublishertagPresent, gdprString) { const url = 'https://gum.criteo.com/sid/json?origin=prebid' + `${topUrl ? '&topUrl=' + encodeURIComponent(topUrl) : ''}` + `${domain ? '&domain=' + encodeURIComponent(domain) : ''}` + `${bundle ? '&bundle=' + encodeURIComponent(bundle) : ''}` + + `${gdprString ? '&gdprString=' + encodeURIComponent(gdprString) : ''}` + `${areCookiesWriteable ? '&cw=1' : ''}` + `${isPublishertagPresent ? '&pbt=1' : ''}` return url; } -function callCriteoUserSync(parsedCriteoData) { +function callCriteoUserSync(parsedCriteoData, gdprString) { const cw = areCookiesWriteable(); const topUrl = extractProtocolHost(getRefererInfo().referer); const domain = extractProtocolHost(document.location.href, true); @@ -78,7 +81,8 @@ function callCriteoUserSync(parsedCriteoData) { domain, parsedCriteoData.bundle, cw, - isPublishertagPresent + isPublishertagPresent, + gdprString ); ajax.ajaxBuilder()( @@ -119,11 +123,16 @@ export const criteoIdSubmodule = { /** * get the Criteo Id from local storages and initiate a new user sync * @function + * @param {SubmoduleParams} [configParams] + * @param {ConsentData} [consentData] * @returns {{id: {criteoId: string} | undefined}}} */ - getId() { + getId(configParams, consentData) { + const hasGdprData = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies; + const gdprConsentString = hasGdprData ? consentData.consentString : undefined; + let localData = getCriteoDataFromAllStorages(); - callCriteoUserSync(localData); + callCriteoUserSync(localData, gdprConsentString); return { id: localData.bidId ? { criteoId: localData.bidId } : undefined } } diff --git a/modules/currency.js b/modules/currency.js index 84ca5e42783..0464d9b5cdb 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -1,3 +1,4 @@ +import { getGlobal } from '../src/prebidGlobal.js'; import { createBid } from '../src/bidfactory.js'; import { STATUS } from '../src/constants.json'; import { ajax } from '../src/ajax.js'; @@ -122,6 +123,8 @@ function initCurrency(url) { utils.logInfo('Installing addBidResponse decorator for currency module', arguments); + // Adding conversion function to prebid global for external module and on page use + getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); getHook('addBidResponse').before(addBidResponseHook, 100); // call for the file if we haven't already @@ -149,6 +152,7 @@ function resetCurrency() { utils.logInfo('Uninstalling addBidResponse decorator for currency module', arguments); getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + delete getGlobal().convertCurrency; adServerCurrency = 'USD'; conversionCache = {}; diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 32b18592417..b00a3eae659 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -1,7 +1,6 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { parse as parseUrl } from '../src/url.js'; const NATIVE_MAP = { 'body': 2, 'body2': 10, @@ -61,7 +60,7 @@ export const spec = { let imps = {}; let site = {}; let device = {}; - let refurl = parseUrl(bidderRequest.referrer); + let refurl = utils.parseUrl(bidderRequest.referrer); let requests = []; validBidRequests.forEach(bidRequest => { diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 426da05efcf..71a8471d554 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -4,8 +4,7 @@ import { registerVideoSupport } from '../src/adServerManager.js'; import { targeting } from '../src/targeting.js'; -import { formatQS, format as buildUrl, parse } from '../src/url.js'; -import { deepAccess, isEmpty, logError, parseSizesInput } from '../src/utils.js'; +import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, buildUrl } from '../src/utils.js'; import { config } from '../src/config.js'; import { getHook, submodule } from '../src/hook.js'; import { auctionManager } from '../src/auctionManager.js'; @@ -74,7 +73,7 @@ export function buildDfpVideoUrl(options) { if (options.url) { // when both `url` and `params` are given, parsed url will be overwriten // with any matching param components - urlComponents = parse(options.url, {noDecodeWholeURL: true}); + urlComponents = parseUrl(options.url, {noDecodeWholeURL: true}); if (isEmpty(options.params)) { return buildUrlFromAdserverUrlComponents(urlComponents, bid, options); diff --git a/modules/digiTrustIdSystem.js b/modules/digiTrustIdSystem.js index 9d0b5c25e1b..d8aa8be9376 100644 --- a/modules/digiTrustIdSystem.js +++ b/modules/digiTrustIdSystem.js @@ -12,6 +12,10 @@ import * as utils from '../src/utils.js' import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const DT_VENDOR_ID = 64; // cmp gvlVendorId +const storage = getStorageManager(DT_VENDOR_ID); var fallbackTimeout = 1550; // timeout value that allows userId system to execute first var fallbackTimer = 0; // timer Id for fallback init so we don't double call @@ -40,13 +44,19 @@ var noop = function () { const MAX_RETRIES = 2; const DT_ID_SVC = 'https://prebid.digitru.st/id/v1'; -const DT_VENDOR_ID = 64; // cmp gvlVendorId var isFunc = function (fn) { return typeof (fn) === 'function'; } +var _savedId = null; // closure variable for storing Id to avoid additional requests + function callApi(options) { + var ajaxOptions = { + method: 'GET', + withCredentials: true + }; + ajax( DT_ID_SVC, { @@ -54,9 +64,7 @@ function callApi(options) { error: options.fail }, null, - { - method: 'GET' - } + ajaxOptions ); } @@ -83,7 +91,32 @@ function writeDigiId(id) { var key = 'DigiTrust.v1.identity'; var date = new Date(); date.setTime(date.getTime() + 604800000); - utils.setCookie(key, encId(id), date.toUTCString(), 'none'); + storage.setCookie(key, encId(id), date.toUTCString(), 'none'); +} + +/** + * Tests to see if the current browser is FireFox + */ +function isFirefoxBrowser(ua) { + ua = ua || navigator.userAgent; + ua = ua.toLowerCase(); + if (ua.indexOf('firefox') !== -1) { + return true; + } + return false; +} + +/** + * Test to see if the user has a browser that is disallowed for making AJAX + * requests due to the behavior not supported DigiTrust ID Cookie. + */ +function isDisallowedBrowserForApiCall() { + if (utils.isSafariBrowser()) { + return true; + } else if (isFirefoxBrowser()) { + return true; + } + return false; } /** @@ -91,8 +124,6 @@ function writeDigiId(id) { * */ function initDigitrustFacade(config) { - var _savedId = null; // closure variable for storing Id to avoid additional requests - clearTimeout(fallbackTimer); fallbackTimer = 0; @@ -116,7 +147,7 @@ function initDigitrustFacade(config) { try { inter.initCallback(idResponse); } catch (ex) { - utils.logError('Exception in passed DigiTrust init callback'); + utils.logError('Exception in passed DigiTrust init callback', ex); } } } @@ -146,9 +177,9 @@ function initDigitrustFacade(config) { success: true } try { - writeDigiId(respText); idResult.identity = JSON.parse(respText); - _savedId = idResult; + _savedId = idResult; // Save result to the cache variable + writeDigiId(respText); } catch (ex) { idResult.success = false; delete idResult.identity; @@ -163,6 +194,14 @@ function initDigitrustFacade(config) { // check gdpr vendor here. Full DigiTrust library has vendor check built in gdprConsent.hasConsent(null, function (hasConsent) { if (hasConsent) { + if (isDisallowedBrowserForApiCall()) { + let resultObj = { + success: false, + err: 'Your browser does not support DigiTrust Identity' + } + checkAndCallInitializeCb(resultObj); + return; + } callApi(opts); } }) @@ -253,6 +292,10 @@ var ResultWrapper = function (opts) { */ this.userIdCallback = function (callback) { idSystemFn = callback; + if (me.idObj == null) { + me.idObj = _savedId; + } + if (me.idObj != null && isFunc(callback)) { callback(wrapIdResult()); } @@ -262,6 +305,10 @@ var ResultWrapper = function (opts) { * Return a wrapped result formatted for userId system */ function wrapIdResult() { + if (me.idObj == null) { + me.idObj = _savedId; + } + if (me.idObj == null) { return null; } diff --git a/modules/districtmDMXBidAdapter.js b/modules/districtmDMXBidAdapter.js index 83f3ac5b3c9..0dbaf2b2336 100644 --- a/modules/districtmDMXBidAdapter.js +++ b/modules/districtmDMXBidAdapter.js @@ -72,6 +72,7 @@ export const spec = { publisher: { id: String(bidRequest[0].params.memberid) || null } } } + try { let params = config.getConfig('dmx'); dmxRequest.user = params.user || {}; @@ -80,6 +81,14 @@ export const spec = { } catch (e) { } + + let eids = []; + if (bidRequest && bidRequest.userId) { + bindUserId(eids, utils.deepAccess(bidRequest, `userId.idl_env`), 'liveramp.com', 1); + dmxRequest.user = dmxRequest.user || {}; + dmxRequest.user.ext = dmxRequest.user.ext || {}; + dmxRequest.user.ext.eids = eids; + } if (!dmxRequest.test) { delete dmxRequest.test; } @@ -91,6 +100,8 @@ export const spec = { dmxRequest.user.ext = {}; dmxRequest.user.ext.consent = bidderRequest.gdprConsent.consentString; } + dmxRequest.regs = dmxRequest.regs || {}; + dmxRequest.regs.coppa = config.getConfig('coppa') === true ? 1 : 0; if (bidderRequest && bidderRequest.uspConsent) { dmxRequest.regs = dmxRequest.regs || {}; dmxRequest.regs.ext = dmxRequest.regs.ext || {}; @@ -281,4 +292,18 @@ export function defaultSize(thebidObj) { returnObject.height = checkDeepArray(sizes)[1]; return returnObject; } + +export function bindUserId(eids, value, source, atype) { + if (utils.isStr(value) && Array.isArray(eids)) { + eids.push({ + source, + uids: [ + { + id: value, + atype + } + ] + }) + } +} registerBidder(spec); diff --git a/modules/districtmDmxBidAdapter.md b/modules/districtmDmxBidAdapter.md index 5bc1e5d5780..792cf2e7305 100644 --- a/modules/districtmDmxBidAdapter.md +++ b/modules/districtmDmxBidAdapter.md @@ -13,6 +13,8 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman * Single Request * Multi-Size Support * GDPR Compliant +* CCPA Compliant +* COPPA Compliant * Bids returned in **NET** ## Media Types @@ -23,8 +25,8 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman | Key | Scope | Type | Description | --- | --- | --- | --- -| dmxid | Mandatory | Integer | Unique identifier of the placement, dmxid can be obtained in the district m Boost platform. -| memberid | Mandatory | Integer | Unique identifier for your account, memberid can be obtained in the district m Boost platform. +| `dmxid` | Mandatory | Integer | Unique identifier of the placement, dmxid can be obtained in the district m Boost platform. +| `memberid` | Mandatory | Integer | Unique identifier for your account, memberid can be obtained in the district m Boost platform. # Ad Unit Configuration Example @@ -47,6 +49,9 @@ The `districtmDmxAdapter` module allows publishers to include DMX Exchange deman ``` +# Ad Unit Configuration when COPPA is needed + + # Quick Start Guide ###### 1. Including the `districtmDmxAdapter` in your build process. diff --git a/modules/dspxBidAdapter.js b/modules/dspxBidAdapter.js index 0bde2b6ec10..16764e76ede 100644 --- a/modules/dspxBidAdapter.js +++ b/modules/dspxBidAdapter.js @@ -74,6 +74,7 @@ export const spec = { dealId: dealId, currency: currency, netRevenue: netRevenue, + type: response.type, ttl: config.getConfig('_bidderTimeout'), ad: response.adTag }; diff --git a/modules/evolution_techBidAdapter.js b/modules/e_volutionBidAdapter.js similarity index 100% rename from modules/evolution_techBidAdapter.js rename to modules/e_volutionBidAdapter.js diff --git a/modules/evolution_techBidAdapter.md b/modules/e_volutionBidAdapter.md similarity index 100% rename from modules/evolution_techBidAdapter.md rename to modules/e_volutionBidAdapter.md diff --git a/modules/emoteevBidAdapter.js b/modules/emoteevBidAdapter.js index 254373b354a..e0f88725d8a 100644 --- a/modules/emoteevBidAdapter.js +++ b/modules/emoteevBidAdapter.js @@ -24,10 +24,12 @@ import { isArray, isInteger, getParameterByName, - getCookie + buildUrl } from '../src/utils.js'; import {config} from '../src/config.js'; -import * as url from '../src/url.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); export const BIDDER_CODE = 'emoteev'; @@ -232,7 +234,7 @@ export const domain = (env) => { * @param {string} env Emoteev environment parameter * @returns {string} The full URL which events is sent to. */ -export const eventsUrl = env => url.format({ +export const eventsUrl = env => buildUrl({ protocol: (env === DEVELOPMENT) ? 'http' : 'https', hostname: domain(env), pathname: EVENTS_PATH @@ -244,7 +246,7 @@ export const eventsUrl = env => url.format({ * @param {string} env Emoteev environment parameter * @returns {string} The full URL which bidderRequest is sent to. */ -export const bidderUrl = env => url.format({ +export const bidderUrl = env => buildUrl({ protocol: (env === DEVELOPMENT) ? 'http' : 'https', hostname: domain(env), pathname: BIDDER_PATH @@ -256,7 +258,7 @@ export const bidderUrl = env => url.format({ * @param {string} env Emoteev environment parameter * @returns {string} The full URL called for iframe-based user sync */ -export const userSyncIframeUrl = env => url.format({ +export const userSyncIframeUrl = env => buildUrl({ protocol: (env === DEVELOPMENT) ? 'http' : 'https', hostname: domain(env), pathname: USER_SYNC_IFRAME_PATH @@ -268,7 +270,7 @@ export const userSyncIframeUrl = env => url.format({ * @param {string} env Emoteev environment parameter * @returns {string} The full URL called for image-based user sync */ -export const userSyncImageUrl = env => url.format({ +export const userSyncImageUrl = env => buildUrl({ protocol: (env === DEVELOPMENT) ? 'http' : 'https', hostname: domain(env), pathname: USER_SYNC_IMAGE_PATH @@ -506,12 +508,12 @@ export const spec = { bidderRequest), interpretResponse: interpretResponse, onBidWon: (bidObject) => - triggerPixel(url.format(onBidWon( + triggerPixel(buildUrl(onBidWon( resolveEnv(config.getConfig(), getParameterByName('emoteevEnv')), - getCookie('_pubcid'), + storage.getCookie('_pubcid'), bidObject))), onTimeout: (bidRequest) => - triggerPixel(url.format(onTimeout( + triggerPixel(buildUrl(onTimeout( resolveEnv(config.getConfig(), getParameterByName('emoteevEnv')), bidRequest))), getUserSyncs: (syncOptions) => diff --git a/modules/emx_digitalBidAdapter.js b/modules/emx_digitalBidAdapter.js index 22a7bb6f643..83689aa76f1 100644 --- a/modules/emx_digitalBidAdapter.js +++ b/modules/emx_digitalBidAdapter.js @@ -3,7 +3,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { Renderer } from '../src/Renderer.js'; import includes from 'core-js/library/fn/array/includes.js'; -import {parse as parseUrl} from '../src/url.js'; const BIDDER_CODE = 'emx_digital'; const ENDPOINT = 'hb.emxdgt.com'; @@ -130,7 +129,7 @@ export const emxAdapter = { } }, getSite: (refInfo) => { - let url = parseUrl(refInfo.referer); + let url = utils.parseUrl(refInfo.referer); return { domain: url.hostname, page: refInfo.referer, diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index 979f6dbf464..a4b4f2d6728 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -1,5 +1,8 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); const BIDDER_CODE = 'eplanning'; const rnd = Math.random(); @@ -43,7 +46,7 @@ export const spec = { url = 'https://' + (urlConfig.sv || DEFAULT_SV) + '/hb/1/' + urlConfig.ci + '/' + dfpClientId + '/' + getDomain(pageUrl) + '/' + sec; const referrerUrl = bidderRequest.refererInfo.referer.reachedTop ? encodeURIComponent(window.top.document.referrer) : encodeURIComponent(bidderRequest.refererInfo.referer); - if (utils.hasLocalStorage()) { + if (storage.hasLocalStorage()) { registerViewabilityAllBids(bidRequests); } @@ -204,7 +207,7 @@ function getSpaces(bidRequests, ml) { function getVs(bid) { let s; let vs = ''; - if (utils.hasLocalStorage()) { + if (storage.hasLocalStorage()) { s = getViewabilityData(bid); vs += s.render >= 4 ? s.ratio.toString(16) : 'F'; } else { @@ -214,8 +217,8 @@ function getVs(bid) { } function getViewabilityData(bid) { - let r = utils.getDataFromLocalStorage(STORAGE_RENDER_PREFIX + bid.adUnitCode) || 0; - let v = utils.getDataFromLocalStorage(STORAGE_VIEW_PREFIX + bid.adUnitCode) || 0; + let r = storage.getDataFromLocalStorage(STORAGE_RENDER_PREFIX + bid.adUnitCode) || 0; + let v = storage.getDataFromLocalStorage(STORAGE_VIEW_PREFIX + bid.adUnitCode) || 0; let ratio = r > 0 ? (v / r) : 0; return { render: r, @@ -408,9 +411,9 @@ function visibilityHandler(obj) { function registerAuction(storageID) { let value; try { - value = utils.getDataFromLocalStorage(storageID); + value = storage.getDataFromLocalStorage(storageID); value = value ? window.parseInt(value, 10) + 1 : 1; - utils.setDataInLocalStorage(storageID, value); + storage.setDataInLocalStorage(storageID, value); } catch (exc) { return false; } diff --git a/modules/fidelityBidAdapter.js b/modules/fidelityBidAdapter.js index a0589986af3..f16a8ea96be 100644 --- a/modules/fidelityBidAdapter.js +++ b/modules/fidelityBidAdapter.js @@ -6,6 +6,7 @@ const BIDDER_SERVER = 'x.fidelity-media.com'; const FIDELITY_VENDOR_ID = 408; export const spec = { code: BIDDER_CODE, + gvlid: 408, isBidRequestValid: function isBidRequestValid(bid) { return !!(bid && bid.params && bid.params.zoneid); }, @@ -26,6 +27,7 @@ export const spec = { tmax: bidderRequest.timeout, defloc: bidderRequest.refererInfo.referer, referrer: getTopWindowReferrer(), + schain: getSupplyChain(bidRequest.schain), }; setConsentParams(bidderRequest.gdprConsent, bidderRequest.uspConsent, payload); @@ -117,4 +119,25 @@ function setConsentParams(gdprConsent, uspConsent, payload) { } } +function getSupplyChain(schain) { + var supplyChain = ''; + if (schain != null && schain.nodes) { + supplyChain = schain.ver + ',' + schain.complete; + for (let i = 0; i < schain.nodes.length; i++) { + supplyChain += '!'; + supplyChain += (schain.nodes[i].asi) ? encodeURIComponent(schain.nodes[i].asi) : ''; + supplyChain += ','; + supplyChain += (schain.nodes[i].sid) ? encodeURIComponent(schain.nodes[i].sid) : ''; + supplyChain += ','; + supplyChain += (schain.nodes[i].hp) ? encodeURIComponent(schain.nodes[i].hp) : ''; + supplyChain += ','; + supplyChain += (schain.nodes[i].rid) ? encodeURIComponent(schain.nodes[i].rid) : ''; + supplyChain += ','; + supplyChain += (schain.nodes[i].name) ? encodeURIComponent(schain.nodes[i].name) : ''; + supplyChain += ','; + supplyChain += (schain.nodes[i].domain) ? encodeURIComponent(schain.nodes[i].domain) : ''; + } + } + return supplyChain; +} registerBidder(spec); diff --git a/modules/fintezaAnalyticsAdapter.js b/modules/fintezaAnalyticsAdapter.js index 41343cdf371..8729035491f 100644 --- a/modules/fintezaAnalyticsAdapter.js +++ b/modules/fintezaAnalyticsAdapter.js @@ -2,8 +2,9 @@ import { ajax } from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; -import { parse as parseURL } from '../src/url.js'; +import { getStorageManager } from '../src/storageManager.js'; +const storage = getStorageManager(); const CONSTANTS = require('../src/constants.json'); const ANALYTICS_TYPE = 'endpoint'; @@ -27,7 +28,7 @@ function getPageInfo() { } if (document.referrer) { - pageInfo.referrerDomain = parseURL(document.referrer).hostname; + pageInfo.referrerDomain = utils.parseUrl(document.referrer).hostname; } return pageInfo; @@ -46,8 +47,8 @@ function getUniqId() { let uniq = cookies[ UNIQ_ID_KEY ]; if (!uniq) { try { - if (utils.hasLocalStorage()) { - uniq = utils.getDataFromLocalStorage(UNIQ_ID_KEY) || ''; + if (storage.hasLocalStorage()) { + uniq = storage.getDataFromLocalStorage(UNIQ_ID_KEY) || ''; isUniqFromLS = true; } } catch (b) {} @@ -62,7 +63,7 @@ function getUniqId() { expires.setFullYear(expires.getFullYear() + 10); try { - utils.setCookie(UNIQ_ID_KEY, uniq, expires.toUTCString()); + storage.setCookie(UNIQ_ID_KEY, uniq, expires.toUTCString()); } catch (e) {} } @@ -90,7 +91,7 @@ function initFirstVisit() { now.setFullYear(now.getFullYear() + 20); try { - utils.setCookie(FIRST_VISIT_DATE, visitDate, now.toUTCString()); + storage.setCookie(FIRST_VISIT_DATE, visitDate, now.toUTCString()); } catch (e) {} } @@ -110,7 +111,7 @@ function parseCookies(cookie) { let param, value; let i, j; - if (!cookie || !utils.cookiesAreEnabled()) { + if (!cookie || !storage.cookiesAreEnabled()) { return {}; } @@ -203,7 +204,7 @@ function initSession() { } try { - utils.setCookie(SESSION_ID, sessionId, expires.toUTCString()); + storage.setCookie(SESSION_ID, sessionId, expires.toUTCString()); } catch (e) {} return { @@ -249,10 +250,10 @@ function saveTrackRequestTime() { const expires = new Date(now + SESSION_DURATION); try { - if (utils.hasLocalStorage()) { - utils.setDataInLocalStorage(TRACK_TIME_KEY, now.toString()); + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage(TRACK_TIME_KEY, now.toString()); } else { - utils.setCookie(TRACK_TIME_KEY, now.toString(), expires.toUTCString()); + storage.setCookie(TRACK_TIME_KEY, now.toString(), expires.toUTCString()); } } catch (a) {} } @@ -261,9 +262,9 @@ function getTrackRequestLastTime() { let cookie; try { - if (utils.hasLocalStorage()) { + if (storage.hasLocalStorage()) { return parseInt( - utils.getDataFromLocalStorage(TRACK_TIME_KEY) || 0, + storage.getDataFromLocalStorage(TRACK_TIME_KEY) || 0, 10, ); } diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index 69668aa288b..34724658631 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -1,4 +1,5 @@ import * as utils from '../src/utils.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'freewheel-ssp'; @@ -213,7 +214,7 @@ var getOutstreamScript = function(bid) { export const spec = { code: BIDDER_CODE, - supportedMediaTypes: ['banner', 'video'], + supportedMediaTypes: [BANNER, VIDEO], aliases: ['stickyadstv'], // former name for freewheel-ssp /** * Determines whether or not the given bid request is valid. @@ -234,77 +235,82 @@ export const spec = { buildRequests: function(bidRequests, bidderRequest) { // var currency = config.getConfig(currency); - var currentBidRequest = bidRequests[0]; - if (bidRequests.length > 1) { - utils.logMessage('Prebid.JS - freewheel bid adapter: only one ad unit is required.'); - } - - var zone = currentBidRequest.params.zoneId; - var timeInMillis = new Date().getTime(); - var keyCode = hashcode(zone + '' + timeInMillis); - var requestParams = { - reqType: 'AdsSetup', - protocolVersion: '2.0', - zoneId: zone, - componentId: getComponentId(currentBidRequest.params.format), - timestamp: timeInMillis, - pKey: keyCode - }; + let buildRequest = (currentBidRequest, bidderRequest) => { + var zone = currentBidRequest.params.zoneId; + var timeInMillis = new Date().getTime(); + var keyCode = hashcode(zone + '' + timeInMillis); + var requestParams = { + reqType: 'AdsSetup', + protocolVersion: '2.0', + zoneId: zone, + componentId: getComponentId(currentBidRequest.params.format), + timestamp: timeInMillis, + pKey: keyCode + }; - // Add GDPR flag and consent string - if (bidderRequest && bidderRequest.gdprConsent) { - requestParams._fw_gdpr_consent = bidderRequest.gdprConsent.consentString; + // Add GDPR flag and consent string + if (bidderRequest && bidderRequest.gdprConsent) { + requestParams._fw_gdpr_consent = bidderRequest.gdprConsent.consentString; - if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { - requestParams._fw_gdpr = bidderRequest.gdprConsent.gdprApplies; + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + requestParams._fw_gdpr = bidderRequest.gdprConsent.gdprApplies; + } } - } - if (currentBidRequest.params.gdpr_consented_providers) { - requestParams._fw_gdpr_consented_providers = currentBidRequest.params.gdpr_consented_providers; - } + if (currentBidRequest.params.gdpr_consented_providers) { + requestParams._fw_gdpr_consented_providers = currentBidRequest.params.gdpr_consented_providers; + } - // Add CCPA consent string - if (bidderRequest && bidderRequest.uspConsent) { - requestParams._fw_us_privacy = bidderRequest.uspConsent; - } + // Add CCPA consent string + if (bidderRequest && bidderRequest.uspConsent) { + requestParams._fw_us_privacy = bidderRequest.uspConsent; + } - var vastParams = currentBidRequest.params.vastUrlParams; - if (typeof vastParams === 'object') { - for (var key in vastParams) { - if (vastParams.hasOwnProperty(key)) { - requestParams[key] = vastParams[key]; + var vastParams = currentBidRequest.params.vastUrlParams; + if (typeof vastParams === 'object') { + for (var key in vastParams) { + if (vastParams.hasOwnProperty(key)) { + requestParams[key] = vastParams[key]; + } } } - } - var location = (bidderRequest && bidderRequest.refererInfo) ? bidderRequest.refererInfo.referer : getTopMostWindow().location.href; - if (isValidUrl(location)) { - requestParams.loc = location; - } + var location = (bidderRequest && bidderRequest.refererInfo) ? bidderRequest.refererInfo.referer : getTopMostWindow().location.href; + if (isValidUrl(location)) { + requestParams.loc = location; + } - var playerSize = []; - if (currentBidRequest.mediaTypes.video && currentBidRequest.mediaTypes.video.playerSize) { - // If mediaTypes is video, get size from mediaTypes.video.playerSize per http://prebid.org/blog/pbjs-3 - playerSize = currentBidRequest.mediaTypes.video.playerSize; - } else if (currentBidRequest.mediaTypes.banner.sizes) { - // If mediaTypes is banner, get size from mediaTypes.banner.sizes per http://prebid.org/blog/pbjs-3 - playerSize = getBiggerSizeWithLimit(currentBidRequest.mediaTypes.banner.sizes, currentBidRequest.mediaTypes.banner.minSizeLimit, currentBidRequest.mediaTypes.banner.maxSizeLimit); - } else { - // Backward compatible code, in case size still pass by sizes in bid request - playerSize = getBiggerSize(currentBidRequest.sizes); - } + var playerSize = []; + if (currentBidRequest.mediaTypes.video && currentBidRequest.mediaTypes.video.playerSize) { + // If mediaTypes is video, get size from mediaTypes.video.playerSize per http://prebid.org/blog/pbjs-3 + if (utils.isArray(currentBidRequest.mediaTypes.video.playerSize[0])) { + playerSize = currentBidRequest.mediaTypes.video.playerSize[0]; + } else { + playerSize = currentBidRequest.mediaTypes.video.playerSize; + } + } else if (currentBidRequest.mediaTypes.banner.sizes) { + // If mediaTypes is banner, get size from mediaTypes.banner.sizes per http://prebid.org/blog/pbjs-3 + playerSize = getBiggerSizeWithLimit(currentBidRequest.mediaTypes.banner.sizes, currentBidRequest.mediaTypes.banner.minSizeLimit, currentBidRequest.mediaTypes.banner.maxSizeLimit); + } else { + // Backward compatible code, in case size still pass by sizes in bid request + playerSize = getBiggerSize(currentBidRequest.sizes); + } - if (playerSize[0] > 0 || playerSize[1] > 0) { - requestParams.playerSize = playerSize[0] + 'x' + playerSize[1]; - } + if (playerSize[0] > 0 || playerSize[1] > 0) { + requestParams.playerSize = playerSize[0] + 'x' + playerSize[1]; + } - return { - method: 'GET', - url: FREEWHEEL_ADSSETUP, - data: requestParams, - bidRequest: currentBidRequest + return { + method: 'GET', + url: FREEWHEEL_ADSSETUP, + data: requestParams, + bidRequest: currentBidRequest + }; }; + + return bidRequests.map(function(currentBidRequest) { + return buildRequest(currentBidRequest, bidderRequest); + }); }, /** @@ -319,7 +325,11 @@ export const spec = { var playerSize = []; if (bidrequest.mediaTypes.video && bidrequest.mediaTypes.video.playerSize) { // If mediaTypes is video, get size from mediaTypes.video.playerSize per http://prebid.org/blog/pbjs-3 - playerSize = bidrequest.mediaTypes.video.playerSize; + if (utils.isArray(bidrequest.mediaTypes.video.playerSize[0])) { + playerSize = bidrequest.mediaTypes.video.playerSize[0]; + } else { + playerSize = bidrequest.mediaTypes.video.playerSize; + } } else if (bidrequest.mediaTypes.banner.sizes) { // If mediaTypes is banner, get size from mediaTypes.banner.sizes per http://prebid.org/blog/pbjs-3 playerSize = getBiggerSizeWithLimit(bidrequest.mediaTypes.banner.sizes, bidrequest.mediaTypes.banner.minSizeLimit, bidrequest.mediaTypes.banner.maxSizeLimit); @@ -363,6 +373,12 @@ export const spec = { netRevenue: true, ttl: 360 }; + + if (bidrequest.mediaTypes.video) { + bidResponse.vastXml = serverResponse; + bidResponse.mediaType = 'video'; + } + bidResponse.ad = formatAdHTML(bidrequest, playerSize); bidResponses.push(bidResponse); } diff --git a/modules/gamoshiBidAdapter.js b/modules/gamoshiBidAdapter.js index e052e31cf40..1316d74e430 100644 --- a/modules/gamoshiBidAdapter.js +++ b/modules/gamoshiBidAdapter.js @@ -42,7 +42,7 @@ export const helper = { export const spec = { code: 'gamoshi', - aliases: ['gambid', 'cleanmedia', '9MediaOnline', 'adtarget'], + aliases: ['gambid', 'cleanmedia', '9MediaOnline'], supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js new file mode 100644 index 00000000000..363d0e396f8 --- /dev/null +++ b/modules/gdprEnforcement.js @@ -0,0 +1,218 @@ +/** + * This module gives publishers extra set of features to enforce individual purposes of TCF v2 + */ + +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { hasDeviceAccess } from '../src/utils.js'; +import adapterManager, { gdprDataHandler } from '../src/adapterManager.js'; +import find from 'core-js/library/fn/array/find.js'; +import includes from 'core-js/library/fn/array/includes.js'; +import { registerSyncInner } from '../src/adapters/bidderFactory.js'; +import { getHook } from '../src/hook.js'; +import { validateStorageEnforcement } from '../src/storageManager.js'; + +const purpose1 = 'storage'; + +let addedDeviceAccessHook = false; +let enforcementRules; + +function getGvlid() { + let gvlid; + const bidderCode = config.getCurrentBidder(); + if (bidderCode) { + const bidder = adapterManager.getBidAdapter(bidderCode); + gvlid = bidder.getSpec().gvlid; + } else { + utils.logWarn('Current module not found'); + } + return gvlid; +} + +/** + * This function takes in rules and consentData as input and validates against the consentData provided. If it returns true Prebid will allow the next call else it will log a warning + * @param {Object} rules enforcement rules set in config + * @param {Object} consentData gdpr consent data + * @returns {boolean} + */ +function validateRules(rule, consentData, currentModule, gvlid) { + let isAllowed = false; + if (!rule.vendorExceptions) rule.vendorExceptions = []; + if (rule.enforcePurpose && rule.enforceVendor) { + if ( + includes(rule.vendorExceptions, currentModule) || + ( + utils.deepAccess(consentData, 'vendorData.purpose.consents.1') === true && + utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true + ) + ) { + isAllowed = true; + } + } else if (rule.enforcePurpose === false && rule.enforceVendor === true) { + if ( + includes(rule.vendorExceptions, currentModule) || + ( + utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true + ) + ) { + isAllowed = true; + } + } else if (rule.enforcePurpose === false && rule.enforceVendor === false) { + if ( + !includes(rule.vendorExceptions, currentModule) || + ( + (utils.deepAccess(consentData, 'vendorData.purpose.consents.1') === true) && + (utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true) + ) + ) { + isAllowed = true; + } + } else if (rule.enforcePurpose === true && rule.enforceVendor === false) { + if ( + (utils.deepAccess(consentData, 'vendorData.purpose.consents.1') === true) && + ( + !includes(rule.vendorExceptions, currentModule) || + (utils.deepAccess(consentData, `vendorData.vendor.consents.${gvlid}`) === true) + ) + ) { + isAllowed = true; + } + } + return isAllowed; +} + +/** + * This hook checks whether module has permission to access device or not. Device access include cookie and local storage + * @param {Function} fn reference to original function (used by hook logic) + * @param {Number=} gvlid gvlid of the module + * @param {string=} moduleName name of the module + */ +export function deviceAccessHook(fn, gvlid, moduleName, result) { + result = Object.assign({}, { + hasEnforcementHook: true + }); + if (!hasDeviceAccess()) { + utils.logWarn('Device access is disabled by Publisher'); + result.valid = false; + fn.call(this, gvlid, moduleName, result); + } else { + const consentData = gdprDataHandler.getConsentData(); + if (consentData && consentData.gdprApplies) { + if (consentData.apiVersion === 2) { + if (!gvlid) { + gvlid = getGvlid(); + } + const curModule = moduleName || config.getCurrentBidder(); + const purpose1Rule = find(enforcementRules, hasPurpose1); + let isAllowed = validateRules(purpose1Rule, consentData, curModule, gvlid); + if (isAllowed) { + result.valid = true; + fn.call(this, gvlid, moduleName, result); + } else { + utils.logWarn(`User denied Permission for Device access for ${curModule}`); + result.valid = false; + fn.call(this, gvlid, moduleName, result); + } + } else { + utils.logInfo('Enforcing TCF2 only'); + result.valid = true; + fn.call(this, gvlid, moduleName, result); + } + } else { + result.valid = true; + fn.call(this, gvlid, moduleName, result); + } + } +} + +/** + * This hook checks if a bidder has consent for user sync or not + * @param {Function} fn reference to original function (used by hook logic) + * @param {...any} args args + */ +export function userSyncHook(fn, ...args) { + const consentData = gdprDataHandler.getConsentData(); + if (consentData && consentData.gdprApplies) { + if (consentData.apiVersion === 2) { + const gvlid = getGvlid(); + const curBidder = config.getCurrentBidder(); + if (gvlid) { + const purpose1Rule = find(enforcementRules, hasPurpose1); + let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); + if (isAllowed) { + fn.call(this, ...args); + } else { + utils.logWarn(`User sync not allowed for ${curBidder}`); + } + } else { + utils.logWarn(`User sync not allowed for ${curBidder}`); + } + } else { + utils.logInfo('Enforcing TCF2 only'); + fn.call(this, ...args); + } + } else { + fn.call(this, ...args); + } +} + +/** + * This hook checks if user id module is given consent or not + * @param {Function} fn reference to original function (used by hook logic) + * @param {Submodule[]} submodules Array of user id submodules + * @param {Object} consentData GDPR consent data + */ +export function userIdHook(fn, submodules, consentData) { + if (consentData && consentData.gdprApplies) { + if (consentData.apiVersion === 2) { + let userIdModules = submodules.map((submodule) => { + const gvlid = submodule.submodule.gvlid; + const moduleName = submodule.submodule.name; + if (gvlid) { + const purpose1Rule = find(enforcementRules, hasPurpose1); + let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); + if (isAllowed) { + return submodule; + } else { + utils.logWarn(`User denied permission to fetch user id for ${moduleName} User id module`); + } + } else { + utils.logWarn(`User denied permission to fetch user id for ${moduleName} User id module`); + } + return undefined; + }).filter(module => module) + fn.call(this, userIdModules, consentData); + } else { + utils.logInfo('Enforcing TCF2 only'); + fn.call(this, submodules, consentData); + } + } else { + fn.call(this, submodules, consentData); + } +} + +const hasPurpose1 = (rule) => { return rule.purpose === purpose1 } + +/** + * A configuration function that initializes some module variables, as well as add hooks + * @param {Object} config GDPR enforcement config object + */ +export function setEnforcementConfig(config) { + const rules = utils.deepAccess(config, 'gdpr.rules'); + if (!rules) { + utils.logWarn('GDPR enforcement rules not defined, exiting enforcement module'); + return; + } + + enforcementRules = rules; + const hasDefinedPurpose1 = find(enforcementRules, hasPurpose1); + if (hasDefinedPurpose1 && !addedDeviceAccessHook) { + addedDeviceAccessHook = true; + validateStorageEnforcement.before(deviceAccessHook, 49); + registerSyncInner.before(userSyncHook, 48); + // Using getHook as user id and gdprEnforcement are both optional modules. Using import will auto include the file in build + getHook('validateGdprEnforcement').before(userIdHook, 47); + } +} + +config.getConfig('consentManagement', config => setEnforcementConfig(config.consentManagement)); diff --git a/modules/gridNMBidAdapter.js b/modules/gridNMBidAdapter.js new file mode 100644 index 00000000000..7e244f8293c --- /dev/null +++ b/modules/gridNMBidAdapter.js @@ -0,0 +1,234 @@ +import * as utils from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { Renderer } from '../src/Renderer.js'; +import { VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'gridNM'; +const ENDPOINT_URL = 'https://grid.bidswitch.net/hbnm'; +const SYNC_URL = 'https://x.bidswitch.net/sync?ssp=iponweblabs'; +const TIME_TO_LIVE = 360; +const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + +let hasSynced = false; + +const LOG_ERROR_MESS = { + noAdm: 'Bid from response has no adm parameter - ', + noPrice: 'Bid from response has no price parameter - ', + wrongContentType: 'Bid from response has wrong content_type parameter - ', + noBid: 'Array of bid objects is empty', + noPlacementCode: 'Can\'t find in requested bids the bid with auid - ', + emptyUids: 'Uids should be not empty', + emptySeatbid: 'Seatbid array from response has empty item', + emptyResponse: 'Response is empty', + hasEmptySeatbidArray: 'Response has empty seatbid array', + hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [ VIDEO ], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + let invalid = + !bid.params.source || !utils.isStr(bid.params.source) || + !bid.params.secid || !utils.isStr(bid.params.secid) || + !bid.params.pubid || !utils.isStr(bid.params.pubid); + + if (!invalid) { + invalid = !bid.params.video || !bid.params.video.protocols || !bid.params.video.mimes; + } + if (!invalid) { + const {protocols, mimes} = bid.params.video; + invalid = !utils.isArray(mimes) || !mimes.length || mimes.filter((it) => !(it && utils.isStr(it))).length; + if (!invalid) { + invalid = !utils.isArray(protocols) || !protocols.length || protocols.filter((it) => !(utils.isNumber(it) && it > 0 && !(it % 1))).length; + } + } + return !invalid; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - an array of bids + * @param {bidderRequest} bidderRequest bidder request object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const bids = validBidRequests || []; + const requests = []; + + bids.forEach(bid => { + const {params, bidderRequestId, sizes} = bid; + const payload = { + sizes: utils.parseSizesInput(sizes).join(','), + r: bidderRequestId, + wrapperType: 'Prebid_js', + wrapperVersion: '$prebid.version$' + }; + + if (bidderRequest) { + if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { + payload.u = bidderRequest.refererInfo.referer; + } + if (bidderRequest.timeout) { + payload.wtimeout = bidderRequest.timeout; + } + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + payload.gdpr_applies = + (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') + ? Number(bidderRequest.gdprConsent.gdprApplies) : 1; + } + if (bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } + } + + requests.push({ + method: 'POST', + url: ENDPOINT_URL + '?' + utils.parseQueryStringParameters(payload).replace(/\&$/, ''), + bid: bid, + data: params // content + }); + }); + + return requests; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @param {*} bidRequest + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + serverResponse = serverResponse && serverResponse.body; + const bidResponses = []; + + let errorMessage; + + if (!serverResponse) errorMessage = LOG_ERROR_MESS.emptyResponse; + else if (serverResponse.seatbid && !serverResponse.seatbid.length) { + errorMessage = LOG_ERROR_MESS.hasEmptySeatbidArray; + } + + if (!errorMessage && serverResponse.seatbid) { + const serverBid = _getBidFromResponse(serverResponse.seatbid[0]); + if (serverBid) { + if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); + else if (!serverBid.price) errorMessage = LOG_ERROR_MESS.noPrice + JSON.stringify(serverBid); + else if (serverBid.content_type !== 'video') errorMessage = LOG_ERROR_MESS.wrongContentType + serverBid.content_type; + if (!errorMessage) { + const bid = bidRequest.bid; + if (!serverBid.w || !serverBid.h) { + const size = utils.parseSizesInput(bid.sizes)[0].split('x'); + serverBid.w = size[0]; + serverBid.h = size[1]; + } + const bidResponse = { + requestId: bid.bidId, + bidderCode: spec.code, + cpm: serverBid.price, + width: serverBid.w, + height: serverBid.h, + creativeId: serverBid.auid || bid.bidderRequestId, + currency: 'USD', + netRevenue: false, + ttl: TIME_TO_LIVE, + dealId: serverBid.dealid, + vastXml: serverBid.adm, + mediaType: VIDEO, + adResponse: { + content: serverBid.adm + } + }; + + if (!bid.renderer && (!bid.mediaTypes || !bid.mediaTypes.video || bid.mediaTypes.video.context === 'outstream')) { + bidResponse.renderer = createRenderer(bidResponse, { + id: bid.bidId, + url: RENDERER_URL + }); + } + bidResponses.push(bidResponse); + } + } + } + if (errorMessage) utils.logError(errorMessage); + return bidResponses; + }, + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + if (!hasSynced && syncOptions.pixelEnabled) { + let params = ''; + + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + params += `&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent) { + params += `&us_privacy=${uspConsent}`; + } + + hasSynced = true; + return { + type: 'image', + url: SYNC_URL + params + }; + } + } +}; + +function _getBidFromResponse(respItem) { + if (!respItem) { + utils.logError(LOG_ERROR_MESS.emptySeatbid); + } else if (!respItem.bid) { + utils.logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); + } else if (!respItem.bid[0]) { + utils.logError(LOG_ERROR_MESS.noBid); + } + return respItem && respItem.bid && respItem.bid[0]; +} + +function outstreamRender (bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: bid.adResponse + }); + }); +} + +function createRenderer (bid, rendererParams) { + const renderer = Renderer.install({ + id: rendererParams.id, + url: rendererParams.url, + loaded: false + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; +} + +export function resetUserSync() { + hasSynced = false; +} + +export function getSyncUrl() { + return SYNC_URL; +} + +registerBidder(spec); diff --git a/modules/gridNMBidAdapter.md b/modules/gridNMBidAdapter.md new file mode 100644 index 00000000000..6decdde7f4c --- /dev/null +++ b/modules/gridNMBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +Module Name: The Grid Media Bidder Adapter +Module Type: Bidder Adapter +Maintainer: grid-tech@themediagrid.com + +# Description + +Module that connects to Grid demand source to fetch bids. +Grid bid adapter supports Banner and Video (instream and outstream). + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + playerSize: [728, 90], + context: 'outstream' + } + }, + bids: [ + { + bidder: "gridNM", + params: { + source: 'jwp', + secid: '11', + pubid: '22', + video: { + mimes: ['video/mp4', 'video/x-ms-wmv'], + protocols: [1,2,3,4,5,6] + } + } + } + ] + } + ]; +``` diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index baa79ecfe04..d071127f6b7 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -4,6 +4,9 @@ import { config } from '../src/config.js' import { BANNER, VIDEO } from '../src/mediaTypes.js'; import includes from 'core-js/library/fn/array/includes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'gumgum' const ALIAS_BIDDER_CODE = ['gg'] @@ -55,9 +58,9 @@ function _getBrowserParams(topWindowUrl) { sw: topScreen.width, sh: topScreen.height, pu: topUrl, - ce: utils.cookiesAreEnabled(), + ce: storage.cookiesAreEnabled(), dpr: topWindow.devicePixelRatio || 1, - jcsi: JSON.stringify({ t: 0, rq: 8 }), + jcsi: encodeURIComponent(JSON.stringify({ t: 0, rq: 8 })), ogu: getOgURL() } diff --git a/modules/hybridBidAdapter.js b/modules/hybridBidAdapter.js new file mode 100644 index 00000000000..34a4cd9e5d6 --- /dev/null +++ b/modules/hybridBidAdapter.js @@ -0,0 +1,198 @@ +import * as utils from '../src/utils.js' +import { registerBidder } from '../src/adapters/bidderFactory.js' +import { auctionManager } from '../src/auctionManager.js' +import { BANNER, VIDEO } from '../src/mediaTypes.js' +import {Renderer} from '../src/Renderer.js'; +import find from 'core-js/library/fn/array/find'; + +const BIDDER_CODE = 'hybrid'; +const DSP_ENDPOINT = 'https://hbe198.hybrid.ai/prebidhb'; +const TRAFFIC_TYPE_WEB = 1; +const PLACEMENT_TYPE_BANNER = 1; +const PLACEMENT_TYPE_VIDEO = 2; +const TTL = 60; +const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + +const placementTypes = { + 'banner': PLACEMENT_TYPE_BANNER, + 'video': PLACEMENT_TYPE_VIDEO +}; + +function buildBidRequests(validBidRequests) { + return utils._map(validBidRequests, function(validBidRequest) { + const params = validBidRequest.params; + const bidRequest = { + bidId: validBidRequest.bidId, + transactionId: validBidRequest.transactionId, + sizes: validBidRequest.sizes, + placement: placementTypes[params.placement], + placeId: params.placeId + }; + + return bidRequest; + }) +} + +const outstreamRender = bid => { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [bid.width, bid.height], + targetId: bid.adUnitCode, + rendererOptions: { + showBigPlayButton: false, + showProgressBar: 'bar', + showVolume: false, + allowFullscreen: true, + skippable: false, + content: bid.vastXml + } + }); + }); +} + +const createRenderer = (bid) => { + const renderer = Renderer.install({ + targetId: bid.adUnitCode, + url: RENDERER_URL, + loaded: false + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + return renderer; +} + +function buildBid(bidData) { + const bid = { + requestId: bidData.bidId, + cpm: bidData.price, + width: bidData.width, + height: bidData.height, + creativeId: bidData.bidId, + currency: bidData.currency, + netRevenue: true, + ttl: TTL + }; + + if (bidData.placement === PLACEMENT_TYPE_VIDEO) { + bid.vastXml = bidData.content; + bid.mediaType = VIDEO; + + let adUnit = find(auctionManager.getAdUnits(), function (unit) { + return unit.transactionId === bidData.transactionId; + }); + + if (adUnit) { + bid.width = adUnit.mediaTypes.video.playerSize[0][0]; + bid.height = adUnit.mediaTypes.video.playerSize[0][1]; + + if (adUnit.mediaTypes.video.context === 'outstream') { + bid.renderer = createRenderer(bid); + } + } + } else { + bid.ad = bidData.content; + bid.mediaType = BANNER; + } + + return bid; +} + +function getMediaTypeFromBid(bid) { + return bid.mediaTypes && Object.keys(bid.mediaTypes)[0] +} + +function hasVideoMandatoryParams(mediaTypes) { + const isHasVideoContext = !!mediaTypes.video && (mediaTypes.video.context === 'instream' || mediaTypes.video.context === 'outstream'); + + const isPlayerSize = + !!utils.deepAccess(mediaTypes, 'video.playerSize') && + utils.isArray(utils.deepAccess(mediaTypes, 'video.playerSize')); + + return isHasVideoContext && isPlayerSize; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + placementTypes: placementTypes, + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid(bid) { + return ( + !!bid.params.placeId && + !!bid.params.placement && + ( + (getMediaTypeFromBid(bid) === BANNER && bid.params.placement === 'banner') || + (getMediaTypeFromBid(bid) === VIDEO && bid.params.placement === 'video' && hasVideoMandatoryParams(bid.mediaTypes)) + ) + ); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests(validBidRequests, bidderRequest) { + const payload = { + url: bidderRequest.refererInfo.referer, + cmp: !!bidderRequest.gdprConsent, + trafficType: TRAFFIC_TYPE_WEB, + bidRequests: buildBidRequests(validBidRequests) + }; + + if (payload.cmp) { + const gdprApplies = bidderRequest.gdprConsent.gdprApplies; + if (gdprApplies !== undefined) payload['ga'] = gdprApplies; + payload['cs'] = bidderRequest.gdprConsent.consentString; + } + + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: DSP_ENDPOINT, + data: payloadString, + options: { + contentType: 'application/json' + } + } + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + let bidRequests = JSON.parse(bidRequest.data).bidRequests; + const serverBody = serverResponse.body; + + if (serverBody && serverBody.bids && utils.isArray(serverBody.bids)) { + return utils._map(serverBody.bids, function(bid) { + let rawBid = find(bidRequests, function (item) { + return item.bidId === bid.bidId; + }); + bid.placement = rawBid.placement; + bid.transactionId = rawBid.transactionId; + bid.placeId = rawBid.placeId; + return buildBid(bid); + }); + } else { + return []; + } + } + +} +registerBidder(spec); diff --git a/modules/hybridBidAdapter.md b/modules/hybridBidAdapter.md new file mode 100644 index 00000000000..245f010970a --- /dev/null +++ b/modules/hybridBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + + +**Module Name**: Hybrid.ai Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: prebid@hybrid.ai + +# Description + +You can use this adapter to get a bid from Hybrid.ai + +About us: https://hybrid.ai + +## Sample Banner Ad Unit + +```js +var adUnits = [{ + code: 'banner_ad_unit', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bids: [{ + bidder: "hybrid", + params: { + placement: "banner", // required + placeId: "5af45ad34d506ee7acad0c26" // required + } + }] +}]; +``` + +## Sample Video Ad Unit + +```js +var adUnits = [{ + code: 'video_ad_unit', + mediaTypes: { + video: { + context: 'outstream', // required + playerSize: [640, 480] // required + } + }, + bids: [{ + bidder: 'hybrid', + params: { + placement: "video", // required + placeId: "5af45ad34d506ee7acad0c26" // required + } + }] +}]; +``` + diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 32617d2f328..1d55450239b 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -16,6 +16,11 @@ export const id5IdSubmodule = { * @type {string} */ name: 'id5Id', + /** + * Vendor id of ID5 + * @type {Number} + */ + gvlid: 131, /** * decode the stored id value for passing to bid requests * @function decode diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index a5b93a7f0ee..47d8ad9ac05 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -42,9 +42,10 @@ export const identityLinkSubmodule = { // use protocol relative urls for http or https const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? '&ct=1&cv=' + gdprConsentString : ''}`; let resp; - // if ats library is initialised, use it to retrieve envelope. If not use standard third party endpoint - if (window.ats) { - resp = function(callback) { + resp = function(callback) { + // Check ats during callback so it has a chance to initialise. + // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint + if (window.ats) { window.ats.retrieveEnvelope(function (envelope) { if (envelope) { callback(JSON.parse(envelope).envelope); @@ -52,15 +53,14 @@ export const identityLinkSubmodule = { getEnvelope(url, callback); } }); - } - } else { - resp = function (callback) { + } else { getEnvelope(url, callback); } - } + }; + return {callback: resp}; } -} +}; // return envelope from third party endpoint function getEnvelope(url, callback) { ajax(url, response => { diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 6d9aa5d159c..dfabf4eef55 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -2,12 +2,15 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import {Renderer} from '../src/Renderer.js'; const BIDDER_CODE = 'improvedigital'; +const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; export const spec = { - version: '6.1.0', + version: '7.0.0', code: BIDDER_CODE, + gvlid: 253, aliases: ['id'], supportedMediaTypes: [BANNER, NATIVE, VIDEO], @@ -61,7 +64,7 @@ export const spec = { if (requestObj.errors && requestObj.errors.length > 0) { utils.logError('ID WARNING 0x01'); } - + requestObj.requests.forEach(request => request.bidderRequest = bidderRequest); return requestObj.requests; }, @@ -71,7 +74,7 @@ export const spec = { * @param {*} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: function (serverResponse, bidRequest) { + interpretResponse: function (serverResponse, {bidderRequest}) { const bids = []; utils._each(serverResponse.body.bid, function (bidObject) { if (!bidObject.price || bidObject.price === null || @@ -79,7 +82,7 @@ export const spec = { (!bidObject.adm && !bidObject.native)) { return; } - + const bidRequest = utils.getBidRequest(bidObject.id, [bidderRequest]); const bid = {}; if (bidObject.native) { @@ -94,6 +97,14 @@ export const spec = { } else if (bidObject.ad_type && bidObject.ad_type === 'video') { bid.vastXml = bidObject.adm; bid.mediaType = VIDEO; + if (isOutstreamVideo(bidRequest)) { + bid.adResponse = { + content: bid.vastXml, + height: bidObject.h, + width: bidObject.w + }; + bid.renderer = createRenderer(bidRequest); + } } else { // Banner let nurl = ''; @@ -136,10 +147,6 @@ export const spec = { if (!bid.width || !bid.height) { bid.width = 1; bid.height = 1; - if (bidRequest.sizes) { - bid.width = bidRequest.sizes[0][0]; - bid.height = bidRequest.sizes[0][1]; - } } bids.push(bid); @@ -180,6 +187,50 @@ function isInstreamVideo(bid) { return bid.mediaType === 'video' || (videoMediaType && context !== 'outstream'); } +function isOutstreamVideo(bid) { + const videoMediaType = utils.deepAccess(bid, 'mediaTypes.video'); + const context = utils.deepAccess(bid, 'mediaTypes.video.context'); + return videoMediaType && context === 'outstream'; +} + +function outstreamRender(bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [bid.width, bid.height], + width: bid.width, + height: bid.height, + targetId: bid.adUnitCode, + adResponse: bid.adResponse, + rendererOptions: { + showBigPlayButton: false, + showProgressBar: 'bar', + showVolume: false, + allowFullscreen: true, + skippable: false, + } + }); + }); +} + +function createRenderer(bidRequest) { + const renderer = Renderer.install({ + id: bidRequest.adUnitCode, + url: RENDERER_URL, + loaded: false, + config: { + player_width: bidRequest.mediaTypes.video.playerSize[0][0], + player_height: bidRequest.mediaTypes.video.playerSize[0][1] + }, + adUnitCode: bidRequest.adUnitCode + }); + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + return renderer; +} + function getNormalizedBidRequest(bid) { let adUnitId = utils.getBidIdParameter('adUnitCode', bid) || null; let placementId = utils.getBidIdParameter('placementId', bid.params) || null; @@ -217,7 +268,7 @@ function getNormalizedBidRequest(bid) { normalizedBidRequest.keyValues = keyValues; } - if (config.getConfig('improvedigital.usePrebidSizes') === true && !isInstreamVideo(bid) && bid.sizes && bid.sizes.length > 0) { + if (config.getConfig('improvedigital.usePrebidSizes') === true && !isInstreamVideo(bid) && !isOutstreamVideo(bid) && bid.sizes && bid.sizes.length > 0) { normalizedBidRequest.format = bid.sizes; } else if (singleSizeFilter && singleSizeFilter.w && singleSizeFilter.h) { normalizedBidRequest.size = {}; diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index 95737c15c3d..e839c173a93 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -1,5 +1,7 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; +const storage = getStorageManager(); const CONSTANTS = { BIDDER_CODE: 'invibes', @@ -276,14 +278,14 @@ function getCappedCampaignsAsString() { let loadData = function () { try { - return JSON.parse(utils.getDataFromLocalStorage(key)) || {}; + return JSON.parse(storage.getDataFromLocalStorage(key)) || {}; } catch (e) { return {}; } }; let saveData = function (data) { - utils.setDataInLocalStorage(key, JSON.stringify(data)); + storage.setDataInLocalStorage(key, JSON.stringify(data)); }; let clearExpired = function () { @@ -319,7 +321,7 @@ function getCappedCampaignsAsString() { const noop = function () { }; function initLogger() { - if (utils.hasLocalStorage() && localStorage.InvibesDEBUG) { + if (storage.hasLocalStorage() && localStorage.InvibesDEBUG) { return window.console; } @@ -384,7 +386,7 @@ invibes.Uid = { let cookieDomain; invibes.getCookie = function (name) { - if (!utils.cookiesAreEnabled()) { return; } + if (!storage.cookiesAreEnabled()) { return; } let i, x, y; let cookies = document.cookie.split(';'); for (i = 0; i < cookies.length; i++) { @@ -398,7 +400,7 @@ invibes.getCookie = function (name) { }; invibes.setCookie = function (name, value, exdays, domain) { - if (!utils.cookiesAreEnabled()) { return; } + if (!storage.cookiesAreEnabled()) { return; } let whiteListed = name == 'ivNoCookie' || name == 'IvbsCampIdsLocal'; if (invibes.noCookies && !whiteListed && (exdays || 0) >= 0) { return; } if (exdays > 365) { exdays = 365; } @@ -406,7 +408,7 @@ invibes.setCookie = function (name, value, exdays, domain) { let exdate = new Date(); let exms = exdays * 24 * 60 * 60 * 1000; exdate.setTime(exdate.getTime() + exms); - utils.setCookie(name, value, exdate.toUTCString(), undefined, domain); + storage.setCookie(name, value, exdate.toUTCString(), undefined, domain); }; let detectTopmostCookieDomain = function () { diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index fd22a21b94f..2725bafdff8 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -17,6 +17,7 @@ const NET_REVENUE = true; const PRICE_TO_DOLLAR_FACTOR = { JPY: 1 }; +const USER_SYNC_URL = 'https://js-sec.indexww.com/um/ixmatch.html'; /** * Transform valid bid request config object to banner impression object that will be sent to ad server. @@ -328,6 +329,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { export const spec = { code: BIDDER_CODE, + gvlid: 10, supportedMediaTypes: SUPPORTED_AD_TYPES, /** @@ -459,6 +461,19 @@ export const spec = { return utils.convertTypes({ 'siteID': 'number' }, params); + }, + + /** + * Determine which user syncs should occur + * @param {object} syncOptions + * @param {array} serverResponses + * @returns {array} User sync pixels + */ + getUserSyncs: function (syncOptions, serverResponses) { + return (syncOptions.iframeEnabled) ? [{ + type: 'iframe', + url: USER_SYNC_URL + }] : []; } }; diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index 4335e496efd..fcbc4ea89e3 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -272,6 +272,23 @@ pbjs.setConfig({ }); ``` +#### User Sync +Add the following code to enable user sync. IX strongly recommends enabling user syncing through iframes. This functionality improves DSP user match rates and increases the IX bid rate and bid price. Be sure to call `pbjs.setConfig()` only once. + +``` +pbjs.setConfig({ + userSync: { + iframeEnabled: true, + filterSettings: { + iframe: { + bidders: ['ix'], + filter: 'include' + } + } + } +}); +``` + ### 2. Include `ixBidAdapter` in your build process When running the build command, include `ixBidAdapter` as a module, as well as `dfpAdServerVideo` if you require video support. diff --git a/modules/justpremiumBidAdapter.js b/modules/justpremiumBidAdapter.js index 7bd32531ccd..1f84feee7d1 100644 --- a/modules/justpremiumBidAdapter.js +++ b/modules/justpremiumBidAdapter.js @@ -100,7 +100,7 @@ export const spec = { getUserSyncs: function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { let url = 'https://pre.ads.justpremium.com/v/1.0/t/sync' + '?_c=' + 'a' + Math.random().toString(36).substring(7) + Date.now(); - if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean')) { + if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean') && gdprConsent.gdprApplies && gdprConsent.consentString) { url = url + '&consentString=' + encodeURIComponent(gdprConsent.consentString) } if (uspConsent) { diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 6d86038e6bd..31c35f4afe3 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -1,6 +1,9 @@ import * as utils from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'kargo'; const HOST = 'https://krk.kargo.com'; const SYNC = 'https://crb.kargo.com/api/v1/initsyncrnd/{UUID}?seed={SEED}&idx={INDEX}'; @@ -98,7 +101,7 @@ export const spec = { // PRIVATE _readCookie(name) { - if (!utils.cookiesAreEnabled()) { + if (!storage.cookiesAreEnabled()) { return null; } let nameEquals = `${name}=`; @@ -173,7 +176,7 @@ export const spec = { _getLocalStorageSafely(key) { try { - return utils.getDataFromLocalStorage(key); + return storage.getDataFromLocalStorage(key); } catch (e) { return null; } diff --git a/modules/konduitWrapper.js b/modules/konduitWrapper.js index c4cb4c8ff81..33bc45f7566 100644 --- a/modules/konduitWrapper.js +++ b/modules/konduitWrapper.js @@ -1,6 +1,5 @@ import { registerVideoSupport } from '../src/adServerManager.js'; import { targeting } from '../src/targeting.js'; -import { format as buildUrl } from '../src/url.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; @@ -52,7 +51,7 @@ export function buildVastUrl(options) { let resultingUrl = null; if (queryParams.konduit_url) { - resultingUrl = buildUrl({ + resultingUrl = utils.buildUrl({ protocol: 'https', host: 'p.konduit.me', pathname: '/api/vastProxy', diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index c020e80d2c6..3f5d0602166 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -9,8 +9,10 @@ import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { LiveConnect } from 'live-connect-js/cjs/live-connect.js'; import { uspDataHandler } from '../src/adapterManager.js'; +import { getStorageManager } from '../src/storageManager.js'; const MODULE_NAME = 'liveIntentId'; +export const storage = getStorageManager(null, MODULE_NAME); let eventFired = false; let liveConnect = null; @@ -83,7 +85,7 @@ function initializeLiveConnect(configParams) { } // The second param is the storage object, which means that all LS & Cookie manipulation will go through PBJS utils. - liveConnect = LiveConnect(liveConnectConfig, utils); + liveConnect = LiveConnect(liveConnectConfig, storage); return liveConnect; } diff --git a/modules/livewrappedAnalyticsAdapter.js b/modules/livewrappedAnalyticsAdapter.js index 47f7f60d141..b331448161e 100644 --- a/modules/livewrappedAnalyticsAdapter.js +++ b/modules/livewrappedAnalyticsAdapter.js @@ -16,8 +16,7 @@ let initOptions; export const BID_WON_TIMEOUT = 500; const cache = { - auctions: {}, - bidAdUnits: {} + auctions: {} }; let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE}), { @@ -28,7 +27,7 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE switch (eventType) { case CONSTANTS.EVENTS.AUCTION_INIT: utils.logInfo('LIVEWRAPPED_AUCTION_INIT:', args); - cache.auctions[args.auctionId] = {bids: {}}; + cache.auctions[args.auctionId] = {bids: {}, bidAdUnits: {}}; break; case CONSTANTS.EVENTS.BID_REQUESTED: utils.logInfo('LIVEWRAPPED_BID_REQUESTED:', args); @@ -64,8 +63,12 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE if (!bidResponse.ttr) { bidResponse.ttr = time - bidResponse.start; } - if (!cache.bidAdUnits[bidResponse.adUnit]) { - cache.bidAdUnits[bidResponse.adUnit] = {sent: 0, timeStamp: cache.auctions[args.auctionId].timeStamp}; + if (!cache.auctions[args.auctionId].bidAdUnits[bidResponse.adUnit]) { + cache.auctions[args.auctionId].bidAdUnits[bidResponse.adUnit] = + { + sent: 0, + timeStamp: cache.auctions[args.auctionId].timeStamp + }; } break; case CONSTANTS.EVENTS.BIDDER_DONE: @@ -240,16 +243,19 @@ function getTimeouts() { function getbidAdUnits() { var bidAdUnits = []; - Object.keys(cache.bidAdUnits).forEach(adUnit => { - let bidAdUnit = cache.bidAdUnits[adUnit]; - if (!bidAdUnit.sent) { - bidAdUnit.sent = 1; - - bidAdUnits.push({ - adUnit: adUnit, - timeStamp: bidAdUnit.timeStamp - }); - } + Object.keys(cache.auctions).forEach(auctionId => { + let auction = cache.auctions[auctionId]; + Object.keys(auction.bidAdUnits).forEach(adUnit => { + let bidAdUnit = auction.bidAdUnits[adUnit]; + if (!bidAdUnit.sent) { + bidAdUnit.sent = 1; + + bidAdUnits.push({ + adUnit: adUnit, + timeStamp: bidAdUnit.timeStamp + }); + } + }); }); return bidAdUnits; diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js index 0e478869b53..57e5a5a7c2f 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -3,10 +3,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import find from 'core-js/library/fn/array/find.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); const BIDDER_CODE = 'livewrapped'; export const URL = 'https://lwadm.com/ad'; -const VERSION = '1.2'; +const VERSION = '1.3'; export const spec = { code: BIDDER_CODE, @@ -26,6 +29,7 @@ export const spec = { * seats: List of bidders and seats Optional. {"bidder name": ["seat 1", "seat 2"], ...} * deviceId: Device id if available Optional. * ifa: Advertising ID Optional. + * bundle: App bundle Optional. Read from config if exists. * options Dynamic data Optional. Optional data to send into adapter. * * @param {BidRequest} bid The bid params to validate. @@ -52,9 +56,10 @@ export const spec = { const seats = find(bidRequests, hasSeatsParam); const deviceId = find(bidRequests, hasDeviceIdParam); const ifa = find(bidRequests, hasIfaParam); + const bundle = find(bidRequests, hasBundleParam); const tid = find(bidRequests, hasTidParam); bidUrl = bidUrl ? bidUrl.params.bidUrl : URL; - url = url ? url.params.url : getTopWindowLocation(bidderRequest); + url = url ? url.params.url : (getAppDomain() || getTopWindowLocation(bidderRequest)); test = test ? test.params.test : undefined; var adRequests = bidRequests.map(bidToAdRequest); @@ -66,12 +71,15 @@ export const spec = { test: test, seats: seats ? seats.params.seats : undefined, deviceId: deviceId ? deviceId.params.deviceId : undefined, - ifa: ifa ? ifa.params.ifa : undefined, + ifa: ifa ? ifa.params.ifa : getDeviceIfa(), + bundle: bundle ? bundle.params.bundle : getAppBundle(), + width: getDeviceWidth(), + height: getDeviceHeight(), tid: tid ? tid.params.tid : undefined, version: VERSION, gdprApplies: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.gdprApplies : undefined, gdprConsent: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.consentString : undefined, - cookieSupport: !utils.isSafariBrowser() && utils.cookiesAreEnabled(), + cookieSupport: !utils.isSafariBrowser() && storage.cookiesAreEnabled(), rcv: getAdblockerRecovered(), adRequests: [...adRequests], rtbData: handleEids(bidRequests) @@ -175,6 +183,10 @@ function hasIfaParam(bid) { return !!bid.params.ifa; } +function hasBundleParam(bid) { + return !!bid.params.bundle; +} + function hasTidParam(bid) { return !!bid.params.tid; } @@ -261,4 +273,40 @@ function getTopWindowLocation(bidderRequest) { return config.getConfig('pageUrl') || url; } +function getAppBundle() { + if (typeof config.getConfig('app') === 'object') { + return config.getConfig('app').bundle; + } +} + +function getAppDomain() { + if (typeof config.getConfig('app') === 'object') { + return config.getConfig('app').domain; + } +} + +function getDeviceIfa() { + if (typeof config.getConfig('device') === 'object') { + return config.getConfig('device').ifa; + } +} + +function getDeviceWidth() { + let device = config.getConfig('device'); + if (typeof device === 'object' && device.width) { + return device.width; + } + + return window.innerWidth; +} + +function getDeviceHeight() { + let device = config.getConfig('device'); + if (typeof device === 'object' && device.height) { + return device.height; + } + + return window.innerHeight; +} + registerBidder(spec); diff --git a/modules/lunamediaBidAdapter.js b/modules/lunamediaBidAdapter.js new file mode 100755 index 00000000000..ee431deea1b --- /dev/null +++ b/modules/lunamediaBidAdapter.js @@ -0,0 +1,383 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { VIDEO, BANNER } from '../src/mediaTypes.js'; +import find from 'core-js/library/fn/array/find.js'; +import includes from 'core-js/library/fn/array/includes.js'; + +const ADAPTER_VERSION = '1.0'; +const BIDDER_CODE = 'lunamedia'; + +export const VIDEO_ENDPOINT = 'https://api.lunamedia.io/xp/get?pubid=';// 0cf8d6d643e13d86a5b6374148a4afac'; +export const BANNER_ENDPOINT = 'https://api.lunamedia.io/xp/get?pubid=';// 0cf8d6d643e13d86a5b6374148a4afac'; +export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; +export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'skip']; +export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; + +let pubid = ''; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid(bidRequest) { + if (typeof bidRequest != 'undefined') { + if (bidRequest.bidder !== BIDDER_CODE && typeof bidRequest.params === 'undefined') { return false; } + if (bidRequest === '' || bidRequest.params.placement === '' || bidRequest.params.pubid === '') { return false; } + return true; + } else { return false; } + }, + + buildRequests(bids, bidderRequest) { + let requests = []; + let videoBids = bids.filter(bid => isVideoBidValid(bid)); + let bannerBids = bids.filter(bid => isBannerBidValid(bid)); + videoBids.forEach(bid => { + pubid = getVideoBidParam(bid, 'pubid'); + requests.push({ + method: 'POST', + url: VIDEO_ENDPOINT + pubid, + data: createVideoRequestData(bid, bidderRequest), + bidRequest: bid + }); + }); + + bannerBids.forEach(bid => { + pubid = getBannerBidParam(bid, 'pubid'); + requests.push({ + method: 'POST', + url: BANNER_ENDPOINT + pubid, + data: createBannerRequestData(bid, bidderRequest), + bidRequest: bid + }); + }); + return requests; + }, + + interpretResponse(serverResponse, {bidRequest}) { + let response = serverResponse.body; + if (response !== null && utils.isEmpty(response) == false) { + if (isVideoBid(bidRequest)) { + let bidResponse = { + requestId: response.id, + bidderCode: BIDDER_CODE, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ttl: response.seatbid[0].bid[0].ttl || 60, + creativeId: response.seatbid[0].bid[0].crid, + currency: response.cur, + mediaType: VIDEO, + netRevenue: true + } + + if (response.seatbid[0].bid[0].adm) { + bidResponse.vastXml = response.seatbid[0].bid[0].adm; + bidResponse.adResponse = { + content: response.seatbid[0].bid[0].adm + }; + } else { + bidResponse.vastUrl = response.seatbid[0].bid[0].nurl; + } + + return bidResponse; + } else { + return { + requestId: response.id, + bidderCode: BIDDER_CODE, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ad: response.seatbid[0].bid[0].adm, + ttl: response.seatbid[0].bid[0].ttl || 60, + creativeId: response.seatbid[0].bid[0].crid, + currency: response.cur, + mediaType: BANNER, + netRevenue: true + } + } + } + } +}; + +function isBannerBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); +} + +function isVideoBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.video'); +} + +function isVideoBidValid(bid) { + return isVideoBid(bid) && getVideoBidParam(bid, 'pubid') && getVideoBidParam(bid, 'placement'); +} + +function isBannerBidValid(bid) { + return isBannerBid(bid) && getBannerBidParam(bid, 'pubid') && getBannerBidParam(bid, 'placement'); +} + +function getVideoBidParam(bid, key) { + return utils.deepAccess(bid, 'params.video.' + key) || utils.deepAccess(bid, 'params.' + key); +} + +function getBannerBidParam(bid, key) { + return utils.deepAccess(bid, 'params.banner.' + key) || utils.deepAccess(bid, 'params.' + key); +} + +function isMobile() { + return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); +} + +function isConnectedTV() { + return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); +} + +function getDoNotTrack() { + return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; +} + +function findAndFillParam(o, key, value) { + try { + if (typeof value === 'function') { + o[key] = value(); + } else { + o[key] = value; + } + } catch (ex) {} +} + +function getOsVersion() { + let clientStrings = [ + { s: 'Android', r: /Android/ }, + { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, + { s: 'Mac OS X', r: /Mac OS X/ }, + { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, + { s: 'Linux', r: /(Linux|X11)/ }, + { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, + { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, + { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, + { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, + { s: 'Windows Vista', r: /Windows NT 6.0/ }, + { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, + { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, + { s: 'UNIX', r: /UNIX/ }, + { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ } + ]; + let cs = find(clientStrings, cs => cs.r.test(navigator.userAgent)); + return cs ? cs.s : 'unknown'; +} + +function getFirstSize(sizes) { + return (sizes && sizes.length) ? sizes[0] : { w: undefined, h: undefined }; +} + +function parseSizes(sizes) { + return utils.parseSizesInput(sizes).map(size => { + let [ width, height ] = size.split('x'); + return { + w: parseInt(width, 10) || undefined, + h: parseInt(height, 10) || undefined + }; + }); +} + +function getVideoSizes(bid) { + return parseSizes(utils.deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); +} + +function getBannerSizes(bid) { + return parseSizes(utils.deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); +} + +function getTopWindowReferrer() { + try { + return window.top.document.referrer; + } catch (e) { + return ''; + } +} + +function getVideoTargetingParams(bid) { + return Object.keys(Object(bid.params.video)) + .filter(param => includes(VIDEO_TARGETING, param)) + .reduce((obj, param) => { + obj[ param ] = bid.params.video[ param ]; + return obj; + }, {}); +} + +function createVideoRequestData(bid, bidderRequest) { + let topLocation = getTopWindowLocation(bidderRequest); + let topReferrer = getTopWindowReferrer(); + + let sizes = getVideoSizes(bid); + let firstSize = getFirstSize(sizes); + + let video = getVideoTargetingParams(bid); + const o = { + 'device': { + 'langauge': (global.navigator.language).split('-')[0], + 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), + 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, + 'js': 1, + 'os': getOsVersion() + }, + 'at': 2, + 'site': {}, + 'tmax': 3000, + 'cur': ['USD'], + 'id': bid.bidId, + 'imp': [], + 'regs': { + 'ext': { + } + }, + 'user': { + 'ext': { + } + } + }; + + o.site['page'] = topLocation.href; + o.site['domain'] = topLocation.hostname; + o.site['search'] = topLocation.search; + o.site['domain'] = topLocation.hostname; + o.site['ref'] = topReferrer; + o.site['mobile'] = isMobile() ? 1 : 0; + const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; + + o.device['dnt'] = getDoNotTrack() ? 1 : 0; + + findAndFillParam(o.site, 'name', function() { + return global.top.document.title; + }); + + findAndFillParam(o.device, 'h', function() { + return global.screen.height; + }); + findAndFillParam(o.device, 'w', function() { + return global.screen.width; + }); + + let placement = getVideoBidParam(bid, 'placement'); + let floor = getVideoBidParam(bid, 'floor'); + if (floor == null) { floor = 0.5; } + + for (let j = 0; j < sizes.length; j++) { + o.imp.push({ + 'id': '' + j, + 'displaymanager': '' + BIDDER_CODE, + 'displaymanagerver': '' + ADAPTER_VERSION, + 'tagId': placement, + 'bidfloor': floor, + 'bidfloorcur': 'USD', + 'secure': secure, + 'video': Object.assign({ + 'id': utils.generateUUID(), + 'pos': 0, + 'w': firstSize.w, + 'h': firstSize.h, + 'mimes': DEFAULT_MIMES + }, video) + + }); + } + + if (bidderRequest && bidderRequest.gdprConsent) { + let { gdprApplies, consentString } = bidderRequest.gdprConsent; + o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; + o.user.ext = {'consent': consentString}; + } + + return o; +} + +function getTopWindowLocation(bidderRequest) { + let url = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer; + return utils.parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); +} + +function createBannerRequestData(bid, bidderRequest) { + let topLocation = getTopWindowLocation(bidderRequest); + let topReferrer = getTopWindowReferrer(); + + let sizes = getBannerSizes(bid); + + const o = { + 'device': { + 'langauge': (global.navigator.language).split('-')[0], + 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), + 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, + 'js': 1 + }, + 'at': 2, + 'site': {}, + 'tmax': 3000, + 'cur': ['USD'], + 'id': bid.bidId, + 'imp': [], + 'regs': { + 'ext': { + } + }, + 'user': { + 'ext': { + } + } + }; + + o.site['page'] = topLocation.href; + o.site['domain'] = topLocation.hostname; + o.site['search'] = topLocation.search; + o.site['domain'] = topLocation.hostname; + o.site['ref'] = topReferrer; + o.site['mobile'] = isMobile() ? 1 : 0; + const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; + + o.device['dnt'] = getDoNotTrack() ? 1 : 0; + + findAndFillParam(o.site, 'name', function() { + return global.top.document.title; + }); + + findAndFillParam(o.device, 'h', function() { + return global.screen.height; + }); + findAndFillParam(o.device, 'w', function() { + return global.screen.width; + }); + + let placement = getBannerBidParam(bid, 'placement'); + for (let j = 0; j < sizes.length; j++) { + let size = sizes[j]; + + let floor = getBannerBidParam(bid, 'floor'); + if (floor == null) { floor = 0.1; } + + o.imp.push({ + 'id': '' + j, + 'displaymanager': '' + BIDDER_CODE, + 'displaymanagerver': '' + ADAPTER_VERSION, + 'tagId': placement, + 'bidfloor': floor, + 'bidfloorcur': 'USD', + 'secure': secure, + 'banner': { + 'id': utils.generateUUID(), + 'pos': 0, + 'w': size['w'], + 'h': size['h'] + } + }); + } + + if (bidderRequest && bidderRequest.gdprConsent) { + let { gdprApplies, consentString } = bidderRequest.gdprConsent; + o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; + o.user.ext = {'consent': consentString}; + } + + return o; +} + +registerBidder(spec); diff --git a/modules/lunamediaBidAdapter.md b/modules/lunamediaBidAdapter.md new file mode 100755 index 00000000000..d0314d0fa41 --- /dev/null +++ b/modules/lunamediaBidAdapter.md @@ -0,0 +1,67 @@ +# Overview + +``` +Module Name: LunaMedia Bidder Adapter +Module Type: Bidder Adapter +Maintainer: lokesh@advangelists.com +``` + +# Description + +Connects to LunaMedia exchange for bids. + +LunaMedia bid adapter supports Banner and Video ads currently. + +For more informatio + +# Sample Display Ad Unit: For Publishers +```javascript + +var displayAdUnit = [ +{ + code: 'display', + mediaTypes: { + banner: { + sizes: [[300, 250],[320, 50]] + } + } + bids: [{ + bidder: 'lunamedia', + params: { + pubid: '121ab139faf7ac67428a23f1d0a9a71b', + placement: 1234 + } + }] +}]; +``` + +# Sample Video Ad Unit: For Publishers +```javascript + +var videoAdUnit = { + code: 'video', + sizes: [320,480], + mediaTypes: { + video: { + playerSize : [[320, 480]], + context: 'instream' + } + }, + bids: [ + { + bidder: 'lunamedia', + params: { + pubid: '121ab139faf7ac67428a23f1d0a9a71b', + placement: 1234, + video: { + id: 123, + skip: 1, + mimes : ['video/mp4', 'application/javascript'], + playbackmethod : [2,6], + maxduration: 30 + } + } + } + ] + }; +``` \ No newline at end of file diff --git a/modules/luponmediaBidAdapter.js b/modules/luponmediaBidAdapter.js new file mode 100644 index 00000000000..0a2eed0ad13 --- /dev/null +++ b/modules/luponmediaBidAdapter.js @@ -0,0 +1,374 @@ +import * as utils from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'luponmedia'; +const ENDPOINT_URL = 'https://rtb.adxpremium.services/openrtb2/auction'; + +const DIGITRUST_PROP_NAMES = { + PREBID_SERVER: { + id: 'id', + keyv: 'keyv' + } +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.siteId && bid.params.keyId); // TODO: check for siteId and keyId + }, + buildRequests: function (bidRequests, bidderRequest) { + const bRequest = { + method: 'POST', + url: ENDPOINT_URL, + data: null, + options: {}, + bidderRequest + }; + + let currentImps = []; + + for (let i = 0, len = bidRequests.length; i < len; i++) { + let newReq = newOrtbBidRequest(bidRequests[i], bidderRequest, currentImps); + currentImps = newReq.imp; + bRequest.data = JSON.stringify(newReq); + } + + return bRequest; + }, + interpretResponse: (response, request) => { + const bidResponses = []; + var respCur = 'USD'; + let parsedRequest = JSON.parse(request.data); + let parsedReferrer = parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : ''; + try { + if (response.body && response.body.seatbid && utils.isArray(response.body.seatbid)) { + // Supporting multiple bid responses for same adSize + respCur = response.body.cur || respCur; + response.body.seatbid.forEach(seatbidder => { + seatbidder.bid && + utils.isArray(seatbidder.bid) && + seatbidder.bid.forEach(bid => { + let newBid = { + requestId: bid.impid, + cpm: (parseFloat(bid.price) || 0).toFixed(2), + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.id, + dealId: bid.dealid, + currency: respCur, + netRevenue: false, + ttl: 300, + referrer: parsedReferrer, + ad: bid.adm + }; + + bidResponses.push(newBid); + }); + }); + } + } catch (error) { + utils.logError(error); + } + return bidResponses; + }, + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + let allUserSyncs = []; + if (!hasSynced && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { + responses.forEach(csResp => { + if (csResp.body && csResp.body.ext && csResp.body.ext.usersyncs) { + try { + let response = csResp.body.ext.usersyncs + let bidders = response.bidder_status; + for (let synci in bidders) { + let thisSync = bidders[synci]; + if (thisSync.no_cookie) { + let url = thisSync.usersync.url; + let type = thisSync.usersync.type; + + if (!url) { + utils.logError(`No sync url for bidder luponmedia.`); + } else if ((type === 'image' || type === 'redirect') && syncOptions.pixelEnabled) { + utils.logMessage(`Invoking image pixel user sync for luponmedia`); + allUserSyncs.push({type: 'image', url: url}); + } else if (type == 'iframe' && syncOptions.iframeEnabled) { + utils.logMessage(`Invoking iframe user sync for luponmedia`); + allUserSyncs.push({type: 'iframe', url: url}); + } else { + utils.logError(`User sync type "${type}" not supported for luponmedia`); + } + } + } + } catch (e) { + utils.logError(e); + } + } + }); + } else { + utils.logWarn('Luponmedia: Please enable iframe/pixel based user sync.'); + } + + hasSynced = true; + return allUserSyncs; + }, +}; + +function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { + bidRequest.startTime = new Date().getTime(); + + const bannerParams = utils.deepAccess(bidRequest, 'mediaTypes.banner'); + + let bannerSizes = []; + + if (bannerParams && bannerParams.sizes) { + const sizes = utils.parseSizesInput(bannerParams.sizes); + + // get banner sizes in form [{ w: , h: }, ...] + const format = sizes.map(size => { + const [ width, height ] = size.split('x'); + const w = parseInt(width, 10); + const h = parseInt(height, 10); + return { w, h }; + }); + + bannerSizes = format; + } + + const data = { + id: bidRequest.transactionId, + test: config.getConfig('debug') ? 1 : 0, + source: { + tid: bidRequest.transactionId + }, + tmax: config.getConfig('timeout') || 1500, + imp: currentImps.concat([{ + id: bidRequest.bidId, + secure: 1, + ext: { + [bidRequest.bidder]: bidRequest.params + }, + banner: { + format: bannerSizes + } + }]), + ext: { + prebid: { + targeting: { + includewinners: true, + // includebidderkeys always false for openrtb + includebidderkeys: false + } + } + }, + user: { + } + } + + const bidFloor = parseFloat(utils.deepAccess(bidRequest, 'params.floor')); + if (!isNaN(bidFloor)) { + data.imp[0].bidfloor = bidFloor; + } + appendSiteAppDevice(data, bidRequest, bidderRequest); + + const digiTrust = _getDigiTrustQueryParams(bidRequest, 'PREBID_SERVER'); + if (digiTrust) { + utils.deepSetValue(data, 'user.ext.digitrust', digiTrust); + } + + if (bidderRequest.gdprConsent) { + // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module + let gdprApplies; + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + gdprApplies = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + + utils.deepSetValue(data, 'regs.ext.gdpr', gdprApplies); + utils.deepSetValue(data, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + } + + if (bidderRequest.uspConsent) { + utils.deepSetValue(data, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + // Set user uuid + utils.deepSetValue(data, 'user.id', utils.generateUUID()); + + // set crumbs + if (bidRequest.crumbs && bidRequest.crumbs.pubcid) { + utils.deepSetValue(data, 'user.buyeruid', bidRequest.crumbs.pubcid); + } else { + utils.deepSetValue(data, 'user.buyeruid', utils.generateUUID()); + } + + if (bidRequest.userId && typeof bidRequest.userId === 'object' && + (bidRequest.userId.tdid || bidRequest.userId.pubcid || bidRequest.userId.lipb || bidRequest.userId.idl_env)) { + utils.deepSetValue(data, 'user.ext.eids', []); + + if (bidRequest.userId.tdid) { + data.user.ext.eids.push({ + source: 'adserver.org', + uids: [{ + id: bidRequest.userId.tdid, + ext: { + rtiPartner: 'TDID' + } + }] + }); + } + + if (bidRequest.userId.pubcid) { + data.user.ext.eids.push({ + source: 'pubcommon', + uids: [{ + id: bidRequest.userId.pubcid, + }] + }); + } + + // support liveintent ID + if (bidRequest.userId.lipb && bidRequest.userId.lipb.lipbid) { + data.user.ext.eids.push({ + source: 'liveintent.com', + uids: [{ + id: bidRequest.userId.lipb.lipbid + }] + }); + + data.user.ext.tpid = { + source: 'liveintent.com', + uid: bidRequest.userId.lipb.lipbid + }; + + if (Array.isArray(bidRequest.userId.lipb.segments) && bidRequest.userId.lipb.segments.length) { + utils.deepSetValue(data, 'rp.target.LIseg', bidRequest.userId.lipb.segments); + } + } + + // support identityLink (aka LiveRamp) + if (bidRequest.userId.idl_env) { + data.user.ext.eids.push({ + source: 'liveramp.com', + uids: [{ + id: bidRequest.userId.idl_env + }] + }); + } + } + + if (config.getConfig('coppa') === true) { + utils.deepSetValue(data, 'regs.coppa', 1); + } + + if (bidRequest.schain && hasValidSupplyChainParams(bidRequest.schain)) { + utils.deepSetValue(data, 'source.ext.schain', bidRequest.schain); + } + + const siteData = Object.assign({}, bidRequest.params.inventory, config.getConfig('fpd.context')); + const userData = Object.assign({}, bidRequest.params.visitor, config.getConfig('fpd.user')); + + if (!utils.isEmpty(siteData) || !utils.isEmpty(userData)) { + const bidderData = { + bidders: [ bidderRequest.bidderCode ], + config: { + fpd: {} + } + }; + + if (!utils.isEmpty(siteData)) { + bidderData.config.fpd.site = siteData; + } + + if (!utils.isEmpty(userData)) { + bidderData.config.fpd.user = userData; + } + + utils.deepSetValue(data, 'ext.prebid.bidderconfig.0', bidderData); + } + + const pbAdSlot = utils.deepAccess(bidRequest, 'fpd.context.pbAdSlot'); + if (typeof pbAdSlot === 'string' && pbAdSlot) { + utils.deepSetValue(data.imp[0].ext, 'context.data.adslot', pbAdSlot); + } + + return data; +} + +export function hasValidSupplyChainParams(schain) { + let isValid = false; + const requiredFields = ['asi', 'sid', 'hp']; + if (!schain.nodes) return isValid; + isValid = schain.nodes.reduce((status, node) => { + if (!status) return status; + return requiredFields.every(field => node[field]); + }, true); + if (!isValid) utils.logError('LuponMedia: required schain params missing'); + return isValid; +} + +function _getDigiTrustQueryParams(bidRequest = {}, endpointName) { + if (!endpointName || !DIGITRUST_PROP_NAMES[endpointName]) { + return null; + } + const propNames = DIGITRUST_PROP_NAMES[endpointName]; + + function getDigiTrustId() { + const bidRequestDigitrust = utils.deepAccess(bidRequest, 'userId.digitrustid.data'); + if (bidRequestDigitrust) { + return bidRequestDigitrust; + } + + let digiTrustUser = (window.DigiTrust && (config.getConfig('digiTrustId') || window.DigiTrust.getUser({member: 'T9QSFKPDN9'}))); + return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; + } + + let digiTrustId = getDigiTrustId(); + // Verify there is an ID and this user has not opted out + if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) { + return null; + } + + const digiTrustQueryParams = { + [propNames.id]: digiTrustId.id, + [propNames.keyv]: digiTrustId.keyv + }; + if (propNames.pref) { + digiTrustQueryParams[propNames.pref] = 0; + } + return digiTrustQueryParams; +} + +function _getPageUrl(bidRequest, bidderRequest) { + let pageUrl = config.getConfig('pageUrl'); + if (bidRequest.params.referrer) { + pageUrl = bidRequest.params.referrer; + } else if (!pageUrl) { + pageUrl = bidderRequest.refererInfo.referer; + } + return bidRequest.params.secure ? pageUrl.replace(/^http:/i, 'https:') : pageUrl; +} + +function appendSiteAppDevice(data, bidRequest, bidderRequest) { + if (!data) return; + + // ORTB specifies app OR site + if (typeof config.getConfig('app') === 'object') { + data.app = config.getConfig('app'); + } else { + data.site = { + page: _getPageUrl(bidRequest, bidderRequest) + } + } + if (typeof config.getConfig('device') === 'object') { + data.device = config.getConfig('device'); + } +} + +var hasSynced = false; + +export function resetUserSync() { + hasSynced = false; +} + +registerBidder(spec); diff --git a/modules/luponmediaBidAdapter.md b/modules/luponmediaBidAdapter.md new file mode 100644 index 00000000000..cec0aa3ec84 --- /dev/null +++ b/modules/luponmediaBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: LuponMedia Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@luponmedia.com +``` + +# Description + +Module that connects to LuponMedia's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: "luponmedia", + params: { + siteId: 12345, + keyId: '4o2c4' + } + } + ] + } + ]; +``` diff --git a/modules/mantisBidAdapter.js b/modules/mantisBidAdapter.js index 4e32f3f70ac..19e70a3e68b 100644 --- a/modules/mantisBidAdapter.js +++ b/modules/mantisBidAdapter.js @@ -1,5 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); function inIframe() { try { @@ -93,9 +95,9 @@ function storeUuid(uuid) { return false; } window.mantis_uuid = uuid; - if (utils.hasLocalStorage()) { + if (storage.hasLocalStorage()) { try { - utils.setDataInLocalStorage('mantis:uuid', uuid); + storage.setDataInLocalStorage('mantis:uuid', uuid); } catch (ex) { } } @@ -176,8 +178,8 @@ function buildMantisUrl(path, data, domain) { } if (window.mantis_uuid) { params.uuid = window.mantis_uuid; - } else if (utils.hasLocalStorage()) { - var localUuid = utils.getDataFromLocalStorage('mantis:uuid'); + } else if (storage.hasLocalStorage()) { + var localUuid = storage.getDataFromLocalStorage('mantis:uuid'); if (localUuid) { params.uuid = localUuid; } diff --git a/modules/marsmediaBidAdapter.js b/modules/marsmediaBidAdapter.js index 146e1495782..b6dc7800dd9 100644 --- a/modules/marsmediaBidAdapter.js +++ b/modules/marsmediaBidAdapter.js @@ -16,7 +16,7 @@ function MarsmediaAdapter() { let SUPPORTED_VIDEO_API = [1, 2, 5]; let slotsToBids = {}; let that = this; - let version = '2.1'; + let version = '2.2'; this.isBidRequestValid = function (bid) { return !!(bid.params && bid.params.zoneId); @@ -172,7 +172,7 @@ function MarsmediaAdapter() { } }, at: 1, - tmax: 1000, + tmax: 650, regs: { ext: { gdpr: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies') ? Boolean(bidderRequest.gdprConsent.gdprApplies & 1) : false @@ -203,7 +203,7 @@ function MarsmediaAdapter() { return []; } - var uri = 'https://bid306.rtbsrv.com/bidder/?bid=3mhdom&zoneId=' + fallbackZoneId; + var uri = 'https://hb.go2speed.media/bidder/?bid=3mhdom&zoneId=' + fallbackZoneId; var fat = /(^v|(\.0)+$)/gi; var prebidVersion = '$prebid.version$'; @@ -240,7 +240,7 @@ function MarsmediaAdapter() { let bid = responses[i]; let bidRequest = slotsToBids[bid.impid]; let bidResponse = { - requestId: bidRequest.id, + requestId: bidRequest.bidId, bidderCode: that.code, cpm: parseFloat(bid.price), width: bid.w, @@ -252,12 +252,17 @@ function MarsmediaAdapter() { }; if (bidRequest.mediaTypes && bidRequest.mediaTypes.video) { - bidResponse.vastUrl = bid.adm; + if (bid.adm.charAt(0) === '<') { + bidResponse.vastXml = bid.adm; + } else { + bidResponse.vastUrl = bid.adm; + } bidResponse.mediaType = 'video'; bidResponse.ttl = 600; } else { bidResponse.ad = bid.adm; } + bids.push(bidResponse); } diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js new file mode 100644 index 00000000000..3da69be92eb --- /dev/null +++ b/modules/medianetAnalyticsAdapter.js @@ -0,0 +1,633 @@ +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; +import * as utils from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { getPriceGranularity, AUCTION_IN_PROGRESS, AUCTION_COMPLETED } from '../src/auction.js' + +const analyticsType = 'endpoint'; +const ENDPOINT = 'https://pb-logs.media.net/log?logid=kfk&evtid=prebid_analytics_events_client'; +const CONFIG_URL = 'https://prebid.media.net/rtb/prebid/analytics/config'; +const EVENT_PIXEL_URL = 'https://qsearch-a.akamaihd.net/log'; +const DEFAULT_LOGGING_PERCENT = 50; +const PRICE_GRANULARITY = { + 'auto': 'pbAg', + 'custom': 'pbCg', + 'dense': 'pbDg', + 'low': 'pbLg', + 'medium': 'pbMg', + 'high': 'pbHg', +}; + +const MEDIANET_BIDDER_CODE = 'medianet'; +// eslint-disable-next-line no-undef +const PREBID_VERSION = $$PREBID_GLOBAL$$.version; +const ERROR_CONFIG_JSON_PARSE = 'analytics_config_parse_fail'; +const ERROR_CONFIG_FETCH = 'analytics_config_ajax_fail'; +const BID_SUCCESS = 1; +const BID_NOBID = 2; +const BID_TIMEOUT = 3; +const DUMMY_BIDDER = '-2'; + +const CONFIG_PENDING = 0; +const CONFIG_PASS = 1; +const CONFIG_ERROR = 3; + +const VALID_URL_KEY = ['canonical_url', 'og_url', 'twitter_url']; +const DEFAULT_URL_KEY = 'page'; + +let auctions = {}; +let config; +let pageDetails; +let logsQueue = []; + +class ErrorLogger { + constructor(event, additionalData) { + this.event = event; + this.logid = 'kfk'; + this.evtid = 'projectevents'; + this.project = 'prebidanalytics'; + this.dn = pageDetails.domain || ''; + this.requrl = pageDetails.requrl || ''; + this.event = this.event; + this.pbversion = PREBID_VERSION; + this.cid = config.cid || ''; + this.rd = additionalData; + } + + send() { + let url = EVENT_PIXEL_URL + '?' + formatQS(this); + utils.triggerPixel(url); + } +} + +class Configure { + constructor(cid) { + this.cid = cid; + this.pubLper = -1; + this.ajaxState = CONFIG_PENDING; + this.loggingPercent = DEFAULT_LOGGING_PERCENT; + this.urlToConsume = DEFAULT_URL_KEY; + this.debug = false; + this.gdprConsent = undefined; + this.uspConsent = undefined; + } + + set publisherLper(plper) { + this.pubLper = plper; + } + + getLoggingData() { + return { + cid: this.cid, + lper: Math.round(100 / this.loggingPercent), + plper: this.pubLper, + gdpr: this.gdprConsent, + ccpa: this.uspConsent, + ajx: this.ajaxState, + pbv: PREBID_VERSION, + flt: 1, + } + } + + _configURL() { + return CONFIG_URL + '?cid=' + encodeURIComponent(this.cid) + '&dn=' + encodeURIComponent(pageDetails.domain); + } + + _parseResponse(response) { + try { + response = JSON.parse(response); + if (isNaN(response.percentage)) { + throw new Error('not a number'); + } + this.loggingPercent = response.percentage; + this.urlToConsume = VALID_URL_KEY.includes(response.urlKey) ? response.urlKey : this.urlToConsume; + this.ajaxState = CONFIG_PASS; + } catch (e) { + this.ajaxState = CONFIG_ERROR; + /* eslint no-new: "error" */ + new ErrorLogger(ERROR_CONFIG_JSON_PARSE, e).send(); + } + } + + _errorFetch() { + this.ajaxState = CONFIG_ERROR; + /* eslint no-new: "error" */ + new ErrorLogger(ERROR_CONFIG_FETCH).send(); + } + + init() { + // Forces Logging % to 100% + let urlObj = utils.parseUrl(pageDetails.page); + if (utils.deepAccess(urlObj, 'search.medianet_test') || urlObj.hostname === 'localhost') { + this.loggingPercent = 100; + this.ajaxState = CONFIG_PASS; + this.debug = true; + return; + } + ajax( + this._configURL(), + { + success: this._parseResponse.bind(this), + error: this._errorFetch.bind(this) + } + ); + } +} + +class PageDetail { + constructor () { + const canonicalUrl = this._getUrlFromSelector('link[rel="canonical"]', 'href'); + const ogUrl = this._getUrlFromSelector('meta[property="og:url"]', 'content'); + const twitterUrl = this._getUrlFromSelector('meta[name="twitter:url"]', 'content'); + const refererInfo = getRefererInfo(); + + this.domain = utils.parseUrl(refererInfo.referer).host; + this.page = refererInfo.referer; + this.is_top = refererInfo.reachedTop; + this.referrer = this._getTopWindowReferrer(); + this.canonical_url = canonicalUrl; + this.og_url = ogUrl; + this.twitter_url = twitterUrl; + this.screen = this._getWindowSize() + } + + _getTopWindowReferrer() { + try { + return window.top.document.referrer; + } catch (e) { + return document.referrer; + } + } + + _getWindowSize() { + let w = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth || -1; + let h = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || -1; + return `${w}x${h}`; + } + + _getAttributeFromSelector(selector, attribute) { + try { + let doc = utils.getWindowTop().document; + let element = doc.querySelector(selector); + if (element !== null && element[attribute]) { + return element[attribute]; + } + } catch (e) {} + } + + _getAbsoluteUrl(url) { + let aTag = utils.getWindowTop().document.createElement('a'); + aTag.href = url; + + return aTag.href; + } + + _getUrlFromSelector(selector, attribute) { + let attr = this._getAttributeFromSelector(selector, attribute); + return attr && this._getAbsoluteUrl(attr); + } + + getLoggingData() { + return { + requrl: this[config.urlToConsume] || this.page, + dn: this.domain, + ref: this.referrer, + screen: this.screen + } + } +} + +class AdSlot { + constructor(mediaTypes, bannerSizes, tmax, supplyAdCode, adext) { + this.mediaTypes = mediaTypes; + this.bannerSizes = bannerSizes; + this.tmax = tmax; + this.supplyAdCode = supplyAdCode; + this.adext = adext; + this.logged = false; + this.targeting = undefined; + } + + getLoggingData() { + return Object.assign({ + supcrid: this.supplyAdCode, + mediaTypes: this.mediaTypes && this.mediaTypes.join('|'), + szs: this.bannerSizes.join('|'), + tmax: this.tmax, + targ: JSON.stringify(this.targeting) + }, + this.adext && {'adext': JSON.stringify(this.adext)}, + ); + } +} + +class Bid { + constructor(bidId, bidder, src, start, supplyAdCode) { + this.bidId = bidId; + this.bidder = bidder; + this.src = src; + this.start = start; + this.supplyAdCode = supplyAdCode; + this.iwb = 0; + this.winner = 0; + this.status = bidder === DUMMY_BIDDER ? BID_SUCCESS : BID_TIMEOUT; + this.ext = {}; + this.originalCpm = undefined; + this.cpm = undefined; + this.dfpbd = undefined; + this.width = undefined; + this.height = undefined; + this.mediaType = undefined; + this.timeToRespond = undefined; + this.dealId = undefined; + this.creativeId = undefined; + this.adId = undefined; + this.currency = undefined; + this.crid = undefined; + this.pubcrid = undefined; + this.mpvid = undefined; + } + + get size() { + if (!this.width || !this.height) { + return ''; + } + return this.width + 'x' + this.height; + } + + getLoggingData() { + return { + pvnm: this.bidder, + src: this.src, + ogbdp: this.originalCpm, + bdp: this.cpm, + cbdp: this.dfpbd, + dfpbd: this.dfpbd, + size: this.size, + mtype: this.mediaType, + dId: this.dealId, + winner: this.winner, + curr: this.currency, + rests: this.timeToRespond, + status: this.status, + iwb: this.iwb, + crid: this.crid, + pubcrid: this.pubcrid, + mpvid: this.mpvid, + ext: JSON.stringify(this.ext) + } + } +} + +class Auction { + constructor(acid) { + this.acid = acid; + this.status = AUCTION_IN_PROGRESS; + this.bids = []; + this.adSlots = {}; + this.auctionInitTime = undefined; + this.auctionStartTime = undefined; + this.setTargetingTime = undefined; + this.auctionEndTime = undefined; + this.bidWonTime = undefined; + } + + hasEnded() { + return this.status === AUCTION_COMPLETED; + } + + getLoggingData() { + return { + sts: this.auctionStartTime - this.auctionInitTime, + ets: this.auctionEndTime - this.auctionInitTime, + tts: this.setTargetingTime - this.auctionInitTime, + wts: this.bidWonTime - this.auctionInitTime, + aucstatus: this.status + } + } + + addSlot(supplyAdCode, { mediaTypes, bannerSizes, tmax, adext }) { + if (supplyAdCode && this.adSlots[supplyAdCode] === undefined) { + this.adSlots[supplyAdCode] = new AdSlot(mediaTypes, bannerSizes, tmax, supplyAdCode, adext); + this.addBid( + new Bid('-1', DUMMY_BIDDER, 'client', '-1', supplyAdCode) + ); + } + } + + addBid(bid) { + this.bids.push(bid); + } + + findBid(key, value) { + return this.bids.filter(bid => { + return bid[key] === value + })[0]; + } + + getAdslotBids(adslot) { + return this.bids + .filter((bid) => bid.supplyAdCode === adslot) + .map((bid) => bid.getLoggingData()); + } + + getWinnerAdslotBid(adslot) { + return this.getAdslotBids(adslot).filter((bid) => bid.winner); + } +} + +function auctionInitHandler({auctionId, timestamp}) { + if (auctionId && auctions[auctionId] === undefined) { + auctions[auctionId] = new Auction(auctionId); + auctions[auctionId].auctionInitTime = timestamp; + } +} + +function bidRequestedHandler({ auctionId, auctionStart, bids, start, timeout, uspConsent, gdpr }) { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + + if (gdpr && gdpr.gdprApplies) { + config.gdprConsent = gdpr.consentString || ''; + } + + config.uspConsent = config.uspConsent || uspConsent; + + bids.forEach(bid => { + const { adUnitCode, bidder, mediaTypes, sizes, bidId, src } = bid; + if (!auctions[auctionId].adSlots[adUnitCode]) { + auctions[auctionId].auctionStartTime = auctionStart; + auctions[auctionId].addSlot( + adUnitCode, + Object.assign({}, + (mediaTypes instanceof Object) && { mediaTypes: Object.keys(mediaTypes) }, + { bannerSizes: utils.deepAccess(mediaTypes, 'banner.sizes') || sizes || [] }, + { adext: utils.deepAccess(mediaTypes, 'banner.ext') || '' }, + { tmax: timeout } + ) + ); + } + let bidObj = new Bid(bidId, bidder, src, start, adUnitCode); + auctions[auctionId].addBid(bidObj); + if (bidder === MEDIANET_BIDDER_CODE) { + bidObj.crid = utils.deepAccess(bid, 'params.crid'); + bidObj.pubcrid = utils.deepAccess(bid, 'params.crid'); + } + }); +} + +function bidResponseHandler(bid) { + const { width, height, mediaType, cpm, requestId, timeToRespond, auctionId, dealId } = bid; + const {originalCpm, bidderCode, creativeId, adId, currency} = bid; + + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', requestId); + if (!(bidObj instanceof Bid)) { + return; + } + Object.assign( + bidObj, + { cpm, width, height, mediaType, timeToRespond, dealId, creativeId }, + { adId, currency, originalCpm } + ); + let dfpbd = utils.deepAccess(bid, 'adserverTargeting.hb_pb'); + if (!dfpbd) { + let priceGranularity = getPriceGranularity(mediaType, bid); + let priceGranularityKey = PRICE_GRANULARITY[priceGranularity]; + dfpbd = bid[priceGranularityKey] || cpm; + } + bidObj.dfpbd = dfpbd; + bidObj.status = BID_SUCCESS; + + if (bidderCode === MEDIANET_BIDDER_CODE && bid.ext instanceof Object) { + Object.assign( + bidObj, + { 'ext': bid.ext }, + { 'mpvid': bid.ext.pvid }, + bid.ext.crid && { 'crid': bid.ext.crid } + ); + } +} + +function noBidResponseHandler({ auctionId, bidId }) { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + if (auctions[auctionId].hasEnded()) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', bidId); + if (!(bidObj instanceof Bid)) { + return; + } + bidObj.status = BID_NOBID; +} + +function bidTimeoutHandler(timedOutBids) { + timedOutBids.map(({bidId, auctionId}) => { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', bidId); + if (!(bidObj instanceof Bid)) { + return; + } + bidObj.status = BID_TIMEOUT; + }) +} + +function auctionEndHandler({ auctionId, auctionEnd }) { + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + auctions[auctionId].status = AUCTION_COMPLETED; + auctions[auctionId].auctionEndTime = auctionEnd; +} + +function setTargetingHandler(params) { + for (const adunit of Object.keys(params)) { + for (const auctionId of Object.keys(auctions)) { + let auctionObj = auctions[auctionId]; + let adunitObj = auctionObj.adSlots[adunit]; + if (!(adunitObj instanceof AdSlot)) { + continue; + } + adunitObj.targeting = params[adunit]; + auctionObj.setTargetingTime = Date.now(); + let targetingObj = Object.keys(params[adunit]).reduce((result, key) => { + if (key.indexOf('hb_adid') !== -1) { + result[key] = params[adunit][key] + } + return result; + }, {}); + let bidAdIds = Object.keys(targetingObj).map(k => targetingObj[k]); + auctionObj.bids.filter((bid) => bidAdIds.indexOf(bid.adId) !== -1).map(function(bid) { + bid.iwb = 1; + }); + sendEvent(auctionId, adunit, false); + } + } +} + +function bidWonHandler(bid) { + const { requestId, auctionId, adUnitCode } = bid; + if (!(auctions[auctionId] instanceof Auction)) { + return; + } + let bidObj = auctions[auctionId].findBid('bidId', requestId); + if (!(bidObj instanceof Bid)) { + return; + } + auctions[auctionId].bidWonTime = Date.now(); + bidObj.winner = 1; + sendEvent(auctionId, adUnitCode, true); +} + +function isSampled() { + return Math.random() * 100 < parseFloat(config.loggingPercent); +} + +function isValidAuctionAdSlot(acid, adtag) { + return (auctions[acid] instanceof Auction) && (auctions[acid].adSlots[adtag] instanceof AdSlot); +} + +function sendEvent(id, adunit, isBidWonEvent) { + if (!isValidAuctionAdSlot(id, adunit)) { + return; + } + if (isBidWonEvent) { + fireAuctionLog(id, adunit, isBidWonEvent); + } else if (isSampled() && !auctions[id].adSlots[adunit].logged) { + auctions[id].adSlots[adunit].logged = true; + fireAuctionLog(id, adunit, isBidWonEvent); + } +} + +function getCommonLoggingData(acid, adtag) { + let commonParams = Object.assign(pageDetails.getLoggingData(), config.getLoggingData()); + let adunitParams = auctions[acid].adSlots[adtag].getLoggingData(); + let auctionParams = auctions[acid].getLoggingData(); + return Object.assign(commonParams, adunitParams, auctionParams); +} + +function fireAuctionLog(acid, adtag, isBidWonEvent) { + let commonParams = getCommonLoggingData(acid, adtag); + let targeting = utils.deepAccess(commonParams, 'targ'); + + Object.keys(commonParams).forEach((key) => (commonParams[key] == null) && delete commonParams[key]); + delete commonParams.targ; + + let bidParams; + + if (isBidWonEvent) { + bidParams = auctions[acid].getWinnerAdslotBid(adtag); + commonParams.lper = 1; + } else { + bidParams = auctions[acid].getAdslotBids(adtag).map(({winner, ...restParams}) => restParams); + delete commonParams.wts; + } + let mnetPresent = bidParams.filter(b => b.pvnm === MEDIANET_BIDDER_CODE).length > 0; + if (!mnetPresent) { + bidParams = bidParams.map(({mpvid, crid, ext, pubcrid, ...restParams}) => restParams); + } + + let url = formatQS(commonParams) + '&'; + bidParams.forEach(function(bidParams) { + url = url + formatQS(bidParams) + '&'; + }); + url = url + formatQS({targ: targeting}); + firePixel(url); +} + +function formatQS(data) { + return utils._map(data, (value, key) => { + if (value === undefined) { + return key + '='; + } + if (utils.isPlainObject(value)) { + value = JSON.stringify(value); + } + return key + '=' + encodeURIComponent(value); + }).join('&'); +} + +function firePixel(qs) { + logsQueue.push(ENDPOINT + '&' + qs); + utils.triggerPixel(ENDPOINT + '&' + qs); +} + +let medianetAnalytics = Object.assign(adapter({URL, analyticsType}), { + getlogsQueue() { + return logsQueue; + }, + clearlogsQueue() { + logsQueue = []; + auctions = {}; + }, + track({ eventType, args }) { + if (config.debug) { + utils.logInfo(eventType, args); + } + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: { + auctionInitHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_REQUESTED: { + bidRequestedHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_RESPONSE: { + bidResponseHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_TIMEOUT: { + bidTimeoutHandler(args); + break; + } + case CONSTANTS.EVENTS.NO_BID: { + noBidResponseHandler(args); + break; + } + case CONSTANTS.EVENTS.AUCTION_END: { + auctionEndHandler(args); + break; + } + case CONSTANTS.EVENTS.SET_TARGETING : { + setTargetingHandler(args); + break; + } + case CONSTANTS.EVENTS.BID_WON: { + bidWonHandler(args); + break; + } + } + }}); + +medianetAnalytics.originEnableAnalytics = medianetAnalytics.enableAnalytics; + +medianetAnalytics.enableAnalytics = function (configuration) { + if (!configuration || !configuration.options || !configuration.options.cid) { + utils.logError('Media.net Analytics adapter: cid is required.'); + return; + } + pageDetails = new PageDetail(); + + config = new Configure(configuration.options.cid); + config.publisherLper = configuration.options.sampling || ''; + config.init(); + configuration.options.sampling = 1; + medianetAnalytics.originEnableAnalytics(configuration); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: medianetAnalytics, + code: 'medianetAnalytics' +}); + +export default medianetAnalytics; diff --git a/modules/medianetAnalyticsAdapter.md b/modules/medianetAnalyticsAdapter.md new file mode 100644 index 00000000000..60ab6c0dd6b --- /dev/null +++ b/modules/medianetAnalyticsAdapter.md @@ -0,0 +1,23 @@ +# Overview +Module Name: media.net Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: prebid-support@media.net + +# Description + +Analytics adapter for Media.net +https://media.net/ + +# Test Parameters + +``` +{ + provider: 'medianetAnalytics', + options : { + cid: '8CUX0H51C' + } +} + +``` \ No newline at end of file diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index f68f263a0d5..a7434cac6ab 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -1,7 +1,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; -import * as url from '../src/url.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { getRefererInfo } from '../src/refererDetection.js'; @@ -22,7 +21,7 @@ let refererInfo = getRefererInfo(); let mnData = {}; mnData.urlData = { - domain: url.parse(refererInfo.referer).host, + domain: utils.parseUrl(refererInfo.referer).host, page: refererInfo.referer, isTop: refererInfo.reachedTop } @@ -287,7 +286,7 @@ function logEvent (event, data) { hostname: EVENT_PIXEL_URL, search: getLoggingData(event, data) }; - utils.triggerPixel(url.format(getParams)); + utils.triggerPixel(utils.buildUrl(getParams)); } function clearMnData() { @@ -297,6 +296,7 @@ function clearMnData() { export const spec = { code: BIDDER_CODE, + gvlid: 142, supportedMediaTypes: [BANNER, NATIVE], diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index f423f5d474b..957b9a1d703 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -1,8 +1,10 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; -import * as urlUtils from '../src/url.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const DEFAULT_CUR = 'USD'; const BIDDER_CODE = 'mgid'; const ENDPOINT_URL = 'https://prebid.mgid.com/prebid/'; @@ -120,7 +122,7 @@ export const spec = { } const info = pageInfo(); const page = info.location || utils.deepAccess(bidderRequest, 'refererInfo.referer') || utils.deepAccess(bidderRequest, 'refererInfo.canonicalUrl'); - const hostname = urlUtils.parse(page).hostname; + const hostname = utils.parseUrl(page).hostname; let domain = extractDomainFromHost(hostname) || hostname; const accountId = setOnAny(validBidRequests, 'params.accountId'); const muid = getLocalStorageSafely('mgMuidn'); @@ -342,7 +344,7 @@ function getLanguage() { function getLocalStorageSafely(key) { try { - return utils.getDataFromLocalStorage(key); + return storage.getDataFromLocalStorage(key); } catch (e) { return null; } @@ -350,7 +352,7 @@ function getLocalStorageSafely(key) { function setLocalStorageSafely(key, val) { try { - return utils.setDataInLocalStorage(key, val); + return storage.setDataInLocalStorage(key, val); } catch (e) { return null; } diff --git a/modules/mytargetBidAdapter.js b/modules/mytargetBidAdapter.js index 24ad488d382..bcf8e7ad17f 100644 --- a/modules/mytargetBidAdapter.js +++ b/modules/mytargetBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js'; -import * as url from '../src/url.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -29,7 +28,7 @@ function getSiteName(referrer) { let sitename = config.getConfig('mytarget.sitename'); if (!sitename) { - sitename = url.parse(referrer).hostname; + sitename = utils.parseUrl(referrer).hostname; } return sitename; diff --git a/modules/nafdigitalBidAdapter.js b/modules/nafdigitalBidAdapter.js index 2be845d6fdd..d64e079b52a 100644 --- a/modules/nafdigitalBidAdapter.js +++ b/modules/nafdigitalBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js'; -import * as url from '../src/url.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; @@ -56,7 +55,7 @@ function buildRequests(bidReqs, bidderRequest) { id: utils.getUniqueIdentifierStr(), imp: nafdigitalImps, site: { - domain: url.parse(referrer).host, + domain: utils.parseUrl(referrer).host, page: referrer, publisher: { id: publisherId diff --git a/modules/nanointeractiveBidAdapter.js b/modules/nanointeractiveBidAdapter.js index 3dfd4811bcd..42a343efc03 100644 --- a/modules/nanointeractiveBidAdapter.js +++ b/modules/nanointeractiveBidAdapter.js @@ -1,6 +1,9 @@ import * as utils from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); export const BIDDER_CODE = 'nanointeractive'; export const END_POINT_URL = 'https://ad.audiencemanager.de'; @@ -97,7 +100,7 @@ function createSingleBidRequest(bid, bidderRequest) { function createSingleBidResponse(serverBid) { if (serverBid.userId) { - utils.setDataInLocalStorage('lsUserId', serverBid.userId); + storage.setDataInLocalStorage('lsUserId', serverBid.userId); } return { requestId: serverBid.id, @@ -147,8 +150,8 @@ function getEndpointUrl() { } function getLsUserId() { - if (utils.getDataFromLocalStorage('lsUserId') != null) { - return utils.getDataFromLocalStorage('lsUserId'); + if (storage.getDataFromLocalStorage('lsUserId') != null) { + return storage.getDataFromLocalStorage('lsUserId'); } return null; } diff --git a/modules/newborntownWebBidAdapter.js b/modules/newborntownWebBidAdapter.js index 0ad3e212089..56c63e2bb4d 100644 --- a/modules/newborntownWebBidAdapter.js +++ b/modules/newborntownWebBidAdapter.js @@ -1,6 +1,9 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'newborntownWeb'; const REQUEST_URL = 'https://us-west.solortb.com/adx/api/rtb?from=4' @@ -53,10 +56,10 @@ export const spec = { return null; } var guid; - if (utils.getDataFromLocalStorage('sax_user_id') == null) { - utils.setDataInLocalStorage('sax_user_id', generateGUID()) + if (storage.getDataFromLocalStorage('sax_user_id') == null) { + storage.setDataInLocalStorage('sax_user_id', generateGUID()) } - guid = utils.getDataFromLocalStorage('sax_user_id') + guid = storage.getDataFromLocalStorage('sax_user_id') utils._each(validBidRequests, function(bidRequest) { const bidRequestObj = bidRequest.params var req = { diff --git a/modules/nextrollBidAdapter.js b/modules/nextrollBidAdapter.js index e85efceba68..81c48102bb4 100644 --- a/modules/nextrollBidAdapter.js +++ b/modules/nextrollBidAdapter.js @@ -1,7 +1,6 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -import { parse as parseUrl } from '../src/url.js'; import find from 'core-js/library/fn/array/find.js'; @@ -30,7 +29,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { - let topLocation = parseUrl(utils.deepAccess(bidderRequest, 'refererInfo.referer')); + let topLocation = utils.parseUrl(utils.deepAccess(bidderRequest, 'refererInfo.referer')); let consent = hasCCPAConsent(bidderRequest); return validBidRequests.map((bidRequest, index) => { return { diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index a845693425d..dacb76a9a98 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -1,9 +1,12 @@ import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'nobid'; -window.nobidVersion = '1.2.4'; +window.nobidVersion = '1.2.5'; window.nobid = window.nobid || {}; window.nobid.bidResponses = window.nobid.bidResponses || {}; window.nobid.timeoutTotal = 0; @@ -16,10 +19,10 @@ function nobidSetCookie(cname, cvalue, hours) { var d = new Date(); d.setTime(d.getTime() + (hours * 60 * 60 * 1000)); var expires = 'expires=' + d.toUTCString(); - utils.setCookie(cname, cvalue, expires); + storage.setCookie(cname, cvalue, expires); } function nobidGetCookie(cname) { - return utils.getCookie(cname); + return storage.getCookie(cname); } function nobidBuildRequests(bids, bidderRequest) { var serializeState = function(divIds, siteId, adunits) { @@ -153,9 +156,6 @@ function nobidBuildRequests(bids, bidderRequest) { } else { a.g = {}; } - if (adunitObject.companion) { - a.c = adunitObject.companion; - } if (adunitObject.div) { removeByAttrValue(adunits, 'd', adunitObject.div); } @@ -168,9 +168,11 @@ function nobidBuildRequests(bids, bidderRequest) { if (adunitObject.placementId) { a.pid = adunitObject.placementId; } - /* {"BIDDER_ID":{"WxH":"TAG_ID", "WxH":"TAG_ID"}} */ - if (adunitObject.rtb) { - a.rtb = adunitObject.rtb; + if (adunitObject.ad_type) { + a.at = adunitObject.ad_type; + } + if (adunitObject.params) { + a.params = adunitObject.params; } adunits.push(a); return adunits; @@ -194,10 +196,24 @@ function nobidBuildRequests(bids, bidderRequest) { var sizes = bid.sizes; siteId = (typeof bid.params['siteId'] != 'undefined' && bid.params['siteId']) ? bid.params['siteId'] : siteId; var placementId = bid.params['placementId']; - if (siteId && bid.params && bid.params.tags) { - newAdunit({div: divid, sizes: sizes, rtb: bid.params.tags, siteId: siteId, placementId: placementId}, adunits); - } else if (siteId) { - newAdunit({div: divid, sizes: sizes, siteId: siteId, placementId: placementId}, adunits); + + var adType = 'banner'; + const videoMediaType = utils.deepAccess(bid, 'mediaTypes.video'); + const context = utils.deepAccess(bid, 'mediaTypes.video.context'); + if (bid.mediaType === VIDEO || (videoMediaType && context === 'instream')) { + adType = 'video'; + } + + if (siteId) { + newAdunit({ + div: divid, + sizes: sizes, + siteId: siteId, + placementId: placementId, + ad_type: adType, + params: bid.params + }, + adunits); } } if (siteId) { @@ -243,8 +259,18 @@ function nobidInterpretResponse(response, bidRequest) { netRevenue: true, ttl: 300, ad: bid.adm, - mediaType: BANNER + mediaType: bid.atype || BANNER, }; + if (bid.vastUrl) { + bidResponse.vastUrl = bid.vastUrl; + } + if (bid.vastXml) { + bidResponse.vastXml = bid.vastXml; + } + if (bid.videoCacheKey) { + bidResponse.videoCacheKey = bid.videoCacheKey; + } + bidResponses.push(bidResponse); } return bidResponses; @@ -263,7 +289,7 @@ window.nobid.renderTag = function(doc, id, win) { } export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. * diff --git a/modules/oneVideoBidAdapter.js b/modules/oneVideoBidAdapter.js index 9d3fabcd3b6..5747c5e848d 100644 --- a/modules/oneVideoBidAdapter.js +++ b/modules/oneVideoBidAdapter.js @@ -3,7 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'oneVideo'; export const spec = { code: 'oneVideo', - VERSION: '3.0.0', + VERSION: '3.0.1', ENDPOINT: 'https://ads.adaptv.advertising.com/rtb/openrtb?ext_id=', SYNC_ENDPOINT1: 'https://cm.g.doubleclick.net/pixel?google_nid=adaptv_dbm&google_cm&google_sc', SYNC_ENDPOINT2: 'https://pr-bh.ybp.yahoo.com/sync/adaptv_ortb/{combo_uid}', @@ -214,6 +214,14 @@ function getRequestData(bid, consentData, bidRequest) { if (bid.params.video.placement) { bidData.imp[0].banner.placement = bid.params.video.placement } + if (bid.params.video.maxduration) { + bidData.imp[0].banner.ext = bidData.imp[0].banner.ext || {} + bidData.imp[0].banner.ext.maxduration = bid.params.video.maxduration + } + if (bid.params.video.minduration) { + bidData.imp[0].banner.ext = bidData.imp[0].banner.ext || {} + bidData.imp[0].banner.ext.minduration = bid.params.video.minduration + } } if (bid.params.video.inventoryid) { bidData.imp[0].ext.inventoryid = bid.params.video.inventoryid diff --git a/modules/oneVideoBidAdapter.md b/modules/oneVideoBidAdapter.md index 145af1a6fb9..656f81733a0 100644 --- a/modules/oneVideoBidAdapter.md +++ b/modules/oneVideoBidAdapter.md @@ -37,7 +37,9 @@ Connects to One Video demand source to fetch bids. sid: , rewarded: 1, placement: 1, - inventoryid: 123 + inventoryid: 123, + minduration: 10, + maxduration: 30, }, site: { id: 1, diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index d6ed19c4af5..f2cea34bb1a 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -1,18 +1,11 @@ 'use strict'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; const { registerBidder } = require('../src/adapters/bidderFactory.js'); const ENDPOINT = 'https://onetag-sys.com/prebid-request'; const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; const BIDDER_CODE = 'onetag'; -const BANNER = 'banner'; - -// ======= -// Object BidRequest -// -// .params -// required .pubId: string -// optional .type: "BANNER" | "VIDEO" | "NATIVE" --> only BANNER at present /** * Determines whether or not the given bid request is valid. @@ -21,15 +14,28 @@ const BANNER = 'banner'; * @return boolean True if this is a valid bid, and false otherwise. */ function isBidRequestValid(bid) { - if (typeof bid === 'undefined' || bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { + if (typeof bid === 'undefined' || typeof bid.params === 'undefined' || typeof bid.params.pubId !== 'string') { return false; } + return isValid(BANNER, bid) || isValid(VIDEO, bid); +} - if (typeof bid.params.pubId !== 'string' || typeof bid.sizes === 'undefined' || bid.sizes.length === 0) { - return false; - } +export function hasTypeVideo(bid) { + return typeof bid.mediaTypes !== 'undefined' && typeof bid.mediaTypes.video !== 'undefined'; +} - return true; +export function isValid(type, bid) { + if (type === BANNER) { + return parseSizes(bid).length > 0; + } else if (type === VIDEO && hasTypeVideo(bid)) { + const context = bid.mediaTypes.video.context; + if (context === 'outstream') { + return parseVideoSize(bid).length > 0 && typeof bid.renderer !== 'undefined' && typeof bid.renderer.render !== 'undefined' && typeof bid.renderer.url !== 'undefined'; + } else if (context === 'instream') { + return parseVideoSize(bid).length > 0; + } + } + return false; } /** @@ -40,7 +46,7 @@ function isBidRequestValid(bid) { */ function buildRequests(validBidRequests, bidderRequest) { - const bids = validBidRequests.map(requestsToBids); + const bids = requestsToBids(validBidRequests); const bidObject = {'bids': bids}; const pageInfo = getPageInfo(); @@ -57,6 +63,10 @@ function buildRequests(validBidRequests, bidderRequest) { payload.usPrivacy = bidderRequest.uspConsent; } + if (bidderRequest && bidderRequest.userId) { + payload.userId = bidderRequest.userId; + } + const payloadString = JSON.stringify(payload); return { @@ -81,25 +91,33 @@ function interpretResponse(serverResponse, request) { if (!body || (body.nobid && body.nobid === true)) { return bids; } - - if (body.bids) { - body.bids.forEach(function(bid) { - bids.push({ - requestId: bid.requestId, - cpm: bid.cpm, - width: bid.width, - height: bid.height, - creativeId: bid.creativeId, - dealId: bid.dealId ? bid.dealId : '', - currency: bid.currency, - netRevenue: false, - mediaType: bids.type ? bids.type : BANNER, - ad: bid.ad, - ttl: bid.ttl || 300 - }); - }); + if (!body.bids || !Array.isArray(body.bids) || body.bids.length === 0) { + return bids; } + body.bids.forEach(function(bid) { + let responseBid = { + requestId: bid.requestId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.creativeId, + dealId: bid.dealId ? bid.dealId : '', + currency: bid.currency, + netRevenue: false, + mediaType: bid.mediaType, + ttl: bid.ttl || 300 + }; + + if (bid.mediaType === BANNER) { + responseBid.ad = bid.ad; + } else if (bid.mediaType === VIDEO) { + responseBid.vastXml = bid.ad; + } + + bids.push(responseBid); + }); + return bids; } @@ -117,9 +135,30 @@ function getPageInfo() { break } } - - const params = { - + let isDocHidden; + let xOffset; + let yOffset; + try { + if (typeof w.document.hidden !== 'undefined') { + isDocHidden = w.document.hidden; + } else if (typeof w.document['msHidden'] !== 'undefined') { + isDocHidden = w.document['msHidden']; + } else if (typeof w.document['webkitHidden'] !== 'undefined') { + isDocHidden = w.document['webkitHidden']; + } else { + isDocHidden = null; + } + } catch (e) { + isDocHidden = null; + } + try { + xOffset = w.pageXOffset; + yOffset = w.pageYOffset; + } catch (e) { + xOffset = null; + yOffset = null; + } + return { location: e(l), referrer: e(r) || '0', masked: m, @@ -133,45 +172,86 @@ function getPageInfo() { aHeight: s.availHeight, sLeft: 'screenLeft' in w ? w.screenLeft : w.screenX, sTop: 'screenTop' in w ? w.screenTop : w.screenY, + xOffset: xOffset, + yOffset: yOffset, + docHidden: isDocHidden, hLength: history.length, date: t.toUTCString(), timeOffset: t.getTimezoneOffset() }; - - return params; } -function requestsToBids(bid) { - const toRet = {}; - - const params = bid.params; +function requestsToBids(bidRequests) { + const videoBidRequests = bidRequests.filter(bidRequest => hasTypeVideo(bidRequest)).map(bidRequest => { + const videoObj = {}; + setGeneralInfo.call(videoObj, bidRequest); + // Pass parameters + // Context: instream - outstream - adpod + videoObj['context'] = bidRequest.mediaTypes.video.context; + // MIME Video Types + videoObj['mimes'] = bidRequest.mediaTypes.video.mimes; + // Sizes + videoObj['playerSize'] = parseVideoSize(bidRequest); + // Other params + videoObj['protocols'] = bidRequest.mediaTypes.video.protocols; + videoObj['maxDuration'] = bidRequest.mediaTypes.video.maxduration; + videoObj['api'] = bidRequest.mediaTypes.video.api; + videoObj['type'] = VIDEO; + return videoObj; + }); + const bannerBidRequests = bidRequests.filter(bidRequest => isValid(BANNER, bidRequest)).map(bidRequest => { + const bannerObj = {}; + setGeneralInfo.call(bannerObj, bidRequest); + bannerObj['sizes'] = parseSizes(bidRequest); + bannerObj['type'] = BANNER; + return bannerObj; + }); + return videoBidRequests.concat(bannerBidRequests); +} - toRet['adUnitCode'] = bid.adUnitCode; - toRet['bidId'] = bid.bidId; - toRet['bidderRequestId'] = bid.bidderRequestId; - toRet['auctionId'] = bid.auctionId; - toRet['transactionId'] = bid.transactionId; - toRet['sizes'] = []; - const sizes = bid.sizes; - for (let i = 0, lenght = sizes.length; i < lenght; i++) { - const size = sizes[i]; - toRet['sizes'].push({width: size[0], height: size[1]}) +function setGeneralInfo(bidRequest) { + const params = bidRequest.params; + this['adUnitCode'] = bidRequest.adUnitCode; + this['bidId'] = bidRequest.bidId; + this['bidderRequestId'] = bidRequest.bidderRequestId; + this['auctionId'] = bidRequest.auctionId; + this['transactionId'] = bidRequest.transactionId; + this['pubId'] = params.pubId; + if (params.pubClick) { + this['click'] = params.pubClick; } - - toRet['pubId'] = params.pubId; - if (params.type) { - toRet['type'] = params.type; + if (params.dealId) { + this['dealId'] = params.dealId; } +} - if (params.pubClick) { - toRet['click'] = params.pubClick; +function parseVideoSize(bid) { + const playerSize = bid.mediaTypes.video.playerSize; + if (typeof playerSize !== 'undefined' && Array.isArray(playerSize) && playerSize.length > 0) { + return getSizes(playerSize) } + return []; +} - if (params.dealId) { - toRet['dealId'] = params.dealId; +function parseSizes(bid) { + let ret = []; + if (typeof bid.mediaTypes !== 'undefined' && typeof bid.mediaTypes.banner !== 'undefined' && typeof bid.mediaTypes.banner.sizes !== 'undefined' && Array.isArray(bid.mediaTypes.banner.sizes) && bid.mediaTypes.banner.sizes.length > 0) { + return getSizes(bid.mediaTypes.banner.sizes) } + const isVideoBidRequest = hasTypeVideo(bid); + if (!isVideoBidRequest && bid.sizes && Array.isArray(bid.sizes)) { + return getSizes(bid.sizes); + } + return ret; +} - return toRet; +function getSizes(sizes) { + const ret = []; + for (let i = 0, lenght = sizes.length; i < lenght; i++) { + const size = sizes[i]; + ret.push({width: size[0], height: size[1]}) + } + return ret; } function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { @@ -202,7 +282,7 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: isBidRequestValid, buildRequests: buildRequests, diff --git a/modules/onetagBidAdapter.md b/modules/onetagBidAdapter.md index 38872ad8280..9757be4d440 100644 --- a/modules/onetagBidAdapter.md +++ b/modules/onetagBidAdapter.md @@ -8,27 +8,39 @@ Maintainer: devops@onetag.com # Description -OneTag Bid Adapter supports only banner at present. +OneTag Bid Adapter supports banner and video at present. # Test Parameters ``` var adUnits = [ { - code: "test-div", + code: "banner-space", mediaTypes: { banner: { sizes: [[300, 250]] } }, - bids: [ - { - bidder: "onetag", - params: { - pubId: "your_publisher_id", // required - type: "banner" // optional. Default "banner" - }, + bids: [{ + bidder: "onetag", + params: { + pubId: "your_publisher_id" // required, testing pubId: "386276e072" } - ] - }]; + }] + }, { + code: 'video-space', + mediaTypes: { + video: { + context: "instream", + mimes: ["video/mp4", "video/webm", "application/javascript", "video/ogg"], + playerSize: [640,480] + } + }, + bids: [{ + bidder: "onetag", + params: { + pubId: "your_publisher_id" // required, testing pubId: "386276e072" + } + }] + }]; ``` diff --git a/modules/openxAnalyticsAdapter.md b/modules/openxAnalyticsAdapter.md new file mode 100644 index 00000000000..ac739f36c76 --- /dev/null +++ b/modules/openxAnalyticsAdapter.md @@ -0,0 +1,129 @@ + +# OpenX Analytics Adapter to Prebid.js +## Implementation Guide +#### Internal use only + +--- + +# About this Guide +This implementation guide walks through the flow of onboarding an alpha Publisher to test OpenX’s new Analytics Adapter. + +- [Adding OpenX Analytics Adapter to Prebid.js](#adding-openx-analytics-adapter-to-prebidjs) + - [Publisher Builds Prebid.js File Flow](#publisher-builds-prebidjs-file-flow) + - [OpenX Builds Prebid.js File Flow](#openx-builds-prebidjs-file-flow) +- [Website Configuration](#website-configuration) +- [Configuration Options](#configuration-options) +- [Viewing Data](#viewing-data) + +--- + +# Adding OpenX Analytics Adapter to Prebid.js +A Publisher has two options to add the OpenX Analytics Adapter to Prebid.js: + +1. [Publisher builds the Prebid.js file](#publisher-builds-prebid.js-file-flow): If the Publisher is familiar with building Prebid.js (through the command line and not through the download site), OpenX can provide to the Publisher only the Analytics Adapter code. + +2. [OpenX builds the Prebid.js file](#openx-builds-prebid.js-file-flow): If the Publisher is unfamiliar with building Prebid.js, the Publisher should advise OpenX which modules to include by going to the [Prebid download site](http://prebid.org/download.html) and selecting all the desired items (adapters and modules) for OpenX. + +--- + +## Publisher Builds Prebid.js File Flow +Use this option if the Publisher is building the Prebid.js file. + +1. OpenX sends Publisher the new Analytics Adapter code. + +2. Publisher replaces the file in `/modules/openxAnalyticsAdapter.js` with the file provided by OpenX. + +3. Publisher runs the build command in `` and includes `openxAnalyticsAdapter` as one of the modules to include. + + For example: + + ```shell + gulp build --modules=openxBidAdapter,rubiconBidAdapter,sovrnBidAdapter,openxBidAdapter,openxAnalyticsAdapter,dfpAdServerVideo + ``` + +4. Publisher deploys the Prebid.js file from `/build/dist/prebid.js` to a website. + +--- + +## OpenX Builds Prebid.js File Flow +Use this option if OpenX is building the Prebid.js file on behalf of the Publisher. + +1. Publisher refers to the [Prebid download site](http://prebid.org/download.html) and sends a list of adapters and modules to OpenX. + + >Note: The Publisher must be aware that only Prebid 3.0+ is supported. + + For example (does not have to follow exact format): + + ```yaml + Prebid Version: 3.10+ + Modules: + Bidders + OpenX + Rubicon + Sovrn + Consent Management + US Privacy + User ID + IdentityLink ID + DFP Video + Supply Chain Object + Currency + ``` + +2. OpenX uses the information to build a package to the user’s specification and includes `openxAnalyticsAdapter` as an additional module. + +3. OpenX sends the built package to the Publisher. + +4. Publisher deploys the modified Prebid.js to a website. + +--- + +# Website Configuration +To configure your website, add the following code snippet to your website: + +```javascript +pbjs.que.push(function () { + pbjs.enableAnalytics([{ + provider: "openx", + options: { + publisherPlatformId: "OPENX_PROVIDED_PLATFORM_ID", // eg: "a3aece0c-9e80-4316-8deb-faf804779bd1" + publisherAccountId: PUBLISHER_ACCOUNT_ID, // eg: 537143056 + sampling: 0.05, // 5% sample rate + testCode: 'test-code-1' + } + }]); +}); +``` + +--- + +## Configuration Options +Configuration options are a follows: + +| Property | Type | Required? | Description | Example | +|:---|:---|:---|:---|:---| +| `publisherPlatformId` | `string` | Yes | Used to determine ownership of data. | `a3aece0c-9e80-4316-8deb-faf804779bd1` | +| `publisherAccountId` | `number` | Yes | Used to determine ownership of data. | `1537143056` | +| `sampling` | `number` | Yes | Sampling rate | Undefined or `1.00` - No sampling. Analytics is sent all the time.
0.5 - 50% of users will send analytics data. | +| `testCode` | `string` | No | Used to label analytics data for the purposes of tests.
This label is treated as a dimension and can be compared against other labels. | `timeout_config_1`
`timeout_config_2`
`timeout_default` | + +--- + +# Viewing Data +The Prebid Report available in the Reporting in the Cloud tool, allows you to view your data. + +**To view your data:** + +1. Log in to Reporting in the Cloud. + +2. In the top right, click on the **View** list and then select **Prebidreport**. + +3. On the left icon bar, click on the dimensions icon. + +4. Add the dimensions that you need. + +5. On the left icon bar, click on the metrics icon. + +6. Add the metrics (graphs) that you need. + + The data appears on the Analyze screen. diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index b0d1ae3a075..d5630c2fad4 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -2,22 +2,28 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {parse} from '../src/url.js'; const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'openx'; const BIDDER_CONFIG = 'hb_pb'; -const BIDDER_VERSION = '3.0.1'; - -const USER_ID_CODE_TO_QUERY_ARG = { - idl_env: 'lre', // liveramp - pubcid: 'pubcid', // publisher common id - tdid: 'ttduuid', // the trade desk - criteoId: 'criteoid' // criteo id +const BIDDER_VERSION = '3.0.2'; + +export const USER_ID_CODE_TO_QUERY_ARG = { + britepoolid: 'britepoolid', // BritePool ID + criteoId: 'criteoid', // CriteoID + digitrustid: 'digitrustid', // DigiTrust + id5id: 'id5id', // ID5 ID + idl_env: 'lre', // LiveRamp IdentityLink + lipb: 'lipbid', // LiveIntent ID + netId: 'netid', // netID + parrableid: 'parrableid', // Parrable ID + pubcid: 'pubcid', // PubCommon ID + tdid: 'ttduuid', // The Trade Desk Unified ID }; export const spec = { code: BIDDER_CODE, + gvlid: 69, supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function (bidRequest) { const hasDelDomainOrPlatform = bidRequest.params.delDomain || bidRequest.params.platform; @@ -259,9 +265,20 @@ function buildCommonQueryParamsFromBids(bids, bidderRequest) { } function appendUserIdsToQueryParams(queryParams, userIds) { - utils._each(userIds, (userIdValue, userIdProviderKey) => { + utils._each(userIds, (userIdObjectOrValue, userIdProviderKey) => { + const key = USER_ID_CODE_TO_QUERY_ARG[userIdProviderKey]; + if (USER_ID_CODE_TO_QUERY_ARG.hasOwnProperty(userIdProviderKey)) { - queryParams[USER_ID_CODE_TO_QUERY_ARG[userIdProviderKey]] = userIdValue; + switch (userIdProviderKey) { + case 'digitrustid': + queryParams[key] = utils.deepAccess(userIdObjectOrValue, 'data.id'); + break; + case 'lipb': + queryParams[key] = userIdObjectOrValue.lipbid; + break; + default: + queryParams[key] = userIdObjectOrValue; + } } }); @@ -405,8 +422,8 @@ function generateVideoParameters(bid, bidderRequest) { function createVideoBidResponses(response, {bid, startTime}) { let bidResponses = []; - if (response !== undefined && response.vastUrl !== '' && response.pub_rev !== '') { - let vastQueryParams = parse(response.vastUrl).search || {}; + if (response !== undefined && response.vastUrl !== '' && response.pub_rev > 0) { + let vastQueryParams = utils.parseUrl(response.vastUrl).search || {}; let bidResponse = {}; bidResponse.requestId = bid.bidId; // default 5 mins @@ -414,9 +431,9 @@ function createVideoBidResponses(response, {bid, startTime}) { // true is net, false is gross bidResponse.netRevenue = true; bidResponse.currency = response.currency; - bidResponse.cpm = Number(response.pub_rev) / 1000; - bidResponse.width = response.width; - bidResponse.height = response.height; + bidResponse.cpm = parseInt(response.pub_rev, 10) / 1000; + bidResponse.width = parseInt(response.width, 10); + bidResponse.height = parseInt(response.height, 10); bidResponse.creativeId = response.adid; bidResponse.vastUrl = response.vastUrl; bidResponse.mediaType = VIDEO; diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index 3461aaf66a4..d7ce5aa859a 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -1,7 +1,9 @@ import {detectReferer} from '../src/refererDetection.js'; import {ajax} from '../src/ajax.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); export const spec = { code: 'orbidder', @@ -9,7 +11,7 @@ export const spec = { orbidderHost: (() => { let ret = 'https://orbidder.otto.de'; try { - ret = utils.getDataFromLocalStorage('ov_orbidder_host') || ret; + ret = storage.getDataFromLocalStorage('ov_orbidder_host') || ret; } catch (e) { } return ret; diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index bc900d11e0b..cc569a79062 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -6,8 +6,9 @@ */ import * as utils from '../src/utils.js' -import {ajax} from '../src/ajax.js'; -import {submodule} from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getRefererInfo } from '../src/refererDetection.js'; const PARRABLE_URL = 'https://h.parrable.com/prebid'; @@ -26,9 +27,12 @@ function isValidConfig(configParams) { function fetchId(configParams, consentData, currentStoredId) { if (!isValidConfig(configParams)) return; + const refererInfo = getRefererInfo(); + const data = { eid: currentStoredId || null, - trackers: configParams.partner.split(',') + trackers: configParams.partner.split(','), + url: refererInfo.referer }; const searchParams = { diff --git a/modules/platformioBidAdapter.js b/modules/platformioBidAdapter.js new file mode 100644 index 00000000000..ad25e262d56 --- /dev/null +++ b/modules/platformioBidAdapter.js @@ -0,0 +1,307 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import includes from 'core-js/library/fn/array/includes.js'; + +const NATIVE_DEFAULTS = { + TITLE_LEN: 100, + DESCR_LEN: 200, + SPONSORED_BY_LEN: 50, + IMG_MIN: 150, + ICON_MIN: 50, +}; +const DEFAULT_MIMES = ['video/mp4', 'video/webm', 'application/x-shockwave-flash', 'application/javascript']; +const VIDEO_TARGETING = ['mimes', 'skippable', 'playback_method', 'protocols', 'api']; +const DEFAULT_PROTOCOLS = [2, 3, 5, 6]; +const DEFAULT_APIS = [1, 2]; + +export const spec = { + + code: 'platformio', + supportedMediaTypes: ['banner', 'native', 'video'], + + isBidRequestValid: bid => ( + !!(bid && bid.params && bid.params.pubId && bid.params.placementId) + ), + buildRequests: (bidRequests, bidderRequest) => { + const request = { + id: bidRequests[0].bidderRequestId, + at: 2, + imp: bidRequests.map(slot => impression(slot)), + site: site(bidRequests), + app: app(bidRequests), + device: device(bidRequests), + }; + applyGdpr(bidderRequest, request); + return { + method: 'POST', + url: 'https://piohbdisp.hb.adx1.com/', + data: JSON.stringify(request), + }; + }, + interpretResponse: (response, request) => ( + bidResponseAvailable(request, response.body) + ), +}; + +function bidResponseAvailable(bidRequest, bidResponse) { + const idToImpMap = {}; + const idToBidMap = {}; + const ortbRequest = parse(bidRequest.data); + ortbRequest.imp.forEach(imp => { + idToImpMap[imp.id] = imp; + }); + if (bidResponse) { + bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => { + idToBidMap[bid.impid] = bid; + })); + } + const bids = []; + Object.keys(idToImpMap).forEach(id => { + if (idToBidMap[id]) { + const bid = {}; + bid.requestId = id; + bid.adId = id; + bid.creativeId = id; + bid.cpm = idToBidMap[id].price; + bid.currency = bidResponse.cur; + bid.ttl = 360; + bid.netRevenue = true; + if (idToImpMap[id]['native']) { + bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); + let nurl = idToBidMap[id].nurl; + nurl = nurl.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); + bid['native']['impressionTrackers'] = [nurl]; + bid.mediaType = 'native'; + } else if (idToImpMap[id]['video']) { + bid.vastUrl = idToBidMap[id].adm; + bid.vastUrl = bid.vastUrl.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + bid.crid = idToBidMap[id].crid; + bid.width = idToImpMap[id].video.w; + bid.height = idToImpMap[id].video.h; + bid.mediaType = 'video'; + } else if (idToImpMap[id]['banner']) { + bid.ad = idToBidMap[id].adm; + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_AD_ID(%7D|\})/gi, idToBidMap[id].adid); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); + bid.width = idToBidMap[id].w; + bid.height = idToBidMap[id].h; + bid.mediaType = 'banner'; + } + bids.push(bid); + } + }); + return bids; +} +function impression(slot) { + return { + id: slot.bidId, + secure: window.location.protocol === 'https:' ? 1 : 0, + 'banner': banner(slot), + 'native': nativeImpression(slot), + 'video': videoImpression(slot), + bidfloor: slot.params.bidFloor || '0.000001', + tagid: slot.params.placementId.toString(), + }; +} + +function banner(slot) { + if (slot.mediaType === 'banner' || utils.deepAccess(slot, 'mediaTypes.banner')) { + const sizes = utils.deepAccess(slot, 'mediaTypes.banner.sizes'); + if (sizes.length > 1) { + let format = []; + for (let f = 0; f < sizes.length; f++) { + format.push({'w': sizes[f][0], 'h': sizes[f][1]}); + } + return {'format': format}; + } else { + return { + w: sizes[0][0], + h: sizes[0][1] + } + } + } + return null; +} + +function videoImpression(slot) { + if (slot.mediaType === 'video' || utils.deepAccess(slot, 'mediaTypes.video')) { + const sizes = utils.deepAccess(slot, 'mediaTypes.video.playerSize'); + const video = { + w: sizes[0][0], + h: sizes[0][1], + mimes: DEFAULT_MIMES, + protocols: DEFAULT_PROTOCOLS, + api: DEFAULT_APIS, + }; + if (slot.params.video) { + Object.keys(slot.params.video).filter(param => includes(VIDEO_TARGETING, param)).forEach(param => video[param] = slot.params.video[param]); + } + return video; + } + return null; +} + +function nativeImpression(slot) { + if (slot.mediaType === 'native' || utils.deepAccess(slot, 'mediaTypes.native')) { + const assets = []; + addAsset(assets, titleAsset(1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); + addAsset(assets, dataAsset(2, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); + addAsset(assets, dataAsset(3, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); + addAsset(assets, imageAsset(4, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN)); + addAsset(assets, imageAsset(5, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); + return { + request: JSON.stringify({ assets }), + ver: '1.1', + }; + } + return null; +} + +function addAsset(assets, asset) { + if (asset) { + assets.push(asset); + } +} + +function titleAsset(id, params, defaultLen) { + if (params) { + return { + id, + required: params.required ? 1 : 0, + title: { + len: params.len || defaultLen, + }, + }; + } + return null; +} + +function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) { + return params ? { + id, + required: params.required ? 1 : 0, + img: { + type, + wmin: params.wmin || defaultMinWidth, + hmin: params.hmin || defaultMinHeight, + } + } : null; +} + +function dataAsset(id, params, type, defaultLen) { + return params ? { + id, + required: params.required ? 1 : 0, + data: { + type, + len: params.len || defaultLen, + } + } : null; +} + +function site(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.pubId : '0'; + const siteId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.siteId : '0'; + const appParams = bidderRequest[0].params.app; + if (!appParams) { + return { + publisher: { + id: pubId.toString(), + domain: window.location.hostname, + }, + id: siteId.toString(), + ref: window.top.document.referrer, + page: window.location.href, + } + } + return null; +} + +function app(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.pubId : '0'; + const appParams = bidderRequest[0].params.app; + if (appParams) { + return { + publisher: { + id: pubId.toString(), + }, + id: appParams.id, + name: appParams.name, + bundle: appParams.bundle, + storeurl: appParams.storeUrl, + domain: appParams.domain, + } + } + return null; +} + +function device(bidderRequest) { + const lat = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.latitude : ''; + const lon = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.longitude : ''; + const ifa = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.ifa : ''; + return { + dnt: utils.getDNT() ? 1 : 0, + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + w: (window.screen.width || window.innerWidth), + h: (window.screen.height || window.innerHeigh), + geo: { + lat: lat, + lon: lon, + }, + ifa: ifa, + }; +} + +function parse(rawResponse) { + try { + if (rawResponse) { + return JSON.parse(rawResponse); + } + } catch (ex) { + utils.logError('platformio.parse', 'ERROR', ex); + } + return null; +} + +function applyGdpr(bidderRequest, ortbRequest) { + if (bidderRequest && bidderRequest.gdprConsent) { + ortbRequest.regs = { ext: { gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0 } }; + ortbRequest.user = { ext: { consent: bidderRequest.gdprConsent.consentString } }; + } +} + +function nativeResponse(imp, bid) { + if (imp['native']) { + const nativeAd = parse(bid.adm); + const keys = {}; + keys.image = {}; + keys.icon = {}; + if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) { + nativeAd['native'].assets.forEach(asset => { + keys.title = asset.title ? asset.title.text : keys.title; + keys.body = asset.data && asset.id === 2 ? asset.data.value : keys.body; + keys.sponsoredBy = asset.data && asset.id === 3 ? asset.data.value : keys.sponsoredBy; + keys.icon.url = asset.img && asset.id === 4 ? asset.img.url : keys.icon.url; + keys.icon.width = asset.img && asset.id === 4 ? asset.img.w : keys.icon.width; + keys.icon.height = asset.img && asset.id === 4 ? asset.img.h : keys.icon.height; + keys.image.url = asset.img && asset.id === 5 ? asset.img.url : keys.image.url; + keys.image.width = asset.img && asset.id === 5 ? asset.img.w : keys.image.width; + keys.image.height = asset.img && asset.id === 5 ? asset.img.h : keys.image.height; + }); + if (nativeAd['native'].link) { + keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url); + } + return keys; + } + } + return null; +} + +registerBidder(spec); diff --git a/modules/platformioBidAdapter.md b/modules/platformioBidAdapter.md index ff6335d1d70..863e023f0d7 100644 --- a/modules/platformioBidAdapter.md +++ b/modules/platformioBidAdapter.md @@ -14,7 +14,6 @@ Please use ```platformio``` as the bidder code. ``` var adUnits = [{ code: 'dfp-native-div', - mediaType: 'native', mediaTypes: { native: { title: { @@ -50,7 +49,7 @@ Please use ```platformio``` as the bidder code. mediaTypes: { banner: { sizes: [ - [300, 250] + [300, 250],[300,600] ], } }, @@ -59,16 +58,15 @@ Please use ```platformio``` as the bidder code. params: { pubId: '29521', siteId: '26049', - size: '300X250', placementId: '123', } }] }, { code: 'dfp-video-div', - sizes: [640, 480], mediaTypes: { video: { + playerSize: [[640, 480]], context: "instream" } }, @@ -77,7 +75,6 @@ Please use ```platformio``` as the bidder code. params: { pubId: '29521', siteId: '26049', - size: '640X480', placementId: '123', video: { skipppable: true, diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 3b16330310d..56dfe00c6a1 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -268,10 +268,12 @@ function _appendSiteAppDevice(request, pageUrl) { request.app = config.getConfig('app'); request.app.publisher = {id: _s2sConfig.accountId} } else { - request.site = { - publisher: { id: _s2sConfig.accountId }, - page: pageUrl + request.site = {}; + if (typeof config.getConfig('site') === 'object') { + request.site = config.getConfig('site'); } + utils.deepSetValue(request.site, 'publisher.id', _s2sConfig.accountId); + request.site.page = pageUrl; } if (typeof config.getConfig('device') === 'object') { request.device = config.getConfig('device'); diff --git a/modules/prebidmanagerAnalyticsAdapter.js b/modules/prebidmanagerAnalyticsAdapter.js index b8112628cb6..b98ca864cd5 100644 --- a/modules/prebidmanagerAnalyticsAdapter.js +++ b/modules/prebidmanagerAnalyticsAdapter.js @@ -20,6 +20,7 @@ var _startAuction = 0; var _bidRequestTimeout = 0; let flushInterval; var pmAnalyticsEnabled = false; +const utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; var w = window; var d = document; @@ -68,6 +69,36 @@ prebidmanagerAnalytics.disableAnalytics = function() { prebidmanagerAnalytics.originDisableAnalytics(); }; +function collectUtmTagData() { + let newUtm = false; + let pmUtmTags = {}; + try { + utmTags.forEach(function (utmKey) { + let utmValue = utils.getParameterByName(utmKey); + if (utmValue !== '') { + newUtm = true; + } + pmUtmTags[utmKey] = utmValue; + }); + if (newUtm === false) { + utmTags.forEach(function (utmKey) { + let itemValue = localStorage.getItem(`pm_${utmKey}`); + if (itemValue.length !== 0) { + pmUtmTags[utmKey] = itemValue; + } + }); + } else { + utmTags.forEach(function (utmKey) { + localStorage.setItem(`pm_${utmKey}`, pmUtmTags[utmKey]); + }); + } + } catch (e) { + utils.logError(`${analyticsName}Error`, e); + pmUtmTags['error_utm'] = 1; + } + return pmUtmTags; +} + function flush() { if (!pmAnalyticsEnabled) { return; @@ -78,7 +109,8 @@ function flush() { pageViewId: _pageViewId, ver: _VERSION, bundleId: initOptions.bundleId, - events: _eventQueue + events: _eventQueue, + utmTags: collectUtmTagData(), }; ajax( diff --git a/modules/priceFloors.js b/modules/priceFloors.js new file mode 100644 index 00000000000..0accd48d003 --- /dev/null +++ b/modules/priceFloors.js @@ -0,0 +1,644 @@ +import { getGlobal } from '../src/prebidGlobal.js'; +import { config } from '../src/config.js'; +import * as utils from '../src/utils.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; +import { getHook } from '../src/hook.js'; +import { createBid } from '../src/bidfactory.js'; +import find from 'core-js/library/fn/array/find.js'; +import { getRefererInfo } from '../src/refererDetection.js'; + +/** + * @summary This Module is intended to provide users with the ability to dynamically set and enforce price floors on a per auction basis. + */ +const MODULE_NAME = 'Price Floors'; + +/** + * @summary Instantiate Ajax so we control the timeout + */ +const ajax = ajaxBuilder(10000); + +/** + * @summary Allowed fields for rules to have + */ +export let allowedFields = ['gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType']; + +/** + * @summary This is a flag to indicate if a AJAX call is processing for a floors request +*/ +let fetching = false; + +/** + * @summary so we only register for our hooks once +*/ +let addedFloorsHook = false; + +/** + * @summary The config to be used. Can be updated via: setConfig or a real time fetch + */ +let _floorsConfig = {}; + +/** + * @summary If a auction is to be delayed by an ongoing fetch we hold it here until it can be resumed + */ +let _delayedAuctions = []; + +/** + * @summary Each auction can have differing floors data depending on execution time or per adunit setup + * So we will be saving each auction offset by it's auctionId in order to make sure data is not changed + * Once the auction commences + */ +export let _floorDataForAuction = {}; + +/** + * @summary Simple function to round up to a certain decimal degree + */ +function roundUp(number, precision) { + return Math.ceil(parseFloat(number) * Math.pow(10, precision)) / Math.pow(10, precision); +} + +let referrerHostname; +function getHostNameFromReferer(referer) { + referrerHostname = utils.parseUrl(referer, {noDecodeWholeURL: true}).hostname; + return referrerHostname; +} + +/** + * @summary floor field types with their matching functions to resolve the actual matched value + */ +export let fieldMatchingFunctions = { + 'size': (bidRequest, bidResponse) => utils.parseGPTSingleSizeArray(bidResponse.size) || '*', + 'mediaType': (bidRequest, bidResponse) => bidResponse.mediaType || 'banner', + 'gptSlot': (bidRequest, bidResponse) => utils.getGptSlotInfoForAdUnitCode(bidRequest.adUnitCode).gptSlot, + 'domain': (bidRequest, bidResponse) => referrerHostname || getHostNameFromReferer(getRefererInfo().referer), + 'adUnitCode': (bidRequest, bidResponse) => bidRequest.adUnitCode +} + +/** + * @summary Based on the fields array in floors data, it enumerates all possible matches based on exact match coupled with + * a "*" catch-all match + * Returns array of Tuple [exact match, catch all] for each field in rules file + */ +function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) { + // generate combination of all exact matches and catch all for each field type + return floorFields.reduce((accum, field) => { + let exactMatch = fieldMatchingFunctions[field](bidObject, responseObject) || '*'; + // storing exact matches as lowerCase since we want to compare case insensitively + accum.push(exactMatch === '*' ? ['*'] : [exactMatch.toLowerCase(), '*']); + return accum; + }, []); +} + +/** + * @summary get's the first matching floor based on context provided. + * Generates all possible rule matches and picks the first matching one. + */ +export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) { + let fieldValues = enumeratePossibleFieldValues(utils.deepAccess(floorData, 'schema.fields') || [], bidObject, responseObject); + if (!fieldValues.length) return { matchingFloor: floorData.default }; + + // look to see iof a request for this context was made already + let matchingInput = fieldValues.map(field => field[0]).join('-'); + // if we already have gotten the matching rule from this matching input then use it! No need to look again + let previousMatch = utils.deepAccess(floorData, `matchingInputs.${matchingInput}`); + if (previousMatch) { + return previousMatch; + } + let allPossibleMatches = generatePossibleEnumerations(fieldValues, utils.deepAccess(floorData, 'schema.delimiter') || '|'); + let matchingRule = find(allPossibleMatches, hashValue => floorData.values.hasOwnProperty(hashValue)); + + let matchingData = { + matchingFloor: floorData.values[matchingRule] || floorData.default, + matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters + matchingRule + }; + // save for later lookup if needed + utils.deepSetValue(floorData, `matchingInputs.${matchingInput}`, {...matchingData}); + return matchingData; +} + +/** + * @summary Generates all possible rule hash's based on input array of array's + * The generated list is of all possible key matches based on fields input + * The list is sorted by least amount of * in rule to most with left most fields taking precedence + */ +function generatePossibleEnumerations(arrayOfFields, delimiter) { + return arrayOfFields.reduce((accum, currentVal) => { + let ret = []; + accum.map(obj => { + currentVal.map(obj1 => { + ret.push(obj + delimiter + obj1) + }); + }); + return ret; + }).sort((left, right) => left.split('*').length - right.split('*').length); +} + +/** + * @summary If a the input bidder has a registered cpmadjustment it returns the input CPM after being adjusted + */ +export function getBiddersCpmAdjustment(bidderName, inputCpm) { + const adjustmentFunction = utils.deepAccess(getGlobal(), `bidderSettings.${bidderName}.bidCpmAdjustment`); + if (adjustmentFunction) { + return parseFloat(adjustmentFunction(inputCpm)); + } + return parseFloat(inputCpm); +} + +/** + * @summary This function takes the original floor and the adjusted floor in order to determine the bidders actual floor + * With js rounding errors with decimal division we utilize similar method as shown in cpmBucketManager.js + */ +export function calculateAdjustedFloor(oldFloor, newFloor) { + const pow = Math.pow(10, 10); + return ((oldFloor * pow) / (newFloor * pow) * (oldFloor * pow)) / pow; +} + +/** + * @summary gets the prebid set sizes depending on the input mediaType + */ +const getMediaTypesSizes = { + banner: (bid) => utils.deepAccess(bid, 'mediaTypes.banner.sizes') || [], + video: (bid) => utils.deepAccess(bid, 'mediaTypes.video.playerSize') || [], + native: (bid) => utils.deepAccess(bid, 'mediaTypes.native.image.sizes') ? [utils.deepAccess(bid, 'mediaTypes.native.image.sizes')] : [] +} + +/** + * @summary for getFloor only, before selecting a rule, if a bidAdapter asks for * in their getFloor params + * Then we may be able to get a better rule than the * ones depending on context of the adUnit + */ +function updateRequestParamsFromContext(bidRequest, requestParams) { + // if adapter asks for *'s then we can do some logic to infer if we can get a more specific rule based on context of bid + let mediaTypesOnBid = Object.keys(bidRequest.mediaTypes || {}); + // if there is only one mediaType then we can just use it + if (requestParams.mediaType === '*' && mediaTypesOnBid.length === 1) { + requestParams.mediaType = mediaTypesOnBid[0]; + } + // if they asked for * size, but for the given mediaType there is only one size, we can just use it + if (requestParams.size === '*' && mediaTypesOnBid.indexOf(requestParams.mediaType) !== -1 && getMediaTypesSizes[requestParams.mediaType] && getMediaTypesSizes[requestParams.mediaType](bidRequest).length === 1) { + requestParams.size = getMediaTypesSizes[requestParams.mediaType](bidRequest)[0]; + } + return requestParams; +} + +/** + * @summary This is the function which will return a single floor based on the input requests + * and matching it to a rule for the current auction + */ +export function getFloor(requestParams = {currency: 'USD', mediaType: '*', size: '*'}) { + let bidRequest = this; + let floorData = _floorDataForAuction[bidRequest.auctionId]; + if (!floorData || floorData.skipped) return {}; + + requestParams = updateRequestParamsFromContext(bidRequest, requestParams); + let floorInfo = getFirstMatchingFloor(floorData.data, {...bidRequest}, {mediaType: requestParams.mediaType, size: requestParams.size}); + let currency = requestParams.currency || floorData.data.currency; + + // if bidder asked for a currency which is not what floors are set in convert + if (floorInfo.matchingFloor && currency !== floorData.data.currency) { + try { + floorInfo.matchingFloor = getGlobal().convertCurrency(floorInfo.matchingFloor, floorData.data.currency, currency); + } catch (err) { + utils.logWarn(`${MODULE_NAME}: Unable to get currency conversion for getFloor for bidder ${bidRequest.bidder}. You must have currency module enabled with defaultRates in your currency config`); + // since we were unable to convert to the bidders requested currency, we send back just the actual floors currency to them + currency = floorData.data.currency; + } + } + + // if cpmAdjustment flag is true and we have a valid floor then run the adjustment on it + if (floorData.enforcement.bidAdjustment && floorInfo.matchingFloor) { + let cpmAdjustment = getBiddersCpmAdjustment(bidRequest.bidder, floorInfo.matchingFloor); + floorInfo.matchingFloor = cpmAdjustment ? calculateAdjustedFloor(floorInfo.matchingFloor, cpmAdjustment) : floorInfo.matchingFloor; + } + + if (floorInfo.matchingFloor) { + return { + floor: roundUp(floorInfo.matchingFloor, 4), + currency, + }; + } + return {}; +} + +/** + * @summary Takes a floorsData object and converts it into a hash map with appropriate keys + */ +export function getFloorsDataForAuction(floorData, adUnitCode) { + let auctionFloorData = utils.deepClone(floorData); + auctionFloorData.schema.delimiter = floorData.schema.delimiter || '|'; + auctionFloorData.values = normalizeRulesForAuction(auctionFloorData, adUnitCode); + // default the currency to USD if not passed in + auctionFloorData.currency = auctionFloorData.currency || 'USD'; + return auctionFloorData; +} + +/** + * @summary if adUnitCode needs to be added to the offset then it will add it else just return the values + */ +function normalizeRulesForAuction(floorData, adUnitCode) { + let fields = floorData.schema.fields; + let delimiter = floorData.schema.delimiter + + // if we are building the floor data form an ad unit, we need to append adUnit code as to not cause collisions + let prependAdUnitCode = adUnitCode && fields.indexOf('adUnitCode') === -1 && fields.unshift('adUnitCode'); + return Object.keys(floorData.values).reduce((rulesHash, oldKey) => { + let newKey = prependAdUnitCode ? `${adUnitCode}${delimiter}${oldKey}` : oldKey + // we store the rule keys as lower case for case insensitive compare + rulesHash[newKey.toLowerCase()] = floorData.values[oldKey]; + return rulesHash; + }, {}); +} + +/** + * @summary This function will take the adUnits and generate a floor data object to be used during the auction + * Only called if no set config or fetch level data has returned + */ +export function getFloorDataFromAdUnits(adUnits) { + return adUnits.reduce((accum, adUnit) => { + if (isFloorsDataValid(adUnit.floors)) { + // if values already exist we want to not overwrite them + if (!accum.values) { + accum = getFloorsDataForAuction(adUnit.floors, adUnit.code); + accum.location = 'adUnit'; + } else { + let newRules = getFloorsDataForAuction(adUnit.floors, adUnit.code).values; + // copy over the new rules into our values object + Object.assign(accum.values, newRules); + } + } + return accum; + }, {}); +} + +/** + * @summary This function takes the adUnits for the auction and update them accordingly as well as returns the rules hashmap for the auction + */ +export function updateAdUnitsForAuction(adUnits, floorData, skipped, auctionId) { + adUnits.forEach((adUnit) => { + adUnit.bids.forEach(bid => { + if (skipped) { + delete bid.getFloor; + } else { + bid.getFloor = getFloor; + } + // information for bid and analytics adapters + bid.auctionId = auctionId; + bid.floorData = { + skipped, + modelVersion: utils.deepAccess(floorData, 'data.modelVersion') || '', + location: floorData.data.location, + } + }); + }); +} + +/** + * @summary Updates the adUnits accordingly and returns the necessary floorsData for the current auction + */ +export function createFloorsDataForAuction(adUnits, auctionId) { + let resolvedFloorsData = utils.deepClone(_floorsConfig); + + // if we do not have a floors data set, we will try to use data set on adUnits + let useAdUnitData = Object.keys(utils.deepAccess(resolvedFloorsData, 'data.values') || {}).length === 0; + if (useAdUnitData) { + resolvedFloorsData.data = getFloorDataFromAdUnits(adUnits); + } else { + resolvedFloorsData.data = getFloorsDataForAuction(resolvedFloorsData.data); + } + // if we still do not have a valid floor data then floors is not on for this auction + if (Object.keys(utils.deepAccess(resolvedFloorsData, 'data.values') || {}).length === 0) { + return; + } + // determine the skip rate now + const isSkipped = Math.random() * 100 < parseFloat(utils.deepAccess(resolvedFloorsData, 'data.skipRate') || 0); + resolvedFloorsData.skipped = isSkipped; + updateAdUnitsForAuction(adUnits, resolvedFloorsData, isSkipped, auctionId); + return resolvedFloorsData; +} + +/** + * @summary This is the function which will be called to exit our module and continue the auction. + */ +export function continueAuction(hookConfig) { + // only run if hasExited + if (!hookConfig.hasExited) { + // if this current auction is still fetching, remove it from the _delayedAuctions + _delayedAuctions = _delayedAuctions.filter(auctionConfig => auctionConfig.timer !== hookConfig.timer); + + // We need to know the auctionId at this time. So we will use the passed in one or generate and set it ourselves + hookConfig.reqBidsConfigObj.auctionId = hookConfig.reqBidsConfigObj.auctionId || utils.generateUUID(); + + // now we do what we need to with adUnits and save the data object to be used for getFloor and enforcement calls + _floorDataForAuction[hookConfig.reqBidsConfigObj.auctionId] = createFloorsDataForAuction(hookConfig.reqBidsConfigObj.adUnits || getGlobal().adUnits, hookConfig.reqBidsConfigObj.auctionId); + + hookConfig.nextFn.apply(hookConfig.context, [hookConfig.reqBidsConfigObj]); + hookConfig.hasExited = true; + } +} + +function validateSchemaFields(fields) { + if (Array.isArray(fields) && fields.length > 0 && fields.every(field => allowedFields.indexOf(field) !== -1)) { + return true; + } + utils.logError(`${MODULE_NAME}: Fields recieved do not match allowed fields`); + return false; +} + +function isValidRule(key, floor, numFields, delimiter) { + if (typeof key !== 'string' || key.split(delimiter).length !== numFields) { + return false; + } + return typeof floor === 'number'; +} + +function validateRules(floorsData, numFields, delimiter) { + if (typeof floorsData.values !== 'object') { + return false; + } + // if an invalid rule exists we remove it + floorsData.values = Object.keys(floorsData.values).reduce((filteredRules, key) => { + if (isValidRule(key, floorsData.values[key], numFields, delimiter)) { + filteredRules[key] = floorsData.values[key]; + } + return filteredRules + }, {}); + // rules is only valid if at least one rule remains + return Object.keys(floorsData.values).length > 0; +} + +/** + * @summary Fields array should have at least one entry and all should match allowed fields + * Each rule in the values array should have a 'key' and 'floor' param + * And each 'key' should have the correct number of 'fields' after splitting + * on the delim. If rule does not match remove it. return if still at least 1 rule + */ +export function isFloorsDataValid(floorsData) { + if (typeof floorsData !== 'object') { + return false; + } + // schema.fields has only allowed attributes + if (!validateSchemaFields(utils.deepAccess(floorsData, 'schema.fields'))) { + return false; + } + return validateRules(floorsData, floorsData.schema.fields.length, floorsData.schema.delimiter || '|') +} + +/** + * @summary This function updates the global Floors Data field based on the new one passed in if it is valid + */ +export function parseFloorData(floorsData, location) { + if (floorsData && typeof floorsData === 'object' && isFloorsDataValid(floorsData)) { + return { + ...floorsData, + location + }; + } + utils.logError(`${MODULE_NAME}: The floors data did not contain correct values`, floorsData); +} + +/** + * + * @param {Object} reqBidsConfigObj 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(fn, reqBidsConfigObj) { + // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions) + const hookConfig = { + reqBidsConfigObj, + context: this, + nextFn: fn, + haveExited: false, + timer: null + }; + + // If auction delay > 0 AND we are fetching -> Then wait until it finishes + if (_floorsConfig.auctionDelay > 0 && fetching) { + hookConfig.timer = setTimeout(() => { + utils.logWarn(`${MODULE_NAME}: Fetch attempt did not return in time for auction`); + continueAuction(hookConfig); + }, _floorsConfig.auctionDelay); + _delayedAuctions.push(hookConfig); + } else { + continueAuction(hookConfig); + } +} + +/** + * @summary If an auction was queued to be delayed (waiting for a fetch) then this function will resume + * those delayed auctions when delay is hit or success return or fail return + */ +function resumeDelayedAuctions() { + _delayedAuctions.forEach(auctionConfig => { + // clear the timeout + clearTimeout(auctionConfig.timer); + continueAuction(auctionConfig); + }); + _delayedAuctions = []; +} + +/** + * This function handles the ajax response which comes from the user set URL to fetch floors data from + * @param {object} fetchResponse The floors data response which came back from the url configured in config.floors + */ +export function handleFetchResponse(fetchResponse) { + fetching = false; + let floorResponse; + try { + floorResponse = JSON.parse(fetchResponse); + } catch (ex) { + floorResponse = fetchResponse; + } + // Update the global floors object according to the fetched data + _floorsConfig.data = parseFloorData(floorResponse, 'fetch') || _floorsConfig.data; + + // if any auctions are waiting for fetch to finish, we need to continue them! + resumeDelayedAuctions(); +} + +function handleFetchError(status) { + fetching = false; + utils.logError(`${MODULE_NAME}: Fetch errored with: ${status}`); + + // if any auctions are waiting for fetch to finish, we need to continue them! + resumeDelayedAuctions(); +} + +/** + * This function handles sending and recieving the AJAX call for a floors fetch + * @param {object} floorsConfig the floors config coming from setConfig + */ +export function generateAndHandleFetch(floorEndpoint) { + // if a fetch url is defined and one is not already occuring, fire it! + if (floorEndpoint.url && !fetching) { + // default to GET and we only support GET for now + let requestMethod = floorEndpoint.method || 'GET'; + if (requestMethod !== 'GET') { + utils.logError(`${MODULE_NAME}: 'GET' is the only request method supported at this time!`); + } else { + ajax(floorEndpoint.url, { success: handleFetchResponse, error: handleFetchError }, null, { method: 'GET' }); + fetching = true; + } + } else if (fetching) { + utils.logWarn(`${MODULE_NAME}: A fetch is already occuring. Skipping.`); + } +} + +/** + * @summary Updates our allowedFields and fieldMatchingFunctions with the publisher defined new ones + */ +function addFieldOverrides(overrides) { + Object.keys(overrides).forEach(override => { + // we only add it if it is not already in the allowed fields and if the passed in value is a function + if (allowedFields.indexOf(override) === -1 && typeof overrides[override] === 'function') { + allowedFields.push(override); + fieldMatchingFunctions[override] = overrides[override]; + } + }); +} + +/** + * @summary This is the function which controls what happens during a pbjs.setConfig({...floors: {}}) is called + */ +export function handleSetFloorsConfig(config) { + _floorsConfig = utils.pick(config, [ + 'enabled', enabled => enabled !== false, // defaults to true + 'auctionDelay', auctionDelay => auctionDelay || 0, + 'endpoint', endpoint => endpoint || {}, + 'enforcement', enforcement => utils.pick(enforcement || {}, [ + 'enforceJS', enforceJS => enforceJS !== false, // defaults to true + 'enforcePBS', enforcePBS => enforcePBS === true, // defaults to false + 'floorDeals', floorDeals => floorDeals === true, // defaults to false + 'bidAdjustment', bidAdjustment => bidAdjustment !== false, // defaults to true + ]), + 'additionalSchemaFields', additionalSchemaFields => typeof additionalSchemaFields === 'object' && Object.keys(additionalSchemaFields).length > 0 ? addFieldOverrides(additionalSchemaFields) : undefined, + 'data', data => (data && parseFloorData(data, 'setConfig')) || _floorsConfig.data // do not overwrite if passed in data not valid + ]); + + // if enabled then do some stuff + if (_floorsConfig.enabled) { + // handle the floors fetch + generateAndHandleFetch(_floorsConfig.endpoint); + + if (!addedFloorsHook) { + // register hooks / listening events + // when auction finishes remove it's associated floor data + events.on(CONSTANTS.EVENTS.AUCTION_END, (args) => delete _floorDataForAuction[args.auctionId]); + + // we want our hooks to run after the currency hooks + getGlobal().requestBids.before(requestBidsHook, 50); + // if user has debug on then we want to allow the debugging module to run before this, assuming they are testing priceFloors + // debugging is currently set at 5 priority + getHook('addBidResponse').before(addBidResponseHook, utils.debugTurnedOn() ? 4 : 50); + addedFloorsHook = true; + } + } else { + utils.logInfo(`${MODULE_NAME}: Turning off module`); + + _floorsConfig = {}; + _floorDataForAuction = {}; + + getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + getGlobal().requestBids.getHooks({hook: requestBidsHook}).remove(); + + addedFloorsHook = false; + } +} + +/** + * @summary Analytics adapters especially need context of what the floors module is doing in order + * to best create informed models. This function attaches necessary information to the bidResponse object for processing + */ +function addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm) { + bid.floorData = { + floorValue: floorInfo.matchingFloor, + floorRule: floorInfo.matchingRule, + floorCurrency: floorData.data.currency, + cpmAfterAdjustments: adjustedCpm, + enforcements: {...floorData.enforcement}, + matchedFields: {} + }; + floorData.data.schema.fields.forEach((field, index) => { + let matchedValue = floorInfo.matchingData.split(floorData.data.schema.delimiter)[index]; + bid.floorData.matchedFields[field] = matchedValue; + }); +} + +/** + * @summary takes the enforcement flags and the bid itself and determines if it should be floored + */ +function shouldFloorBid(floorData, floorInfo, bid) { + let enforceJS = utils.deepAccess(floorData, 'enforcement.enforceJS') !== false; + let shouldFloorDeal = utils.deepAccess(floorData, 'enforcement.floorDeals') === true || !bid.dealId; + let bidBelowFloor = bid.floorData.cpmAfterAdjustments < floorInfo.matchingFloor; + return enforceJS && (bidBelowFloor && shouldFloorDeal); +} + +/** + * @summary The main driving force of floors. On bidResponse we hook in and intercept bidResponses. + * And if the rule we find determines a bid should be floored we will do so. + */ +export function addBidResponseHook(fn, adUnitCode, bid) { + let floorData = _floorDataForAuction[this.bidderRequest.auctionId]; + // if no floor data or associated bidRequest then bail + const matchingBidRequest = find(this.bidderRequest.bids, bidRequest => bidRequest.bidId && bidRequest.bidId === bid.requestId); + if (!floorData || !bid || floorData.skipped || !matchingBidRequest) { + return fn.call(this, adUnitCode, bid); + } + + // get the matching rule + let floorInfo = getFirstMatchingFloor(floorData.data, {...matchingBidRequest}, {...bid, size: [bid.width, bid.height]}); + + if (!floorInfo.matchingFloor) { + utils.logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); + return fn.call(this, adUnitCode, bid); + } + + // determine the base cpm to use based on if the currency matches the floor currency + let adjustedCpm; + let floorCurrency = floorData.data.currency.toUpperCase(); + let bidResponseCurrency = bid.currency || 'USD'; // if an adapter does not set a bid currency and currency module not on it may come in as undefined + if (floorCurrency === bidResponseCurrency.toUpperCase()) { + adjustedCpm = bid.cpm; + } else if (bid.originalCurrency && floorCurrency === bid.originalCurrency.toUpperCase()) { + adjustedCpm = bid.originalCpm; + } else { + try { + adjustedCpm = getGlobal().convertCurrency(bid.cpm, bidResponseCurrency.toUpperCase(), floorCurrency); + } catch (err) { + utils.logError(`${MODULE_NAME}: Unable do get currency conversion for bidResponse to Floor Currency. Do you have Currency module enabled? ${bid}`); + return fn.call(this, adUnitCode, bid); + } + } + + // ok we got the bid response cpm in our desired currency. Now we need to run the bidders CPMAdjustment function if it exists + adjustedCpm = getBiddersCpmAdjustment(bid.bidderCode, adjustedCpm); + + // add necessary data information for analytics adapters / floor providers would possibly need + addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm); + + // now do the compare! + if (shouldFloorBid(floorData, floorInfo, bid)) { + // bid fails floor -> throw it out + // create basic bid no-bid with necessary data fro analytics adapters + let flooredBid = createBid(CONSTANTS.STATUS.NO_BID, matchingBidRequest); + Object.assign(flooredBid, utils.pick(bid, [ + 'floorData', + 'width', + 'height', + 'mediaType', + 'currency', + 'originalCpm', + 'originalCurrency', + 'getCpmInNewCurrency', + ])); + flooredBid.status = CONSTANTS.BID_STATUS.BID_REJECTED; + // if floor not met update bid with 0 cpm so it is not included downstream and marked as no-bid + flooredBid.cpm = 0; + utils.logWarn(`${MODULE_NAME}: ${flooredBid.bidderCode}'s Bid Response for ${adUnitCode} was rejected due to floor not met`, bid); + return fn.call(this, adUnitCode, flooredBid); + } + return fn.call(this, adUnitCode, bid); +} + +config.getConfig('floors', config => handleSetFloorsConfig(config.floors)); diff --git a/modules/priceFloors.md b/modules/priceFloors.md new file mode 100644 index 00000000000..d09be78c620 --- /dev/null +++ b/modules/priceFloors.md @@ -0,0 +1,62 @@ +## Dynamic Price Floors + +### Setup +```javascript +pbjs.setConfig({ + floors: { + enabled: true, //defaults to true + enforcement: { + floorDeals: false, //defaults to false + bidAdjustment: true, //defaults to true + enforceJS: true //defaults to true + }, + auctionDelay: 150, // in milliseconds defaults to 0 + endpoint: { + url: 'http://localhost:1500/floor-domains', + method: 'GET' // Only get supported for now + }, + data: { + schema: { + fields: ['mediaType', 'size'] + }, + values: { + 'banner|300x250': 0.86, + 'banner|300x600': 0.97, + 'banner|728x90': 1.12, + 'banner|*': 0.54, + 'video|640x480': 6.76, + 'video|1152x648': 11.76, + 'video|*': 4.55, + '*|*': 0.30 + }, + default: 0.01 + } + } +}); +``` + +| Parameter | Description | +|------------------------|---------------------------------------------------------------------------------------------------------------------| +| enabled | Wether to turn off or on the floors module | +| enforcement | object of booleans which control certain features of the module | +| auctionDelay | The time to suspend and auction while waiting for a real time price floors fetch to come back | +| endpoint | An object describing the endpoint to retrieve floor data from. GET only | +| data | The data to be used to select appropriate floors. See schema for more detail | +| additionalSchemaFields | An object of additional fields to be used in a floor data object. The schema is KEY: function to retrieve the match | + +### Passing floors to Bid Adapters +Because it is possible for many rules to match any given bidRequest, (wether it be due to more than one size or more than one mediaType), an encapsolated function is to be passed to bidders which will allow bidders to have insight as to what the floor could be. + +The function `getFloor` will be attached to every bidRequestObject passed to bid adapters if the price floors are enabled for a given auction. + +This function can takes in an object with the following optional parameters: + +| Parameter | Description | Example | Default | +|-----------|------------------------------------------------------------------------|------------|---------| +| currency | The 3 character currency code which the bid adapter wants the floor in | "JPY" | "USD" | +| mediaType | The specific mediaType to get a floor for | "banner" | "*" | +| size | The specific size to get a matching floor on | [300, 250] | "*" | + +If a bid adapter passes in `*` as an attribute, then the `priceFloors` module will attempt to select the best rule based on context. + +For example, if an adapter passes in a `*`, but the bidRequest only has a single size and a single mediaType, then the `getFloor` function will attempt to get a rule for that size before matching with the `*` catch-all. Similarily, if mediaType can be inferred on the bidRequest, it will use it. \ No newline at end of file diff --git a/modules/proxistoreBidAdapter.js b/modules/proxistoreBidAdapter.js index 6facf5d1480..e8ef4878d8e 100644 --- a/modules/proxistoreBidAdapter.js +++ b/modules/proxistoreBidAdapter.js @@ -2,16 +2,16 @@ const { registerBidder } = require('../src/adapters/bidderFactory.js'); const BIDDER_CODE = 'proxistore'; const PROXISTORE_VENDOR_ID = 418; -function _mapSizes(sizes) { - const flatSize = sizes.reduce((acc, val) => acc.concat(val), []); ; - return flatSize.map(array1d => { return { width: array1d[0], height: array1d[1] } }); -} - function _createServerRequest(bidRequests, bidderRequest) { + const sizeIds = []; + bidRequests.forEach(bid => { + const sizeId = {id: bid.bidId, sizes: bid.sizes.map(size => { return { width: size[0], height: size[1] } })}; + sizeIds.push(sizeId); + }); const payload = { auctionId: bidRequests[0].auctionId, - transactionId: bidRequests[0].transactionId, - sizes: _mapSizes(bidRequests.map(x => x.sizes)), + transactionId: bidRequests[0].auctionId, + bids: sizeIds, website: bidRequests[0].params.website, language: bidRequests[0].params.language, gdpr: { @@ -19,9 +19,6 @@ function _createServerRequest(bidRequests, bidderRequest) { } }; - const bidIds = bidRequests.map(req => req.bidId); - bidIds.length === 1 ? payload.bidId = bidIds[0] : payload.bidIds = bidIds; - const options = { contentType: 'application/json', withCredentials: true @@ -42,7 +39,7 @@ function _createServerRequest(bidRequests, bidderRequest) { return { method: 'POST', - url: bidRequests[0].params.url || 'https://abs.proxistore.com/' + payload.language + '/v3/rtb/prebid', + url: bidRequests[0].params.url || 'https://abs.proxistore.com/' + payload.language + '/v3/rtb/prebid/multi', data: JSON.stringify(payload), options: options }; diff --git a/modules/pubCommonId.js b/modules/pubCommonId.js index f476091ce2b..174fa6ffe6e 100644 --- a/modules/pubCommonId.js +++ b/modules/pubCommonId.js @@ -6,8 +6,10 @@ import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; import events from '../src/events.js'; -import * as url from '../src/url.js'; import CONSTANTS from '../src/constants.json'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const ID_NAME = '_pubcid'; const OPTOUT_NAME = '_pubcid_optout'; @@ -37,10 +39,10 @@ export function setStorageItem(key, val, expires) { try { if (expires !== undefined && expires != null) { const expStr = (new Date(Date.now() + (expires * 60 * 1000))).toUTCString(); - utils.setDataInLocalStorage(key + EXP_SUFFIX, expStr); + storage.setDataInLocalStorage(key + EXP_SUFFIX, expStr); } - utils.setDataInLocalStorage(key, val); + storage.setDataInLocalStorage(key, val); } catch (e) { utils.logMessage(e); } @@ -55,18 +57,18 @@ export function getStorageItem(key) { let val = null; try { - const expVal = utils.getDataFromLocalStorage(key + EXP_SUFFIX); + const expVal = storage.getDataFromLocalStorage(key + EXP_SUFFIX); if (!expVal) { // If there is no expiry time, then just return the item - val = utils.getDataFromLocalStorage(key); + val = storage.getDataFromLocalStorage(key); } else { // Only return the item if it hasn't expired yet. // Otherwise delete the item. const expDate = new Date(expVal); const isValid = (expDate.getTime() - Date.now()) > 0; if (isValid) { - val = utils.getDataFromLocalStorage(key); + val = storage.getDataFromLocalStorage(key); } else { removeStorageItem(key); } @@ -84,8 +86,8 @@ export function getStorageItem(key) { */ export function removeStorageItem(key) { try { - utils.removeDataFromLocalStorage(key + EXP_SUFFIX); - utils.removeDataFromLocalStorage(key); + storage.removeDataFromLocalStorage(key + EXP_SUFFIX); + storage.removeDataFromLocalStorage(key); } catch (e) { utils.logMessage(e); } @@ -101,7 +103,7 @@ function readValue(name, type) { let value; if (!type) { type = pubcidConfig.typeEnabled; } if (type === COOKIE) { - value = utils.getCookie(name); + value = storage.getCookie(name); } else if (type === LOCAL_STORAGE) { value = getStorageItem(name); } @@ -139,9 +141,9 @@ function queuePixelCallback(pixelUrl, id) { id = id || ''; // Use pubcid as a cache buster - const urlInfo = url.parse(pixelUrl); + const urlInfo = utils.parseUrl(pixelUrl); urlInfo.search.id = encodeURIComponent('pubcid:' + id); - const targetUrl = url.format(urlInfo); + const targetUrl = utils.buildUrl(urlInfo); events.on(CONSTANTS.EVENTS.AUCTION_END, function auctionEndHandler() { events.off(CONSTANTS.EVENTS.AUCTION_END, auctionEndHandler); @@ -223,12 +225,12 @@ export function requestBidHook(next, config) { export function setCookie(name, value, expires, sameSite) { let expTime = new Date(); expTime.setTime(expTime.getTime() + expires * 1000 * 60); - utils.setCookie(name, value, expTime.toGMTString(), sameSite); + storage.setCookie(name, value, expTime.toGMTString(), sameSite); } // Helper to read a cookie export function getCookie(name) { - return utils.getCookie(name); + return storage.getCookie(name); } /** @@ -263,12 +265,12 @@ export function setConfig({ enable, expInterval, type = 'html5,cookie', create, for (let i = 0; i < typeArray.length; ++i) { const name = typeArray[i].trim(); if (name === COOKIE) { - if (utils.cookiesAreEnabled()) { + if (storage.cookiesAreEnabled()) { pubcidConfig.typeEnabled = COOKIE; break; } } else if (name === LOCAL_STORAGE) { - if (utils.hasLocalStorage()) { + if (storage.hasLocalStorage()) { pubcidConfig.typeEnabled = LOCAL_STORAGE; break; } @@ -282,8 +284,8 @@ export function setConfig({ enable, expInterval, type = 'html5,cookie', create, export function initPubcid() { config.getConfig('pubcid', config => setConfig(config.pubcid)); - const optout = (utils.cookiesAreEnabled() && readValue(OPTOUT_NAME, COOKIE)) || - (utils.hasLocalStorage() && readValue(OPTOUT_NAME, LOCAL_STORAGE)); + const optout = (storage.cookiesAreEnabled() && readValue(OPTOUT_NAME, COOKIE)) || + (storage.hasLocalStorage() && readValue(OPTOUT_NAME, LOCAL_STORAGE)); if (!optout) { $$PREBID_GLOBAL$$.requestBids.before(requestBidHook); diff --git a/modules/pubCommonIdSystem.js b/modules/pubCommonIdSystem.js index b12d981c2ba..8e2be1207f5 100644 --- a/modules/pubCommonIdSystem.js +++ b/modules/pubCommonIdSystem.js @@ -6,7 +6,6 @@ */ import * as utils from '../src/utils.js'; -import * as url from '../src/url.js'; import {submodule} from '../src/hook.js'; const PUB_COMMON_ID = 'PublisherCommonId'; @@ -32,9 +31,9 @@ export const pubCommonIdSubmodule = { } // Use pubcid as a cache buster - const urlInfo = url.parse(pixelUrl); + const urlInfo = utils.parseUrl(pixelUrl); urlInfo.search.id = encodeURIComponent('pubcid:' + id); - const targetUrl = url.format(urlInfo); + const targetUrl = utils.buildUrl(urlInfo); return function () { utils.triggerPixel(targetUrl); diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js new file mode 100755 index 00000000000..9d2f45159e9 --- /dev/null +++ b/modules/pubmaticAnalyticsAdapter.js @@ -0,0 +1,443 @@ +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; +import { ajax } from '../src/ajax.js'; +import { config } from '../src/config.js'; +import * as utils from '../src/utils.js'; + +/// /////////// CONSTANTS ////////////// +const ADAPTER_CODE = 'pubmatic'; +const SEND_TIMEOUT = 2000; +const END_POINT_HOST = 'https://t.pubmatic.com/'; +const END_POINT_BID_LOGGER = END_POINT_HOST + 'wl?'; +const END_POINT_WIN_BID_LOGGER = END_POINT_HOST + 'wt?'; +const LOG_PRE_FIX = 'PubMatic-Analytics: '; +const cache = { + auctions: {} +}; +const SUCCESS = 'success'; +const NO_BID = 'no-bid'; +const ERROR = 'error'; +const REQUEST_ERROR = 'request-error'; +const TIMEOUT_ERROR = 'timeout-error'; +const EMPTY_STRING = ''; +const MEDIA_TYPE_BANNER = 'banner'; +const CURRENCY_USD = 'USD'; +const BID_PRECISION = 2; +// todo: input profileId and profileVersionId ; defaults to zero or one +const DEFAULT_PUBLISHER_ID = 0; +const DEFAULT_PROFILE_ID = 0; +const DEFAULT_PROFILE_VERSION_ID = 0; +const enc = window.encodeURIComponent; + +/// /////////// VARIABLES ////////////// +let publisherId = DEFAULT_PUBLISHER_ID; // int: mandatory +let profileId = DEFAULT_PROFILE_ID; // int: optional +let profileVersionId = DEFAULT_PROFILE_VERSION_ID; // int: optional +let s2sBidders = []; + +/// /////////// HELPER FUNCTIONS ////////////// + +function sizeToDimensions(size) { + return { + width: size.w || size[0], + height: size.h || size[1] + }; +} + +function validMediaType(type) { + return ({'banner': 1, 'native': 1, 'video': 1}).hasOwnProperty(type); +} + +function formatSource(src) { + if (typeof src === 'undefined') { + src = 'client'; + } else if (src === 's2s') { + src = 'server'; + } + return src.toLowerCase(); +} + +function setMediaTypes(types, bid) { + if (bid.mediaType && validMediaType(bid.mediaType)) { + return [bid.mediaType]; + } + if (Array.isArray(types)) { + return types.filter(validMediaType); + } + if (typeof types === 'object') { + if (!bid.sizes) { + bid.dimensions = []; + utils._each(types, (type) => + bid.dimensions = bid.dimensions.concat( + type.sizes.map(sizeToDimensions) + ) + ); + } + return Object.keys(types).filter(validMediaType); + } + return [MEDIA_TYPE_BANNER]; +} + +function copyRequiredBidDetails(bid) { + return utils.pick(bid, [ + 'bidder', bidder => bidder.toLowerCase(), + 'bidId', + 'status', () => NO_BID, // default a bid to NO_BID until response is recieved or bid is timed out + 'finalSource as source', + 'params', + 'adUnit', () => utils.pick(bid, [ + 'adUnitCode', + 'transactionId', + 'sizes as dimensions', sizes => sizes.map(sizeToDimensions), + 'mediaTypes', (types) => setMediaTypes(types, bid) + ]) + ]); +} + +function setBidStatus(bid, args) { + switch (args.getStatusCode()) { + case CONSTANTS.STATUS.GOOD: + bid.status = SUCCESS; + delete bid.error; // it's possible for this to be set by a previous timeout + break; + case CONSTANTS.STATUS.NO_BID: + bid.status = NO_BID; + delete bid.error; + break; + default: + bid.status = ERROR; + bid.error = { + code: REQUEST_ERROR + }; + } +} + +function parseBidResponse(bid) { + return utils.pick(bid, [ + 'bidPriceUSD', () => { + // todo: check whether currency cases are handled here + if (typeof bid.currency === 'string' && bid.currency.toUpperCase() === CURRENCY_USD) { + return window.parseFloat(Number(bid.cpm).toFixed(BID_PRECISION)); + } + // use currency conversion function if present + if (typeof bid.getCpmInNewCurrency === 'function') { + return window.parseFloat(Number(bid.getCpmInNewCurrency(CURRENCY_USD)).toFixed(BID_PRECISION)); + } + utils.logWarn(LOG_PRE_FIX + 'Could not determine the bidPriceUSD of the bid ', bid); + }, + 'dealId', + 'currency', + 'cpm', () => window.parseFloat(Number(bid.cpm).toFixed(BID_PRECISION)), + 'originalCpm', () => window.parseFloat(Number(bid.originalCpm).toFixed(BID_PRECISION)), + 'originalCurrency', + 'dealChannel', + 'meta', + 'status', + 'error', + 'bidId', + 'mediaType', + 'params', + 'mi', + 'dimensions', () => utils.pick(bid, [ + 'width', + 'height' + ]) + ]); +} + +function getDomainFromUrl(url) { + let a = window.document.createElement('a'); + a.href = url; + return a.hostname; +} + +function getHighestBidForAdUnit(adUnit) { + return Object.keys(adUnit.bids).reduce(function(currentHighestBid, bidId) { + // todo: later we will need to consider grossECPM and netECPM + let bid = adUnit.bids[bidId]; + if (bid.bidResponse && bid.bidResponse.bidPriceUSD > currentHighestBid.bidPriceUSD) { + currentHighestBid.bidPriceUSD = bid.bidResponse.bidPriceUSD; + currentHighestBid.bidId = bidId; + } + return currentHighestBid; + }, {bidId: '', bidPriceUSD: 0}); +} + +function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId) { + const highestsBid = getHighestBidForAdUnit(adUnit); + return Object.keys(adUnit.bids).reduce(function(partnerBids, bidId) { + let bid = adUnit.bids[bidId]; + partnerBids.push({ + 'pn': bid.bidder, + 'bidid': bid.bidId, + 'db': bid.bidResponse ? 0 : 1, + 'kgpv': bid.params.kgpv ? bid.params.kgpv : adUnitId, + 'kgpsv': bid.params.kgpv ? bid.params.kgpv : adUnitId, + 'psz': bid.bidResponse ? (bid.bidResponse.dimensions.width + 'x' + bid.bidResponse.dimensions.height) : '0x0', + 'eg': bid.bidResponse ? bid.bidResponse.bidPriceUSD : 0, // todo: later we will need to consider grossECPM and netECPM, precision + 'en': bid.bidResponse ? bid.bidResponse.bidPriceUSD : 0, // todo: later we will need to consider grossECPM and netECPM, precision + 'di': bid.bidResponse ? (bid.bidResponse.dealId || EMPTY_STRING) : EMPTY_STRING, + 'dc': bid.bidResponse ? (bid.bidResponse.dealChannel || EMPTY_STRING) : EMPTY_STRING, + 'l1': bid.bidResponse ? bid.clientLatencyTimeMs : 0, + 'l2': 0, + // 'ss': (bid.source === 'server' ? 1 : 0), // todo: is there any special handling required as per OW? + 'ss': (s2sBidders.indexOf(bid.bidder) > -1) ? 1 : 0, + 't': (bid.status == ERROR && bid.error.code == TIMEOUT_ERROR) ? 1 : 0, + 'wb': highestsBid.bidId === bid.bidId ? 1 : 0, + 'mi': bid.bidResponse ? (bid.bidResponse.mi || undefined) : undefined, + 'af': bid.bidResponse ? (bid.bidResponse.mediaType || undefined) : undefined, + 'ocpm': bid.bidResponse ? (bid.bidResponse.originalCpm || 0) : 0, + 'ocry': bid.bidResponse ? (bid.bidResponse.originalCurrency || CURRENCY_USD) : CURRENCY_USD + }); + return partnerBids; + }, []) +} + +function executeBidsLoggerCall(auctionId) { + let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; + let auctionCache = cache.auctions[auctionId]; + let outputObj = { s: [] }; + let pixelURL = END_POINT_BID_LOGGER; + + if (!auctionCache) { + return; + } + + if (auctionCache.sent) { + return; + } + + pixelURL += 'pubid=' + publisherId; + outputObj['pubid'] = '' + publisherId; + outputObj['iid'] = '' + auctionId; + outputObj['to'] = '' + auctionCache.timeout; + outputObj['purl'] = referrer; + outputObj['orig'] = getDomainFromUrl(referrer); + outputObj['tst'] = Math.round((new window.Date()).getTime() / 1000); + outputObj['pid'] = '' + profileId; + outputObj['pdvid'] = '' + profileVersionId; + + // GDPR support + if (auctionCache.gdprConsent) { + outputObj['cns'] = auctionCache.gdprConsent.consentString || ''; + outputObj['gdpr'] = auctionCache.gdprConsent.gdprApplies === true ? 1 : 0; + pixelURL += '&gdEn=1'; + } + + outputObj.s = Object.keys(auctionCache.adUnitCodes).reduce(function(slotsArray, adUnitId) { + let adUnit = auctionCache.adUnitCodes[adUnitId]; + let slotObject = { + 'sn': adUnitId, + 'sz': adUnit.dimensions.map(e => e[0] + 'x' + e[1]), + 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId) + }; + slotsArray.push(slotObject); + return slotsArray; + }, []); + + auctionCache.sent = true; + + ajax( + pixelURL, + null, + 'json=' + enc(JSON.stringify(outputObj)), + { + contentType: 'application/x-www-form-urlencoded', + withCredentials: true, + method: 'POST' + } + ); +} + +function executeBidWonLoggerCall(auctionId, adUnitId) { + const winningBidId = cache.auctions[auctionId].adUnitCodes[adUnitId].bidWon; + const winningBid = cache.auctions[auctionId].adUnitCodes[adUnitId].bids[winningBidId]; + let pixelURL = END_POINT_WIN_BID_LOGGER; + pixelURL += 'pubid=' + publisherId; + pixelURL += '&purl=' + enc(config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''); + pixelURL += '&tst=' + Math.round((new window.Date()).getTime() / 1000); + pixelURL += '&iid=' + enc(auctionId); + pixelURL += '&bidid=' + enc(winningBidId); + pixelURL += '&pid=' + enc(profileId); + pixelURL += '&pdvid=' + enc(profileVersionId); + pixelURL += '&slot=' + enc(adUnitId); + pixelURL += '&pn=' + enc(winningBid.bidder); + pixelURL += '&en=' + enc(winningBid.bidResponse.bidPriceUSD); // todo: later we will need to consider grossECPM and netECPM + pixelURL += '&eg=' + enc(winningBid.bidResponse.bidPriceUSD); // todo: later we will need to consider grossECPM and netECPM + pixelURL += '&kgpv=' + enc(winningBid.params.kgpv || adUnitId); + ajax( + pixelURL, + null, + null, + { + contentType: 'application/x-www-form-urlencoded', + withCredentials: true, + method: 'GET' + } + ); +} + +/// /////////// ADAPTER EVENT HANDLER FUNCTIONS ////////////// + +function auctionInitHandler(args) { + s2sBidders = (function() { + let s2sConf = config.getConfig('s2sConfig'); + return (s2sConf && utils.isArray(s2sConf.bidders)) ? s2sConf.bidders : []; + }()); + let cacheEntry = utils.pick(args, [ + 'timestamp', + 'timeout', + 'bidderDonePendingCount', () => args.bidderRequests.length, + ]); + cacheEntry.adUnitCodes = {}; + cacheEntry.referer = args.bidderRequests[0].refererInfo.referer; + cache.auctions[args.auctionId] = cacheEntry; +} + +function bidRequestedHandler(args) { + cache.auctions[args.auctionId].gdprConsent = args.gdprConsent || undefined; + args.bids.forEach(function(bid) { + if (!cache.auctions[args.auctionId].adUnitCodes.hasOwnProperty(bid.adUnitCode)) { + cache.auctions[args.auctionId].adUnitCodes[bid.adUnitCode] = { + bids: {}, + bidWon: false, + dimensions: bid.sizes + }; + } + cache.auctions[args.auctionId].adUnitCodes[bid.adUnitCode].bids[bid.bidId] = copyRequiredBidDetails(bid); + }) +} + +function bidResponseHandler(args) { + let bid = cache.auctions[args.auctionId].adUnitCodes[args.adUnitCode].bids[args.requestId]; + if (!bid) { + utils.logError(LOG_PRE_FIX + 'Could not find associated bid request for bid response with requestId: ', args.requestId); + return; + } + bid.source = formatSource(bid.source || args.source); + setBidStatus(bid, args); + bid.clientLatencyTimeMs = Date.now() - cache.auctions[args.auctionId].timestamp; + bid.bidResponse = parseBidResponse(args); +} + +function bidderDoneHandler(args) { + cache.auctions[args.auctionId].bidderDonePendingCount--; + args.bids.forEach(bid => { + let cachedBid = cache.auctions[bid.auctionId].adUnitCodes[bid.adUnitCode].bids[bid.bidId || bid.requestId]; + if (typeof bid.serverResponseTimeMs !== 'undefined') { + cachedBid.serverLatencyTimeMs = bid.serverResponseTimeMs; + } + if (!cachedBid.status) { + cachedBid.status = NO_BID; + } + if (!cachedBid.clientLatencyTimeMs) { + cachedBid.clientLatencyTimeMs = Date.now() - cache.auctions[bid.auctionId].timestamp; + } + }); +} + +function bidWonHandler(args) { + let auctionCache = cache.auctions[args.auctionId]; + auctionCache.adUnitCodes[args.adUnitCode].bidWon = args.requestId; + executeBidWonLoggerCall(args.auctionId, args.adUnitCode); +} + +function auctionEndHandler(args) { + // if for the given auction bidderDonePendingCount == 0 then execute logger call sooners + setTimeout(() => { + executeBidsLoggerCall.call(this, args.auctionId); + }, (cache.auctions[args.auctionId].bidderDonePendingCount === 0 ? 500 : SEND_TIMEOUT)); +} + +function bidTimeoutHandler(args) { + // db = 1 and t = 1 means bidder did NOT respond with a bid but we got a timeout notification + // db = 0 and t = 1 means bidder did respond with a bid but post timeout + args.forEach(badBid => { + let auctionCache = cache.auctions[badBid.auctionId]; + let bid = auctionCache.adUnitCodes[badBid.adUnitCode].bids[ badBid.bidId || badBid.requestId ]; + if (bid) { + bid.status = ERROR; + bid.error = { + code: TIMEOUT_ERROR + }; + } else { + utils.logWarn(LOG_PRE_FIX + 'bid not found'); + } + }); +} + +/// /////////// ADAPTER DEFINITION ////////////// + +let baseAdapter = adapter({analyticsType: 'endpoint'}); +let pubmaticAdapter = Object.assign({}, baseAdapter, { + + enableAnalytics(conf = {}) { + let error = false; + + if (typeof conf.options === 'object') { + if (conf.options.publisherId) { + publisherId = Number(conf.options.publisherId); + } + profileId = Number(conf.options.profileId) || DEFAULT_PROFILE_ID; + profileVersionId = Number(conf.options.profileVersionId) || DEFAULT_PROFILE_VERSION_ID; + } else { + utils.logError(LOG_PRE_FIX + 'Config not found.'); + error = true; + } + + if (!publisherId) { + utils.logError(LOG_PRE_FIX + 'Missing publisherId(Number).'); + error = true; + } + + if (error) { + utils.logError(LOG_PRE_FIX + 'Not collecting data due to error(s).'); + } else { + baseAdapter.enableAnalytics.call(this, conf); + } + }, + + disableAnalytics() { + publisherId = DEFAULT_PUBLISHER_ID; + profileId = DEFAULT_PROFILE_ID; + profileVersionId = DEFAULT_PROFILE_VERSION_ID; + s2sBidders = []; + baseAdapter.disableAnalytics.apply(this, arguments); + }, + + track({eventType, args}) { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + auctionInitHandler(args); + break; + case CONSTANTS.EVENTS.BID_REQUESTED: + bidRequestedHandler(args); + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + bidResponseHandler(args); + break; + case CONSTANTS.EVENTS.BIDDER_DONE: + bidderDoneHandler(args); + break; + case CONSTANTS.EVENTS.BID_WON: + bidWonHandler(args); + break; + case CONSTANTS.EVENTS.AUCTION_END: + auctionEndHandler(args); + break; + case CONSTANTS.EVENTS.BID_TIMEOUT: + bidTimeoutHandler(args); + break; + } + } +}); + +/// /////////// ADAPTER REGISTRATION ////////////// + +adapterManager.registerAnalyticsAdapter({ + adapter: pubmaticAdapter, + code: ADAPTER_CODE +}); + +export default pubmaticAdapter; diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 1a5de695c3c..4502dc66a65 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -6,7 +6,8 @@ import {config} from '../src/config.js'; const BIDDER_CODE = 'pubmatic'; const LOG_WARN_PREFIX = 'PubMatic: '; const ENDPOINT = 'https://hbopenbid.pubmatic.com/translator?source=prebid-client'; -const USYNCURL = 'https://ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p='; +const USER_SYNC_URL_IFRAME = 'https://ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p='; +const USER_SYNC_URL_IMAGE = 'https://image8.pubmatic.com/AdServer/ImgSync?p='; const DEFAULT_CURRENCY = 'USD'; const AUCTION_TYPE = 1; const PUBMATIC_DIGITRUST_KEY = 'nFIn8aLzbd'; @@ -671,6 +672,7 @@ function _handleEids(payload, validBidRequests) { _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.lipb.lipbid`), 'liveintent.com', 1); _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.parrableid`), 'parrable.com', 1); _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.britepoolid`), 'britepool.com', 1); + _addExternalUserId(eids, utils.deepAccess(bidRequest, `userId.netId`), 'netid.de', 1); } if (eids.length > 0) { payload.user.eids = eids; @@ -816,6 +818,7 @@ function _handleDealCustomTargetings(payload, dctrArr, validBidRequests) { export const spec = { code: BIDDER_CODE, + gvlid: 76, supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid @@ -918,6 +921,11 @@ export const spec = { payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim(); payload.site.domain = _getDomainFromURL(payload.site.page); + // merge the device from config.getConfig('device') + if (typeof config.getConfig('device') === 'object') { + payload.device = Object.assign(payload.device, config.getConfig('device')); + } + // passing transactionId in source.tid utils.deepSetValue(payload, 'source.tid', conf.transactionId); @@ -951,6 +959,16 @@ export const spec = { _handleEids(payload, validBidRequests); _blockedIabCategoriesValidation(payload, blockedIabCategories); + // Note: Do not move this block up + // if site object is set in Prebid config then we need to copy required fields from site into app and unset the site object + if (typeof config.getConfig('app') === 'object') { + payload.app = config.getConfig('app'); + // not copying domain from site as it is a derived value from page + payload.app.publisher = payload.site.publisher; + payload.app.ext = payload.site.ext || UNDEFINED; + delete payload.site; + } + return { method: 'POST', url: ENDPOINT, @@ -988,7 +1006,9 @@ export const spec = { netRevenue: NET_REVENUE, ttl: 300, referrer: parsedReferrer, - ad: bid.adm + ad: bid.adm, + pm_seat: seatbidder.seat || null, + pm_dspid: bid.ext && bid.ext.dspid ? bid.ext.dspid : null }; if (parsedRequest.imp && parsedRequest.imp.length > 0) { parsedRequest.imp.forEach(req => { @@ -1024,6 +1044,13 @@ export const spec = { newBid.meta.clickUrl = bid.adomain[0]; } + // adserverTargeting + if (seatbidder.ext && seatbidder.ext.buyid) { + newBid.adserverTargeting = { + 'hb_buyid_pubmatic': seatbidder.ext.buyid + }; + } + bidResponses.push(newBid); }); }); @@ -1038,7 +1065,7 @@ export const spec = { * Register User Sync. */ getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { - let syncurl = USYNCURL + publisherId; + let syncurl = '' + publisherId; // Attaching GDPR Consent Params in UserSync url if (gdprConsent) { @@ -1059,10 +1086,13 @@ export const spec = { if (syncOptions.iframeEnabled) { return [{ type: 'iframe', - url: syncurl + url: USER_SYNC_URL_IFRAME + syncurl }]; } else { - utils.logWarn(LOG_WARN_PREFIX + 'Please enable iframe based user sync.'); + return [{ + type: 'image', + url: USER_SYNC_URL_IMAGE + syncurl + }]; } }, diff --git a/modules/pubmaticBidAdapter.md b/modules/pubmaticBidAdapter.md index 4a5d881a637..a045bed3e2b 100644 --- a/modules/pubmaticBidAdapter.md +++ b/modules/pubmaticBidAdapter.md @@ -202,5 +202,7 @@ pbjs.setConfig({ ``` Note: Combine the above the configuration with any other UserSync configuration. Multiple setConfig() calls overwrite each other and only last call for a given attribute will take effect. -Note: PubMatic will return a test-bid if "pubmaticTest=true" is present in page URL +# Notes: +- PubMatic will return a test-bid if "pubmaticTest=true" is present in page URL +- PubMatic will set bid.adserverTargeting.hb_buyid_pubmatic targeting key while submitting a bid into Prebid diff --git a/modules/pubstackAnalyticsAdapter.js b/modules/pubstackAnalyticsAdapter.js new file mode 100644 index 00000000000..b1da40c5b89 --- /dev/null +++ b/modules/pubstackAnalyticsAdapter.js @@ -0,0 +1,15 @@ +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; + +const pubstackAnalytics = adapter({ + global: 'PubstackAnalytics', + handler: 'on', + analyticsType: 'bundle' +}); + +adapterManager.registerAnalyticsAdapter({ + adapter: pubstackAnalytics, + code: 'pubstack', +}); + +export default pubstackAnalytics; diff --git a/modules/pubstackAnalyticsAdapter.md b/modules/pubstackAnalyticsAdapter.md new file mode 100644 index 00000000000..0b473bd39d7 --- /dev/null +++ b/modules/pubstackAnalyticsAdapter.md @@ -0,0 +1,9 @@ +# Overview + +Module Name: Pubstack Analytics Adapter +Module Type: Analytics Adapter +Maintainer: support@pubstack.io + +# Description + +This is the Pubstack Analytics Adapter. [Contact Guillaume from our Support Team](mailto:support@pubstack.io) to start analyzing your HB stack. diff --git a/modules/pubwiseAnalyticsAdapter.js b/modules/pubwiseAnalyticsAdapter.js index df38b3ab076..915aeb58f99 100644 --- a/modules/pubwiseAnalyticsAdapter.js +++ b/modules/pubwiseAnalyticsAdapter.js @@ -2,8 +2,11 @@ import {ajax} from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; +import { getStorageManager } from '../src/storageManager.js'; const utils = require('../src/utils.js'); +const storage = getStorageManager(); + /**** * PubWise.io Analytics * Contact: support@pubwise.io @@ -59,14 +62,14 @@ function enrichWithUTM(dataBag) { if (newUtm === false) { for (let prop in utmKeys) { - let itemValue = utils.getDataFromLocalStorage(`pw-${prop}`); + let itemValue = storage.getDataFromLocalStorage(`pw-${prop}`); if (itemValue.length !== 0) { dataBag[prop] = itemValue; } } } else { for (let prop in utmKeys) { - utils.setDataInLocalStorage(`pw-${prop}`, utmKeys[prop]); + storage.setDataInLocalStorage(`pw-${prop}`, utmKeys[prop]); } } } catch (e) { diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index cbe37e66cd7..7bfa8686728 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -28,6 +28,8 @@ export const spec = { code: 'pulsepoint', + gvlid: 81, + aliases: ['pulseLite', 'pulsepointLite'], supportedMediaTypes: ['banner', 'native', 'video'], @@ -133,8 +135,8 @@ function bidResponseAvailable(request, response) { bid.height = idToBidMap[id].h; } else { bid.ad = idToBidMap[id].adm; - bid.width = idToImpMap[id].banner.w; - bid.height = idToImpMap[id].banner.h; + bid.width = idToBidMap[id].w || idToImpMap[id].banner.w; + bid.height = idToBidMap[id].h || idToImpMap[id].banner.h; } bids.push(bid); } @@ -161,14 +163,30 @@ function impression(slot) { * Produces an OpenRTB Banner object for the slot given. */ function banner(slot) { - const size = adSize(slot); + const sizes = parseSizes(slot); + const size = adSize(slot, sizes); return (slot.nativeParams || slot.params.video) ? null : { w: size[0], h: size[1], battr: slot.params.battr, + format: sizes }; } +/** + * Produce openrtb format objects based on the sizes configured for the slot. + */ +function parseSizes(slot) { + const sizes = utils.deepAccess(slot, 'mediaTypes.banner.sizes'); + if (sizes && utils.isArray(sizes)) { + return sizes.filter(sz => utils.isArray(sz) && sz.length === 2).map(sz => ({ + w: sz[0], + h: sz[1] + })); + } + return null; +} + /** * Produces an OpenRTB Video object for the slot given */ @@ -372,12 +390,14 @@ function parse(rawResponse) { /** * Determines the AdSize for the slot. */ -function adSize(slot) { +function adSize(slot, sizes) { if (slot.params.cf) { const size = slot.params.cf.toUpperCase().split('X'); const width = parseInt(slot.params.cw || size[0], 10); const height = parseInt(slot.params.ch || size[1], 10); return [width, height]; + } else if (sizes && sizes.length > 0) { + return [sizes[0].w, sizes[0].h]; } return [1, 1]; } diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index bcd6929cd04..63d0a6f54e5 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -2,10 +2,15 @@ import * as utils from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import find from 'core-js/library/fn/array/find.js'; const BIDDER_CODE = 'quantcast'; const DEFAULT_BID_FLOOR = 0.0000000001; +const QUANTCAST_VENDOR_ID = '11'; +// Check other required purposes on server +const PURPOSE_DATA_COLLECT = '1'; + export const QUANTCAST_DOMAIN = 'qcx.quantserve.com'; export const QUANTCAST_TEST_DOMAIN = 's2s-canary.quantserve.com'; export const QUANTCAST_NET_REVENUE = true; @@ -72,13 +77,44 @@ function getDomain(url) { return url.replace('http://', '').replace('https://', '').replace('www.', '').split(/[/?#]/)[0]; } +function checkTCFv1(vendorData) { + let vendorConsent = vendorData.vendorConsents && vendorData.vendorConsents[QUANTCAST_VENDOR_ID]; + let purposeConsent = vendorData.purposeConsents && vendorData.purposeConsents[PURPOSE_DATA_COLLECT]; + + return !!(vendorConsent && purposeConsent); +} + +function checkTCFv2(tcData) { + if (tcData.purposeOneTreatment && tcData.publisherCC === 'DE') { + // special purpose 1 treatment for Germany + return true; + } + + let restrictions = tcData.publisher ? tcData.publisher.restrictions : {}; + let qcRestriction = restrictions && restrictions[PURPOSE_DATA_COLLECT] + ? restrictions[PURPOSE_DATA_COLLECT][QUANTCAST_VENDOR_ID] + : null; + + if (qcRestriction === 0 || qcRestriction === 2) { + // Not allowed by publisher, or requires legitimate interest + return false; + } + + let vendorConsent = tcData.vendor && tcData.vendor.consents && tcData.vendor.consents[QUANTCAST_VENDOR_ID]; + let purposeConsent = tcData.purpose && tcData.purpose.consents && tcData.purpose.consents[PURPOSE_DATA_COLLECT]; + + return !!(vendorConsent && purposeConsent); +} + /** * The documentation for Prebid.js Adapter 1.0 can be found at link below, * http://prebid.org/dev-docs/bidder-adapter-1.html */ export const spec = { code: BIDDER_CODE, + GVLID: 11, supportedMediaTypes: ['banner', 'video'], + hasUserSynced: false, /** * Verify the `AdUnits.bids` response with `true` for valid request and `false` @@ -107,6 +143,21 @@ export const spec = { const page = utils.deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || config.getConfig('pageUrl') || utils.deepAccess(window, 'location.href'); const domain = getDomain(page); + // Check for GDPR consent for purpose 1, and drop request if consent has not been given + // Remaining consent checks are performed server-side. + if (gdprConsent.gdprApplies) { + if (gdprConsent.vendorData) { + if (gdprConsent.apiVersion === 1 && !checkTCFv1(gdprConsent.vendorData)) { + utils.logInfo(`${BIDDER_CODE}: No purpose 1 consent for TCF v1`); + return; + } + if (gdprConsent.apiVersion === 2 && !checkTCFv2(gdprConsent.vendorData)) { + utils.logInfo(`${BIDDER_CODE}: No purpose 1 consent for TCF v2`); + return; + } + } + } + let bidRequestsList = []; bids.forEach(bid => { @@ -141,6 +192,7 @@ export const spec = { gdprConsent: gdprConsent.consentString, uspSignal: uspConsent ? 1 : 0, uspConsent, + coppa: config.getConfig('coppa') === true ? 1 : 0, prebidJsVersion: '$prebid.version$' }; @@ -221,6 +273,27 @@ export const spec = { onTimeout(timeoutData) { const url = `${QUANTCAST_PROTOCOL}://${QUANTCAST_DOMAIN}:${QUANTCAST_PORT}/qchb_notify?type=timeout`; ajax(url, null, null); + }, + getUserSyncs(syncOptions, serverResponses) { + const syncs = [] + if (!this.hasUserSynced && syncOptions.pixelEnabled) { + const responseWithUrl = find(serverResponses, serverResponse => + utils.deepAccess(serverResponse.body, 'userSync.url') + ); + + if (responseWithUrl) { + const url = utils.deepAccess(responseWithUrl.body, 'userSync.url') + syncs.push({ + type: 'image', + url: url + }); + } + this.hasUserSynced = true; + } + return syncs; + }, + resetUserSync() { + this.hasUserSynced = false; } }; diff --git a/modules/quantcastBidAdapter.md b/modules/quantcastBidAdapter.md index 2b55eae9026..edbbc538b65 100644 --- a/modules/quantcastBidAdapter.md +++ b/modules/quantcastBidAdapter.md @@ -27,7 +27,10 @@ const adUnits = [{ battr: [1, 2] // OPTIONAL - Array of blocked creative attributes as per OpenRTB Spec List 5.3 } } - ] + ], + userSync: { + url: 'https://quantcast.com/pixelUrl' + } }]; ``` @@ -63,6 +66,9 @@ var adUnits = [{ } } } - ] + ], + userSync: { + url: 'https://quantcast.com/pixelUrl' + } }]; ``` diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index 56006aea144..c72bbdd658f 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -1,8 +1,7 @@ -import { logError, replaceAuctionPrice } from '../src/utils.js'; +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 { parse as parseUrl } from '../src/url.js'; export const ENDPOINT = 'https://app.readpeak.com/header/prebid'; diff --git a/modules/realvuAnalyticsAdapter.js b/modules/realvuAnalyticsAdapter.js index 4b05a998788..95e62397c2c 100644 --- a/modules/realvuAnalyticsAdapter.js +++ b/modules/realvuAnalyticsAdapter.js @@ -2,6 +2,9 @@ import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const utils = require('../src/utils.js'); @@ -779,7 +782,7 @@ export let lib = { writePos: function (a) { try { let v = a.x + ',' + a.y + ',' + a.w + ',' + a.h; - utils.setDataInLocalStorage(this.keyPos(a), v); + storage.setDataInLocalStorage(this.keyPos(a), v); } catch (ex) { /* continue regardless of error */ } @@ -787,7 +790,7 @@ export let lib = { readPos: function (a) { try { - let s = utils.getDataFromLocalStorage(this.keyPos(a)); + let s = storage.getDataFromLocalStorage(this.keyPos(a)); if (s) { let v = s.split(','); a.x = parseInt(v[0], 10); @@ -806,7 +809,7 @@ export let lib = { incrMem: function(a, evt, name) { try { let k1 = this.keyPos(a) + '.' + name; - let vmem = utils.getDataFromLocalStorage(k1); + let vmem = storage.getDataFromLocalStorage(k1); if (vmem == null) vmem = '1:3'; let vr = vmem.split(':'); let nv = parseInt(vr[0], 10); @@ -819,7 +822,7 @@ export let lib = { if (evt == 'v') { nv |= 1; } - utils.setDataInLocalStorage(k1, nv + ':' + nr); + storage.setDataInLocalStorage(k1, nv + ':' + nr); } catch (ex) { /* do nothing */ } @@ -827,7 +830,7 @@ export let lib = { score: function (a, name) { try { - let vstr = utils.getDataFromLocalStorage(this.keyPos(a) + '.' + name); + let vstr = storage.getDataFromLocalStorage(this.keyPos(a) + '.' + name); if (vstr != null) { let vr = vstr.split(':'); let nv = parseInt(vr[0], 10); diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js new file mode 100644 index 00000000000..22551877a93 --- /dev/null +++ b/modules/relaidoBidAdapter.js @@ -0,0 +1,303 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const BIDDER_CODE = 'relaido'; +const BIDDER_DOMAIN = 'api.relaido.jp'; +const ADAPTER_VERSION = '1.0.0'; +const DEFAULT_TTL = 300; +const UUID_KEY = 'relaido_uuid'; + +const storage = getStorageManager(); + +function isBidRequestValid(bid) { + if (!utils.isSafariBrowser() && !hasUuid()) { + utils.logWarn('uuid is not found.'); + return false; + } + if (!utils.deepAccess(bid, 'params.placementId')) { + utils.logWarn('placementId param is reqeuired.'); + return false; + } + if (hasVideoMediaType(bid)) { + if (!isVideoValid(bid)) { + utils.logWarn('Invalid mediaType video.'); + return false; + } + } else if (hasBannerMediaType(bid)) { + if (!isBannerValid(bid)) { + utils.logWarn('Invalid mediaType banner.'); + return false; + } + } else { + utils.logWarn('Invalid mediaTypes input banner or video.'); + return false; + } + return true; +} + +function buildRequests(validBidRequests, bidderRequest) { + let bidRequests = []; + + for (let i = 0; i < validBidRequests.length; i++) { + const bidRequest = validBidRequests[i]; + const placementId = utils.getBidIdParameter('placementId', bidRequest.params); + const bidDomain = bidRequest.params.domain || BIDDER_DOMAIN; + const bidUrl = `https://${bidDomain}/vast/v1/out/bid/${placementId}`; + const uuid = getUuid(); + const mediaType = getMediaType(bidRequest); + + let payload = { + version: ADAPTER_VERSION, + ref: bidderRequest.refererInfo.referer, + timeout_ms: bidderRequest.timeout, + ad_unit_code: bidRequest.adUnitCode, + auction_id: bidRequest.auctionId, + bidder: bidRequest.bidder, + bidder_request_id: bidRequest.bidderRequestId, + bid_requests_count: bidRequest.bidRequestsCount, + bid_id: bidRequest.bidId, + transaction_id: bidRequest.transactionId, + media_type: mediaType, + uuid: uuid, + }; + + if (hasVideoMediaType(bidRequest)) { + const playerSize = utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + payload.width = playerSize[0][0]; + payload.height = playerSize[0][1]; + } else if (hasBannerMediaType(bidRequest)) { + const sizes = utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes'); + payload.width = sizes[0][0]; + payload.height = sizes[0][1]; + } + + bidRequests.push({ + method: 'GET', + url: bidUrl, + data: payload, + options: { + withCredentials: true + }, + bidId: bidRequest.bidId, + player: bidRequest.params.player, + width: payload.width, + height: payload.height, + mediaType: mediaType, + }); + } + + return bidRequests; +} + +function interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; + const body = serverResponse.body; + if (!body || body.status != 'ok') { + return []; + } + + if (body.uuid) { + storage.setDataInLocalStorage(UUID_KEY, body.uuid); + } + + const playerUrl = bidRequest.player || body.playerUrl; + const mediaType = bidRequest.mediaType || VIDEO; + + let bidResponse = { + requestId: bidRequest.bidId, + width: bidRequest.width, + height: bidRequest.height, + cpm: body.price, + currency: body.currency, + creativeId: body.creativeId, + dealId: body.dealId || '', + ttl: body.ttl || DEFAULT_TTL, + netRevenue: true, + mediaType: mediaType, + }; + if (mediaType === VIDEO) { + bidResponse.vastXml = body.vast; + bidResponse.renderer = newRenderer(bidRequest.bidId, playerUrl); + } else { + const playerTag = createPlayerTag(playerUrl); + const renderTag = createRenderTag(bidRequest.width, bidRequest.height, body.vast); + bidResponse.ad = `
${playerTag}${renderTag}
`; + } + bidResponses.push(bidResponse); + + return bidResponses; +} + +function getUserSyncs(syncOptions, serverResponses) { + if (!syncOptions.iframeEnabled) { + return []; + } + let syncUrl = `https://${BIDDER_DOMAIN}/tr/v1/prebid/sync.html`; + if (serverResponses.length > 0) { + syncUrl = utils.deepAccess(serverResponses, '0.body.syncUrl') || syncUrl; + } + receiveMessage(); + return [{ + type: 'iframe', + url: syncUrl + }]; +} + +function onBidWon(bid) { + let query = utils.parseQueryStringParameters({ + placement_id: utils.deepAccess(bid, 'params.0.placementId'), + creative_id: utils.deepAccess(bid, 'creativeId'), + price: utils.deepAccess(bid, 'cpm'), + auction_id: utils.deepAccess(bid, 'auctionId'), + bid_id: utils.deepAccess(bid, 'requestId'), + ad_id: utils.deepAccess(bid, 'adId'), + ad_unit_code: utils.deepAccess(bid, 'adUnitCode'), + ref: window.location.href, + }).replace(/\&$/, ''); + const bidDomain = utils.deepAccess(bid, 'params.0.domain') || BIDDER_DOMAIN; + const burl = `https://${bidDomain}/tr/v1/prebid/win.gif?${query}`; + utils.triggerPixel(burl); +} + +function onTimeout(data) { + let query = utils.parseQueryStringParameters({ + placement_id: utils.deepAccess(data, '0.params.0.placementId'), + timeout: utils.deepAccess(data, '0.timeout'), + auction_id: utils.deepAccess(data, '0.auctionId'), + bid_id: utils.deepAccess(data, '0.bidId'), + ad_unit_code: utils.deepAccess(data, '0.adUnitCode'), + ref: window.location.href, + }).replace(/\&$/, ''); + const bidDomain = utils.deepAccess(data, '0.params.0.domain') || BIDDER_DOMAIN; + const timeoutUrl = `https://${bidDomain}/tr/v1/prebid/timeout.gif?${query}`; + utils.triggerPixel(timeoutUrl); +} + +function createPlayerTag(playerUrl) { + return ``; +} + +function createRenderTag(width, height, vast) { + return ``; +}; + +function newRenderer(bidId, playerUrl) { + const renderer = Renderer.install({ + id: bidId, + url: playerUrl, + loaded: false + }); + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('renderer.setRender Error', err); + } + return renderer; +} + +function outstreamRender(bid) { + bid.renderer.push(() => { + window.RelaidoPlayer.renderAd({ + adUnitCode: bid.adUnitCode, + width: bid.width, + height: bid.height, + vastXml: bid.vastXml, + mediaType: bid.mediaType, + }); + }); +} + +function receiveMessage() { + window.addEventListener('message', setUuid); +} + +function setUuid(e) { + if (utils.isPlainObject(e.data) && e.data.relaido_uuid) { + storage.setDataInLocalStorage(UUID_KEY, e.data.relaido_uuid); + window.removeEventListener('message', setUuid); + } +} + +function isBannerValid(bid) { + if (!isMobile()) { + return false; + } + const sizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes'); + if (sizes && utils.isArray(sizes)) { + if (utils.isArray(sizes[0])) { + const width = sizes[0][0]; + const height = sizes[0][1]; + if (width >= 300 && height >= 250) { + return true; + } + } + } + return false; +} + +function isVideoValid(bid) { + const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); + if (playerSize && utils.isArray(playerSize) && playerSize.length > 0) { + const context = utils.deepAccess(bid, 'mediaTypes.video.context'); + if (context && context === 'outstream') { + return true; + } + } + return false; +} + +function hasUuid() { + return !!storage.getDataFromLocalStorage(UUID_KEY); +} + +function getUuid() { + return storage.getDataFromLocalStorage(UUID_KEY) || ''; +} + +export function isMobile() { + const ua = navigator.userAgent; + if (ua.indexOf('iPhone') > -1 || ua.indexOf('iPod') > -1 || (ua.indexOf('Android') > -1 && ua.indexOf('Tablet') == -1)) { + return true; + } + return false; +} + +function getMediaType(bid) { + if (hasVideoMediaType(bid)) { + return VIDEO; + } else if (hasBannerMediaType(bid)) { + return BANNER; + } + return ''; +} + +function hasBannerMediaType(bid) { + return !!utils.deepAccess(bid, 'mediaTypes.banner'); +} + +function hasVideoMediaType(bid) { + return !!utils.deepAccess(bid, 'mediaTypes.video'); +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs: getUserSyncs, + onBidWon, + onTimeout +} + +registerBidder(spec); diff --git a/modules/relaidoBidAdapter.md b/modules/relaidoBidAdapter.md new file mode 100644 index 00000000000..459f772c66b --- /dev/null +++ b/modules/relaidoBidAdapter.md @@ -0,0 +1,48 @@ +# Overview + +``` +Module Name: Relaido Bidder Adapter +Module Type: Bidder Adapter +Maintainer: video-dev@cg.relaido.co.jp +``` + +# Description + +Connects to Relaido exchange for bids. + +Relaido bid adapter supports Outstream Video. + +# Test Parameters + +```javascript + var adUnits=[{ + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bids: [{ + bidder: 'relaido', + params: { + placementId: '9900' + } + }] + },{ + code: 'video-ad-player', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 360] + } + }, + bids: [{ + bidder: 'relaido', + params: { + placementId: '9900' + } + }] + }]; +``` \ No newline at end of file diff --git a/modules/reloadBidAdapter.js b/modules/reloadBidAdapter.js index dd4514c99c8..94ea4be281f 100644 --- a/modules/reloadBidAdapter.js +++ b/modules/reloadBidAdapter.js @@ -3,6 +3,10 @@ import { } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); + const BIDDER_CODE = 'reload'; const VERSION_ADAPTER = '1.10'; export const spec = { @@ -390,14 +394,14 @@ function ReloadClientTool(args) { var stgFileStr = JSON.stringify(stgFileObj); - utils.setDataInLocalStorage(name, stgFileStr); + storage.setDataInLocalStorage(name, stgFileStr); return true; } function _getItem (name) { try { - var obStgFileStr = utils.getDataFromLocalStorage(name); + var obStgFileStr = storage.getDataFromLocalStorage(name); if (obStgFileStr === null) return null; var stgFileObj = JSON.parse(obStgFileStr); diff --git a/modules/roxotAnalyticsAdapter.js b/modules/roxotAnalyticsAdapter.js index a077c7ac7e0..0e9cda9b6bc 100644 --- a/modules/roxotAnalyticsAdapter.js +++ b/modules/roxotAnalyticsAdapter.js @@ -3,6 +3,9 @@ import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; import includes from 'core-js/library/fn/array/includes.js'; import {ajaxBuilder} from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const utils = require('../src/utils.js'); let ajax = ajaxBuilder(0); @@ -68,17 +71,17 @@ function detectDevice() { function checkIsNewFlag() { let key = buildLocalStorageKey(isNewKey); - let lastUpdate = Number(utils.getDataFromLocalStorage(key)); - utils.setDataInLocalStorage(key, Date.now()); + let lastUpdate = Number(storage.getDataFromLocalStorage(key)); + storage.setDataInLocalStorage(key, Date.now()); return Date.now() - lastUpdate > isNewTtl; } function updateUtmTimeout() { - utils.setDataInLocalStorage(buildLocalStorageKey(utmTtlKey), Date.now()); + storage.setDataInLocalStorage(buildLocalStorageKey(utmTtlKey), Date.now()); } function isUtmTimeoutExpired() { - let utmTimestamp = utils.getDataFromLocalStorage(buildLocalStorageKey(utmTtlKey)); + let utmTimestamp = storage.getDataFromLocalStorage(buildLocalStorageKey(utmTtlKey)); return (Date.now() - utmTimestamp) > utmTtl; } @@ -356,11 +359,11 @@ roxotAdapter.buildUtmTagData = function () { }); utmTags.forEach(function (utmTagKey) { if (utmTagsDetected) { - utils.setDataInLocalStorage(buildLocalStorageKey(utmTagKey), utmTagData[utmTagKey]); + storage.setDataInLocalStorage(buildLocalStorageKey(utmTagKey), utmTagData[utmTagKey]); updateUtmTimeout(); } else { if (!isUtmTimeoutExpired()) { - utmTagData[utmTagKey] = utils.getDataFromLocalStorage(buildLocalStorageKey(utmTagKey)) ? utils.getDataFromLocalStorage(buildLocalStorageKey(utmTagKey)) : ''; + utmTagData[utmTagKey] = storage.getDataFromLocalStorage(buildLocalStorageKey(utmTagKey)) ? storage.getDataFromLocalStorage(buildLocalStorageKey(utmTagKey)) : ''; updateUtmTimeout(); } } diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 9bb8a924b98..28c11ef8264 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -4,7 +4,7 @@ import CONSTANTS from '../src/constants.json'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import * as utils from '../src/utils.js'; -import * as urlLib from '../src/url.js' +import { getGlobal } from '../src/prebidGlobal.js'; const { EVENTS: { @@ -20,6 +20,9 @@ const { STATUS: { GOOD, NO_BID + }, + BID_STATUS: { + BID_REJECTED } } = CONSTANTS; @@ -39,7 +42,7 @@ const cache = { export function getHostNameFromReferer(referer) { try { - rubiconAdapter.referrerHostname = urlLib.parse(referer, {noDecodeWholeURL: true}).hostname; + rubiconAdapter.referrerHostname = utils.parseUrl(referer, {noDecodeWholeURL: true}).hostname; } catch (e) { utils.logError('Rubicon Analytics: Unable to parse hostname from supplied url: ', referer, e); rubiconAdapter.referrerHostname = ''; @@ -101,7 +104,9 @@ function sendMessage(auctionId, bidWonId) { 'bidPriceUSD', 'dealId', 'dimensions', - 'mediaType' + 'mediaType', + 'floorValue', + 'floorRule' ]) : undefined ]); } @@ -143,9 +148,11 @@ function sendMessage(auctionId, bidWonId) { 'transactionId', 'mediaTypes', 'dimensions', - 'adserverTargeting', () => stringProperties(cache.targeting[bid.adUnit.adUnitCode] || {}) + 'adserverTargeting', () => stringProperties(cache.targeting[bid.adUnit.adUnitCode] || {}), + 'adSlot' ]); adUnit.bids = []; + adUnit.status = 'no-bid'; // default it to be no bid } // Add site and zone id if not there and if we found a rubicon bidder @@ -186,6 +193,17 @@ function sendMessage(auctionId, bidWonId) { adUnits: Object.keys(adUnitMap).map(i => adUnitMap[i]) }; + // pick our of top level floor data we want to send! + if (auctionCache.floorData) { + auction.floors = utils.pick(auctionCache.floorData, [ + 'location', + 'modelName', () => auctionCache.floorData.modelVersion || '', + 'skipped', + 'enforcement', () => utils.deepAccess(auctionCache.floorData, 'enforcements.enforceJS'), + 'dealsEnforced', () => utils.deepAccess(auctionCache.floorData, 'enforcements.floorDeals') + ]); + } + if (serverConfig) { auction.serverTimeoutMillis = serverConfig.timeout; } @@ -222,14 +240,31 @@ function sendMessage(auctionId, bidWonId) { } function getBidPrice(bid) { - if (typeof bid.currency === 'string' && bid.currency.toUpperCase() === 'USD') { + // get the cpm from bidResponse + let cpm; + let currency; + if (bid.status === BID_REJECTED && utils.deepAccess(bid, 'floorData.cpmAfterAdjustments')) { + // if bid was rejected and bid.floorData.cpmAfterAdjustments use it + cpm = bid.floorData.cpmAfterAdjustments; + currency = bid.floorData.floorCurrency; + } else if (typeof bid.currency === 'string' && bid.currency.toUpperCase() === 'USD') { + // bid is in USD use it return Number(bid.cpm); + } else { + // else grab cpm + cpm = bid.cpm; + currency = bid.currency; } - // use currency conversion function if present - if (typeof bid.getCpmInNewCurrency === 'function') { - return Number(bid.getCpmInNewCurrency('USD')); + // if after this it is still going and is USD then return it. + if (currency === 'USD') { + return Number(cpm); + } + // otherwise we convert and return + try { + return Number(getGlobal().convertCurrency(cpm, currency, 'USD')); + } catch (err) { + utils.logWarn('Rubicon Analytics Adapter: Could not determine the bidPriceUSD of the bid ', bid); } - utils.logWarn('Rubicon Analytics Adapter: Could not determine the bidPriceUSD of the bid ', bid); } export function parseBidResponse(bid, previousBidResponse) { @@ -249,6 +284,8 @@ export function parseBidResponse(bid, previousBidResponse) { 'height' ]), 'seatBidId', + 'floorValue', () => utils.deepAccess(bid, 'floorData.floorValue'), + 'floorRule', () => utils.debugTurnedOn() ? utils.deepAccess(bid, 'floorData.floorRule') : undefined ]); } @@ -329,6 +366,9 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.bids = {}; cacheEntry.bidsWon = {}; cacheEntry.referrer = args.bidderRequests[0].refererInfo.referer; + if (utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData')) { + cacheEntry.floorData = {...utils.deepAccess(args, 'bidderRequests.0.bids.0.floorData')}; + } cache.auctions[args.auctionId] = cacheEntry; break; case BID_REQUESTED: @@ -399,14 +439,23 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { return Object.keys(types).filter(validMediaType); } return ['banner']; - } + }, ]) ]); return memo; }, {})); break; case BID_RESPONSE: - let bid = cache.auctions[args.auctionId].bids[args.requestId]; + let auctionEntry = cache.auctions[args.auctionId]; + let bid = auctionEntry.bids[args.requestId]; + // If floor resolved gptSlot but we have not yet, then update the adUnit to have the adSlot name + if (!utils.deepAccess(bid, 'adUnit.adSlot') && utils.deepAccess(args, 'floorData.matchedFields.gptSlot')) { + bid.adUnit.adSlot = args.floorData.matchedFields.gptSlot; + } + // if we have not set enforcements yet set it + if (!utils.deepAccess(auctionEntry, 'floorData.enforcements') && utils.deepAccess(args, 'floorData.enforcements')) { + auctionEntry.floorData.enforcements = {...args.floorData.enforcements}; + } if (!bid) { utils.logError('Rubicon Anlytics Adapter Error: Could not find associated bid request for bid response with requestId: ', args.requestId); break; @@ -418,7 +467,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { delete bid.error; // it's possible for this to be set by a previous timeout break; case NO_BID: - bid.status = 'no-bid'; + bid.status = args.status === BID_REJECTED ? 'rejected' : 'no-bid'; delete bid.error; break; default: diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index f8288503493..9131a8cb581 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -10,6 +10,7 @@ export const FASTLANE_ENDPOINT = 'https://fastlane.rubiconproject.com/a/api/fast export const VIDEO_ENDPOINT = 'https://prebid-server.rubiconproject.com/openrtb2/auction'; export const SYNC_ENDPOINT = 'https://eus.rubiconproject.com/usync.html'; +const GVLID = 52; const DIGITRUST_PROP_NAMES = { FASTLANE: { id: 'dt.id', @@ -67,7 +68,10 @@ var sizeMap = { 79: '980x300', 80: '980x400', 83: '480x300', + 85: '300x120', + 90: '548x150', 94: '970x310', + 95: '970x100', 96: '970x210', 101: '480x320', 102: '768x1024', @@ -91,10 +95,14 @@ var sizeMap = { 214: '980x360', 221: '1x1', 229: '320x180', + 230: '2000x1400', 232: '580x400', 234: '6x6', 251: '2x2', + 256: '480x820', 257: '400x600', + 258: '500x200', + 259: '998x200', 264: '970x1000', 265: '1920x1080', 274: '1800x200', @@ -106,6 +114,7 @@ utils._each(sizeMap, (item, key) => sizeMap[item] = key); export const spec = { code: 'rubicon', + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], /** * @param {object} bid @@ -185,7 +194,17 @@ export const spec = { } } - const bidFloor = parseFloat(utils.deepAccess(bidRequest, 'params.floor')); + let bidFloor; + if (typeof bidRequest.getFloor === 'function' && !config.getConfig('rubicon.disableFloors')) { + let floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'video', + size: parseSizes(bidRequest, 'video') + }); + bidFloor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; + } else { + bidFloor = parseFloat(utils.deepAccess(bidRequest, 'params.floor')); + } if (!isNaN(bidFloor)) { data.imp[0].bidfloor = bidFloor; } @@ -487,6 +506,16 @@ export const spec = { 'rf': _getPageUrl(bidRequest, bidderRequest) }; + // If floors module is enabled and we get USD floor back, send it in rp_hard_floor else undfined + if (typeof bidRequest.getFloor === 'function' && !config.getConfig('rubicon.disableFloors')) { + let floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'banner', + size: '*' + }); + data['rp_hard_floor'] = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; + } + // add p_pos only if specified and valid // For SRA we need to explicitly put empty semi colons so AE treats it as empty, instead of copying the latter value data['p_pos'] = (params.position === 'atf' || params.position === 'btf') ? params.position : ''; @@ -546,7 +575,7 @@ export const spec = { const keywords = (params.keywords || []).concat( utils.deepAccess(config.getConfig('fpd.user'), 'keywords') || [], utils.deepAccess(config.getConfig('fpd.context'), 'keywords') || []); - data.kw = keywords.length ? keywords.join(',') : ''; + data.kw = Array.isArray(keywords) && keywords.length ? keywords.join(',') : ''; /** * Prebid AdSlot diff --git a/modules/showheroes-bsBidAdapter.js b/modules/showheroes-bsBidAdapter.js index 2ae7cc56c0f..2831d793aad 100644 --- a/modules/showheroes-bsBidAdapter.js +++ b/modules/showheroes-bsBidAdapter.js @@ -29,6 +29,7 @@ export const spec = { return !!bid.params.playerId; }, buildRequests: function(validBidRequests, bidderRequest) { + let adUnits = []; const pageURL = validBidRequests[0].params.contentPageUrl || bidderRequest.refererInfo.referer; const isStage = !!validBidRequests[0].params.stage; const isOutstream = utils.deepAccess(validBidRequests[0], 'mediaTypes.video.context') === 'outstream'; @@ -38,53 +39,53 @@ export const spec = { const outstreamOptions = utils.deepAccess(validBidRequests[0], 'params.outstreamOptions'); const isBanner = !!validBidRequests[0].mediaTypes.banner || (isOutstream && !(isCustomRender || isNativeRender || isNodeRender)); - let adUnits = validBidRequests.map((bid) => { + validBidRequests.forEach((bid) => { + const videoSizes = getVideoSizes(bid); + const bannerSizes = getBannerSizes(bid); const vpaidMode = utils.getBidIdParameter('vpaidMode', bid.params); - let sizes = bid.sizes.length === 1 ? bid.sizes[0] : bid.sizes; - if (sizes && !sizes.length) { - let mediaSize; - let mediaVideoSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); - if (utils.isArray(mediaVideoSize)) { - mediaSize = mediaVideoSize; + const makeBids = (type, size) => { + let context = ''; + let streamType = 2; + + if (type === BANNER) { + streamType = 5; } else { - mediaSize = bid.mediaTypes.banner.sizes; - } - if (utils.isArray(mediaSize[0])) { - sizes = mediaSize[0]; - } else if (utils.isNumber(mediaSize[0])) { - sizes = mediaSize; + context = utils.deepAccess(bid, 'mediaTypes.video.context'); + if (vpaidMode && context === 'instream') { + streamType = 1; + } + if (context === 'outstream') { + streamType = 5; + } } - } - const context = utils.deepAccess(bid, 'mediaTypes.video.context'); - - let streamType = 2; + return { + type: streamType, + bidId: bid.bidId, + mediaType: type, + context: context, + playerId: utils.getBidIdParameter('playerId', bid.params), + auctionId: bidderRequest.auctionId, + bidderCode: BIDDER_CODE, + gdprConsent: bidderRequest.gdprConsent, + start: +new Date(), + timeout: 3000, + size: { + width: size[0], + height: size[1] + }, + params: bid.params, + }; + }; - if (vpaidMode && context === 'instream') { - streamType = 1; - } - if (isBanner || context === 'outstream') { - streamType = 5; - } + videoSizes.forEach((size) => { + adUnits.push(makeBids(VIDEO, size)); + }); - return { - type: streamType, - bidId: bid.bidId, - mediaType: isBanner ? BANNER : VIDEO, - context: context, - playerId: utils.getBidIdParameter('playerId', bid.params), - auctionId: bidderRequest.auctionId, - bidderCode: BIDDER_CODE, - gdprConsent: bidderRequest.gdprConsent, - start: +new Date(), - timeout: 3000, - video: { - width: sizes[0], - height: sizes[1] - }, - params: bid.params, - }; + bannerSizes.forEach((size) => { + adUnits.push(makeBids(BANNER, size)); + }); }); return { @@ -94,6 +95,7 @@ export const spec = { data: { 'user': [], 'meta': { + 'adapterVersion': 2, 'pageURL': encodeURIComponent(pageURL), 'vastCacheEnabled': (!!config.getConfig('cache') && !isBanner && !outstreamOptions) || false, 'isDesktop': utils.getWindowTop().document.documentElement.clientWidth > 700, @@ -156,12 +158,12 @@ function createBids(bidRes, reqData) { bidUnit.cpm = bid.cpm; bidUnit.requestId = bid.bidId; bidUnit.currency = bid.currency; - bidUnit.mediaType = reqBid.mediaType || VIDEO; + bidUnit.mediaType = bid.mediaType || VIDEO; bidUnit.ttl = TTL; bidUnit.creativeId = 'c_' + bid.bidId; bidUnit.netRevenue = true; - bidUnit.width = bid.video.width; - bidUnit.height = bid.video.height; + bidUnit.width = bid.size.width; + bidUnit.height = bid.size.height; if (bid.vastXml) { bidUnit.vastXml = bid.vastXml; bidUnit.adResponse = { @@ -171,16 +173,16 @@ function createBids(bidRes, reqData) { if (bid.vastTag) { bidUnit.vastUrl = bid.vastTag; } - if (reqBid.mediaType === BANNER) { + if (bid.mediaType === BANNER) { bidUnit.ad = getBannerHtml(bid, reqBid, reqData); - } else if (reqBid.context === 'outstream') { + } else if (bid.context === 'outstream') { const renderer = Renderer.install({ id: bid.bidId, url: '//', config: { playerId: reqBid.playerId, - width: bid.video.width, - height: bid.video.height, + width: bid.size.width, + height: bid.size.height, vastUrl: bid.vastTag, vastXml: bid.vastXml, debug: reqData.debug, @@ -267,4 +269,19 @@ function getBannerHtml (bid, reqBid, reqData) { `; } +function getVideoSizes(bidRequest) { + return formatSizes(utils.deepAccess(bidRequest, 'mediaTypes.video.playerSize') || []); +} + +function getBannerSizes(bidRequest) { + return formatSizes(utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes') || bidRequest.sizes || []); +} + +function formatSizes(sizes) { + if (!sizes || !sizes.length) { + return [] + } + return Array.isArray(sizes[0]) ? sizes : [sizes]; +} + registerBidder(spec); diff --git a/modules/sigmoidAnalyticsAdapter.js b/modules/sigmoidAnalyticsAdapter.js index fac23730864..88bb5eb23a9 100644 --- a/modules/sigmoidAnalyticsAdapter.js +++ b/modules/sigmoidAnalyticsAdapter.js @@ -4,6 +4,9 @@ import includes from 'core-js/library/fn/array/includes.js'; import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const utils = require('../src/utils.js'); @@ -54,31 +57,31 @@ function buildSessionIdTimeoutLocalStorageKey() { function updateSessionId() { if (isSessionIdTimeoutExpired()) { let newSessionId = utils.generateUUID(); - utils.setDataInLocalStorage(buildSessionIdLocalStorageKey(), newSessionId); + storage.setDataInLocalStorage(buildSessionIdLocalStorageKey(), newSessionId); } initOptions.sessionId = getSessionId(); updateSessionIdTimeout(); } function updateSessionIdTimeout() { - utils.setDataInLocalStorage(buildSessionIdTimeoutLocalStorageKey(), Date.now()); + storage.setDataInLocalStorage(buildSessionIdTimeoutLocalStorageKey(), Date.now()); } function isSessionIdTimeoutExpired() { - let cpmSessionTimestamp = utils.getDataFromLocalStorage(buildSessionIdTimeoutLocalStorageKey()); + let cpmSessionTimestamp = storage.getDataFromLocalStorage(buildSessionIdTimeoutLocalStorageKey()); return Date.now() - cpmSessionTimestamp > sessionTimeout; } function getSessionId() { - return utils.getDataFromLocalStorage(buildSessionIdLocalStorageKey()) ? utils.getDataFromLocalStorage(buildSessionIdLocalStorageKey()) : ''; + return storage.getDataFromLocalStorage(buildSessionIdLocalStorageKey()) ? storage.getDataFromLocalStorage(buildSessionIdLocalStorageKey()) : ''; } function updateUtmTimeout() { - utils.setDataInLocalStorage(buildUtmLocalStorageTimeoutKey(), Date.now()); + storage.setDataInLocalStorage(buildUtmLocalStorageTimeoutKey(), Date.now()); } function isUtmTimeoutExpired() { - let utmTimestamp = utils.getDataFromLocalStorage(buildUtmLocalStorageTimeoutKey()); + let utmTimestamp = storage.getDataFromLocalStorage(buildUtmLocalStorageTimeoutKey()); return (Date.now() - utmTimestamp) > utmTimeout; } @@ -219,11 +222,11 @@ sigmoidAdapter.buildUtmTagData = function () { }); utmTags.forEach(function(utmTagKey) { if (utmTagsDetected) { - utils.setDataInLocalStorage(buildUtmLocalStorageKey(utmTagKey), utmTagData[utmTagKey]); + storage.setDataInLocalStorage(buildUtmLocalStorageKey(utmTagKey), utmTagData[utmTagKey]); updateUtmTimeout(); } else { if (!isUtmTimeoutExpired()) { - utmTagData[utmTagKey] = utils.getDataFromLocalStorage(buildUtmLocalStorageKey(utmTagKey)) ? utils.getDataFromLocalStorage(buildUtmLocalStorageKey(utmTagKey)) : ''; + utmTagData[utmTagKey] = storage.getDataFromLocalStorage(buildUtmLocalStorageKey(utmTagKey)) ? storage.getDataFromLocalStorage(buildUtmLocalStorageKey(utmTagKey)) : ''; updateUtmTimeout(); } } diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index ec3604faa97..98effb301fb 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -273,21 +273,21 @@ export function isLabelActivated(bidOrAdUnit, activeLabels, adUnitCode) { let labelOperator; const labelsFound = Object.keys(bidOrAdUnit).filter(prop => prop === 'labelAny' || prop === 'labelAll'); if (labelsFound && labelsFound.length > 1) { - utils.logWarn(`SizeMappingV2:: ${(bidOrAdUnit.code) - ? (`Ad Unit: ${bidOrAdUnit.code} has multiple label operators. Using the first declared operator: ${labelsFound[0]}`) - : (`Bidder: ${bidOrAdUnit.bidder} in Ad Unit: ${adUnitCode} has multiple label operators. Using the first declared operator: ${labelsFound[0]}`)}`); + utils.logWarn(`Size Mapping V2:: ${(bidOrAdUnit.code) + ? (`Ad Unit: ${bidOrAdUnit.code} => Ad unit has multiple label operators. Using the first declared operator: ${labelsFound[0]}`) + : (`Ad Unit: ${adUnitCode}, Bidder: ${bidOrAdUnit.bidder} => Bidder has multiple label operators. Using the first declared operator: ${labelsFound[0]}`)}`); } labelOperator = labelsFound[0]; if (labelOperator === 'labelAll' && Array.isArray(bidOrAdUnit[labelOperator])) { if (bidOrAdUnit.labelAll.length === 0) { - utils.logWarn(`SizeMappingV2:: Ad Unit: ${bidOrAdUnit.code} has declared property labelAll with an empty array. Ad Unit is still enabled!`); + utils.logWarn(`Size Mapping V2:: Ad Unit: ${bidOrAdUnit.code} => Ad unit has declared property 'labelAll' with an empty array. Ad Unit is still enabled!`); return true; } return bidOrAdUnit.labelAll.every(label => includes(activeLabels, label)); } else if (labelOperator === 'labelAny' && Array.isArray(bidOrAdUnit[labelOperator])) { if (bidOrAdUnit.labelAny.length === 0) { - utils.logWarn(`SizeMappingV2:: Ad Unit: ${bidOrAdUnit.code} has declared property labelAny with an empty array. Ad Unit is still enabled!`); + utils.logWarn(`Size Mapping V2:: Ad Unit: ${bidOrAdUnit.code} => Ad unit has declared property 'labelAny' with an empty array. Ad Unit is still enabled!`); return true; } return bidOrAdUnit.labelAny.some(label => includes(activeLabels, label)); @@ -430,7 +430,11 @@ export function getRelevantMediaTypesForBidder(sizeConfig, activeViewport) { // sets sizeMappingInternalStore for a given auctionId with relevant adUnit information returned from the call to 'getFilteredMediaTypes' function // returns adUnit details object. export function getAdUnitDetail(auctionId, adUnit) { - const adUnitDetail = sizeMappingInternalStore.getAuctionDetail(auctionId).adUnits.filter(adUnitDetail => adUnitDetail.adUnitCode === adUnit.code); + // fetch all adUnits for an auction from the sizeMappingInternalStore + const adUnitsForAuction = sizeMappingInternalStore.getAuctionDetail(auctionId).adUnits; + + // check if the adUnit exists already in the sizeMappingInterStore (check for equivalence of 'code' && 'mediaTypes' properties) + const adUnitDetail = adUnitsForAuction.filter(adUnitDetail => adUnitDetail.adUnitCode === adUnit.code && utils.deepEqual(adUnitDetail.mediaTypes, adUnit.mediaTypes)); if (adUnitDetail.length > 0) { return adUnitDetail[0]; @@ -445,16 +449,17 @@ export function getAdUnitDetail(auctionId, adUnit) { transformedMediaTypes }; + // set adUnitDetail in sizeMappingInternalStore against the correct 'auctionId'. + sizeMappingInternalStore.setAuctionDetail(auctionId, adUnitDetail); + // 'filteredMediaTypes' are the mediaTypes that got removed/filtered-out from adUnit.mediaTypes after sizeConfig filtration. - const filteredMediaTypes = Object.keys(mediaTypes).filter(mt => Object.keys(transformedMediaTypes).indexOf(mt) === -1) + const filteredMediaTypes = Object.keys(mediaTypes).filter(mt => Object.keys(transformedMediaTypes).indexOf(mt) === -1); - utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code} - Active size buckets after filtration: `, sizeBucketToSizeMap); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Active size buckets after filtration: `, sizeBucketToSizeMap); if (filteredMediaTypes.length > 0) { - utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code} - mediaTypes that got filtered out: ${filteredMediaTypes}`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Media types that got filtered out: ${filteredMediaTypes}`); } - // set adUnitDetail in sizeMappingInternalStore against the correct 'auctionId'. - sizeMappingInternalStore.setAuctionDetail(auctionId, adUnitDetail); return adUnitDetail; } } @@ -468,7 +473,7 @@ export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, label // check if adUnit has any active media types remaining, if not drop the adUnit from auction, // else proceed to evaluate the bids object. if (Object.keys(transformedMediaTypes).length === 0) { - utils.logInfo(`SizeMappingV2:: Ad Unit: ${adUnit.code} is disabled since there are no active media types after sizeConfig filtration.`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Ad unit disabled since there are no active media types after sizeConfig filtration.`); return result; } result @@ -488,7 +493,7 @@ export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, label if (bid.sizeConfig) { const relevantMediaTypes = internal.getRelevantMediaTypesForBidder(bid.sizeConfig, activeViewport); if (relevantMediaTypes.length === 0) { - utils.logError(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bidderCode} - sizeConfig is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); + utils.logError(`Size Mapping V2:: Ad Unit: ${adUnit.code}, Bidder: ${bidderCode} => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); bid = Object.assign({}, bid); } else if (relevantMediaTypes[0] !== 'none') { const bidderMediaTypes = Object @@ -502,11 +507,11 @@ export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, label if (Object.keys(bidderMediaTypes).length > 0) { bid = Object.assign({}, bid, { mediaTypes: bidderMediaTypes }); } else { - utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bid.bidder} - 'relevantMediaTypes' for this bidder does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}, Bidder: ${bid.bidder} => 'relevantMediaTypes' does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); return bids; } } else { - utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bid.bidder} - 'relevantMediaTypes' is set to 'none' in sizeConfig for current viewport size. This bidder is disabled.`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}, Bidder: ${bid.bidder} => 'relevantMediaTypes' is set to 'none' in sizeConfig for current viewport size. This bidder is disabled.`); return bids; } } @@ -525,15 +530,15 @@ export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, label })); return bids; } else { - utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bid.bidder} - Label check for this bidder has failed. This bidder is disabled.`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}, Bidder: ${bid.bidder} => Label check for this bidder has failed. This bidder is disabled.`); return bids; } }, [])); } else { - utils.logWarn(`SizeMappingV2:: Ad Unit: ${adUnit.code} has declared invalid mediaTypes or has not declared a mediaTypes property`); + utils.logWarn(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Ad unit has declared invalid 'mediaTypes' or has not declared a 'mediaTypes' property`); } } else { - utils.logInfo(`SizeMappingV2:: Ad Unit: ${adUnit.code} is disabled due to failing label check.`); + utils.logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Ad unit is disabled due to failing label check.`); return result; } return result; diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index bddde2afd70..bff592383a4 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -23,38 +23,54 @@ export const spec = { isBidRequestValid: function (bid) { return !!(bid.params && bid.params.siteId && bid.params.pageId && bid.params.formatId); }, + + /** + * Serialize a supply chain object to a string uri encoded + * + * @param {*} schain object + */ + serializeSupplyChain: function(schain) { + if (!schain || !schain.nodes) return null; + const nodesProperties = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + return `${schain.ver},${schain.complete}!` + + schain.nodes.map(node => nodesProperties.map(prop => + node[prop] ? encodeURIComponent(node[prop]) : '') + .join(',')) + .join('!'); + }, + /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids - * @param {bidderRequest} - bidder request object - * @return ServerRequest Info describing the request to the server. + * @param {BidRequest[]} validBidRequests an array of bids + * @param {BidderRequest} bidderRequest bidder request object + * @return {ServerRequest[]} Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { // use bidderRequest.bids[] to get bidder-dependent request info - // if your bidder supports multiple currencies, use config.getConfig(currency) // to find which one the ad server needs // pull requested transaction ID from bidderRequest.bids[].transactionId return validBidRequests.map(bid => { // Common bid request attributes for banner, outstream and instream. - var payload = { + let payload = { siteid: bid.params.siteId, pageid: bid.params.pageId, formatid: bid.params.formatId, currencyCode: config.getConfig('currency.adServerCurrency'), bidfloor: bid.params.bidfloor || 0.0, - targeting: bid.params.target && bid.params.target != '' ? bid.params.target : undefined, - buid: bid.params.buId && bid.params.buId != '' ? bid.params.buId : undefined, - appname: bid.params.appName && bid.params.appName != '' ? bid.params.appName : undefined, + targeting: bid.params.target && bid.params.target !== '' ? bid.params.target : undefined, + buid: bid.params.buId && bid.params.buId !== '' ? bid.params.buId : undefined, + appname: bid.params.appName && bid.params.appName !== '' ? bid.params.appName : undefined, ckid: bid.params.ckId || 0, tagId: bid.adUnitCode, pageDomain: bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer ? bidderRequest.refererInfo.referer : undefined, transactionId: bid.transactionId, timeout: config.getConfig('bidderTimeout'), bidId: bid.bidId, - prebidVersion: '$prebid.version$' + prebidVersion: '$prebid.version$', + schain: spec.serializeSupplyChain(bid.schain) }; const videoMediaType = utils.deepAccess(bid, 'mediaTypes.video'); @@ -66,7 +82,7 @@ export const spec = { })); } else if (videoMediaType && videoMediaType.context === 'instream') { // Specific attributes for instream. - var playerSize = videoMediaType.playerSize[0]; + let playerSize = videoMediaType.playerSize[0]; payload.isVideo = true; payload.videoData = { videoProtocol: bid.params.video.protocol, @@ -83,7 +99,12 @@ export const spec = { payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side } + if (bidderRequest && bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } + var payloadString = JSON.stringify(payload); + return { method: 'POST', url: (bid.params.domain !== undefined ? bid.params.domain : 'https://prg.smartadserver.com') + '/prebid/v1', @@ -91,20 +112,22 @@ export const spec = { }; }); }, + /** * Unpack the response from the server into a list of bids. * * @param {*} serverResponse A successful response from the server. + * @param {*} bidRequestString * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function (serverResponse, bidRequestString) { const bidResponses = []; - var response = serverResponse.body; + let response = serverResponse.body; try { if (response) { const bidRequest = JSON.parse(bidRequestString.data); - var bidResponse = { + let bidResponse = { requestId: bidRequest.bidId, cpm: response.cpm, width: response.width, @@ -132,15 +155,16 @@ export const spec = { } return bidResponses; }, + /** * User syncs. * * @param {*} syncOptions Publisher prebid configuration. * @param {*} serverResponses A successful response from the server. - * @return {Syncs[]} An array of syncs that should be executed. + * @return {syncs[]} An array of syncs that should be executed. */ getUserSyncs: function (syncOptions, serverResponses) { - const syncs = [] + const syncs = []; if (syncOptions.iframeEnabled && serverResponses.length > 0) { syncs.push({ type: 'iframe', @@ -149,5 +173,6 @@ export const spec = { } return syncs; } -} +}; + registerBidder(spec); diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index 5a43ea0b5c9..ee201a72bf2 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -143,9 +143,15 @@ export const spec = { return null; } + let url = STR_ENDPOINT; + + if (deepAccess(validBidRequests[0], 'params.bid_request_url')) { + url = deepAccess(validBidRequests[0], 'params.bid_request_url'); + } + return { method: 'GET', - url: STR_ENDPOINT, + url: url, withCredentials: true, data: payload, bidderRequests: validBidRequests diff --git a/modules/sortableBidAdapter.js b/modules/sortableBidAdapter.js index 9fbb02ad08f..f5d3a6b1bb5 100644 --- a/modules/sortableBidAdapter.js +++ b/modules/sortableBidAdapter.js @@ -1,7 +1,6 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; -import { parse } from '../src/url.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'sortable'; @@ -157,7 +156,7 @@ export const spec = { buildRequests: function(validBidReqs, bidderRequest) { const sortableConfig = config.getConfig('sortable') || {}; const globalSiteId = sortableConfig.siteId; - let loc = parse(bidderRequest.refererInfo.referer); + let loc = utils.parseUrl(bidderRequest.refererInfo.referer); const sortableImps = utils._map(validBidReqs, bid => { const rv = { diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 2566f314018..98c8c8e3b33 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,5 +1,4 @@ import * as utils from '../src/utils.js' -import {parse} from '../src/url.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER } from '../src/mediaTypes.js' @@ -62,7 +61,7 @@ export const spec = { const page = bidderRequest.refererInfo.referer // clever trick to get the domain - const domain = parse(page).hostname + const domain = utils.parseUrl(page).hostname const sovrnBidReq = { id: utils.getUniqueIdentifierStr(), diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index fe42523e737..210153548b2 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -10,6 +10,7 @@ export const GOOGLE_CONSENT = { consented_providers: ['3', '7', '11', '12', '15' export const spec = { code: BIDDER_CODE, + gvlid: 165, aliases: ['spotx'], supportedMediaTypes: [VIDEO], diff --git a/modules/staqAnalyticsAdapter.js b/modules/staqAnalyticsAdapter.js index 2bcb175e531..4e3914bccdd 100644 --- a/modules/staqAnalyticsAdapter.js +++ b/modules/staqAnalyticsAdapter.js @@ -2,9 +2,11 @@ import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; -import { parse } from '../src/url.js'; import * as utils from '../src/utils.js'; import { ajax } from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storageObj = getStorageManager(); const ANALYTICS_VERSION = '1.0.0'; const DEFAULT_QUEUE_TIMEOUT = 4000; @@ -212,10 +214,10 @@ const ORGANIC = '(organic)'; export let storage = { getItem: (name) => { - return utils.getDataFromLocalStorage(name); + return storageObj.getDataFromLocalStorage(name); }, setItem: (name, value) => { - utils.setDataInLocalStorage(name, value); + storageObj.setDataInLocalStorage(name, value); } }; @@ -246,7 +248,7 @@ export function getUmtSource(pageUrl, referrer) { if (se) { return asUtm(se, ORGANIC, ORGANIC); } - let parsedUrl = parse(pageUrl); + let parsedUrl = utils.parseUrl(pageUrl); let [refHost, refPath] = getReferrer(referrer); if (refHost && refHost !== parsedUrl.hostname) { return asUtm(refHost, REFERRAL, REFERRAL, '', refPath); @@ -273,12 +275,12 @@ export function getUmtSource(pageUrl, referrer) { } function getReferrer(referrer) { - let ref = parse(referrer); + let ref = utils.parseUrl(referrer); return [ref.hostname, ref.pathname]; } function getUTM(pageUrl) { - let urlParameters = parse(pageUrl).search; + let urlParameters = utils.parseUrl(pageUrl).search; if (!urlParameters['utm_campaign'] || !urlParameters['utm_source']) { return; } diff --git a/modules/sublimeBidAdapter.js b/modules/sublimeBidAdapter.js index 63f851c5b4d..1f8cb59f442 100644 --- a/modules/sublimeBidAdapter.js +++ b/modules/sublimeBidAdapter.js @@ -1,135 +1,217 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; +import * as utils from '../src/utils.js'; const BIDDER_CODE = 'sublime'; +const BIDDER_GVLID = 114; const DEFAULT_BID_HOST = 'pbjs.sskzlabs.com'; const DEFAULT_CURRENCY = 'EUR'; const DEFAULT_PROTOCOL = 'https'; const DEFAULT_TTL = 600; -const SUBLIME_VERSION = '0.4.0'; +const SUBLIME_ANTENNA = 'antenna.ayads.co'; +const SUBLIME_VERSION = '0.5.2'; + +/** + * Debug log message + * @param {String} msg + * @param {Object=} obj + */ +export function log(msg, obj) { + utils.logInfo('SublimeBidAdapter - ' + msg, obj); +} + +// Default state +export const state = { + zoneId: '', + transactionId: '' +}; -export const spec = { - code: BIDDER_CODE, - aliases: [], +/** + * Set a new state + * @param {Object} value + */ +export function setState(value) { + Object.assign(state, value); + log('State has been updated :', state); +} + +/** + * Send pixel to our debug endpoint + * @param {string} eventName - Event name that will be send in the e= query string + */ +export function sendEvent(eventName) { + const ts = Date.now(); + const eventObject = { + t: ts, + tse: ts, + z: state.zoneId, + e: eventName, + src: 'pa', + puid: state.transactionId, + trId: state.transactionId, + ver: SUBLIME_VERSION, + }; + + log('Sending pixel for event: ' + eventName, eventObject); + + const queryString = utils.formatQS(eventObject); + utils.triggerPixel('https://' + SUBLIME_ANTENNA + '/?' + queryString); +} + +/** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return {Boolean} True if this is a valid bid, and false otherwise. + */ +function isBidRequestValid(bid) { + return !!Number(bid.params.zoneId); +} + +/** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - An array of bids + * @param {Object} bidderRequest - Info describing the request to the server. + * @return {ServerRequest|ServerRequest[]} - Info describing the request to the server. + */ +function buildRequests(validBidRequests, bidderRequest) { + const commonPayload = { + pbav: SUBLIME_VERSION, + // Current Prebid params + prebidVersion: '$prebid.version$', + currencyCode: config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY, + timeout: (typeof bidderRequest === 'object' && !!bidderRequest) ? bidderRequest.timeout : config.getConfig('bidderTimeout'), + }; + + // RefererInfo + if (bidderRequest && bidderRequest.refererInfo) { + commonPayload.referer = bidderRequest.refererInfo.referer; + commonPayload.numIframes = bidderRequest.refererInfo.numIframes; + } + // GDPR handling + if (bidderRequest && bidderRequest.gdprConsent) { + commonPayload.gdprConsent = bidderRequest.gdprConsent.consentString; + commonPayload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side + } + + return validBidRequests.map(bid => { + const bidHost = bid.params.bidHost || DEFAULT_BID_HOST; + const protocol = bid.params.protocol || DEFAULT_PROTOCOL; + + setState({ + transactionId: bid.transactionId, + zoneId: bid.params.zoneId, + debug: bid.params.debug || false, + }); - /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: (bid) => { - return !!bid.params.zoneId; - }, - - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests An array of bids - * @param {Object} bidderRequest - Info describing the request to the server. - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: (validBidRequests, bidderRequest) => { - let commonPayload = { - sublimeVersion: SUBLIME_VERSION, - // Current Prebid params - prebidVersion: '$prebid.version$', - currencyCode: config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY, - timeout: config.getConfig('bidderTimeout'), + const bidPayload = { + adUnitCode: bid.adUnitCode, + auctionId: bid.auctionId, + bidder: bid.bidder, + bidderRequestId: bid.bidderRequestId, + bidRequestsCount: bid.bidRequestsCount, + requestId: bid.bidId, + sizes: bid.sizes.map(size => ({ + w: size[0], + h: size[1], + })), + transactionId: bid.transactionId, + zoneId: bid.params.zoneId, }; - // RefererInfo - if (bidderRequest && bidderRequest.refererInfo) { - commonPayload.referer = bidderRequest.refererInfo.referer; - commonPayload.numIframes = bidderRequest.refererInfo.numIframes; + const payload = Object.assign({}, commonPayload, bidPayload); + + return { + method: 'POST', + url: protocol + '://' + bidHost + '/bid', + data: payload, + options: { + contentType: 'application/json', + withCredentials: true + }, } - // GDPR handling - if (bidderRequest && bidderRequest.gdprConsent) { - commonPayload.gdprConsent = bidderRequest.gdprConsent.consentString; - commonPayload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side + }); +} + +/** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @param {*} bidRequest An object with bid request informations + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; + const response = serverResponse.body; + + if (response) { + if (response.timeout || !response.ad || //gmi.test(response.ad)) { + return bidResponses; } - return validBidRequests.map(bid => { - let bidPayload = { - adUnitCode: bid.adUnitCode, - auctionId: bid.auctionId, - bidder: bid.bidder, - bidderRequestId: bid.bidderRequestId, - bidRequestsCount: bid.bidRequestsCount, - requestId: bid.bidId, - sizes: bid.sizes.map(size => ({ - w: size[0], - h: size[1], - })), - transactionId: bid.transactionId, - zoneId: bid.params.zoneId, - }; + // Setting our returned sizes object to default values + let returnedSizes = { + width: 1800, + height: 1000 + }; - let protocol = bid.params.protocol || DEFAULT_PROTOCOL; - let bidHost = bid.params.bidHost || DEFAULT_BID_HOST; - let payload = Object.assign({}, commonPayload, bidPayload); - - return { - method: 'POST', - url: protocol + '://' + bidHost + '/bid', - data: payload, - options: { - contentType: 'application/json', - withCredentials: true - }, - }; - }); - }, - - /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @param {*} bidRequest An object with bid request informations - * @return {Bid[]} An array of bids which were nested inside the server. - */ - interpretResponse: (serverResponse, bidRequest) => { - const bidResponses = []; - const response = serverResponse.body; - - if (response) { - if (response.timeout || !response.ad || response.ad.match(//gmi)) { - return bidResponses; - } - - // Setting our returned sizes object to default values - let returnedSizes = { - width: 1800, - height: 1000 + // Verifying Banner sizes + if (bidRequest && bidRequest.data && bidRequest.data.w === 1 && bidRequest.data.h === 1) { + // If banner sizes are 1x1 we set our default size object to 1x1 + returnedSizes = { + width: 1, + height: 1 }; + } - // Verifying Banner sizes - if (bidRequest && bidRequest.data && bidRequest.data.w === 1 && bidRequest.data.h === 1) { - // If banner sizes are 1x1 we set our default size object to 1x1 - returnedSizes = { - width: 1, - height: 1 - }; - } - - const bidResponse = { - requestId: response.requestId || '', - cpm: response.cpm || 0, - width: response.width || returnedSizes.width, - height: response.height || returnedSizes.height, - creativeId: response.creativeId || 1, - dealId: response.dealId || 1, - currency: response.currency || DEFAULT_CURRENCY, - netRevenue: response.netRevenue || true, - ttl: response.ttl || DEFAULT_TTL, - ad: response.ad, - }; + const bidResponse = { + requestId: response.requestId || '', + cpm: response.cpm || 0, + width: response.width || returnedSizes.width, + height: response.height || returnedSizes.height, + creativeId: response.creativeId || 1, + dealId: response.dealId || 1, + currency: response.currency || DEFAULT_CURRENCY, + netRevenue: response.netRevenue || true, + ttl: response.ttl || DEFAULT_TTL, + ad: response.ad, + pbav: SUBLIME_VERSION + }; - bidResponses.push(bidResponse); - } + bidResponses.push(bidResponse); + } + + return bidResponses; +} + +/** + * Send pixel when bidWon event is triggered + * @param {Object} timeoutData + */ +function onBidWon(bid) { + log('Bid won', bid); + sendEvent('bidwon'); +} + +/** + * Send debug when we timeout + * @param {Object} timeoutData + */ +function onTimeout(timeoutData) { + log('Timeout from adapter', timeoutData); + sendEvent('bidtimeout'); +} - return bidResponses; - }, +export const spec = { + code: BIDDER_CODE, + gvlid: BIDDER_GVLID, + aliases: [], + isBidRequestValid: isBidRequestValid, + buildRequests: buildRequests, + interpretResponse: interpretResponse, + onBidWon: onBidWon, + onTimeout: onTimeout, }; registerBidder(spec); diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index c0f11dc4639..38c261745c7 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -54,10 +54,13 @@ export const spec = { if (bidderRequest && gdpr) { let isCmp = (typeof gdpr.gdprApplies === 'boolean') let isConsentString = (typeof gdpr.consentString === 'string') - let status = isCmp ? findGdprStatus(gdpr.gdprApplies, gdpr.vendorData) : gdprStatus.CMP_NOT_FOUND_OR_ERROR + let status = isCmp + ? findGdprStatus(gdpr.gdprApplies, gdpr.vendorData, gdpr.apiVersion) + : gdprStatus.CMP_NOT_FOUND_OR_ERROR payload.gdpr_iab = { consent: isConsentString ? gdpr.consentString : '', - status: status + status: status, + apiVersion: gdpr.apiVersion }; } @@ -101,33 +104,6 @@ export const spec = { } return bidResponses; }, - - getUserSyncs: function(syncOptions, responses, gdprConsent) { - let queryParams = { - hb_provider: 'prebid', - hb_version: '$prebid.version$' - }; - - if (gdprConsent) { - let gdprIab = { - status: findGdprStatus(gdprConsent.gdprApplies, gdprConsent.vendorData), - consent: gdprConsent.consentString - }; - - queryParams.gdprIab = JSON.stringify(gdprIab) - } - - if (utils.deepAccess(responses[0], 'body.responses.0.placementId')) { - queryParams.placementId = responses[0].body.responses[0].placementId - }; - - if (syncOptions.iframeEnabled) { - return [{ - type: 'iframe', - url: 'https://sync.teads.tv/iframe?' + utils.parseQueryStringParameters(queryParams) - }]; - } - } }; function getReferrerInfo(bidderRequest) { @@ -138,15 +114,22 @@ function getReferrerInfo(bidderRequest) { return ref; } -function findGdprStatus(gdprApplies, gdprData) { - let status = gdprStatus.GDPR_APPLIES_PUBLISHER; - +function findGdprStatus(gdprApplies, gdprData, apiVersion) { + let status = gdprStatus.GDPR_APPLIES_PUBLISHER if (gdprApplies) { - if (gdprData.hasGlobalScope || gdprData.hasGlobalConsent) status = gdprStatus.GDPR_APPLIES_GLOBAL + if (isGlobalConsent(gdprData, apiVersion)) status = gdprStatus.GDPR_APPLIES_GLOBAL } else status = gdprStatus.GDPR_DOESNT_APPLY return status; } +function isGlobalConsent(gdprData, apiVersion) { + return gdprData && apiVersion === 1 + ? (gdprData.hasGlobalScope || gdprData.hasGlobalConsent) + : gdprData && apiVersion === 2 + ? !gdprData.isServiceSpecific + : false +} + function buildRequestObject(bid) { const reqObj = {}; let placementId = utils.getValue(bid.params, 'placementId'); diff --git a/modules/terceptAnalyticsAdapter.js b/modules/terceptAnalyticsAdapter.js new file mode 100644 index 00000000000..54da2bd06d2 --- /dev/null +++ b/modules/terceptAnalyticsAdapter.js @@ -0,0 +1,144 @@ +import { ajax } from '../src/ajax.js'; +import adapter from '../src/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; +import * as utils from '../src/utils.js'; + +const emptyUrl = ''; +const analyticsType = 'endpoint'; +const terceptAnalyticsVersion = 'v1.0.0'; +const defaultHostName = 'us-central1-quikr-ebay.cloudfunctions.net'; +const defaultPathName = '/prebid-analytics'; + +let initOptions; +let auctionTimestamp; +let events = { + bids: [] +}; + +var terceptAnalyticsAdapter = Object.assign(adapter( + { + emptyUrl, + analyticsType + }), { + track({ eventType, args }) { + if (typeof args !== 'undefined') { + if (eventType === CONSTANTS.EVENTS.BID_TIMEOUT) { + args.forEach(item => { mapBidResponse(item, 'timeout'); }); + } else if (eventType === CONSTANTS.EVENTS.AUCTION_INIT) { + events.auctionInit = args; + auctionTimestamp = args.timestamp; + } else if (eventType === CONSTANTS.EVENTS.BID_REQUESTED) { + mapBidRequests(args).forEach(item => { events.bids.push(item) }); + } else if (eventType === CONSTANTS.EVENTS.BID_RESPONSE) { + mapBidResponse(args, 'response'); + } else if (eventType === CONSTANTS.EVENTS.BID_WON) { + send({ + bidWon: mapBidResponse(args, 'win') + }, 'won'); + } + } + + if (eventType === CONSTANTS.EVENTS.AUCTION_END) { + send(events, 'auctionEnd'); + } + } +}); + +function mapBidRequests(params) { + let arr = []; + if (typeof params.bids !== 'undefined' && params.bids.length) { + params.bids.forEach(function (bid) { + arr.push({ + bidderCode: bid.bidder, + bidId: bid.bidId, + adUnitCode: bid.adUnitCode, + requestId: bid.bidderRequestId, + auctionId: bid.auctionId, + transactionId: bid.transactionId, + sizes: utils.parseSizesInput(bid.mediaTypes.banner.sizes).toString(), + renderStatus: 1, + requestTimestamp: params.auctionStart + }); + }); + } + return arr; +} + +function mapBidResponse(bidResponse, status) { + if (status !== 'win') { + let bid = events.bids.filter(o => o.bidId === bidResponse.bidId || o.bidId === bidResponse.requestId)[0]; + Object.assign(bid, { + bidderCode: bidResponse.bidder, + bidId: status === 'timeout' ? bidResponse.bidId : bidResponse.requestId, + adUnitCode: bidResponse.adUnitCode, + auctionId: bidResponse.auctionId, + creativeId: bidResponse.creativeId, + transactionId: bidResponse.transactionId, + currency: bidResponse.currency, + cpm: bidResponse.cpm, + netRevenue: bidResponse.netRevenue, + mediaType: bidResponse.mediaType, + statusMessage: bidResponse.statusMessage, + status: bidResponse.status, + renderStatus: status === 'timeout' ? 3 : 2, + timeToRespond: bidResponse.timeToRespond, + requestTimestamp: bidResponse.requestTimestamp, + responseTimestamp: bidResponse.responseTimestamp + }); + } else { + return { + bidderCode: bidResponse.bidder, + bidId: bidResponse.requestId, + adUnitCode: bidResponse.adUnitCode, + auctionId: bidResponse.auctionId, + creativeId: bidResponse.creativeId, + transactionId: bidResponse.transactionId, + currency: bidResponse.currency, + cpm: bidResponse.cpm, + netRevenue: bidResponse.netRevenue, + renderedSize: bidResponse.size, + mediaType: bidResponse.mediaType, + statusMessage: bidResponse.statusMessage, + status: bidResponse.status, + renderStatus: 4, + timeToRespond: bidResponse.timeToRespond, + requestTimestamp: bidResponse.requestTimestamp, + responseTimestamp: bidResponse.responseTimestamp + } + } +} + +function send(data, status) { + let location = utils.getWindowLocation(); + if (typeof data !== 'undefined' && typeof data.auctionInit !== 'undefined') { + Object.assign(data.auctionInit, { host: location.host, path: location.pathname, search: location.search }); + } + data.initOptions = initOptions; + + let terceptAnalyticsRequestUrl = utils.buildUrl({ + protocol: 'https', + hostname: (initOptions && initOptions.hostName) || defaultHostName, + pathname: (initOptions && initOptions.pathName) || defaultPathName, + search: { + auctionTimestamp: auctionTimestamp, + terceptAnalyticsVersion: terceptAnalyticsVersion, + prebidVersion: $$PREBID_GLOBAL$$.version + } + }); + + ajax(terceptAnalyticsRequestUrl, undefined, JSON.stringify(data), { method: 'POST', contentType: 'text/plain' }); +} + +terceptAnalyticsAdapter.originEnableAnalytics = terceptAnalyticsAdapter.enableAnalytics; +terceptAnalyticsAdapter.enableAnalytics = function (config) { + initOptions = config.options; + terceptAnalyticsAdapter.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: terceptAnalyticsAdapter, + code: 'tercept' +}); + +export default terceptAnalyticsAdapter; diff --git a/modules/terceptAnalyticsAdapter.md b/modules/terceptAnalyticsAdapter.md new file mode 100644 index 00000000000..39578d06730 --- /dev/null +++ b/modules/terceptAnalyticsAdapter.md @@ -0,0 +1,22 @@ +# Overview +Module Name: Tercept Analytics Adapter +Module Type: Analytics Adapter +Maintainer: gourav.chindlur@tercept.com + +# Description + +Analytics adapter for prebid provided by Tercept. Contact gourav.chindlur@tercept.com for information. + +# Test Parameters + +``` +{ + provider: 'Tercept', + options : { + pubId : 50357 //id provided by Tercept + pubKey: 'xxx' //key provided by Tercept + hostName: 'us-central1-quikr-ebay.cloudfunctions.net' //Tercept endpoint host + pathName: '/prebid-analytics' //Tercept endpoint path + } +} +``` diff --git a/modules/theAdxBidAdapter.js b/modules/theAdxBidAdapter.js index 5306b89a5da..91e36077e88 100644 --- a/modules/theAdxBidAdapter.js +++ b/modules/theAdxBidAdapter.js @@ -7,9 +7,6 @@ import { import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { - parse as parseUrl -} from '../src/url.js'; const BIDDER_CODE = 'theadx'; const ENDPOINT_URL = 'https://ssp.theadx.com/request'; @@ -317,7 +314,7 @@ export const spec = { } let buildSiteComponent = (bidRequest, bidderRequest) => { - let loc = parseUrl(bidderRequest.refererInfo.referer, { + let loc = utils.parseUrl(bidderRequest.refererInfo.referer, { decodeSearchAsString: true }); diff --git a/modules/tpmnBidAdapter.js b/modules/tpmnBidAdapter.js index d80c87987fa..ec9d30c0e29 100644 --- a/modules/tpmnBidAdapter.js +++ b/modules/tpmnBidAdapter.js @@ -2,7 +2,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; -import {parse as parseUrl} from '../src/url.js'; export const ADAPTER_VERSION = '1'; const SUPPORTED_AD_TYPES = [BANNER]; @@ -73,7 +72,7 @@ registerBidder(spec); * Creates site description object */ function createSite(refInfo) { - let url = parseUrl(refInfo.referer); + let url = utils.parseUrl(refInfo.referer); let site = { 'domain': url.hostname, 'page': url.protocol + '://' + url.hostname + url.pathname diff --git a/modules/trionBidAdapter.js b/modules/trionBidAdapter.js index 846711a2721..37d54e60cd2 100644 --- a/modules/trionBidAdapter.js +++ b/modules/trionBidAdapter.js @@ -1,5 +1,8 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BID_REQUEST_BASE_URL = 'https://in-appadvertising.com/api/bidRequest'; const USER_SYNC_URL = 'https://in-appadvertising.com/api/userSync.html'; @@ -176,8 +179,8 @@ function handlePostMessage() { export function getStorageData(key) { var item = null; try { - if (utils.hasLocalStorage()) { - item = utils.getDataFromLocalStorage(key); + if (storage.hasLocalStorage()) { + item = storage.getDataFromLocalStorage(key); } } catch (e) { } @@ -186,8 +189,8 @@ export function getStorageData(key) { export function setStorageData(key, item) { try { - if (utils.hasLocalStorage()) { - utils.setDataInLocalStorage(key, item); + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage(key, item); } } catch (e) { } diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index f232915f8f2..8b21f334233 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -203,7 +203,8 @@ function _buildResponseObject(bidderRequest, bid) { creativeId: creativeId, dealId: dealId, currency: 'USD', - ttl: 33, + ttl: 300, + tl_source: bid.tl_source, }; }; return bidResponse; diff --git a/modules/undertoneBidAdapter.js b/modules/undertoneBidAdapter.js index a253c6aae5f..6ead453b622 100644 --- a/modules/undertoneBidAdapter.js +++ b/modules/undertoneBidAdapter.js @@ -2,7 +2,7 @@ * Adapter to send bids to Undertone */ -import * as urlUtils from '../src/url.js'; +import { parseUrl } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'undertone'; @@ -51,6 +51,26 @@ function getGdprQueryParams(gdprConsent) { return `gdpr=${gdpr}&gdprstr=${gdprstr}`; } +function getBannerCoords(id) { + let element = document.getElementById(id); + let left = -1; + let top = -1; + if (element) { + left = element.offsetLeft; + top = element.offsetTop; + + let parent = element.offsetParent; + if (parent) { + left += parent.offsetLeft; + top += parent.offsetTop; + } + + return [left, top]; + } else { + return null; + } +} + export const spec = { code: BIDDER_CODE, isBidRequestValid: function(bid) { @@ -60,15 +80,19 @@ export const spec = { } }, buildRequests: function(validBidRequests, bidderRequest) { + const vw = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); + const vh = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); + const pageSizeArray = vw == 0 || vh == 0 ? null : [vw, vh]; const payload = { 'x-ut-hb-params': [], 'commons': { 'adapterVersion': '$prebid.version$', - 'uids': validBidRequests[0].userId + 'uids': validBidRequests[0].userId, + 'pageSize': pageSizeArray } }; const referer = bidderRequest.refererInfo.referer; - const hostname = urlUtils.parse(referer).hostname; + const hostname = parseUrl(referer).hostname; let domain = extractDomainFromHost(hostname); const pageUrl = getCanonicalUrl() || referer; @@ -87,6 +111,7 @@ export const spec = { validBidRequests.map(bidReq => { const bid = { bidRequestId: bidReq.bidId, + coordinates: getBannerCoords(bidReq.adUnitCode), hbadaptor: 'prebid', url: pageUrl, domain: domain, diff --git a/modules/unicornBidAdapter.js b/modules/unicornBidAdapter.js index b782c716323..cc320dc68a8 100644 --- a/modules/unicornBidAdapter.js +++ b/modules/unicornBidAdapter.js @@ -1,6 +1,9 @@ import * as utils from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const storage = getStorageManager(); const BIDDER_CODE = 'unicorn'; const UNICORN_ENDPOINT = 'https://ds.uncn.jp/pb/0/bid.json'; const UNICORN_DEFAULT_CURRENCY = 'JPY'; @@ -119,7 +122,7 @@ const interpretResponse = (serverResponse, request) => { * Get or Create Uid for First Party Cookie */ const getUid = () => { - const ck = utils.getCookie(UNICORN_PB_COOKIE_KEY); + const ck = storage.getCookie(UNICORN_PB_COOKIE_KEY); if (ck) { return JSON.parse(ck)['uid']; } else { @@ -127,7 +130,7 @@ const getUid = () => { uid: utils.generateUUID() }; const expireIn = new Date(Date.now() + 24 * 60 * 60 * 10000).toUTCString(); - utils.setCookie(UNICORN_PB_COOKIE_KEY, JSON.stringify(newCk), expireIn); + storage.setCookie(UNICORN_PB_COOKIE_KEY, JSON.stringify(newCk), expireIn); return newCk.uid; } }; diff --git a/modules/userId/eids.js b/modules/userId/eids.js index bd3b9bac0ff..5ca9e40866b 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -91,7 +91,7 @@ function createEidObject(userIdData, subModuleKey) { let eid = {}; eid.source = conf['source']; const value = utils.isFn(conf['getValue']) ? conf['getValue'](userIdData) : userIdData; - if (value) { + if (utils.isStr(value)) { const uid = { id: value, atype: conf['atype'] }; // getUidExt if (utils.isFn(conf['getUidExt'])) { diff --git a/modules/userId/index.js b/modules/userId/index.js index 89d3ee9e5c7..5de8ad75978 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -115,14 +115,16 @@ import * as utils from '../../src/utils.js'; import {getGlobal} from '../../src/prebidGlobal.js'; import {gdprDataHandler} from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; -import {module} from '../../src/hook.js'; +import {module, hook} from '../../src/hook.js'; import {createEidsArray} from './eids.js'; +import { getCoreStorageManager } from '../../src/storageManager.js'; const MODULE_NAME = 'User ID'; const COOKIE = 'cookie'; const LOCAL_STORAGE = 'html5'; const DEFAULT_SYNC_DELAY = 500; const NO_AUCTION_DELAY = 0; +export const coreStorage = getCoreStorageManager('userid'); /** @type {string[]} */ let validStorageTypes = []; @@ -142,6 +144,9 @@ let configRegistry = []; /** @type {Submodule[]} */ let submoduleRegistry = []; +/** @type {(number|undefined)} */ +let timeoutID; + /** @type {(number|undefined)} */ export let syncDelay; @@ -162,15 +167,15 @@ function setStoredValue(storage, value) { const valueStr = utils.isPlainObject(value) ? JSON.stringify(value) : value; const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString(); if (storage.type === COOKIE) { - utils.setCookie(storage.name, valueStr, expiresStr, 'Lax'); + coreStorage.setCookie(storage.name, valueStr, expiresStr, 'Lax'); if (typeof storage.refreshInSeconds === 'number') { - utils.setCookie(`${storage.name}_last`, new Date().toUTCString(), expiresStr); + coreStorage.setCookie(`${storage.name}_last`, new Date().toUTCString(), expiresStr); } } else if (storage.type === LOCAL_STORAGE) { - utils.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); - utils.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); + coreStorage.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); + coreStorage.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); if (typeof storage.refreshInSeconds === 'number') { - utils.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); + coreStorage.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); } } } catch (error) { @@ -188,15 +193,15 @@ function getStoredValue(storage, key = undefined) { let storedValue; try { if (storage.type === COOKIE) { - storedValue = utils.getCookie(storedKey); + storedValue = coreStorage.getCookie(storedKey); } else if (storage.type === LOCAL_STORAGE) { - const storedValueExp = utils.getDataFromLocalStorage(`${storage.name}_exp`); + const storedValueExp = coreStorage.getDataFromLocalStorage(`${storage.name}_exp`); // empty string means no expiration set if (storedValueExp === '') { - storedValue = utils.getDataFromLocalStorage(storedKey); + storedValue = coreStorage.getDataFromLocalStorage(storedKey); } else if (storedValueExp) { if ((new Date(storedValueExp)).getTime() - Date.now() > 0) { - storedValue = decodeURIComponent(utils.getDataFromLocalStorage(storedKey)); + storedValue = decodeURIComponent(coreStorage.getDataFromLocalStorage(storedKey)); } } } @@ -254,6 +259,7 @@ function processSubmoduleCallbacks(submodules, cb) { // clear callback, this prop is used to test if all submodule callbacks are complete below submodule.callback = undefined; }); + clearTimeout(timeoutID); } /** @@ -320,9 +326,9 @@ function initializeSubmodulesAndExecuteCallbacks(continueAuction) { } } utils.logInfo(`${MODULE_NAME} - auction delayed by ${auctionDelay} at most to fetch ids`); - processSubmoduleCallbacks(submodulesWithCallbacks, continueCallback); - setTimeout(continueCallback, auctionDelay); + timeoutID = setTimeout(continueCallback, auctionDelay); + processSubmoduleCallbacks(submodulesWithCallbacks, continueCallback); } else { // wait for auction complete before processing submodule callbacks events.on(CONSTANTS.EVENTS.AUCTION_END, function auctionEndHandler() { @@ -376,6 +382,23 @@ function getUserIds() { return 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 getUserIdsAsEids() { + // initialize submodules only when undefined + initializeSubmodulesAndExecuteCallbacks(); + return createEidsArray(getCombinedSubmoduleIds(initializedSubmodules)); +} + +/** + * This hook returns updated list of submodules which are allowed to do get user id based on TCF 2 enforcement rules configured + */ +export const validateGdprEnforcement = hook('sync', function (submodules, consentData) { + return submodules; +}, 'validateGdprEnforcement'); + /** * @param {SubmoduleContainer[]} submodules * @param {ConsentData} consentData @@ -383,12 +406,13 @@ function getUserIds() { */ function initSubmodules(submodules, consentData) { // gdpr consent with purpose one is required, otherwise exit immediately + let userIdModules = validateGdprEnforcement(submodules, consentData); if (!hasGDPRConsent(consentData)) { utils.logWarn(`${MODULE_NAME} - gdpr permission not valid for local storage or cookies, exit module`); return []; } - return submodules.reduce((carry, submodule) => { + return userIdModules.reduce((carry, submodule) => { // There are two submodule configuration types to handle: storage or value // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method // 2. value: pass directly to bids @@ -534,17 +558,17 @@ export function init(config) { // list of browser enabled storage types validStorageTypes = [ - utils.localStorageIsEnabled() ? LOCAL_STORAGE : null, - utils.cookiesAreEnabled() ? COOKIE : null + coreStorage.localStorageIsEnabled() ? LOCAL_STORAGE : null, + coreStorage.cookiesAreEnabled() ? COOKIE : null ].filter(i => i !== null); // exit immediately if opt out cookie or local storage keys exists. - if (validStorageTypes.indexOf(COOKIE) !== -1 && (utils.getCookie('_pbjs_id_optout') || utils.getCookie('_pubcid_optout'))) { + if (validStorageTypes.indexOf(COOKIE) !== -1 && (coreStorage.getCookie('_pbjs_id_optout') || coreStorage.getCookie('_pubcid_optout'))) { utils.logInfo(`${MODULE_NAME} - opt-out cookie found, exit module`); return; } // _pubcid_optout is checked for compatiblility with pubCommonId - if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && (utils.getDataFromLocalStorage('_pbjs_id_optout') || utils.getDataFromLocalStorage('_pubcid_optout'))) { + if (validStorageTypes.indexOf(LOCAL_STORAGE) !== -1 && (coreStorage.getDataFromLocalStorage('_pbjs_id_optout') || coreStorage.getDataFromLocalStorage('_pubcid_optout'))) { utils.logInfo(`${MODULE_NAME} - opt-out localStorage found, exit module`); return; } @@ -562,6 +586,7 @@ 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; } // init config update listener to start the application diff --git a/modules/valueimpressionBidAdapter.js b/modules/valueimpressionBidAdapter.js new file mode 100644 index 00000000000..41bb6e6cacc --- /dev/null +++ b/modules/valueimpressionBidAdapter.js @@ -0,0 +1,152 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +const BIDDER_CODE = 'valueimpression'; +const ENDPOINT = 'https://adapter.valueimpression.com/bid'; +const USER_SYNC_URL = 'https://adapter.valueimpression.com/usersync'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['banner', 'video'], + aliases: ['vi'], + isBidRequestValid: function (bid) { + if (!bid.params) { + return false; + } + if (!bid.params.siteId) { + return false; + } + if (!utils.deepAccess(bid, 'mediaTypes.banner') && !utils.deepAccess(bid, 'mediaTypes.video')) { + return false; + } + if (utils.deepAccess(bid, 'mediaTypes.banner')) { // Valueimpression does not support multi type bids, favor banner over video + if (!utils.deepAccess(bid, 'mediaTypes.banner.sizes')) { + // sizes at the banner is required. + return false; + } + } else if (utils.deepAccess(bid, 'mediaTypes.video')) { + if (!utils.deepAccess(bid, 'mediaTypes.video.playerSize')) { + // playerSize is required for instream adUnits. + return false; + } + } + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const payload = {}; + payload.device = {}; + payload.device.ua = navigator.userAgent; + payload.device.height = window.innerHeight; + payload.device.width = window.innerWidth; + payload.device.dnt = _getDoNotTrack(); + payload.device.language = navigator.language; + + payload.site = {}; + payload.site.id = validBidRequests[0].params.siteId; + payload.site.page = window.location.href; + payload.site.referrer = document.referrer; + payload.site.hostname = window.location.hostname; + + // Apply GDPR parameters to request. + payload.gdpr = {}; + if (bidderRequest && bidderRequest.gdprConsent) { + payload.gdpr.gdprApplies = bidderRequest.gdprConsent.gdprApplies ? 'true' : 'false'; + if (bidderRequest.gdprConsent.consentString) { + payload.gdpr.consentString = bidderRequest.gdprConsent.consentString; + } + } + if (validBidRequests[0].schain) { + payload.schain = JSON.stringify(validBidRequests[0].schain) + } + if (bidderRequest && bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } + + payload.bids = validBidRequests; + + return { + method: 'POST', + url: ENDPOINT, + data: payload, + withCredentials: true, + bidderRequests: validBidRequests + }; + }, + interpretResponse: function (serverResponse, bidRequest) { + const serverBody = serverResponse.body; + const serverBids = serverBody.bids; + // check overall response + if (!serverBody || typeof serverBody !== 'object') { + return []; + } + if (!serverBids || typeof serverBids !== 'object') { + return []; + } + + const bidResponses = []; + serverBids.forEach(bid => { + const bidResponse = { + requestId: bid.requestId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.creativeId, + dealId: bid.dealId, + currency: bid.currency, + netRevenue: bid.netRevenue, + ttl: bid.ttl, + mediaType: bid.mediaType + }; + if (bid.vastXml) { + bidResponse.vastXml = utils.replaceAuctionPrice(bid.vastXml, bid.cpm); + } else { + bidResponse.ad = utils.replaceAuctionPrice(bid.ad, bid.cpm); + } + bidResponses.push(bidResponse); + }); + return bidResponses; + }, + getUserSyncs: function (syncOptions, serverResponses) { + const syncs = []; + try { + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: USER_SYNC_URL + }); + } + if (syncOptions.pixelEnabled && serverResponses.length > 0) { + serverResponses[0].body.pixel.forEach(px => { + syncs.push({ + type: px.type, + url: px.url + }); + }); + } + } catch (e) { } + return syncs; + }, + + onTimeout: function (timeoutData) { + }, + + onBidWon: function (bid) { + }, + + onSetTargeting: function (bid) { + } +}; + +function _getDoNotTrack() { + if (window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack) { + if (window.doNotTrack == '1' || navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') { + return 1; + } else { + return 0; + } + } else { + return 0; + } +} + +registerBidder(spec); diff --git a/modules/valueimpressionBidAdapter.md b/modules/valueimpressionBidAdapter.md new file mode 100644 index 00000000000..11400f23834 --- /dev/null +++ b/modules/valueimpressionBidAdapter.md @@ -0,0 +1,56 @@ +# Overview + +``` +Module Name: Valueimpression Bidder Adapter +Module Type: Bidder Adapter +Maintainer: thuyhq@83.com.vn +``` + +# Description + +Module that connects to Valueimpression's exchange for bids. +Valueimpression Bidder adapter supports Banner and Video ads. + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [ + { + bidder: 'valueimpression', + params: { + siteId: 'vi-site-id', // siteId provided by Valueimpression + } + } + ] + } +]; +``` + +# Video Test Parameters +``` +var videoAdUnit = { + code: 'test-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + }, + }, + bids: [ + { + bidder: 'valueimpression', + params: { + siteId: 'vi-site-id', // siteId provided by Valueimpression + } + } + ] +}; +``` \ No newline at end of file diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index b3ab8a4d275..d974c0e1eb7 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -1,6 +1,7 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; + export const URL = 'https://prebid.cootlogix.com'; const BIDDER_CODE = 'vidazoo'; const CURRENCY = 'USD'; @@ -19,25 +20,29 @@ function isBidRequestValid(bid) { return !!(params.cId && params.pId); } -function buildRequest(bid, topWindowUrl, size, bidderRequest) { +function buildRequest(bid, topWindowUrl, sizes, bidderRequest) { const {params, bidId} = bid; const {bidFloor, cId, pId, ext} = params; - // Prebid's util function returns AppNexus style sizes (i.e. 300x250) - const [width, height] = size.split('x'); - - const dto = { - method: 'GET', - url: `${URL}/prebid/${cId}`, - data: { - url: encodeURIComponent(topWindowUrl), - cb: Date.now(), - bidFloor: bidFloor, - bidId: bidId, - publisherId: pId, - consent: bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString, - width, - height + let data = { + url: encodeURIComponent(topWindowUrl), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + publisherId: pId, + sizes: sizes, + }; + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + const dto = { + method: 'POST', + url: `${URL}/prebid/multi/${cId}`, + data: data }; utils._each(ext, (value, key) => { @@ -52,10 +57,8 @@ function buildRequests(validBidRequests, bidderRequest) { const requests = []; validBidRequests.forEach(validBidRequest => { const sizes = utils.parseSizesInput(validBidRequest.sizes); - sizes.forEach(size => { - const request = buildRequest(validBidRequest, topWindowUrl, size, bidderRequest); - requests.push(request); - }); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest); + requests.push(request); }); return requests; } @@ -64,23 +67,30 @@ function interpretResponse(serverResponse, request) { if (!serverResponse || !serverResponse.body) { return []; } - const {creativeId, ad, price, exp} = serverResponse.body; - if (!ad || !price) { - return []; - } - const {bidId, width, height} = request.data; + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + try { - return [{ - requestId: bidId, - cpm: price, - width: width, - height: height, - creativeId: creativeId, - currency: CURRENCY, - netRevenue: true, - ttl: exp || TTL_SECONDS, - ad: ad - }]; + results.forEach(result => { + const {creativeId, ad, price, exp, width, height, currency} = result; + if (!ad || !price) { + return; + } + output.push({ + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + ad: ad + }) + }); + return output; } catch (e) { return []; } @@ -101,20 +111,18 @@ function getUserSyncs(syncOptions, responses) { const syncs = []; responses.forEach(response => { const {body} = response; - const cookies = body ? body.cookies || [] : []; - cookies.forEach(cookie => { - switch (cookie.type) { - case INTERNAL_SYNC_TYPE.IFRAME: - break; - case INTERNAL_SYNC_TYPE.IMAGE: + const results = body ? body.results || [] : []; + results.forEach(result => { + (result.cookies || []).forEach(cookie => { + if (cookie.type === INTERNAL_SYNC_TYPE.IMAGE) { if (pixelEnabled && !lookup[cookie.src]) { syncs.push({ type: EXTERNAL_SYNC_TYPE.IMAGE, url: cookie.src }); } - break; - } + } + }); }); }); return syncs; diff --git a/modules/vidazooBidAdapter.md b/modules/vidazooBidAdapter.md index 1e9dc47dd51..df7700cb05a 100644 --- a/modules/vidazooBidAdapter.md +++ b/modules/vidazooBidAdapter.md @@ -4,7 +4,7 @@ **Module Type:** Bidder Adapter -**Maintainer:** server-dev@getintent.com +**Maintainer:** dev@vidazoo.com # Description diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index 75011ddad03..e76b6035cd3 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -36,6 +36,8 @@ export const spec = { config.getConfig('currency.adServerCurrency') || DEFAULT_CUR; let reqId; + let payloadSchain; + let payloadUserId; if (currencyWhiteList.indexOf(currency) === -1) { utils.logError(LOG_ERROR_MESS.notAllowedCurrency + currency); @@ -44,8 +46,14 @@ export const spec = { bids.forEach(bid => { reqId = bid.bidderRequestId; - const {params: {uid}, adUnitCode} = bid; + const {params: {uid}, adUnitCode, schain, userId} = bid; auids.push(uid); + if (!payloadSchain && schain) { + payloadSchain = schain; + } + if (!payloadUserId && userId) { + payloadUserId = userId; + } const sizesId = utils.parseSizesInput(bid.sizes); if (!slotsMapByUid[uid]) { @@ -84,6 +92,22 @@ export const spec = { wrapperVersion: '$prebid.version$' }; + if (payloadSchain) { + payload.schain = JSON.stringify(payloadSchain); + } + + if (payloadUserId) { + if (payloadUserId.tdid) { + payload.tdid = payloadUserId.tdid; + } + if (payloadUserId.id5id) { + payload.id5 = payloadUserId.id5id; + } + if (payloadUserId.digitrustid && payloadUserId.digitrustid.data && payloadUserId.digitrustid.data.id) { + payload.dtid = payloadUserId.digitrustid.data.id; + } + } + if (bidderRequest) { if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { payload.u = bidderRequest.refererInfo.referer; diff --git a/modules/widespaceBidAdapter.js b/modules/widespaceBidAdapter.js index 26ced2bb802..5165b00a98c 100644 --- a/modules/widespaceBidAdapter.js +++ b/modules/widespaceBidAdapter.js @@ -1,19 +1,17 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { - cookiesAreEnabled, - hasLocalStorage, parseQueryStringParameters, parseSizesInput } from '../src/utils.js'; import includes from 'core-js/library/fn/array/includes.js'; import find from 'core-js/library/fn/array/find.js'; -import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); const BIDDER_CODE = 'widespace'; const WS_ADAPTER_VERSION = '2.0.1'; -const LOCAL_STORAGE_AVAILABLE = hasLocalStorage(); -const COOKIE_ENABLED = cookiesAreEnabled(); const LS_KEYS = { PERF_DATA: 'wsPerfData', LC_UID: 'wsLcuid', @@ -177,37 +175,37 @@ export const spec = { function storeData(data, name, stringify = true) { const value = stringify ? JSON.stringify(data) : data; - if (LOCAL_STORAGE_AVAILABLE) { - utils.setDataInLocalStorage(name, value); + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage(name, value); return true; - } else if (COOKIE_ENABLED) { + } else if (storage.cookiesAreEnabled()) { const theDate = new Date(); const expDate = new Date(theDate.setMonth(theDate.getMonth() + 12)).toGMTString(); - utils.setCookie(name, value, expDate); + storage.setCookie(name, value, expDate); return true; } } function getData(name, remove = true) { let data = []; - if (LOCAL_STORAGE_AVAILABLE) { + if (storage.hasLocalStorage()) { Object.keys(localStorage).filter((key) => { if (key.indexOf(name) > -1) { - data.push(utils.getDataFromLocalStorage(key)); + data.push(storage.getDataFromLocalStorage(key)); if (remove) { - utils.removeDataFromLocalStorage(key); + storage.removeDataFromLocalStorage(key); } } }); } - if (COOKIE_ENABLED) { + if (storage.cookiesAreEnabled()) { document.cookie.split(';').forEach((item) => { let value = item.split('='); if (value[0].indexOf(name) > -1) { data.push(value[1]); if (remove) { - utils.setCookie(value[0], '', 'Thu, 01 Jan 1970 00:00:01 GMT'); + storage.setCookie(value[0], '', 'Thu, 01 Jan 1970 00:00:01 GMT'); } } }); diff --git a/modules/windtalkerBidAdapter.js b/modules/windtalkerBidAdapter.js new file mode 100644 index 00000000000..0c0ef571954 --- /dev/null +++ b/modules/windtalkerBidAdapter.js @@ -0,0 +1,307 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import includes from 'core-js/library/fn/array/includes.js'; + +const NATIVE_DEFAULTS = { + TITLE_LEN: 100, + DESCR_LEN: 200, + SPONSORED_BY_LEN: 50, + IMG_MIN: 150, + ICON_MIN: 50, +}; +const DEFAULT_MIMES = ['video/mp4', 'video/webm', 'application/x-shockwave-flash', 'application/javascript']; +const VIDEO_TARGETING = ['mimes', 'skippable', 'playback_method', 'protocols', 'api']; +const DEFAULT_PROTOCOLS = [2, 3, 5, 6]; +const DEFAULT_APIS = [1, 2]; + +export const spec = { + + code: 'windtalker', + supportedMediaTypes: ['banner', 'native', 'video'], + + isBidRequestValid: bid => ( + !!(bid && bid.params && bid.params.pubId && bid.params.placementId) + ), + buildRequests: (bidRequests, bidderRequest) => { + const request = { + id: bidRequests[0].bidderRequestId, + at: 2, + imp: bidRequests.map(slot => impression(slot)), + site: site(bidRequests), + app: app(bidRequests), + device: device(bidRequests), + }; + applyGdpr(bidderRequest, request); + return { + method: 'POST', + url: 'https://windtalkerdisplay.hb.adp3.net/', + data: JSON.stringify(request), + }; + }, + interpretResponse: (response, request) => ( + bidResponseAvailable(request, response.body) + ), +}; + +function bidResponseAvailable(bidRequest, bidResponse) { + const idToImpMap = {}; + const idToBidMap = {}; + const ortbRequest = parse(bidRequest.data); + ortbRequest.imp.forEach(imp => { + idToImpMap[imp.id] = imp; + }); + if (bidResponse) { + bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => { + idToBidMap[bid.impid] = bid; + })); + } + const bids = []; + Object.keys(idToImpMap).forEach(id => { + if (idToBidMap[id]) { + const bid = {}; + bid.requestId = id; + bid.adId = id; + bid.creativeId = id; + bid.cpm = idToBidMap[id].price; + bid.currency = bidResponse.cur; + bid.ttl = 360; + bid.netRevenue = true; + if (idToImpMap[id]['native']) { + bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); + let nurl = idToBidMap[id].nurl; + nurl = nurl.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); + bid['native']['impressionTrackers'] = [nurl]; + bid.mediaType = 'native'; + } else if (idToImpMap[id]['video']) { + bid.vastUrl = idToBidMap[id].adm; + bid.vastUrl = bid.vastUrl.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + bid.crid = idToBidMap[id].crid; + bid.width = idToImpMap[id].video.w; + bid.height = idToImpMap[id].video.h; + bid.mediaType = 'video'; + } else if (idToImpMap[id]['banner']) { + bid.ad = idToBidMap[id].adm; + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_AD_ID(%7D|\})/gi, idToBidMap[id].adid); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); + bid.width = idToBidMap[id].w; + bid.height = idToBidMap[id].h; + bid.mediaType = 'banner'; + } + bids.push(bid); + } + }); + return bids; +} +function impression(slot) { + return { + id: slot.bidId, + secure: window.location.protocol === 'https:' ? 1 : 0, + 'banner': banner(slot), + 'native': nativeImpression(slot), + 'video': videoImpression(slot), + bidfloor: slot.params.bidFloor || '0.000001', + tagid: slot.params.placementId.toString(), + }; +} + +function banner(slot) { + if (slot.mediaType === 'banner' || utils.deepAccess(slot, 'mediaTypes.banner')) { + const sizes = utils.deepAccess(slot, 'mediaTypes.banner.sizes'); + if (sizes.length > 1) { + let format = []; + for (let f = 0; f < sizes.length; f++) { + format.push({'w': sizes[f][0], 'h': sizes[f][1]}); + } + return {'format': format}; + } else { + return { + w: sizes[0][0], + h: sizes[0][1] + } + } + } + return null; +} + +function videoImpression(slot) { + if (slot.mediaType === 'video' || utils.deepAccess(slot, 'mediaTypes.video')) { + const sizes = utils.deepAccess(slot, 'mediaTypes.video.playerSize'); + const video = { + w: sizes[0][0], + h: sizes[0][1], + mimes: DEFAULT_MIMES, + protocols: DEFAULT_PROTOCOLS, + api: DEFAULT_APIS, + }; + if (slot.params.video) { + Object.keys(slot.params.video).filter(param => includes(VIDEO_TARGETING, param)).forEach(param => video[param] = slot.params.video[param]); + } + return video; + } + return null; +} + +function nativeImpression(slot) { + if (slot.mediaType === 'native' || utils.deepAccess(slot, 'mediaTypes.native')) { + const assets = []; + addAsset(assets, titleAsset(1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); + addAsset(assets, dataAsset(2, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); + addAsset(assets, dataAsset(3, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); + addAsset(assets, imageAsset(4, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN)); + addAsset(assets, imageAsset(5, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); + return { + request: JSON.stringify({ assets }), + ver: '1.1', + }; + } + return null; +} + +function addAsset(assets, asset) { + if (asset) { + assets.push(asset); + } +} + +function titleAsset(id, params, defaultLen) { + if (params) { + return { + id, + required: params.required ? 1 : 0, + title: { + len: params.len || defaultLen, + }, + }; + } + return null; +} + +function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) { + return params ? { + id, + required: params.required ? 1 : 0, + img: { + type, + wmin: params.wmin || defaultMinWidth, + hmin: params.hmin || defaultMinHeight, + } + } : null; +} + +function dataAsset(id, params, type, defaultLen) { + return params ? { + id, + required: params.required ? 1 : 0, + data: { + type, + len: params.len || defaultLen, + } + } : null; +} + +function site(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.pubId : '0'; + const siteId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.siteId : '0'; + const appParams = bidderRequest[0].params.app; + if (!appParams) { + return { + publisher: { + id: pubId.toString(), + domain: window.location.hostname, + }, + id: siteId.toString(), + ref: window.top.document.referrer, + page: window.location.href, + } + } + return null; +} + +function app(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.pubId : '0'; + const appParams = bidderRequest[0].params.app; + if (appParams) { + return { + publisher: { + id: pubId.toString(), + }, + id: appParams.id, + name: appParams.name, + bundle: appParams.bundle, + storeurl: appParams.storeUrl, + domain: appParams.domain, + } + } + return null; +} + +function device(bidderRequest) { + const lat = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.latitude : ''; + const lon = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.longitude : ''; + const ifa = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.ifa : ''; + return { + dnt: utils.getDNT() ? 1 : 0, + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + w: (window.screen.width || window.innerWidth), + h: (window.screen.height || window.innerHeigh), + geo: { + lat: lat, + lon: lon, + }, + ifa: ifa, + }; +} + +function parse(rawResponse) { + try { + if (rawResponse) { + return JSON.parse(rawResponse); + } + } catch (ex) { + utils.logError('windtalker.parse', 'ERROR', ex); + } + return null; +} + +function applyGdpr(bidderRequest, ortbRequest) { + if (bidderRequest && bidderRequest.gdprConsent) { + ortbRequest.regs = { ext: { gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0 } }; + ortbRequest.user = { ext: { consent: bidderRequest.gdprConsent.consentString } }; + } +} + +function nativeResponse(imp, bid) { + if (imp['native']) { + const nativeAd = parse(bid.adm); + const keys = {}; + keys.image = {}; + keys.icon = {}; + if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) { + nativeAd['native'].assets.forEach(asset => { + keys.title = asset.title ? asset.title.text : keys.title; + keys.body = asset.data && asset.id === 2 ? asset.data.value : keys.body; + keys.sponsoredBy = asset.data && asset.id === 3 ? asset.data.value : keys.sponsoredBy; + keys.icon.url = asset.img && asset.id === 4 ? asset.img.url : keys.icon.url; + keys.icon.width = asset.img && asset.id === 4 ? asset.img.w : keys.icon.width; + keys.icon.height = asset.img && asset.id === 4 ? asset.img.h : keys.icon.height; + keys.image.url = asset.img && asset.id === 5 ? asset.img.url : keys.image.url; + keys.image.width = asset.img && asset.id === 5 ? asset.img.w : keys.image.width; + keys.image.height = asset.img && asset.id === 5 ? asset.img.h : keys.image.height; + }); + if (nativeAd['native'].link) { + keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url); + } + return keys; + } + } + return null; +} + +registerBidder(spec); diff --git a/modules/windtalkerBidAdapter.md b/modules/windtalkerBidAdapter.md new file mode 100644 index 00000000000..f7441effc47 --- /dev/null +++ b/modules/windtalkerBidAdapter.md @@ -0,0 +1,86 @@ +# Overview + +**Module Name**: Windtalker Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: corbin@windtalker.io + +# Description + +Connects to Windtalker demand source to fetch bids. +Banner, Native, Video formats are supported. +Please use ```windtalker``` as the bidder code. + +# Test Parameters +``` + var adUnits = [{ + code: 'dfp-native-div', + mediaTypes: { + native: { + title: { + required: true, + len: 75 + }, + image: { + required: true + }, + body: { + len: 200 + }, + icon: { + required: false + } + } + }, + bids: [{ + bidder: 'windtalker', + params: { + pubId: '584971', + siteId: '584971', + placementId: '123', + bidFloor: '0.001', // optional + ifa: 'XXX-XXX', // optional + latitude: '40.712775', // optional + longitude: '-74.005973', // optional + } + }] + }, + { + code: 'dfp-banner-div', + mediaTypes: { + banner: { + sizes: [ + [300, 250],[300,600] + ], + } + }, + bids: [{ + bidder: 'windtalker', + params: { + pubId: '584971', + siteId: '584971', + placementId: '123', + } + }] + }, + { + code: 'dfp-video-div', + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: "instream" + } + }, + bids: [{ + bidder: 'windtalker', + params: { + pubId: '584971', + siteId: '584971', + placementId: '123', + video: { + skipppable: true, + } + } + }] + } + ]; +``` diff --git a/modules/wipesBidAdapter.js b/modules/wipesBidAdapter.js new file mode 100644 index 00000000000..5a2c860130f --- /dev/null +++ b/modules/wipesBidAdapter.js @@ -0,0 +1,71 @@ +import * as utils from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'wipes'; +const ALIAS_BIDDER_CODE = ['wi']; +const SUPPORTED_MEDIA_TYPES = [BANNER] +const ENDPOINT_URL = 'https://adn-srv.reckoner-api.com/v1/prebid'; + +function isBidRequestValid(bid) { + switch (true) { + case !!(bid.params.asid): + break; + default: + utils.logWarn(`isBidRequestValid Error. ${bid.params}, please check your implementation.`); + return false; + } + return true; +} + +function buildRequests(validBidRequests, bidderRequest) { + return validBidRequests.map(bidRequest => { + const bidId = bidRequest.bidId + const params = bidRequest.params; + const asid = params.asid; + return { + method: 'GET', + url: ENDPOINT_URL, + data: { + asid: asid, + bid_id: bidId, + } + } + }); +} + +function interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; + const response = serverResponse.body; + const cpm = response.cpm * 1000 || 0; + if (cpm !== 0) { + const netRevenue = (response.netRevenue === undefined) ? true : response.netRevenue; + const bidResponse = { + requestId: response.bid_id, + cpm: cpm, + width: response.width, + height: response.height, + creativeId: response.video_creative_id || 0, + dealId: response.deal_id, + currency: 'JPY', + netRevenue: netRevenue, + ttl: config.getConfig('_bidderTimeout'), + referrer: bidRequest.data.r || '', + mediaType: BANNER, + ad: response.ad_tag, + }; + bidResponses.push(bidResponse); + } + return bidResponses; +} + +export const spec = { + code: BIDDER_CODE, + aliases: ALIAS_BIDDER_CODE, + isBidRequestValid, + buildRequests, + interpretResponse, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES +} +registerBidder(spec); diff --git a/modules/wipesBidAdapter.md b/modules/wipesBidAdapter.md new file mode 100644 index 00000000000..712bf0b15d8 --- /dev/null +++ b/modules/wipesBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: WIPES Bidder Adapter +Module Type: Bidder Adapter +Maintainer: contact@3-shake.com +``` + +# Description + +Connect to WIPES for bids. +Publishers needs to be set up and approved by WIPES team to enable this adapter. +Please contact support@wipestream.com for further information. + +# Test Parameters +```javascript +var adUnits = [ + // adUnit + { + code: 'video-div', + mediaTypes: { + banner: { + sizes: [[160, 300]], + } + }, + bids: [{ + bidder: 'wipes', + params: { + asid: 'dWyPondh2EGB_bNlrVjzIXRZO9F0k1dpo0I8ZvQ' + } + }] + }] +``` diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index ca07449f12e..e1e491cdce9 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -39,6 +39,9 @@ export const spec = { if (bid.params.targeting) { query.t = createQueryString(bid.params.targeting) } + if (bid.userIdAsEids && Array.isArray(bid.userIdAsEids)) { + query.ids = createUserIdString(bid.userIdAsEids) + } }) if (bidderRequest && bidderRequest.gdprConsent) { @@ -162,6 +165,19 @@ function parseSize (size) { return size.split('x').map(Number) } +/** + * Creates a string out of an array of eids with source and uid + * @param {Array} eids + * @returns {String} + */ +function createUserIdString (eids) { + let str = [] + for (let i = 0; i < eids.length; i++) { + str.push(eids[i].source + ':' + eids[i].uids[0].id) + } + return str.join(',') +} + /** * Creates a querystring out of an object with key-values * @param {Object} obj diff --git a/modules/yieldliftBidAdapter.js b/modules/yieldliftBidAdapter.js new file mode 100644 index 00000000000..87a598bd335 --- /dev/null +++ b/modules/yieldliftBidAdapter.js @@ -0,0 +1,144 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const ENDPOINT_URL = 'https://x.yieldlift.com/auction'; + +const DEFAULT_BID_TTL = 30; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; + +export const spec = { + code: 'yieldlift', + aliases: ['yl'], + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return (!!bid.params.unitId && typeof bid.params.unitId === 'string') || + (!!bid.params.networkId && typeof bid.params.networkId === 'string') || + (!!bid.params.publisherId && typeof bid.params.publisherId === 'string'); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + if (!validBidRequests || !bidderRequest) { + return; + } + const publisherId = validBidRequests[0].params.publisherId; + const networkId = validBidRequests[0].params.networkId; + const impressions = validBidRequests.map(bidRequest => ({ + id: bidRequest.bidId, + banner: { + format: bidRequest.mediaTypes.banner.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1] + })) + }, + ext: { + exchange: { + unitId: bidRequest.params.unitId + } + } + })); + + const openrtbRequest = { + id: bidderRequest.auctionId, + imp: impressions, + site: { + domain: window.location.hostname, + page: window.location.href, + ref: bidderRequest.refererInfo ? bidderRequest.refererInfo.referer || null : null + }, + ext: { + exchange: { + publisherId: publisherId, + networkId: networkId, + } + } + }; + + // adding schain object + if (validBidRequests[0].schain) { + utils.deepSetValue(openrtbRequest, 'source.ext.schain', validBidRequests[0].schain); + } + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + utils.deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + utils.deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + utils.deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + const payloadString = JSON.stringify(openrtbRequest); + return { + method: 'POST', + url: ENDPOINT_URL, + data: payloadString, + }; + }, + + interpretResponse: function (serverResponse) { + const bidResponses = []; + const response = (serverResponse || {}).body; + // response is always one seat (exchange) with (optional) bids for each impression + if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) { + response.seatbid[0].bid.forEach(bid => { + bidResponses.push({ + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + ad: bid.adm, + ttl: DEFAULT_BID_TTL, + creativeId: bid.crid, + netRevenue: DEFAULT_NET_REVENUE, + currency: DEFAULT_CURRENCY, + }) + }) + } else { + utils.logInfo('yieldlift.interpretResponse :: no valid responses to interpret'); + } + return bidResponses; + }, + getUserSyncs: function (syncOptions, serverResponses) { + utils.logInfo('yieldlift.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses); + let syncs = []; + + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + return syncs; + } + + serverResponses.forEach(resp => { + const userSync = utils.deepAccess(resp, 'body.ext.usersync'); + if (userSync) { + let syncDetails = []; + Object.keys(userSync).forEach(key => { + const value = userSync[key]; + if (value.syncs && value.syncs.length) { + syncDetails = syncDetails.concat(value.syncs); + } + }); + syncDetails.forEach(syncDetails => { + syncs.push({ + type: syncDetails.type === 'iframe' ? 'iframe' : 'image', + url: syncDetails.url + }); + }); + + if (!syncOptions.iframeEnabled) { + syncs = syncs.filter(s => s.type !== 'iframe') + } + if (!syncOptions.pixelEnabled) { + syncs = syncs.filter(s => s.type !== 'image') + } + } + }); + utils.logInfo('yieldlift.getUserSyncs result=%o', syncs); + return syncs; + }, + +}; +registerBidder(spec); diff --git a/modules/yieldliftBidAdapter.md b/modules/yieldliftBidAdapter.md new file mode 100644 index 00000000000..96b5de1b618 --- /dev/null +++ b/modules/yieldliftBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +``` +Module Name: YieldLift Bid Adapter +Module Type: Bidder Adapter +Maintainer: info@yieldlift.com +``` + +# Description + +Module that connects to YieldLift's demand sources + +# Test Parameters +``` +var adUnits = [ + { + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'yieldlift', + params: { + unitId: 'test' + } + }] + } +]; +``` diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 7a33dd0db91..5ef146cb2d0 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -13,10 +13,10 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: ['banner'], /** - * Determines whether or not the given bid request is valid. - * @param {object} bid, bid to validate - * @return boolean, true if valid, otherwise false - */ + * Determines whether or not the given bid request is valid. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ isBidRequestValid: function(bid) { return !!(bid && bid.adUnitCode && bid.bidId); }, @@ -41,13 +41,19 @@ export const spec = { h: localWindow.innerHeight, userConsent: JSON.stringify({ // case of undefined, stringify will remove param - gdprApplies: bidderRequest && bidderRequest.gdprConsent ? bidderRequest.gdprConsent.gdprApplies : undefined, - cmp: bidderRequest && bidderRequest.gdprConsent ? bidderRequest.gdprConsent.consentString : undefined + gdprApplies: + bidderRequest && bidderRequest.gdprConsent + ? bidderRequest.gdprConsent.gdprApplies + : undefined, + cmp: + bidderRequest && bidderRequest.gdprConsent + ? bidderRequest.gdprConsent.consentString + : undefined }), - us_privacy: bidderRequest && bidderRequest.us_privacy, + us_privacy: bidderRequest && bidderRequest.us_privacy }; - bidRequests.forEach((request) => { + bidRequests.forEach(request => { serverRequest.p.push(addPlacement(request)); const pubcid = getId(request, 'pubcid'); if (pubcid) { @@ -61,6 +67,10 @@ export const spec = { if (tdid) { serverRequest.tdid = tdid; } + const criteoId = getId(request, 'criteoId'); + if (criteoId) { + serverRequest.cri_prebid = criteoId; + } if (request.schain) { serverRequest.schain = JSON.stringify(request.schain); } @@ -70,7 +80,7 @@ export const spec = { method: 'GET', url: SERVER_ENDPOINT, data: serverRequest - } + }; }, /** * Makes Yieldmo Ad Server response compatible to Prebid specs @@ -81,7 +91,7 @@ export const spec = { let bids = []; let data = serverResponse.body; if (data.length > 0) { - data.forEach((response) => { + data.forEach(response => { if (response.cpm && response.cpm > 0) { bids.push(createNewBid(response)); } @@ -91,15 +101,17 @@ export const spec = { }, getUserSync: function(syncOptions) { if (trackingEnabled(syncOptions)) { - return [{ - type: 'iframe', - url: SYNC_ENDPOINT + utils.getOrigin() - }]; + return [ + { + type: 'iframe', + url: SYNC_ENDPOINT + utils.getOrigin() + } + ]; } else { return []; } } -} +}; registerBidder(spec); /*************************************** @@ -115,7 +127,7 @@ function addPlacement(request) { placement_id: request.adUnitCode, callback_id: request.bidId, sizes: request.mediaTypes.banner.sizes - } + }; if (request.params) { if (request.params.placementId) { placementInfo.ym_placement_id = request.params.placementId; @@ -128,9 +140,9 @@ function addPlacement(request) { } /** - * creates a new bid with response information - * @param response server response - */ + * creates a new bid with response information + * @param response server response + */ function createNewBid(response) { return { requestId: response['callback_id'], @@ -150,23 +162,25 @@ function createNewBid(response) { * @returns false if dnt or if not iframe/pixel enabled */ function trackingEnabled(options) { - return (isIOS() && !getDNT() && options.iframeEnabled); + return isIOS() && !getDNT() && options.iframeEnabled; } /** - * Detects whether we're in iOS - * @returns true if in iOS - */ + * Detects whether we're in iOS + * @returns true if in iOS + */ function isIOS() { return /iPhone|iPad|iPod/i.test(window.navigator.userAgent); } /** - * Detects whether dnt is true - * @returns true if user enabled dnt - */ + * Detects whether dnt is true + * @returns true if user enabled dnt + */ function getDNT() { - return window.doNotTrack === '1' || window.navigator.doNotTrack === '1' || false; + return ( + window.doNotTrack === '1' || window.navigator.doNotTrack === '1' || false + ); } /** @@ -174,7 +188,9 @@ function getDNT() { */ function getPageDescription() { if (document.querySelector('meta[name="description"]')) { - return document.querySelector('meta[name="description"]').getAttribute('content'); // Value of the description metadata from the publisher's page. + return document + .querySelector('meta[name="description"]') + .getAttribute('content'); // Value of the description metadata from the publisher's page. } else { return ''; } @@ -200,9 +216,9 @@ function getPageDescription() { */ /** - * Detects what environment we're in - * @returns Environment kind - */ + * Detects what environment we're in + * @returns Environment kind + */ function getEnvironment() { if (isSuperSandboxedIframe()) { return 89; @@ -228,28 +244,31 @@ function getEnvironment() { } /** - * @returns true if we are running on the top window at dispatch time - */ + * @returns true if we are running on the top window at dispatch time + */ function isCodeOnPage() { return window === window.parent; } /** - * @returns true if the environment is both DFP and AMP - */ + * @returns true if the environment is both DFP and AMP + */ function isDfpInAmp() { return isDfp() && isAmp(); } /** - * @returns true if the window is in an iframe whose id and parent element id match DFP - */ + * @returns true if the window is in an iframe whose id and parent element id match DFP + */ function isDfp() { try { const frameElement = window.frameElement; const parentElement = window.frameElement.parentNode; if (frameElement && parentElement) { - return frameElement.id.indexOf('google_ads_iframe') > -1 && parentElement.id.indexOf('google_ads_iframe') > -1; + return ( + frameElement.id.indexOf('google_ads_iframe') > -1 && + parentElement.id.indexOf('google_ads_iframe') > -1 + ); } return false; } catch (e) { @@ -258,8 +277,8 @@ function isDfp() { } /** -* @returns true if there is an AMP context object -*/ + * @returns true if there is an AMP context object + */ function isAmp() { try { const ampContext = window.context || window.parent.context; @@ -285,7 +304,11 @@ function isSafeFrame() { function isDFPSafeFrame() { if (window.location && window.location.href) { const href = window.location.href; - return isSafeFrame() && href.indexOf('google') !== -1 && href.indexOf('safeframe') !== -1; + return ( + isSafeFrame() && + href.indexOf('google') !== -1 && + href.indexOf('safeframe') !== -1 + ); } return false; } @@ -318,7 +341,7 @@ function isSuperSandboxedIframe() { * @returns true if the window has the attribute identifying MRAID */ function isMraid() { - return !!(window.mraid); + return !!window.mraid; } /** @@ -329,7 +352,12 @@ function isMraid() { */ function getId(request, idType) { let id; - if (request && request.userId && request.userId[idType] && typeof request.userId === 'object') { + if ( + request && + request.userId && + request.userId[idType] && + typeof request.userId === 'object' + ) { id = request.userId[idType]; } return id; diff --git a/modules/yieldmoBidAdapter.md b/modules/yieldmoBidAdapter.md index ef5f07a4eb9..0f86d2507d1 100644 --- a/modules/yieldmoBidAdapter.md +++ b/modules/yieldmoBidAdapter.md @@ -4,6 +4,7 @@ Module Name: Yieldmo Bid Adapter Module Type: Bidder Adapter Maintainer: opensource@yieldmo.com +Note: Our ads will only render in mobile ``` # Description diff --git a/modules/yuktamediaAnalyticsAdapter.js b/modules/yuktamediaAnalyticsAdapter.js index 268f31ea402..b346a26c843 100644 --- a/modules/yuktamediaAnalyticsAdapter.js +++ b/modules/yuktamediaAnalyticsAdapter.js @@ -2,7 +2,6 @@ import { ajax } from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import * as url from '../src/url.js'; import * as utils from '../src/utils.js'; const emptyUrl = ''; @@ -115,7 +114,7 @@ function send(data, status) { } data.initOptions = initOptions; - let yuktamediaAnalyticsRequestUrl = url.format({ + let yuktamediaAnalyticsRequestUrl = utils.buildUrl({ protocol: 'https', hostname: 'analytics-prebid.yuktamedia.com', pathname: status == 'auctionEnd' ? '/api/bids' : '/api/bid/won', diff --git a/package-lock.json b/package-lock.json index 2a9d671e441..0a7b276db79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.12.0", + "version": "3.17.0-pre", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json old mode 100755 new mode 100644 index fb8d864727e..d33d08034f2 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "3.13.0-pre", + "version": "3.12.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { diff --git a/src/adapterManager.js b/src/adapterManager.js index 9886934ab47..f658a36aeda 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -368,19 +368,25 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request request: requestCallbacks.request.bind(null, bidRequest.bidderCode), done: requestCallbacks.done } : undefined); - config.runWithBidder( - bidRequest.bidderCode, - bind.call( - adapter.callBids, - adapter, - bidRequest, - addBidResponse.bind(bidRequest), - doneCb.bind(bidRequest), - ajax, - onTimelyResponse, - config.callbackWithBidder(bidRequest.bidderCode) - ) - ); + const adapterDone = doneCb.bind(bidRequest); + try { + config.runWithBidder( + bidRequest.bidderCode, + bind.call( + adapter.callBids, + adapter, + bidRequest, + addBidResponse.bind(bidRequest), + adapterDone, + ajax, + onTimelyResponse, + config.callbackWithBidder(bidRequest.bidderCode) + ) + ); + } catch (e) { + utils.logError(`${bidRequest.bidderCode} Bid Adapter emitted an uncaught error when parsing their bidRequest`, {e, bidRequest}); + adapterDone(); + } }); }; diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 64bfe27c0c5..af3ab5ed8c1 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -9,9 +9,12 @@ import CONSTANTS from '../constants.json'; import events from '../events.js'; import includes from 'core-js/library/fn/array/includes.js'; import { ajax } from '../ajax.js'; -import { logWarn, logError, parseQueryStringParameters, delayExecution, parseSizesInput, getBidderRequest, flatten, uniques, timestamp, setDataInLocalStorage, getDataFromLocalStorage, deepAccess, isArray } from '../utils.js'; +import { logWarn, logError, parseQueryStringParameters, delayExecution, parseSizesInput, getBidderRequest, flatten, uniques, timestamp, deepAccess, isArray } from '../utils.js'; import { ADPOD } from '../mediaTypes.js'; -import { getHook } from '../hook.js'; +import { getHook, hook } from '../hook.js'; +import { getCoreStorageManager } from '../storageManager.js'; + +export const storage = getCoreStorageManager('bidderFactory'); /** * This file aims to support Adapters during the Prebid 0.x -> 1.x transition. @@ -331,21 +334,7 @@ export function newBidder(spec) { }); function registerSyncs(responses, gdprConsent, uspConsent) { - if (spec.getUserSyncs && !adapterManager.aliasRegistry[spec.code]) { - let filterConfig = config.getConfig('userSync.filterSettings'); - let syncs = spec.getUserSyncs({ - iframeEnabled: !!(filterConfig && (filterConfig.iframe || filterConfig.all)), - pixelEnabled: !!(filterConfig && (filterConfig.image || filterConfig.all)), - }, responses, gdprConsent, uspConsent); - if (syncs) { - if (!Array.isArray(syncs)) { - syncs = [syncs]; - } - syncs.forEach((sync) => { - userSync.registerSync(sync.type, spec.code, sync.url) - }); - } - } + registerSyncInner(spec, responses, gdprConsent, uspConsent); } function filterAndWarn(bid) { @@ -357,6 +346,25 @@ export function newBidder(spec) { } } +export const registerSyncInner = hook('async', function(spec, responses, gdprConsent, uspConsent) { + const aliasSyncEnabled = config.getConfig('userSync.aliasSyncEnabled'); + if (spec.getUserSyncs && (aliasSyncEnabled || !adapterManager.aliasRegistry[spec.code])) { + let filterConfig = config.getConfig('userSync.filterSettings'); + let syncs = spec.getUserSyncs({ + iframeEnabled: !!(filterConfig && (filterConfig.iframe || filterConfig.all)), + pixelEnabled: !!(filterConfig && (filterConfig.image || filterConfig.all)), + }, responses, gdprConsent, uspConsent); + if (syncs) { + if (!Array.isArray(syncs)) { + syncs = [syncs]; + } + syncs.forEach((sync) => { + userSync.registerSync(sync.type, spec.code, sync.url) + }); + } + } +}, 'registerSyncs') + export function preloadBidderMappingFile(fn, adUnits) { if (!config.getConfig('adpod.brandCategoryExclusion')) { return fn.call(this, adUnits); @@ -373,7 +381,7 @@ export function preloadBidderMappingFile(fn, adUnits) { let info = bidderSpec.getSpec().getMappingFileInfo(); let refreshInDays = (info.refreshInDays) ? info.refreshInDays : DEFAULT_REFRESHIN_DAYS; let key = (info.localStorageKey) ? info.localStorageKey : bidderSpec.getSpec().code; - let mappingData = getDataFromLocalStorage(key); + let mappingData = storage.getDataFromLocalStorage(key); if (!mappingData || timestamp() < mappingData.lastUpdated + refreshInDays * 24 * 60 * 60 * 1000) { ajax(info.url, { @@ -384,7 +392,7 @@ export function preloadBidderMappingFile(fn, adUnits) { lastUpdated: timestamp(), mapping: response.mapping } - setDataInLocalStorage(key, JSON.stringify(mapping)); + storage.setDataInLocalStorage(key, JSON.stringify(mapping)); } catch (error) { logError(`Failed to parse ${bidder} bidder translation mapping file`); } @@ -412,7 +420,7 @@ export function getIabSubCategory(bidderCode, category) { if (bidderSpec.getSpec().getMappingFileInfo) { let info = bidderSpec.getSpec().getMappingFileInfo(); let key = (info.localStorageKey) ? info.localStorageKey : bidderSpec.getBidderCode(); - let data = getDataFromLocalStorage(key); + let data = storage.getDataFromLocalStorage(key); if (data) { try { data = JSON.parse(data); diff --git a/src/adserver.js b/src/adserver.js index c189037a9da..61af8862972 100644 --- a/src/adserver.js +++ b/src/adserver.js @@ -1,4 +1,4 @@ -import { formatQS } from './url.js'; +import { formatQS } from './utils.js'; import { targeting } from './targeting.js'; // Adserver parent class diff --git a/src/ajax.js b/src/ajax.js index 16f7b365092..b1e4cbdbdff 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -1,4 +1,3 @@ -import {parse as parseURL, format as formatURL} from './url.js'; import { config } from './config.js'; var utils = require('./utils.js'); @@ -61,9 +60,9 @@ export function ajaxBuilder(timeout = 3000, {request, done} = {}) { } if (method === 'GET' && data) { - let urlInfo = parseURL(url, options); + let urlInfo = utils.parseUrl(url, options); Object.assign(urlInfo.search, data); - url = formatURL(urlInfo); + url = utils.buildUrl(urlInfo); } x.open(method, url, true); diff --git a/src/auction.js b/src/auction.js index 628c164a537..d953763cea8 100644 --- a/src/auction.js +++ b/src/auction.js @@ -57,8 +57,7 @@ * @property {function(): void} callBids - sends requests to all adapters for bids */ -import {flatten, timestamp, adUnitsFilter, deepAccess, getBidRequest, getValue} from './utils.js'; -import { parse as parseURL } from './url.js'; +import {flatten, timestamp, adUnitsFilter, deepAccess, getBidRequest, getValue, parseUrl} from './utils.js'; import { getPriceBucketString } from './cpmBucketManager.js'; import { getNativeTargeting } from './native.js'; import { getCacheUrl, store } from './videoCache.js'; @@ -651,7 +650,7 @@ export function getStandardBidderSettings(mediaType, bidderCode, bidReq) { // Adding hb_cache_host if (config.getConfig('cache.url') && (!bidderCode || utils.deepAccess(bidderSettings, `${bidderCode}.sendStandardTargeting`) !== false)) { - const urlInfo = parseURL(config.getConfig('cache.url')); + const urlInfo = parseUrl(config.getConfig('cache.url')); if (typeof find(adserverTargeting, targetingKeyVal => targetingKeyVal.key === TARGETING_KEYS.CACHE_HOST) === 'undefined') { adserverTargeting.push(createKeyVal(TARGETING_KEYS.CACHE_HOST, function(bidResponse) { diff --git a/src/config.js b/src/config.js index 8484b87b77c..af0fd12de88 100644 --- a/src/config.js +++ b/src/config.js @@ -16,13 +16,13 @@ import { isValidPriceConfig } from './cpmBucketManager.js'; import find from 'core-js/library/fn/array/find.js'; import includes from 'core-js/library/fn/array/includes.js'; import Set from 'core-js/library/fn/set.js'; -import { parseQS } from './url.js'; +import { mergeDeep } from './utils.js'; const from = require('core-js/library/fn/array/from.js'); const utils = require('./utils.js'); const CONSTANTS = require('./constants.json'); -const DEFAULT_DEBUG = (parseQS(window.location.search)[CONSTANTS.DEBUG_MODE] || '').toUpperCase() === 'TRUE'; +const DEFAULT_DEBUG = utils.getParameterByName(CONSTANTS.DEBUG_MODE).toUpperCase() === 'TRUE'; const DEFAULT_BIDDER_TIMEOUT = 3000; const DEFAULT_PUBLISHER_DOMAIN = window.location.origin; const DEFAULT_ENABLE_SEND_ALL_BIDS = true; @@ -255,7 +255,7 @@ export function newConfig() { memo[topic] = currBidderConfig[topic]; } else { if (utils.isPlainObject(currBidderConfig[topic])) { - memo[topic] = Object.assign({}, config[topic], currBidderConfig[topic]); + memo[topic] = mergeDeep({}, config[topic], currBidderConfig[topic]); } else { memo[topic] = currBidderConfig[topic]; } @@ -450,9 +450,14 @@ export function newConfig() { } } + function getCurrentBidder() { + return currBidder; + } + resetConfig(); return { + getCurrentBidder, getConfig, setConfig, setDefaults, diff --git a/src/constants.json b/src/constants.json index 0b6b144a336..7ffef9db1aa 100644 --- a/src/constants.json +++ b/src/constants.json @@ -95,7 +95,8 @@ }, "BID_STATUS" : { "BID_TARGETING_SET": "targetingSet", - "RENDERED": "rendered" + "RENDERED": "rendered", + "BID_REJECTED": "bidRejected" }, "SUBMODULES_THAT_ALWAYS_REFRESH_ID": { "parrableId": true diff --git a/src/events.js b/src/events.js index 2d99ed4c561..e7a11635476 100644 --- a/src/events.js +++ b/src/events.js @@ -113,14 +113,14 @@ module.exports = (function () { utils._each(event[id].que, function (_handler) { var que = event[id].que; if (_handler === handler) { - que.splice(utils.indexOf.call(que, _handler), 1); + que.splice(que.indexOf(_handler), 1); } }); } else { utils._each(event.que, function (_handler) { var que = event.que; if (_handler === handler) { - que.splice(utils.indexOf.call(que, _handler), 1); + que.splice(que.indexOf(_handler), 1); } }); } diff --git a/src/prebid.js b/src/prebid.js index aa2d2493e7b..f45a15d5f19 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -13,6 +13,7 @@ import includes from 'core-js/library/fn/array/includes.js'; import { adunitCounter } from './adUnits.js'; import { isRendererRequired, executeRenderer } from './Renderer.js'; import { createBid } from './bidfactory.js'; +import { storageCallbacks } from './storageManager.js'; const $$PREBID_GLOBAL$$ = getGlobal(); const CONSTANTS = require('./constants.json'); @@ -277,7 +278,7 @@ $$PREBID_GLOBAL$$.setTargetingForGPTAsync = function (adUnit, customSlotMatching let targetingSet = targeting.getAllTargeting(adUnit); // first reset any old targeting - targeting.resetPresetTargeting(adUnit); + targeting.resetPresetTargeting(adUnit, customSlotMatching); // now set new targeting keys targeting.setTargetingForGPT(targetingSet, customSlotMatching); @@ -520,9 +521,22 @@ $$PREBID_GLOBAL$$.requestBids = hook('async', function ({ bidsBackHandler, timeo adUnitCodes.forEach(code => targeting.setLatestAuctionForAdUnit(code, auction.getAuctionId())); auction.callBids(); - return auction; }); +export function executeStorageCallbacks(fn, reqBidsConfigObj) { + runAll(storageCallbacks); + fn.call(this, reqBidsConfigObj); + function runAll(queue) { + var queued; + while ((queued = queue.shift())) { + queued(); + } + } +} + +// This hook will execute all storage callbacks which were registered before gdpr enforcement hook was added. Some bidders, user id modules use storage functions when module is parsed but gdpr enforcement hook is not added at that stage as setConfig callbacks are yet to be called. Hence for such calls we execute all the stored callbacks just before requestBids. At this hook point we will know for sure that gdprEnforcement module is added or not +$$PREBID_GLOBAL$$.requestBids.before(executeStorageCallbacks, 49); + /** * * Add adunit(s) diff --git a/src/storageManager.js b/src/storageManager.js new file mode 100644 index 00000000000..75ad10908dc --- /dev/null +++ b/src/storageManager.js @@ -0,0 +1,311 @@ +import { hook } from './hook.js'; +import * as utils from './utils.js'; +import includes from 'core-js/library/fn/array/includes.js'; + +const moduleTypeWhiteList = ['core', 'prebid-module']; + +export let storageCallbacks = []; + +/** + * Storage options + * @typedef {Object} storageOptions + * @property {Number=} gvlid - Vendor id + * @property {string} moduleName - Module name + * @property {string=} moduleType - Module type, value can be anyone of core or prebid-module + */ + +/** + * Returns list of storage related functions with gvlid, module name and module type in its scope. + * All three argument are optional here. Below shows the usage of of these + * - GVL Id: Pass GVL id if you are a vendor + * - Module name: All modules need to pass module name + * - Module type: Some modules may need these functions but are not vendor. e.g prebid core files in src and modules like currency. + * @param {storageOptions} options + */ +export function newStorageManager({gvlid, moduleName, moduleType} = {}) { + function isValid(cb) { + if (includes(moduleTypeWhiteList, moduleType)) { + let result = { + valid: true + } + return cb(result); + } else { + let value; + let hookDetails = { + hasEnforcementHook: false + } + validateStorageEnforcement(gvlid, moduleName, hookDetails, function(result) { + if (result && result.hasEnforcementHook) { + value = cb(result); + } else { + let result = { + hasEnforcementHook: false, + valid: utils.hasDeviceAccess() + } + value = cb(result); + } + }); + return value; + } + } + + /** + * @param {string} key + * @param {string} value + * @param {string} [expires=''] + * @param {string} [sameSite='/'] + * @param {string} [domain] domain (e.g., 'example.com' or 'subdomain.example.com'). + * If not specified, defaults to the host portion of the current document location. + * If a domain is specified, subdomains are always included. + * Domain must match the domain of the JavaScript origin. Setting cookies to foreign domains will be silently ignored. + */ + const setCookie = function (key, value, expires, sameSite, domain, done) { + let cb = function (result) { + if (result && result.valid) { + const domainPortion = (domain && domain !== '') ? ` ;domain=${encodeURIComponent(domain)}` : ''; + const expiresPortion = (expires && expires !== '') ? ` ;expires=${expires}` : ''; + const isNone = (sameSite != null && sameSite.toLowerCase() == 'none') + const secure = (isNone) ? '; Secure' : ''; + document.cookie = `${key}=${encodeURIComponent(value)}${expiresPortion}; path=/${domainPortion}${sameSite ? `; SameSite=${sameSite}` : ''}${secure}`; + } + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + }; + + /** + * @param {string} name + * @returns {(string|null)} + */ + const getCookie = function(name, done) { + let cb = function (result) { + if (result && result.valid) { + let m = window.document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]*)\\s*(;|$)'); + return m ? decodeURIComponent(m[2]) : null; + } + return null; + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + }; + + /** + * @returns {boolean} + */ + const localStorageIsEnabled = function (done) { + let cb = function (result) { + if (result && result.valid) { + try { + localStorage.setItem('prebid.cookieTest', '1'); + return localStorage.getItem('prebid.cookieTest') === '1'; + } catch (error) {} + } + return false; + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * @returns {boolean} + */ + const cookiesAreEnabled = function (done) { + let cb = function (result) { + if (result && result.valid) { + if (utils.checkCookieSupport()) { + return true; + } + window.document.cookie = 'prebid.cookieTest'; + return window.document.cookie.indexOf('prebid.cookieTest') !== -1; + } + return false; + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * @param {string} key + * @param {string} value + */ + const setDataInLocalStorage = function (key, value, done) { + let cb = function (result) { + if (result && result.valid) { + window.localStorage.setItem(key, value); + } + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * @param {string} key + * @returns {(string|null)} + */ + const getDataFromLocalStorage = function (key, done) { + let cb = function (result) { + if (result && result.valid) { + return window.localStorage.getItem(key); + } + return null; + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * @param {string} key + */ + const removeDataFromLocalStorage = function (key, done) { + let cb = function (result) { + if (result && result.valid) { + window.localStorage.removeItem(key); + } + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * @returns {boolean} + */ + const hasLocalStorage = function (done) { + let cb = function (result) { + if (result && result.valid) { + try { + return !!window.localStorage; + } catch (e) { + utils.logError('Local storage api disabled'); + } + } + return false; + } + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + /** + * Returns all cookie values from the jar whose names contain the `keyLike` + * Needs to exist in `utils.js` as it follows the StorageHandler interface defined in live-connect-js. If that module were to be removed, this function can go as well. + * @param {string} keyLike + * @return {[]} + */ + const findSimilarCookies = function(keyLike, done) { + let cb = function (result) { + if (result && result.valid) { + const all = []; + if (utils.hasDeviceAccess()) { + const cookies = document.cookie.split(';'); + while (cookies.length) { + const cookie = cookies.pop(); + let separatorIndex = cookie.indexOf('='); + separatorIndex = separatorIndex < 0 ? cookie.length : separatorIndex; + const cookieName = decodeURIComponent(cookie.slice(0, separatorIndex).replace(/^\s+/, '')); + if (cookieName.indexOf(keyLike) >= 0) { + all.push(decodeURIComponent(cookie.slice(separatorIndex + 1))); + } + } + } + return all; + } + } + + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(cb); + done(result); + }); + } else { + return isValid(cb); + } + } + + return { + setCookie, + getCookie, + localStorageIsEnabled, + cookiesAreEnabled, + setDataInLocalStorage, + getDataFromLocalStorage, + removeDataFromLocalStorage, + hasLocalStorage, + findSimilarCookies + } +} + +/** + * This hook validates the storage enforcement if gdprEnforcement module is included + */ +export const validateStorageEnforcement = hook('async', function(gvlid, moduleName, hookDetails, callback) { + callback(hookDetails); +}, 'validateStorageEnforcement'); + +/** + * This function returns storage functions to access cookies and localstorage. This function will bypass the gdpr enforcement requirement. Prebid as a software needs to use storage in some scenarios and is not a vendor so GDPR enforcement rules does not apply on Prebid. + * @param {string} moduleName Module name + */ +export function getCoreStorageManager(moduleName) { + return newStorageManager({moduleName: moduleName, moduleType: 'core'}); +} + +/** + * Note: Core modules or Prebid modules like Currency, SizeMapping should use getCoreStorageManager + * This function returns storage functions to access cookies and localstorage. Bidders and User id modules should import this and use it in their module if needed. GVL ID and Module name are optional param but gvl id is needed for when gdpr enforcement module is used. + * @param {Number=} gvlid Vendor id + * @param {string=} moduleName BidderCode or module name + */ +export function getStorageManager(gvlid, moduleName) { + return newStorageManager({gvlid: gvlid, moduleName: moduleName}); +} + +export function resetData() { + storageCallbacks = []; +} diff --git a/src/targeting.js b/src/targeting.js index d960d800a25..f843dd13a6a 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -110,16 +110,18 @@ export function newTargeting(auctionManager) { latestAuctionForAdUnit[adUnitCode] = auctionId; }; - targeting.resetPresetTargeting = function(adUnitCode) { + targeting.resetPresetTargeting = function(adUnitCode, customSlotMatching) { if (isGptPubadsDefined()) { const adUnitCodes = getAdUnitCodes(adUnitCode); const adUnits = auctionManager.getAdUnits().filter(adUnit => includes(adUnitCodes, adUnit.code)); window.googletag.pubads().getSlots().forEach(slot => { + let customSlotMatchingFunc = utils.isFn(customSlotMatching) && customSlotMatching(slot); pbTargetingKeys.forEach(function(key) { // reset only registered adunits adUnits.forEach(function(unit) { if (unit.code === slot.getAdUnitPath() || - unit.code === slot.getSlotElementId()) { + unit.code === slot.getSlotElementId() || + (utils.isFn(customSlotMatchingFunc) && customSlotMatchingFunc(unit.code))) { slot.setTargeting(key, null); } }); diff --git a/src/url.js b/src/url.js deleted file mode 100644 index c63bca2ca41..00000000000 --- a/src/url.js +++ /dev/null @@ -1,55 +0,0 @@ -export function parseQS(query) { - return !query ? {} : query - .replace(/^\?/, '') - .split('&') - .reduce((acc, criteria) => { - let [k, v] = criteria.split('='); - if (/\[\]$/.test(k)) { - k = k.replace('[]', ''); - acc[k] = acc[k] || []; - acc[k].push(v); - } else { - acc[k] = v || ''; - } - return acc; - }, {}); -} - -export function formatQS(query) { - return Object - .keys(query) - .map(k => Array.isArray(query[k]) - ? query[k].map(v => `${k}[]=${v}`).join('&') - : `${k}=${query[k]}`) - .join('&'); -} - -export function parse(url, options) { - let parsed = document.createElement('a'); - if (options && 'noDecodeWholeURL' in options && options.noDecodeWholeURL) { - parsed.href = url; - } else { - parsed.href = decodeURIComponent(url); - } - // in window.location 'search' is string, not object - let qsAsString = (options && 'decodeSearchAsString' in options && options.decodeSearchAsString); - return { - href: parsed.href, - protocol: (parsed.protocol || '').replace(/:$/, ''), - hostname: parsed.hostname, - port: +parsed.port, - pathname: parsed.pathname.replace(/^(?!\/)/, '/'), - search: (qsAsString) ? parsed.search : parseQS(parsed.search || ''), - hash: (parsed.hash || '').replace(/^#/, ''), - host: parsed.host || window.location.host - }; -} - -export function format(obj) { - return (obj.protocol || 'http') + '://' + - (obj.host || - obj.hostname + (obj.port ? `:${obj.port}` : '')) + - (obj.pathname || '') + - (obj.search ? `?${formatQS(obj.search || '')}` : '') + - (obj.hash ? `#${obj.hash}` : ''); -} diff --git a/src/userSync.js b/src/userSync.js index e19f64d8082..5da22254f2c 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -1,6 +1,7 @@ import * as utils from './utils.js'; import { config } from './config.js'; import includes from 'core-js/library/fn/array/includes.js'; +import { getCoreStorageManager } from './storageManager.js'; export const USERSYNC_DEFAULT_CONFIG = { syncEnabled: true, @@ -20,6 +21,8 @@ config.setDefaults({ 'userSync': utils.deepClone(USERSYNC_DEFAULT_CONFIG) }); +const storage = getCoreStorageManager('usersync'); + /** * Factory function which creates a new UserSyncPool. * @@ -299,7 +302,7 @@ export function newUserSync(userSyncDependencies) { return publicApi; } -const browserSupportsCookies = !utils.isSafariBrowser() && utils.cookiesAreEnabled(); +const browserSupportsCookies = !utils.isSafariBrowser() && storage.cookiesAreEnabled(); export const userSync = newUserSync({ config: config.getConfig('userSync'), diff --git a/src/utils.js b/src/utils.js index fbc0e76d167..e7b937a970d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import { config } from './config.js'; import clone from 'just-clone'; +import deepequal from 'deep-equal'; import find from 'core-js/library/fn/array/find.js'; import includes from 'core-js/library/fn/array/includes.js'; @@ -36,7 +37,10 @@ export const internal = { logError, logWarn, logMessage, - logInfo + logInfo, + parseQS, + formatQS, + deepEqual }; var uniqueRef = {}; @@ -50,28 +54,6 @@ export let bind = function(a, b) { return b; }.bind(null, 1, uniqueRef)() === un }; }; -/* - * Substitutes into a string from a given map using the token - * Usage - * var str = 'text %%REPLACE%% this text with %%SOMETHING%%'; - * var map = {}; - * map['replace'] = 'it was subbed'; - * map['something'] = 'something else'; - * console.log(replaceTokenInString(str, map, '%%')); => "text it was subbed this text with something else" - */ -export function replaceTokenInString(str, map, token) { - _each(map, function (value, key) { - value = (value === undefined) ? '' : value; - - var keyString = token + key.toUpperCase() + token; - var re = new RegExp(keyString, 'g'); - - str = str.replace(re, value); - }); - - return str; -} - /* utility method to get incremental integer starting from 1 */ var getIncrementalInteger = (function () { var count = 0; @@ -120,7 +102,7 @@ export function getBidIdParameter(key, paramsObj) { export function tryAppendQueryString(existingUrl, key, value) { if (value) { - return existingUrl += key + '=' + encodeURIComponent(value) + '&'; + return existingUrl + key + '=' + encodeURIComponent(value) + '&'; } return existingUrl; @@ -128,14 +110,8 @@ export function tryAppendQueryString(existingUrl, key, value) { // parse a query string object passed in bid params // bid params should be an object such as {key: "value", key1 : "value1"} -export function parseQueryStringParameters(queryObj) { - let result = ''; - for (var k in queryObj) { - if (queryObj.hasOwnProperty(k)) { result += k + '=' + encodeURIComponent(queryObj[k]) + '&'; } - } - - return result; -} +// aliases to formatQS +export let parseQueryStringParameters = internal.formatQS; // transform an AdServer targeting bids into a query string to send to the adserver export function transformAdServerTargetingObj(targeting) { @@ -320,53 +296,9 @@ export function createInvisibleIframe() { * and if it does return the value */ export function getParameterByName(name) { - var regexS = '[\\?&]' + name + '=([^&#]*)'; - var regex = new RegExp(regexS); - var results = regex.exec(window.location.search); - if (results === null) { - return ''; - } - - return decodeURIComponent(results[1].replace(/\+/g, ' ')); -} - -/** - * This function validates paramaters. - * @param {Object} paramObj [description] - * @param {string[]} requiredParamsArr [description] - * @return {boolean} Bool if paramaters are valid - */ -export function hasValidBidRequest(paramObj, requiredParamsArr, adapter) { - var found = false; - - function findParam(value, key) { - if (key === requiredParamsArr[i]) { - found = true; - } - } - - for (var i = 0; i < requiredParamsArr.length; i++) { - found = false; - - _each(paramObj, findParam); - - if (!found) { - logError('Params are missing for bid request. One of these required paramaters are missing: ' + requiredParamsArr, adapter); - return false; - } - } - - return true; + return parseQS(getWindowLocation().search)[name] || ''; } -// Handle addEventListener gracefully in older browsers -export function addEventHandler(element, event, func) { - if (element.addEventListener) { - element.addEventListener(event, func, true); - } else if (element.attachEvent) { - element.attachEvent('on' + event, func); - } -} /** * Return if the object is of the * given type. @@ -471,15 +403,6 @@ export function contains(a, obj) { return false; } -export let indexOf = (function () { - if (Array.prototype.indexOf) { - return Array.prototype.indexOf; - } - - // ie8 no longer supported - // return polyfills.indexOf; -}()); - /** * Map an array or object into another array * given a function @@ -647,32 +570,6 @@ export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') `; } -/** - * Returns iframe document in a browser agnostic way - * @param {Object} iframe reference - * @return {Object} iframe `document` reference - */ -export function getIframeDocument(iframe) { - if (!iframe) { - return; - } - - let doc; - try { - if (iframe.contentWindow) { - doc = iframe.contentWindow.document; - } else if (iframe.contentDocument.document) { - doc = iframe.contentDocument.document; - } else { - doc = iframe.contentDocument; - } - } catch (e) { - internal.logError('Cannot get iframe document', e); - } - - return doc; -} - export function getValueString(param, val, defaultValue) { if (val === undefined || val === null) { return defaultValue; @@ -792,16 +689,6 @@ export function adUnitsFilter(filter, bid) { return includes(filter, bid && bid.adUnitCode); } -/** - * Check if parent iframe of passed document supports content rendering via 'srcdoc' property - * @param {HTMLDocument} doc document to check support of 'srcdoc' - */ -export function isSrcdocSupported(doc) { - // Firefox is excluded due to https://bugzilla.mozilla.org/show_bug.cgi?id=1265961 - return doc.defaultView && doc.defaultView.frameElement && - 'srcdoc' in doc.defaultView.frameElement && !/firefox/i.test(navigator.userAgent); -} - export function deepClone(obj) { return clone(obj); } @@ -815,7 +702,7 @@ export function inIframe() { } export function isSafariBrowser() { - return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); } export function replaceAuctionPrice(str, cpm) { @@ -844,86 +731,6 @@ export function checkCookieSupport() { } } -/** - * @returns {boolean} - */ -export function cookiesAreEnabled() { - if (hasDeviceAccess()) { - if (internal.checkCookieSupport()) { - return true; - } - window.document.cookie = 'prebid.cookieTest'; - return window.document.cookie.indexOf('prebid.cookieTest') !== -1; - } - return false; -} - -/** - * @param {string} name - * @returns {(string|null)} - */ -export function getCookie(name) { - if (hasDeviceAccess()) { - let m = window.document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]*)\\s*(;|$)'); - return m ? decodeURIComponent(m[2]) : null; - } - return null; -} - -/** - * @param {string} key - * @param {string} value - * @param {string} [expires=''] - * @param {string} [sameSite='/'] - * @param {string} [domain] domain (e.g., 'example.com' or 'subdomain.example.com'). - * If not specified, defaults to the host portion of the current document location. - * If a domain is specified, subdomains are always included. - * Domain must match the domain of the JavaScript origin. Setting cookies to foreign domains will be silently ignored. - */ -export function setCookie(key, value, expires, sameSite, domain) { - if (hasDeviceAccess()) { - const domainPortion = (domain && domain !== '') ? ` ;domain=${encodeURIComponent(domain)}` : ''; - const expiresPortion = (expires && expires !== '') ? ` ;expires=${expires}` : ''; - document.cookie = `${key}=${encodeURIComponent(value)}${expiresPortion}; path=/${domainPortion}${sameSite ? `; SameSite=${sameSite}` : ''}`; - } -} - -/** - * Returns all cookie values from the jar whose names contain the `keyLike` - * Needs to exist in `utils.js` as it follows the StorageHandler interface defined in live-connect-js. If that module were to be removed, this function can go as well. - * @param {string} keyLike - * @return {[]} - */ -export function findSimilarCookies(keyLike) { - const all = []; - if (hasDeviceAccess()) { - const cookies = document.cookie.split(';'); - while (cookies.length) { - const cookie = cookies.pop(); - let separatorIndex = cookie.indexOf('='); - separatorIndex = separatorIndex < 0 ? cookie.length : separatorIndex; - const cookieName = decodeURIComponent(cookie.slice(0, separatorIndex).replace(/^\s+/, '')); - if (cookieName.indexOf(keyLike) >= 0) { - all.push(decodeURIComponent(cookie.slice(separatorIndex + 1))); - } - } - } - return all; -} - -/** - * @returns {boolean} - */ -export function localStorageIsEnabled () { - if (hasDeviceAccess()) { - try { - localStorage.setItem('prebid.cookieTest', '1'); - return localStorage.getItem('prebid.cookieTest') === '1'; - } catch (error) {} - } - return false; -} - /** * Given a function, return a function which only executes the original after * it's been called numRequiredCalls times. @@ -963,19 +770,6 @@ export function groupBy(xs, key) { }, {}); } -/** - * Returns content for a friendly iframe to execute a URL in script tag - * @param {string} url URL to be executed in a script tag in a friendly iframe - * and are macros left to be replaced if required - */ -export function createContentToExecuteExtScriptInFriendlyFrame(url) { - if (!url) { - return ''; - } - - return ``; -} - /** * Build an object consisting of only defined parameters to avoid creating an * object with defined keys and undefined values. @@ -1078,6 +872,24 @@ export function isSlotMatchingAdUnitCode(adUnitCode) { return (slot) => compareCodeAndSlot(slot, adUnitCode); } +/** + * @summary Uses the adUnit's code in order to find a matching gptSlot on the page + */ +export function getGptSlotInfoForAdUnitCode(adUnitCode) { + let matchingSlot; + if (isGptPubadsDefined()) { + // find the first matching gpt slot on the page + matchingSlot = find(window.googletag.pubads().getSlots(), isSlotMatchingAdUnitCode(adUnitCode)); + } + if (matchingSlot) { + return { + gptSlot: matchingSlot.getAdUnitPath(), + divId: matchingSlot.getSlotElementId() + } + } + return {}; +}; + /** * Constructs warning message for when unsupported bidders are dropped from an adunit * @param {Object} adUnit ad unit from which the bidder is being dropped @@ -1094,18 +906,6 @@ export function unsupportedBidderMessage(adUnit, bidder) { `; } -/** - * Delete property from object - * @param {Object} object - * @param {string} prop - * @return {Object} object - */ -export function deletePropertyFromObject(object, prop) { - let result = Object.assign({}, object); - delete result[prop]; - return result; -} - /** * Checks input is integer or not * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger @@ -1242,50 +1042,6 @@ export function convertTypes(types, params) { return params; } -/** - * @param {string} key - * @param {string} value - */ -export function setDataInLocalStorage(key, value) { - if (hasLocalStorage()) { - window.localStorage.setItem(key, value); - } -} - -/** - * @param {string} key - * @returns {(string|null)} - */ -export function getDataFromLocalStorage(key) { - if (hasLocalStorage()) { - return window.localStorage.getItem(key); - } - return null; -} - -/** - * @param {string} key - */ -export function removeDataFromLocalStorage(key) { - if (hasLocalStorage()) { - window.localStorage.removeItem(key); - } -} - -/** - * @returns {boolean} - */ -export function hasLocalStorage() { - if (hasDeviceAccess()) { - try { - return !!window.localStorage; - } catch (e) { - logError('Local storage api disabled'); - } - } - return false; -} - export function isArrayOfNums(val, size) { return (isArray(val)) && ((size) ? val.length === size : true) && (val.every(v => isInteger(v))); } @@ -1348,3 +1104,93 @@ export function compareOn(property) { return 0; } } + +export function parseQS(query) { + return !query ? {} : query + .replace(/^\?/, '') + .split('&') + .reduce((acc, criteria) => { + let [k, v] = criteria.split('='); + if (/\[\]$/.test(k)) { + k = k.replace('[]', ''); + acc[k] = acc[k] || []; + acc[k].push(v); + } else { + acc[k] = v || ''; + } + return acc; + }, {}); +} + +export function formatQS(query) { + return Object + .keys(query) + .map(k => Array.isArray(query[k]) + ? query[k].map(v => `${k}[]=${v}`).join('&') + : `${k}=${query[k]}`) + .join('&'); +} + +export function parseUrl(url, options) { + let parsed = document.createElement('a'); + if (options && 'noDecodeWholeURL' in options && options.noDecodeWholeURL) { + parsed.href = url; + } else { + parsed.href = decodeURIComponent(url); + } + // in window.location 'search' is string, not object + let qsAsString = (options && 'decodeSearchAsString' in options && options.decodeSearchAsString); + return { + href: parsed.href, + protocol: (parsed.protocol || '').replace(/:$/, ''), + hostname: parsed.hostname, + port: +parsed.port, + pathname: parsed.pathname.replace(/^(?!\/)/, '/'), + search: (qsAsString) ? parsed.search : internal.parseQS(parsed.search || ''), + hash: (parsed.hash || '').replace(/^#/, ''), + host: parsed.host || window.location.host + }; +} + +export function buildUrl(obj) { + return (obj.protocol || 'http') + '://' + + (obj.host || + obj.hostname + (obj.port ? `:${obj.port}` : '')) + + (obj.pathname || '') + + (obj.search ? `?${internal.formatQS(obj.search || '')}` : '') + + (obj.hash ? `#${obj.hash}` : ''); +} + +/** + * This function compares two objects for checking their equivalence. + * @param {Object} obj1 + * @param {Object} obj2 + * @returns {boolean} + */ +export function deepEqual(obj1, obj2) { + return deepequal(obj1, obj2); +} + +export function mergeDeep(target, ...sources) { + if (!sources.length) return target; + const source = sources.shift(); + + if (isPlainObject(target) && isPlainObject(source)) { + for (const key in source) { + if (isPlainObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + mergeDeep(target[key], source[key]); + } else if (isArray(source[key])) { + if (!target[key]) { + Object.assign(target, { [key]: source[key] }); + } else if (isArray(target[key])) { + target[key] = target[key].concat(source[key]); + } + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return mergeDeep(target, ...sources); +} diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index b49a591d20d..8996b288576 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { getAdagioScript, spec } from 'modules/adagioBidAdapter.js'; +import { adagioScriptFromLocalStorageCb, spec } from 'modules/adagioBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; @@ -7,7 +7,7 @@ describe('adagioAdapter', () => { let utilsMock; const adapter = newBidder(spec); const ENDPOINT = 'https://mp.4dex.io/prebid'; - const VERSION = '2.1.0'; + const VERSION = '2.2.1'; beforeEach(function() { localStorage.removeItem('adagioScript'); @@ -301,7 +301,29 @@ describe('adagioAdapter', () => { 'gdprConsent': { consentString: consentString, gdprApplies: true, - allowAuctionWithoutConsent: true + allowAuctionWithoutConsent: true, + apiVersion: 1, + }, + 'refererInfo': { + 'numIframes': 0, + 'reachedTop': true, + 'referer': 'http://test.io/index.html?pbjs_debug=true' + } + }; + + let bidderRequestTCF2 = { + 'bidderCode': 'adagio', + 'auctionId': '12jejebn', + 'bidderRequestId': 'hehehehbeheh', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + vendorData: { + tcString: consentString, + gdprApplies: true + }, + gdprApplies: true, + apiVersion: 2 }, 'refererInfo': { 'numIframes': 0, @@ -433,6 +455,17 @@ describe('adagioAdapter', () => { expect(request.data.gdpr).to.exist; expect(request.data.gdpr.consentString).to.exist.and.to.equal(consentString); expect(request.data.gdpr.consentRequired).to.exist.and.to.equal(1); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(1); + }); + + it('GDPR consent is applied w/ TCF2', () => { + const requests = spec.buildRequests(bidRequests, bidderRequestTCF2); + expect(requests).to.have.lengthOf(2); + const request = requests[0]; + expect(request.data.gdpr).to.exist; + expect(request.data.gdpr.consentString).to.exist.and.to.equal(consentString); + expect(request.data.gdpr.consentRequired).to.exist.and.to.equal(1); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(2); }); it('GDPR consent is not applied', () => { @@ -443,6 +476,18 @@ describe('adagioAdapter', () => { expect(request.data.gdpr).to.exist; expect(request.data.gdpr.consentString).to.exist.and.to.equal(consentString); expect(request.data.gdpr.consentRequired).to.exist.and.to.equal(0); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(1); + }); + + it('GDPR consent is not applied w/ TCF2', () => { + bidderRequestTCF2.gdprConsent.gdprApplies = false; + const requests = spec.buildRequests(bidRequests, bidderRequestTCF2); + expect(requests).to.have.lengthOf(2); + const request = requests[0]; + expect(request.data.gdpr).to.exist; + expect(request.data.gdpr.consentString).to.exist.and.to.equal(consentString); + expect(request.data.gdpr.consentRequired).to.exist.and.to.equal(0); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(2); }); it('GDPR consent is undefined', () => { @@ -456,6 +501,20 @@ describe('adagioAdapter', () => { expect(request.data.gdpr).to.not.have.property('consentString'); expect(request.data.gdpr).to.not.have.property('gdprApplies'); expect(request.data.gdpr).to.not.have.property('allowAuctionWithoutConsent'); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(1); + }); + + it('GDPR consent is undefined w/ TCF2', () => { + delete bidderRequestTCF2.gdprConsent.consentString; + delete bidderRequestTCF2.gdprConsent.gdprApplies; + delete bidderRequestTCF2.gdprConsent.vendorData; + const requests = spec.buildRequests(bidRequests, bidderRequestTCF2); + expect(requests).to.have.lengthOf(2); + const request = requests[0]; + expect(request.data.gdpr).to.exist; + expect(request.data.gdpr).to.not.have.property('consentString'); + expect(request.data.gdpr).to.not.have.property('gdprApplies'); + expect(request.data.gdpr.apiVersion).to.exist.and.to.equal(2); }); it('GDPR consent bidderRequest does not have gdprConsent', () => { @@ -480,6 +539,40 @@ describe('adagioAdapter', () => { }); expect(requests).to.be.empty; }); + + it('Should add the schain if available at bidder level', () => { + const bidRequest = Object.assign({}, bidRequests[0], { + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'ssp.test', + sid: '00001', + hp: 1 + }] + } + }); + + const requests = spec.buildRequests([bidRequest], bidderRequest); + const request = requests[0]; + + expect(request.data.schain).to.exist; + expect(request.data.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'ssp.test', + sid: '00001', + hp: 1 + }] + }); + }); + + it('Schain should not be added to the request', () => { + const requests = spec.buildRequests([bidRequests[0]], bidderRequest); + const request = requests[0]; + expect(request.data.schain).to.not.exist; + }); }); describe('interpretResponse', () => { @@ -645,7 +738,7 @@ describe('adagioAdapter', () => { }); }); - describe('getAdagioScript', () => { + describe('adagioScriptFromLocalStorageCb', () => { const VALID_HASH = 'Lddcw3AADdQDrPtbRJkKxvA+o1CtScGDIMNRpHB3NnlC/FYmy/9RKXelKrYj/sjuWusl5YcOpo+lbGSkk655i8EKuDiOvK6ae/imxSrmdziIp+S/TA6hTFJXcB8k1Q9OIp4CMCT52jjXgHwX6G0rp+uYoCR25B1jHaHnpH26A6I='; const INVALID_HASH = 'invalid'; const VALID_SCRIPT_CONTENT = 'var _ADAGIO=function(){};(_ADAGIO)();\n'; @@ -659,7 +752,7 @@ describe('adagioAdapter', () => { utilsMock.expects('logWarn').withExactArgs('No hash found in Adagio script').never(); utilsMock.expects('logWarn').withExactArgs('Invalid Adagio script found').never(); - getAdagioScript(); + adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)); expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.equals('// hash: ' + VALID_HASH + '\n' + VALID_SCRIPT_CONTENT); utilsMock.verify(); @@ -672,7 +765,7 @@ describe('adagioAdapter', () => { utilsMock.expects('logWarn').withExactArgs('No hash found in Adagio script').never(); utilsMock.expects('logWarn').withExactArgs('Invalid Adagio script found').once(); - getAdagioScript(); + adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)); expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null; utilsMock.verify(); @@ -685,7 +778,7 @@ describe('adagioAdapter', () => { utilsMock.expects('logWarn').withExactArgs('No hash found in Adagio script').never(); utilsMock.expects('logWarn').withExactArgs('Invalid Adagio script found').once(); - getAdagioScript(); + adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)); expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null; utilsMock.verify(); @@ -698,7 +791,7 @@ describe('adagioAdapter', () => { utilsMock.expects('logWarn').withExactArgs('No hash found in Adagio script').once(); utilsMock.expects('logWarn').withExactArgs('Invalid Adagio script found').never(); - getAdagioScript(); + adagioScriptFromLocalStorageCb(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)); expect(localStorage.getItem(ADAGIO_LOCALSTORAGE_KEY)).to.be.null; utilsMock.verify(); diff --git a/test/spec/modules/adformBidAdapter_spec.js b/test/spec/modules/adformBidAdapter_spec.js index 8b7ecb3f9a7..9233ca1dd7a 100644 --- a/test/spec/modules/adformBidAdapter_spec.js +++ b/test/spec/modules/adformBidAdapter_spec.js @@ -1,5 +1,4 @@ import {assert, expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/adformBidAdapter.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; diff --git a/test/spec/modules/adformOpenRTBBidAdapter_spec.js b/test/spec/modules/adformOpenRTBBidAdapter_spec.js index caa2fc56208..77dbc17cdb2 100644 --- a/test/spec/modules/adformOpenRTBBidAdapter_spec.js +++ b/test/spec/modules/adformOpenRTBBidAdapter_spec.js @@ -1,6 +1,5 @@ // jshint esversion: 6, es3: false, node: true import {assert, expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/adformOpenRTBBidAdapter.js'; import { NATIVE } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 3ae37592f1e..ef95febf13d 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from 'modules/adkernelBidAdapter.js'; import * as utils from 'src/utils.js'; +import {NATIVE, BANNER, VIDEO} from 'src/mediaTypes'; describe('Adkernel adapter', function () { const bid1_zone1 = { @@ -105,6 +106,57 @@ describe('Adkernel adapter', function () { bidId: 'Bid_01', bidderRequestId: 'req-001', auctionId: 'auc-001' + }, bid_native = { + bidder: 'adkernel', + params: {zoneId: 1, host: 'rtb.adkernel.com'}, + mediaTypes: { + native: { + title: { + required: true, + len: 80 + }, + body: { + required: true + }, + body2: { + required: true + }, + icon: { + required: true, + aspect_ratios: [{min_width: 50, min_height: 50}] + }, + image: { + required: true, + sizes: [300, 200] + }, + clickUrl: { + required: true + }, + rating: { + required: false + }, + price: { + required: false + }, + privacyLink: { + required: false + }, + cta: { + required: false + }, + sponsoredBy: { + required: false + }, + displayUrl: { + required: false + } + } + }, + adUnitCode: 'ad-unit-1', + transactionId: 'f82c64b8-c602-42a4-9791-4a268f6559ed', + bidId: 'Bid_01', + bidderRequestId: 'req-001', + auctionId: 'auc-001' }; const bidResponse1 = { @@ -125,20 +177,6 @@ describe('Adkernel adapter', function () { ext: { adk_usersync: ['https://adk.sync.com/sync'] } - }, bidResponse2 = { - id: 'bid2', - seatbid: [{ - bid: [{ - id: '2', - impid: 'Bid_02', - crid: '100_002', - price: 1.31, - adm: '', - w: 300, - h: 250 - }] - }], - cur: 'USD' }, videoBidResponse = { id: '47ce4badcf7482', seatbid: [{ @@ -158,6 +196,36 @@ describe('Adkernel adapter', function () { ext: { adk_usersync: ['https://adk.sync.com/sync'] } + }, nativeResponse = { + id: '56fbc713-b737-4651-9050-13376aed9818', + seatbid: [{ + bid: [{ + id: 'someid_01', + impid: 'Bid_01', + price: 2.25, + adid: '4', + adm: JSON.stringify({ + native: { + assets: [ + {id: 0, title: {text: 'Title'}}, + {id: 3, data: {value: 'Description'}}, + {id: 4, data: {value: 'Additional description'}}, + {id: 1, img: {url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0&imgt=icon', w: 50, h: 50}}, + {id: 2, img: {url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0', w: 300, h: 200}}, + {id: 5, data: {value: 'Sponsor.com'}}, + {id: 14, data: {value: 'displayurl.com'}} + ], + link: {url: 'http://rtb.com/click?i=pTuOlf5KHUo_0'}, + imptrackers: ['http://rtb.com/win?i=pTuOlf5KHUo_0&f=imp'] + } + }), + adomain: ['displayurl.com'], + cid: '1', + crid: '4' + }] + }], + bidid: 'pTuOlf5KHUo', + cur: 'USD' }; function buildBidderRequest(url = 'https://example.com/index.html', params = {}) { @@ -191,6 +259,10 @@ describe('Adkernel adapter', function () { it('empty request shouldn\'t generate exception', function () { expect(spec.isBidRequestValid(bid_with_wrong_zoneId)).to.be.equal(false); }); + + it('valid native requests should pass', () => { + expect(spec.isBidRequestValid(bid_native)).to.be.equal(true); + }) }); describe('banner request building', function () { @@ -270,7 +342,6 @@ describe('Adkernel adapter', function () { it('should forward default bidder timeout', function() { let [_, bidRequests] = buildRequest([bid1_zone1], DEFAULT_BIDDER_REQUEST); - let bidRequest = bidRequests[0]; expect(bidRequests[0]).to.have.property('tmax', 3000); }); }); @@ -343,7 +414,7 @@ describe('Adkernel adapter', function () { expect(resp).to.have.property('creativeId', '100_001'); expect(resp).to.have.property('currency'); expect(resp).to.have.property('ttl'); - expect(resp).to.have.property('mediaType', 'banner'); + expect(resp).to.have.property('mediaType', BANNER); expect(resp).to.have.property('ad'); expect(resp.ad).to.have.string(''); }); @@ -352,7 +423,7 @@ describe('Adkernel adapter', function () { let [pbRequests, _] = buildRequest([bid_video]); let resp = spec.interpretResponse({body: videoBidResponse}, pbRequests[0])[0]; expect(resp).to.have.property('requestId', 'Bid_Video'); - expect(resp.mediaType).to.equal('video'); + expect(resp.mediaType).to.equal(VIDEO); expect(resp.cpm).to.equal(0.00145); expect(resp.vastUrl).to.equal('https://rtb.com/win?i=sZSYq5zYMxo_0&f=nurl'); expect(resp.width).to.equal(640); @@ -388,4 +459,49 @@ describe('Adkernel adapter', function () { expect(spec.aliases).to.include.members(['headbidding', 'adsolut', 'oftmediahb', 'audiencemedia', 'waardex_ak', 'roqoon']); }); }); + + describe('native support', () => { + let _, bidRequests; + before(function () { + [_, bidRequests] = buildRequest([bid_native]); + }); + + it('native request building', () => { + expect(bidRequests[0].imp).to.have.length(1); + expect(bidRequests[0].imp[0]).to.have.property('native'); + expect(bidRequests[0].imp[0].native).to.have.property('request'); + let request = JSON.parse(bidRequests[0].imp[0].native.request); + expect(request).to.have.property('ver', '1.1'); + expect(request.assets).to.have.length(10); + expect(request.assets[0]).to.be.eql({id: 0, required: 1, title: {len: 80}}); + expect(request.assets[1]).to.be.eql({id: 3, required: 1, data: {type: 2}}); + expect(request.assets[2]).to.be.eql({id: 4, required: 1, data: {type: 10}}); + expect(request.assets[3]).to.be.eql({id: 1, required: 1, img: {wmin: 50, hmin: 50, type: 1}}); + expect(request.assets[4]).to.be.eql({id: 2, required: 1, img: {w: 300, h: 200, type: 3}}); + expect(request.assets[5]).to.be.eql({id: 11, required: 0, data: {type: 3}}); + expect(request.assets[6]).to.be.eql({id: 8, required: 0, data: {type: 6}}); + expect(request.assets[7]).to.be.eql({id: 10, required: 0, data: {type: 12}}); + expect(request.assets[8]).to.be.eql({id: 5, required: 0, data: {type: 1}}); + expect(request.assets[9]).to.be.eql({id: 14, required: 0, data: {type: 11}}); + }); + + it('native response processing', () => { + let [pbRequests, _] = buildRequest([bid_native]); + let resp = spec.interpretResponse({body: nativeResponse}, pbRequests[0])[0]; + expect(resp).to.have.property('requestId', 'Bid_01'); + expect(resp).to.have.property('cpm', 2.25); + expect(resp).to.have.property('currency', 'USD'); + expect(resp).to.have.property('mediaType', NATIVE); + expect(resp).to.have.property('native'); + expect(resp.native).to.have.property('clickUrl', 'http://rtb.com/click?i=pTuOlf5KHUo_0'); + expect(resp.native.impressionTrackers).to.be.eql(['http://rtb.com/win?i=pTuOlf5KHUo_0&f=imp']); + expect(resp.native).to.have.property('title', 'Title'); + expect(resp.native).to.have.property('body', 'Description'); + expect(resp.native).to.have.property('body2', 'Additional description'); + expect(resp.native.icon).to.be.eql({url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0&imgt=icon', width: 50, height: 50}); + expect(resp.native.image).to.be.eql({url: 'http://rtb.com/thumbnail?i=pTuOlf5KHUo_0', width: 300, height: 200}); + expect(resp.native).to.have.property('sponsoredBy', 'Sponsor.com'); + expect(resp.native).to.have.property('displayUrl', 'displayurl.com'); + }); + }); }); diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js new file mode 100644 index 00000000000..54ff038c083 --- /dev/null +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -0,0 +1,119 @@ +// import or require modules necessary for the test, e.g.: +import { expect } from 'chai'; // may prefer 'assert' in place of 'expect' +import { spec } from 'modules/adnuntiusBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('adnuntiusBidAdapter', function () { + const ENDPOINT_URL = 'https://delivery.adnuntius.com/i?tzo=-60&format=json'; + const adapter = newBidder(spec); + const bidRequests = [ + { + bidder: 'adnuntius', + params: { + auId: '8b6bc', + network: 'adnuntius', + }, + bidId: '123' + } + ]; + + const serverResponse = { + body: { + 'adUnits': [ + { + 'auId': '000000000008b6bc', + 'targetId': '', + 'html': '

hi!

', + 'matchedAdCount': 1, + 'responseId': 'adn-rsp-1460129238', + 'ads': [ + { + 'destinationUrlEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fc%2F52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN%3Fct%3D2501%26r%3Dhttp%253A%252F%252Fgoogle.com', + 'assets': { + 'image': { + 'cdnId': 'https://assets.adnuntius.com/oEmZa5uYjxENfA1R692FVn6qIveFpO8wUbpyF2xSOCc.jpg', + 'width': '980', + 'height': '120' + } + }, + 'clickUrl': 'https://delivery.adnuntius.com/c/52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'urls': { + 'destination': 'https://delivery.adnuntius.com/c/52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN?ct=2501&r=http%3A%2F%2Fgoogle.com' + }, + 'urlsEsc': { + 'destination': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fc%2F52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN%3Fct%3D2501%26r%3Dhttp%253A%252F%252Fgoogle.com' + }, + 'destinationUrls': { + 'destination': 'http://google.com' + }, + 'cpm': { 'amount': 5.0, 'currency': 'NOK' }, + 'bid': { 'amount': 0.005, 'currency': 'NOK' }, + 'cost': { 'amount': 0.005, 'currency': 'NOK' }, + 'impressionTrackingUrls': [], + 'impressionTrackingUrlsEsc': [], + 'adId': 'adn-id-1347343135', + 'selectedColumn': '0', + 'selectedColumnPosition': '0', + 'renderedPixel': 'https://delivery.adnuntius.com/b/52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN.html', + 'renderedPixelEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fb%2F52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN.html', + 'visibleUrl': 'https://delivery.adnuntius.com/s?rt=52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'visibleUrlEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fs%3Frt%3D52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'viewUrl': 'https://delivery.adnuntius.com/v?rt=52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'viewUrlEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fv%3Frt%3D52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'rt': '52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + 'creativeWidth': '980', + 'creativeHeight': '120', + 'creativeId': 'wgkq587vgtpchsx1', + 'lineItemId': 'scyjdyv3mzgdsnpf', + 'layoutId': 'sw6gtws2rdj1kwby', + 'layoutName': 'Responsive image' + } + ] + } + ] + } + } + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bidRequests[0])).to.equal(true); + }); + }); + + describe('buildRequests', function () { + it('Test requests', function () { + const request = spec.buildRequests(bidRequests); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('bid'); + const bid = request[0].bid[0] + expect(bid).to.have.property('bidId'); + expect(request[0]).to.have.property('url'); + expect(request[0].url).to.equal(ENDPOINT_URL); + expect(request[0]).to.have.property('data'); + expect(request[0].data).to.equal('{\"adUnits\":[{\"auId\":\"8b6bc\"}]}'); + }); + }); + + describe('interpretResponse', function () { + it('should return valid response when passed valid server response', function () { + const request = spec.buildRequests(bidRequests); + const interpretedResponse = spec.interpretResponse(serverResponse, request[0]); + const ad = serverResponse.body.adUnits[0].ads[0] + expect(interpretedResponse).to.have.lengthOf(1); + expect(interpretedResponse[0].cpm).to.equal(ad.cpm.amount); + expect(interpretedResponse[0].width).to.equal(Number(ad.creativeWidth)); + expect(interpretedResponse[0].height).to.equal(Number(ad.creativeHeight)); + expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); + expect(interpretedResponse[0].currency).to.equal(ad.cpm.currency); + expect(interpretedResponse[0].netRevenue).to.equal(false); + expect(interpretedResponse[0].ad).to.equal(serverResponse.body.adUnits[0].html); + expect(interpretedResponse[0].ttl).to.equal(360); + }); + }); +}); diff --git a/test/spec/modules/adomikAnalyticsAdapter_spec.js b/test/spec/modules/adomikAnalyticsAdapter_spec.js index a067ce2770e..1414b2402b9 100644 --- a/test/spec/modules/adomikAnalyticsAdapter_spec.js +++ b/test/spec/modules/adomikAnalyticsAdapter_spec.js @@ -7,6 +7,13 @@ let constants = require('src/constants.json'); describe('Adomik Prebid Analytic', function () { let sendEventStub; let sendWonEventStub; + let clock; + before(function () { + clock = sinon.useFakeTimers(); + }); + after(function () { + clock.restore(); + }); describe('enableAnalytics', function () { beforeEach(function () { @@ -120,7 +127,6 @@ describe('Adomik Prebid Analytic', function () { expect(adomikAnalytics.currentContext.timeouted).to.equal(true); // Step 7: Send auction end event - var clock = sinon.useFakeTimers(); events.emit(constants.EVENTS.AUCTION_END, {}); setTimeout(function() { @@ -130,7 +136,6 @@ describe('Adomik Prebid Analytic', function () { }, 3000); clock.tick(5000); - clock.restore(); sinon.assert.callCount(adomikAnalytics.track, 6); }); diff --git a/test/spec/modules/adotBidAdapter_spec.js b/test/spec/modules/adotBidAdapter_spec.js new file mode 100644 index 00000000000..594fc4ac7b7 --- /dev/null +++ b/test/spec/modules/adotBidAdapter_spec.js @@ -0,0 +1,3118 @@ +import { expect } from 'chai'; +import { executeRenderer } from 'src/Renderer.js'; +import * as utils from 'src/utils.js'; +import { spec } from 'modules/adotBidAdapter.js'; + +const BIDDER_URL = 'https://dsp.adotmob.com/headerbidding/bidrequest'; + +describe('Adot Adapter', function () { + const examples = { + adUnit_banner: { + adUnitCode: 'ad_unit_banner', + bidder: 'adot', + bidderRequestId: 'bid_request_id', + bidId: 'bid_id', + params: {}, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }, + + adUnit_video_outstream: { + adUnitCode: 'ad_unit_video_outstream', + bidder: 'adot', + bidderRequestId: 'bid_request_id', + bidId: 'bid_id', + params: { + video: { + mimes: ['video/mp4'], + minDuration: 5, + maxDuration: 30, + protocols: [2, 3] + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[300, 250]] + } + } + }, + + adUnit_video_instream: { + adUnitCode: 'ad_unit_video_instream', + bidder: 'adot', + bidderRequestId: 'bid_request_id', + bidId: 'bid_id', + params: { + video: { + instreamContext: 'pre-roll', + mimes: ['video/mp4'], + minDuration: 5, + maxDuration: 30, + protocols: [2, 3] + } + }, + mediaTypes: { + video: { + context: 'instream', + playerSize: [[300, 250]] + } + } + }, + + adUnitContext: { + refererInfo: { + referer: 'https://we-are-adot.com/test', + }, + gdprConsent: { + consentString: 'consent_string', + gdprApplies: true + } + }, + + adUnit_position: { + adUnitCode: 'ad_unit_position', + bidder: 'adot', + bidderRequestId: 'bid_request_id', + bidId: 'bid_id', + params: { + position: 1 + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }, + + adUnit_native: { + adUnitCode: 'ad_unit_native', + bidder: 'adot', + bidderRequestId: 'bid_request_id', + bidId: 'bid_id', + params: {}, + mediaTypes: { + native: { + title: {required: true, len: 140}, + icon: {required: true, sizes: [50, 50]}, + image: {required: false, sizes: [320, 200]}, + sponsoredBy: {required: false}, + body: {required: false}, + cta: {required: true} + } + } + }, + + serverRequest_banner: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_banner_0_0', + banner: { + format: [{ + w: 300, + h: 200 + }] + }, + video: null + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_banner_0_0', + adUnitCode: 'ad_unit_banner', + bidId: 'imp_id_banner' + } + ] + } + }, + + serverRequest_banner_twoImps: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_banner_0_0', + banner: { + format: [{ + w: 300, + h: 200 + }] + }, + video: null + }, + { + id: 'imp_id_banner_2_0_0', + banner: { + format: [{ + w: 300, + h: 200 + }] + }, + video: null + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_banner_0_0', + adUnitCode: 'ad_unit_banner', + bidId: 'imp_id_banner' + }, + { + impressionId: 'imp_id_banner_2_0_0', + adUnitCode: 'ad_unit_banner_2', + bidId: 'imp_id_banner_2' + } + ] + } + }, + + serverRequest_video_instream: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_video_instream_0', + banner: null, + video: { + mimes: ['video/mp4'], + w: 300, + h: 200, + startdelay: 0, + minduration: 5, + maxduration: 35, + protocols: [2, 3] + } + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_video_instream_0', + adUnitCode: 'ad_unit_video_instream', + bidId: 'imp_id_video_instream' + } + ] + } + }, + + serverRequest_video_outstream: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_video_outstream_0', + banner: null, + video: { + mimes: ['video/mp4'], + w: 300, + h: 200, + startdelay: null, + minduration: 5, + maxduration: 35, + protocols: [2, 3] + } + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_video_outstream_0', + adUnitCode: 'ad_unit_video_outstream', + bidId: 'imp_id_video_outstream' + } + ] + } + }, + + serverRequest_video_instream_outstream: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_video_instream_0', + banner: null, + video: { + mimes: ['video/mp4'], + w: 300, + h: 200, + startdelay: 0, + minduration: 5, + maxduration: 35, + protocols: [2, 3] + } + }, + { + id: 'imp_id_video_outstream_0', + banner: null, + video: { + mimes: ['video/mp4'], + w: 300, + h: 200, + startdelay: null, + minduration: 5, + maxduration: 35, + protocols: [2, 3] + } + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_video_instream_0', + adUnitCode: 'ad_unit_video_instream', + bidId: 'imp_id_video_instream' + }, + { + impressionId: 'imp_id_video_outstream_0', + adUnitCode: 'ad_unit_video_outstream', + bidId: 'imp_id_video_outstream' + } + ] + } + }, + + serverRequest_position: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_banner', + banner: { + format: [{ + w: 300, + h: 200 + }], + position: 1 + }, + video: null + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_banner', + adUnitCode: 'ad_unit_position' + } + ] + } + }, + + serverRequest_native: { + method: 'POST', + url: 'https://we-are-adot.com/bidrequest', + data: { + id: 'bid_request_id', + imp: [ + { + id: 'imp_id_native_0', + native: { + request: { + assets: [ + { + id: 1, + required: true, + title: { + len: 140 + } + }, + { + id: 2, + required: true, + img: { + type: 1, + wmin: 50, + hmin: 50 + } + }, + { + id: 3, + required: false, + img: { + type: 3, + wmin: 320, + hmin: 200 + } + }, + { + id: 4, + required: false, + data: { + type: 1 + } + }, + { + id: 5, + required: false, + data: { + type: 2 + } + }, + { + id: 6, + required: true, + data: { + type: 12 + } + } + ] + } + }, + video: null, + banner: null + } + ], + site: { + page: 'https://we-are-adot.com/test', + domain: 'we-are-adot.com', + name: 'we-are-adot.com' + }, + device: { + ua: '', + language: 'en' + }, + user: null, + regs: null, + at: 1, + ext: { + adot: { + 'adapter_version': 'v1.0.0' + } + } + }, + _adot_internal: { + impressions: [ + { + impressionId: 'imp_id_native_0', + adUnitCode: 'ad_unit_native', + bidId: 'imp_id_native' + } + ] + } + }, + + serverResponse_banner: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_banner_0_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + h: 350, + w: 300, + ext: { + adot: { + media_type: 'banner' + } + } + } + ] + } + ] + } + }, + + serverResponse_banner_twoBids: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_banner_0_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + h: 350, + w: 300, + ext: { + adot: { + media_type: 'banner' + } + } + }, + { + impid: 'imp_id_banner_2_0_0', + crid: 'creative_id_2', + adm: 'creative_data_2_${AUCTION_PRICE}', + nurl: 'win_notice_url_2_${AUCTION_PRICE}', + price: 2.5, + h: 400, + w: 350, + ext: { + adot: { + media_type: 'banner' + } + } + } + ] + } + ] + } + }, + + serverResponse_video_instream: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_video_instream_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + ext: { + adot: { + media_type: 'video' + } + } + } + ] + } + ] + } + }, + + serverResponse_video_outstream: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_video_outstream_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + ext: { + adot: { + media_type: 'video' + } + } + } + ] + } + ] + } + }, + + serverResponse_video_instream_outstream: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_video_instream_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + ext: { + adot: { + media_type: 'video' + } + } + }, + { + impid: 'imp_id_video_outstream_0', + crid: 'creative_id', + adm: 'creative_data_${AUCTION_PRICE}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + ext: { + adot: { + media_type: 'video' + } + } + } + ] + } + ] + } + }, + + serverResponse_native: { + body: { + cur: 'EUR', + seatbid: [ + { + bid: [ + { + impid: 'imp_id_native_0', + crid: 'creative_id', + adm: '{"native":{"assets":[{"id":1,"title":{"len":140,"text":"Hi everyone"}},{"id":2,"img":{"url":"https://adotmob.com","type":1,"w":50,"h":50}},{"id":3,"img":{"url":"https://adotmob.com","type":3,"w":320,"h":200}},{"id":4,"data":{"type":1,"value":"adotmob"}},{"id":5,"data":{"type":2,"value":"This is a test ad"}},{"id":6,"data":{"type":12,"value":"Click to buy"}}],"link":{"url":"https://adotmob.com?auction=${AUCTION_PRICE}"}}}', + nurl: 'win_notice_url_${AUCTION_PRICE}', + price: 1.5, + ext: { + adot: { + media_type: 'native' + } + } + } + ] + } + ] + } + } + }; + + describe('isBidRequestValid', function () { + describe('General', function () { + it('should return false when not given an ad unit', function () { + const adUnit = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an invalid ad unit', function () { + const adUnit = 'bad_bid'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without bidder code', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidder = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a bad bidder code', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidder = 'unknownBidder'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without ad unit code', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.adUnitCode = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid ad unit code', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.adUnitCode = {}; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without bid request identifier', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidderRequestId = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid bid request identifier', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidderRequestId = {}; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without impression identifier', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidId = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid impression identifier', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.bidId = {}; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without media types', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with empty media types', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes = {}; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with invalid media types', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes = 'bad_media_types'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + }); + + describe('Banner', function () { + it('should return true when given a valid ad unit', function () { + const adUnit = examples.adUnit_banner; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given a valid ad unit without bidder parameters', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.params = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return false when given an ad unit without size', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid size', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = 'bad_banner_size'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an empty size', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = []; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid size value', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = ['bad_banner_size_value']; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a size value with less than 2 dimensions', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[300]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a size value with more than 2 dimensions', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[300, 250, 30]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a negative width value', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[-300, 250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a negative height value', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[300, -250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid width value', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[false, 250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid height value', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [[300, {}]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + }); + + describe('Video', function () { + it('should return true when given a valid outstream ad unit', function () { + const adUnit = examples.adUnit_video_outstream; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given a valid pre-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'pre-roll'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given a valid mid-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'mid-roll'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given a valid post-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'post-roll'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given an ad unit without size', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given an ad unit with an empty size', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = []; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given an ad unit without minimum duration parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.minDuration = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return true when given an ad unit without maximum duration parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.maxDuration = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(true); + }); + + it('should return false when given an ad unit without bidder parameters', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with invalid bidder parameters', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params = 'bad_bidder_parameters'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without video parameters', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with invalid video parameters', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video = 'bad_bidder_parameters'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without mime types parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.mimes = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid mime types parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.mimes = 'bad_mime_types'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an empty mime types parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.mimes = []; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid mime types parameter value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.mimes = [200]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid minimum duration parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.minDuration = 'bad_min_duration'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid maximum duration parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.maxDuration = 'bad_max_duration'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without protocols parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.protocols = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid protocols parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.protocols = 'bad_protocols'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an empty protocols parameter', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.protocols = []; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid protocols parameter value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.protocols = ['bad_protocols_value']; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an instream ad unit without instream context', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an instream ad unit with an invalid instream context', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'bad_instream_context'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit without context', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.context = undefined; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid context', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.context = []; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an adpod ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.context = 'adpod'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an unknown context', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.context = 'invalid_context'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid size', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = 'bad_video_size'; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid size value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = ['bad_video_size_value']; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a size value with less than 2 dimensions', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[300]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a size value with more than 2 dimensions', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[300, 250, 30]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a negative width value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[-300, 250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with a negative height value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[300, -250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid width value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[false, 250]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + + it('should return false when given an ad unit with an invalid height value', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[300, {}]]; + + expect(spec.isBidRequestValid(adUnit)).to.equal(false); + }); + }); + }); + + describe('buildRequests', function () { + describe('ServerRequest', function () { + it('should return a server request when given a valid ad unit and a valid ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.length(1); + expect(serverRequests[0].method).to.exist.and.to.be.a('string').and.to.equal('POST'); + expect(serverRequests[0].url).to.exist.and.to.be.a('string').and.to.equal(BIDDER_URL); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions).to.exist.and.to.be.an('array').and.to.have.length(1); + expect(serverRequests[0]._adot_internal.impressions[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions[0].impressionId).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0]._adot_internal.impressions[0].adUnitCode).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].adUnitCode); + }); + + it('should return a server request containing a position when given a valid ad unit and a valid ad unit context and a position in the bidder params', function () { + const adUnits = [examples.adUnit_position]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.length(1); + expect(serverRequests[0].method).to.exist.and.to.be.a('string').and.to.equal('POST'); + expect(serverRequests[0].url).to.exist.and.to.be.a('string').and.to.equal(BIDDER_URL); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions).to.exist.and.to.be.an('array').and.to.have.length(1); + expect(serverRequests[0]._adot_internal.impressions[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions[0].impressionId).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0]._adot_internal.impressions[0].adUnitCode).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].adUnitCode); + expect(serverRequests[0].data.imp[0].banner.pos).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.position); + }); + + it('should return a server request when given two valid ad units and a valid ad unit context', function () { + const adUnits_1 = utils.deepClone(examples.adUnit_banner); + adUnits_1.bidId = 'bid_id_1'; + adUnits_1.adUnitCode = 'ad_unit_banner_1'; + + const adUnits_2 = utils.deepClone(examples.adUnit_banner); + adUnits_2.bidId = 'bid_id_2'; + adUnits_2.adUnitCode = 'ad_unit_banner_2'; + + const adUnits = [adUnits_1, adUnits_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.length(1); + expect(serverRequests[0].method).to.exist.and.to.be.a('string').and.to.equal('POST'); + expect(serverRequests[0].url).to.exist.and.to.be.a('string').and.to.equal(BIDDER_URL); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions).to.exist.and.to.be.an('array').and.to.have.length(2); + expect(serverRequests[0]._adot_internal.impressions[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions[0].impressionId).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0]._adot_internal.impressions[0].adUnitCode).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].adUnitCode); + expect(serverRequests[0]._adot_internal.impressions[1]).to.exist.and.to.be.an('object'); + expect(serverRequests[0]._adot_internal.impressions[1].impressionId).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0_0`); + expect(serverRequests[0]._adot_internal.impressions[1].adUnitCode).to.exist.and.to.be.a('string').and.to.equal(adUnits[1].adUnitCode); + }); + + it('should return an empty server request list when given an empty ad unit list and a valid ad unit context', function () { + const adUnits = []; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.length(0); + }); + + it('should not return a server request when given no ad unit and a valid ad unit context', function () { + const serverRequests = spec.buildRequests(null, examples.adUnitContext); + + expect(serverRequests).to.equal(null); + }); + + it('should not return a server request when given a valid ad unit and no ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, null); + + expect(serverRequests).to.be.an('array').and.to.have.length(1); + }); + + it('should not return a server request when given a valid ad unit and an invalid ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, {}); + + expect(serverRequests).to.be.an('array').and.to.have.length(1); + }); + }); + + describe('BidRequest', function () { + it('should return a valid server request when given a valid ad unit', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.at).to.exist.and.to.be.a('number').and.to.equal(1); + expect(serverRequests[0].data.ext).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.ext.adot).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.ext.adot.adapter_version).to.exist.and.to.be.a('string').and.to.equal('v1.0.0'); + }); + + it('should return one server request when given one valid ad unit', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].bidderRequestId); + }); + + it('should return one server request when given two valid ad units with different impression identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_banner); + adUnit_1.bidId = 'bid_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_banner); + adUnit_2.bidId = 'bid_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[1].bidderRequestId); + }); + + it('should return two server requests when given two valid ad units with different bid request identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_banner); + adUnit_1.bidderRequestId = 'bidder_request_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_banner); + adUnit_2.bidderRequestId = 'bidder_request_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(2); + expect(serverRequests[0].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[1].data.id).to.exist.and.to.be.a('string').and.to.equal(adUnits[1].bidderRequestId); + }); + }); + + describe('Impression', function () { + describe('Banner', function () { + it('should return a server request with one impression when given a valid ad unit', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[0].banner.format).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[0].mediaTypes.banner.sizes[0][0], + h: adUnits[0].mediaTypes.banner.sizes[0][1] + }); + }); + + it('should return a server request with two impressions containing one banner formats when given a valid ad unit with two banner sizes', function () { + const adUnit = utils.deepClone(examples.adUnit_banner); + adUnit.mediaTypes.banner.sizes = [ + [300, 250], + [350, 300] + ]; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(2); + + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[0].banner.format).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[0].mediaTypes.banner.sizes[0][0], + h: adUnits[0].mediaTypes.banner.sizes[0][1] + }); + + expect(serverRequests[0].data.imp[1]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_1`); + expect(serverRequests[0].data.imp[1].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[1][0]); + expect(serverRequests[0].data.imp[1].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[1][1]); + expect(serverRequests[0].data.imp[1].banner.format).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[1].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[0].mediaTypes.banner.sizes[1][0], + h: adUnits[0].mediaTypes.banner.sizes[1][1] + }); + }); + + it('should return a server request with two impressions when given two valid ad units with different impression identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_banner); + adUnit_1.bidId = 'bid_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_banner); + adUnit_2.bidId = 'bid_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(2); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[0].banner.format).to.exist.and.to.be.an('array'); + expect(serverRequests[0].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[0].mediaTypes.banner.sizes[0][0], + h: adUnits[0].mediaTypes.banner.sizes[0][1] + }); + expect(serverRequests[0].data.imp[1]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0_0`); + expect(serverRequests[0].data.imp[1].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[1].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[1].banner.format).to.exist.and.to.be.an('array'); + expect(serverRequests[0].data.imp[1].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[1].mediaTypes.banner.sizes[0][0], + h: adUnits[1].mediaTypes.banner.sizes[0][1] + }); + }); + + it('should return a server request with one overriden impression when given two valid ad units with identical identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_banner); + adUnit_1.mediaTypes.banner.sizes = [[300, 250]]; + + const adUnit_2 = utils.deepClone(examples.adUnit_banner); + adUnit_2.mediaTypes.banner.sizes = [[350, 300]]; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0_0`); + expect(serverRequests[0].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[0].banner.format).to.exist.and.to.be.an('array'); + expect(serverRequests[0].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[1].mediaTypes.banner.sizes[0][0], + h: adUnits[1].mediaTypes.banner.sizes[0][1] + }); + }); + + it('should return two server requests with one impression when given two valid ad units with different bid request identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_banner); + adUnit_1.bidderRequestId = 'bidder_request_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_banner); + adUnit_2.bidderRequestId = 'bidder_request_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(2); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0_0`); + expect(serverRequests[0].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[0].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[0].data.imp[0].banner.format).to.exist.and.to.be.an('array'); + expect(serverRequests[0].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[0].mediaTypes.banner.sizes[0][0], + h: adUnits[0].mediaTypes.banner.sizes[0][1] + }); + expect(serverRequests[1].data).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[1].bidderRequestId); + expect(serverRequests[1].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[1].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0_0`); + expect(serverRequests[1].data.imp[0].banner).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.imp[0].banner.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][0]); + expect(serverRequests[1].data.imp[0].banner.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.banner.sizes[0][1]); + expect(serverRequests[1].data.imp[0].banner.format).to.exist.and.to.be.an('array'); + expect(serverRequests[1].data.imp[0].banner.format[0]).to.exist.and.to.be.an('object').and.to.deep.equal({ + w: adUnits[1].mediaTypes.banner.sizes[0][0], + h: adUnits[1].mediaTypes.banner.sizes[0][1] + }); + }); + }); + + describe('Video', function () { + it('should return a server request with one impression when given a valid outstream ad unit', function () { + const adUnit = examples.adUnit_video_outstream; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid pre-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'pre-roll'; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.exist.and.to.be.a('number').and.to.equal(0); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid mid-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'mid-roll'; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.exist.and.to.be.a('number').and.to.equal(-1); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid post-roll instream ad unit', function () { + const adUnit = utils.deepClone(examples.adUnit_video_instream); + adUnit.params.video.instreamContext = 'post-roll'; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.exist.and.to.be.a('number').and.to.equal(-2); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid ad unit without player size', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = undefined; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.equal(null); + expect(serverRequests[0].data.imp[0].video.h).to.equal(null); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid ad unit with an empty player size', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = []; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.equal(null); + expect(serverRequests[0].data.imp[0].video.h).to.equal(null); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid ad unit with multiple player sizes', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.mediaTypes.video.playerSize = [[350, 300], [400, 350]]; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid ad unit without minimum duration', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.minDuration = undefined; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.equal(null); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with one impression when given a valid ad unit without maximum duration', function () { + const adUnit = utils.deepClone(examples.adUnit_video_outstream); + adUnit.params.video.maxDuration = undefined; + + const adUnits = [adUnit]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.equal(null); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + }); + + it('should return a server request with two impressions when given two valid ad units with different impression identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_1.bidId = 'bid_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_2.bidId = 'bid_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(2); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + expect(serverRequests[0].data.imp[1]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0`); + expect(serverRequests[0].data.imp[1].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[1].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[1].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[1].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[1].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[1].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.minDuration); + expect(serverRequests[0].data.imp[1].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.maxDuration); + expect(serverRequests[0].data.imp[1].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[1].params.video.protocols); + }); + + it('should return a server request with one overridden impression when given two valid ad units with identical identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_1.params.video.minDuration = 10; + + const adUnit_2 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_2.params.video.minDuration = 15; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[1].params.video.protocols); + }); + + it('should return two server requests with one impression when given two valid ad units with different bid request identifiers', function () { + const adUnit_1 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_1.bidderRequestId = 'bidder_request_id_1'; + + const adUnit_2 = utils.deepClone(examples.adUnit_video_outstream); + adUnit_2.bidderRequestId = 'bidder_request_id_2'; + + const adUnits = [adUnit_1, adUnit_2]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(2); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[0].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[0].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[0].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[0].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.minDuration); + expect(serverRequests[0].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[0].params.video.maxDuration); + expect(serverRequests[0].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.protocols); + expect(serverRequests[1].data).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[1].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[1].bidId}_0`); + expect(serverRequests[1].data.imp[0].video).to.exist.and.to.be.an('object'); + expect(serverRequests[1].data.imp[0].video.mimes).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[0].params.video.mimes); + expect(serverRequests[1].data.imp[0].video.startdelay).to.equal(null); + expect(serverRequests[1].data.imp[0].video.w).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][0]); + expect(serverRequests[1].data.imp[0].video.h).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].mediaTypes.video.playerSize[0][1]); + expect(serverRequests[1].data.imp[0].video.minduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.minDuration); + expect(serverRequests[1].data.imp[0].video.maxduration).to.exist.and.to.be.a('number').and.to.equal(adUnits[1].params.video.maxDuration); + expect(serverRequests[1].data.imp[0].video.protocols).to.exist.and.to.be.an('array').and.to.deep.equal(adUnits[1].params.video.protocols); + }); + }); + + describe('Native', function () { + it('should return a server request with one impression when given a valid ad unit', function () { + const adUnits = [examples.adUnit_native]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnit_native); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp).to.exist.and.to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data.imp[0]).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].id).to.exist.and.to.be.a('string').and.to.equal(`${adUnits[0].bidId}_0`); + expect(serverRequests[0].data.imp[0].native).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.imp[0].native.request).to.exist.and.to.be.a('string').and.to.equal(JSON.stringify(examples.serverRequest_native.data.imp[0].native.request)) + }); + }); + }); + + describe('Site', function () { + it('should return a server request with site information when given a valid ad unit and a valid ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = examples.adUnitContext; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.site.page).to.exist.and.to.be.an('string').and.to.equal(adUnitContext.refererInfo.referer); + expect(serverRequests[0].data.site.id).to.equal(undefined); + expect(serverRequests[0].data.site.domain).to.exist.and.to.be.an('string').and.to.equal('we-are-adot.com'); + expect(serverRequests[0].data.site.name).to.exist.and.to.be.an('string').and.to.equal('we-are-adot.com'); + }); + + it('should return a server request without site information when not given an ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + + it('should return a server request without site information when given an ad unit context without referer information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.refererInfo = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + + it('should return a server request without site information when given an ad unit context with invalid referer information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.refererInfo = 'bad_referer_information'; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + + it('should return a server request without site information when given an ad unit context without referer', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.refererInfo.referer = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + + it('should return a server request without site information when given an ad unit context with an invalid referer', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.refererInfo.referer = {}; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + + it('should return a server request without site information when given an ad unit context with a misformatted referer', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.refererInfo.referer = 'we-are-adot'; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.site).to.equal(null); + }); + }); + + describe('Device', function () { + it('should return a server request with device information when given a valid ad unit and a valid ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const serverRequests = spec.buildRequests(adUnits, examples.adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.device).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.device.ua).to.exist.and.to.be.a('string'); + expect(serverRequests[0].data.device.language).to.exist.and.to.be.a('string'); + }); + }); + + describe('Regs', function () { + it('should return a server request with regulations information when given a valid ad unit and a valid ad unit context with GDPR applying', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = examples.adUnitContext; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.regs.ext).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.regs.ext.gdpr).to.exist.and.to.be.a('boolean').and.to.equal(adUnitContext.gdprConsent.gdprApplies); + }); + + it('should return a server request with regulations information when given a valid ad unit and a valid ad unit context with GDPR not applying', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent.gdprApplies = false; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.regs.ext).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.regs.ext.gdpr).to.exist.and.to.be.a('boolean').and.to.equal(adUnitContext.gdprConsent.gdprApplies); + }); + + it('should return a server request without regulations information when not given an ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.equal(null); + }); + + it('should return a server request without regulations information when given an ad unit context without GDPR information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.equal(null); + }); + + it('should return a server request without regulations information when given an ad unit context with invalid GDPR information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent = 'bad_gdpr_consent'; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.equal(null); + }); + + it('should return a server request without regulations information when given an ad unit context with invalid GDPR application information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent.gdprApplies = 'bad_gdpr_applies'; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.regs).to.equal(null); + }); + }); + + describe('User', function () { + it('should return a server request with user information when given a valid ad unit and a valid ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = examples.adUnitContext; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.user).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.user.ext).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.user.ext.consent).to.exist.and.to.be.a('string').and.to.equal(adUnitContext.gdprConsent.consentString); + }); + + it('should return a server request without user information when not given an ad unit context', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.user).to.equal(null); + }); + + it('should return a server request without user information when given an ad unit context without GDPR information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent = undefined; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.user).to.equal(null); + }); + + it('should return a server request without user information when given an ad unit context with invalid GDPR information', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent = 'bad_gdpr_consent'; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.user).to.equal(null); + }); + + it('should return a server request without user information when given an ad unit context with an invalid consent string', function () { + const adUnits = [examples.adUnit_banner]; + + const adUnitContext = utils.deepClone(examples.adUnitContext); + adUnitContext.gdprConsent.consentString = true; + + const serverRequests = spec.buildRequests(adUnits, adUnitContext); + + expect(serverRequests).to.be.an('array').and.to.have.lengthOf(1); + expect(serverRequests[0].data).to.exist.and.to.be.an('object'); + expect(serverRequests[0].data.id).to.exist.and.to.be.an('string').and.to.equal(adUnits[0].bidderRequestId); + expect(serverRequests[0].data.user).to.equal(null); + }); + }); + }); + + describe('interpretResponse', function () { + describe('General', function () { + it('should return an ad when given a valid server response with one bid with USD currency', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.cur = 'USD'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(null); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return two ads when given a valid server response with two bids', function () { + const serverRequest = examples.serverRequest_banner_twoImps; + + const serverResponse = examples.serverResponse_banner_twoBids; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(2); + + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(null); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[0].renderer).to.equal(null); + expect(ads[1].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[1].bidId); + expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[1].adm); + expect(ads[1].adUrl).to.equal(null); + expect(ads[1].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[1].crid); + expect(ads[1].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].price); + expect(ads[1].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[1].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[1].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[1].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].h); + expect(ads[1].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].w); + expect(ads[1].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[1].renderer).to.equal(null); + }); + + it('should return no ad when not given a server response', function () { + const ads = spec.interpretResponse(null); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when not given a server response body', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given an invalid server response body', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body = 'invalid_body'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response without seat bids', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with invalid seat bids', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid = 'invalid_seat_bids'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with an empty seat bids array', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid = []; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with an invalid seat bid', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid = 'invalid_bids'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with an empty bids array', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid = []; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with an invalid bid', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid = ['invalid_bid']; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without currency', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.cur = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid currency', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.cur = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without impression identifier', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].impid = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid impression identifier', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].impid = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without creative identifier', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].crid = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid creative identifier', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].crid = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without ad markup and ad serving URL', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].adm = undefined; + serverResponse.body.seatbid[0].bid[0].nurl = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid ad markup', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].adm = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an ad markup without auction price macro', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].adm = 'creative_data'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid ad serving URL', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].nurl = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an ad serving URL without auction price macro', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].nurl = 'win_notice_url'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without bid price', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].price = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid bid price', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].price = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without extension', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid extension', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext = 'bad_ext'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without adot extension', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext.adot = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid adot extension', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext.adot = 'bad_adot_ext'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without media type', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext.adot.media_type = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid media type', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext.adot.media_type = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an unknown media type', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].ext.adot.media_type = 'unknown_media_type'; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and no server request', function () { + const serverRequest = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and an invalid server request', function () { + const serverRequest = 'bad_server_request'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without bid request', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request with an invalid bid request', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data = 'bad_bid_request'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without impression', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data.imp = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request with an invalid impression field', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data.imp = 'invalid_impressions'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without matching impression', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data.imp[0].id = 'unknown_imp_id'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without internal data', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request with invalid internal data', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal = 'bad_internal_data'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without internal impression data', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal.impressions = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request with invalid internal impression data', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal.impressions = 'bad_internal_impression_data'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without matching internal impression', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal.impressions[0].impressionId = 'unknown_imp_id'; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without internal impression ad unit code', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal.impressions[0].adUnitCode = undefined; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request with an invalid internal impression ad unit code', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest._adot_internal.impressions[0].adUnitCode = {}; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + }); + + describe('Banner', function () { + it('should return an ad when given a valid server response with one bid', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = examples.serverResponse_banner; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(null); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with one bid without a win notice URL', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].nurl = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(null); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with one bid using an ad serving URL', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].adm = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.equal(null); + expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('banner'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return no ad when given a server response with a bid without height', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].h = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid height', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].h = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid without width', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].w = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid width', function () { + const serverRequest = examples.serverRequest_banner; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + serverResponse.body.seatbid[0].bid[0].w = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without banner impression', function () { + const serverRequest = utils.deepClone(examples.serverRequest_banner); + serverRequest.data.imp[0].banner = undefined; + + const serverResponse = utils.deepClone(examples.serverResponse_banner); + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + }); + + describe('Video', function () { + it('should return an ad when given a valid server response with one bid on an instream impression', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = examples.serverResponse_video_instream; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with one bid on an outstream impression', function () { + const serverRequest = examples.serverRequest_video_outstream; + + const serverResponse = examples.serverResponse_video_outstream; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.be.an('object'); + }); + + it('should return two ads when given a valid server response with two bids on both instream and outstream impressions', function () { + const serverRequest = examples.serverRequest_video_instream_outstream; + + const serverResponse = examples.serverResponse_video_instream_outstream; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(2); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + expect(ads[1].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[1].bidId); + expect(ads[1].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[1].adm); + expect(ads[1].adUrl).to.equal(null); + expect(ads[1].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[1].vastUrl).to.equal(null); + expect(ads[1].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[1].crid); + expect(ads[1].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[1].price); + expect(ads[1].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[1].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[1].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[1].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[1].video.w); + expect(ads[1].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[1].renderer).to.be.an('object'); + }); + + it('should return an ad when given a valid server response with one bid without a win notice URL', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].nurl = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with one bid using an ad serving URL', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].adm = undefined; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.equal(null); + expect(ads[0].adUrl).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].vastXml).to.equal(null); + expect(ads[0].vastUrl).to.equal(serverResponse.body.seatbid[0].bid[0].nurl); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with a bid with a video height', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].h = 500; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with a bid with a video width', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].w = 500; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverRequest.data.imp[0].video.h); + expect(ads[0].width).to.equal(serverRequest.data.imp[0].video.w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response with a bid with a video width and height', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].w = 500; + serverResponse.body.seatbid[0].bid[0].h = 400; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(serverResponse.body.seatbid[0].bid[0].h); + expect(ads[0].width).to.equal(serverResponse.body.seatbid[0].bid[0].w); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response and server request with a video impression without width', function () { + const serverRequest = utils.deepClone(examples.serverRequest_video_instream); + serverRequest.data.imp[0].video.w = null; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(null); + expect(ads[0].width).to.equal(null); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return an ad when given a valid server response and server request with a video impression without height', function () { + const serverRequest = utils.deepClone(examples.serverRequest_video_instream); + serverRequest.data.imp[0].video.h = null; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].ad).to.exist.and.to.be.a('string').and.to.have.string(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].adUrl).to.equal(null); + expect(ads[0].vastXml).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(ads[0].vastUrl).to.equal(null); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].height).to.equal(null); + expect(ads[0].width).to.equal(null); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('video'); + expect(ads[0].renderer).to.equal(null); + }); + + it('should return no ad when given a server response with a bid with an invalid height', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].h = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a server response with a bid with an invalid width', function () { + const serverRequest = examples.serverRequest_video_instream; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + serverResponse.body.seatbid[0].bid[0].w = {}; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + it('should return no ad when given a valid server response and a server request without video impression', function () { + const serverRequest = utils.deepClone(examples.serverRequest_video_instream); + serverRequest.data.imp[0].video = undefined; + + const serverResponse = utils.deepClone(examples.serverResponse_video_instream); + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(0); + }); + + describe('Outstream renderer', function () { + function spyAdRenderingQueue(ad) { + const spy = sinon.spy(ad.renderer, 'push'); + + this.sinonSpies.push(spy); + } + + function executeAdRenderer(ad, onRendererExecution, done) { + executeRenderer(ad.renderer, ad); + + setTimeout(() => { + try { + onRendererExecution(); + } catch (err) { + done(err); + } + + done() + }, 100); + } + + before('Bind helper functions to the Mocha context', function () { + this.spyAdRenderingQueue = spyAdRenderingQueue.bind(this); + + window.VASTPlayer = function VASTPlayer() {}; + window.VASTPlayer.prototype.loadXml = function loadXml() { + return new Promise((resolve, reject) => resolve()) + }; + window.VASTPlayer.prototype.load = function load() { + return new Promise((resolve, reject) => resolve()) + }; + window.VASTPlayer.prototype.on = function on(event, callback) {}; + window.VASTPlayer.prototype.startAd = function startAd() {}; + }); + + beforeEach('Initialize the Sinon spies list', function () { + this.sinonSpies = []; + }); + + afterEach('Clear the registered Sinon spies', function () { + this.sinonSpies.forEach(spy => spy.restore()); + }); + + after('clear data', () => { + window.VASTPlayer = null; + }); + + it('should return an ad with valid renderer', function () { + const serverRequest = examples.serverRequest_video_outstream; + const serverResponse = examples.serverResponse_video_outstream; + + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].renderer).to.be.an('object'); + }); + + it('should append a command to the ad rendering queue when executing the renderer', function (done) { + const serverRequest = examples.serverRequest_video_outstream; + const serverResponse = examples.serverResponse_video_outstream; + + const [ad] = spec.interpretResponse(serverResponse, serverRequest); + + this.spyAdRenderingQueue(ad); + + executeAdRenderer(ad, () => { + expect(ad.renderer.push.calledOnce).to.equal(true); + expect(ad.renderer.push.firstCall.args[0]).to.exist.and.to.be.a('function'); + }, done); + }); + }); + }); + + describe('Native', function () { + it('should return an ad when given a valid server response with one bid', function () { + const serverRequest = examples.serverRequest_native; + const serverResponse = examples.serverResponse_native; + const native = JSON.parse(serverResponse.body.seatbid[0].bid[0].adm).native; + const {link, assets} = native; + const ads = spec.interpretResponse(serverResponse, serverRequest); + + expect(ads).to.be.an('array').and.to.have.length(1); + expect(ads[0].requestId).to.exist.and.to.be.a('string').and.to.equal(serverRequest._adot_internal.impressions[0].bidId); + expect(ads[0].creativeId).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(ads[0].cpm).to.exist.and.to.be.a('number').and.to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(ads[0].currency).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.cur); + expect(ads[0].netRevenue).to.exist.and.to.be.a('boolean').and.to.equal(true); + expect(ads[0].ttl).to.exist.and.to.be.a('number').and.to.equal(10); + expect(ads[0].mediaType).to.exist.and.to.be.a('string').and.to.equal('native'); + expect(ads[0].native).to.exist.and.to.be.an('object'); + expect(Object.keys(ads[0].native)).to.have.length(10); + expect(ads[0].native.title).to.equal(assets[0].title.text); + expect(ads[0].native.icon.url).to.equal(assets[1].img.url); + expect(ads[0].native.icon.width).to.equal(assets[1].img.w); + expect(ads[0].native.icon.height).to.equal(assets[1].img.h); + expect(ads[0].native.image.url).to.equal(assets[2].img.url); + expect(ads[0].native.image.width).to.equal(assets[2].img.w); + expect(ads[0].native.image.height).to.equal(assets[2].img.h); + expect(ads[0].native.sponsoredBy).to.equal(assets[3].data.value); + expect(ads[0].native.body).to.equal(assets[4].data.value); + expect(ads[0].native.cta).to.equal(assets[5].data.value); + expect(ads[0].native.clickUrl).to.equal(link.url); + }); + }); + }); +}); diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index 3ec6a19d20b..306914960c3 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -1,7 +1,6 @@ import {expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/adxcgBidAdapter.js'; -import {deepClone} from '../../../src/utils.js'; +import {deepClone, parseUrl} from 'src/utils.js'; describe('AdxcgAdapter', function () { let bidBanner = { @@ -119,7 +118,7 @@ describe('AdxcgAdapter', function () { let request = spec.buildRequests([bidBanner]); expect(request).to.exist; expect(request.method).to.equal('GET'); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); expect(parsedRequestUrl.hostname).to.equal('hbps.adxcg.net'); expect(parsedRequestUrl.pathname).to.equal('/get/adi'); @@ -152,7 +151,7 @@ describe('AdxcgAdapter', function () { let request = spec.buildRequests([bidVideo]); expect(request).to.exist; expect(request.method).to.equal('GET'); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); expect(parsedRequestUrl.hostname).to.equal('hbps.adxcg.net'); expect(parsedRequestUrl.pathname).to.equal('/get/adi'); @@ -191,7 +190,7 @@ describe('AdxcgAdapter', function () { let request = spec.buildRequests([bidNative]); expect(request).to.exist; expect(request.method).to.equal('GET'); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); expect(parsedRequestUrl.hostname).to.equal('hbps.adxcg.net'); expect(parsedRequestUrl.pathname).to.equal('/get/adi'); @@ -229,7 +228,7 @@ describe('AdxcgAdapter', function () { consentString: 'consentDataString' } }); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.gdpr).to.equal('1'); @@ -243,7 +242,7 @@ describe('AdxcgAdapter', function () { consentString: 'consentDataString' } }); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.gdpr).to.be.undefined; @@ -258,7 +257,7 @@ describe('AdxcgAdapter', function () { it('should send pubcid if available', function () { let request = spec.buildRequests(bid, bidderRequests); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.pubcid).to.equal('pubcidabcd'); }); @@ -272,7 +271,7 @@ describe('AdxcgAdapter', function () { it('should send pubcid if available', function () { let request = spec.buildRequests(bid, bidderRequests); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.tdid).to.equal('tdidabcd'); }); @@ -286,7 +285,7 @@ describe('AdxcgAdapter', function () { it('should send pubcid if available', function () { let request = spec.buildRequests(bid, bidderRequests); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.id5id).to.equal('id5idsample'); }); @@ -300,7 +299,7 @@ describe('AdxcgAdapter', function () { it('should send pubcid if available', function () { let request = spec.buildRequests(bid, bidderRequests); - let parsedRequestUrl = url.parse(request.url); + let parsedRequestUrl = parseUrl(request.url); let query = parsedRequestUrl.search; expect(query.idl_env).to.equal('idl_envsample'); }); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index 0dd6b5ad065..3573681dd17 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -1,5 +1,4 @@ import { expect } from 'chai'; -import { parse } from '../../../src/url.js'; import { spec } from 'modules/adyoulikeBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index a3d019c83d2..8934d1cdaef 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -164,6 +164,7 @@ describe('AppNexusAdapter', function () { const payload = JSON.parse(request.data); expect(payload.tags[0].ad_types).to.deep.equal(['video']); + expect(payload.tags[0].hb_source).to.deep.equal(1); }); it('sends bid request to ENDPOINT via POST', function () { @@ -193,6 +194,7 @@ describe('AppNexusAdapter', function () { id: 123, minduration: 100 }); + expect(payload.tags[0].hb_source).to.deep.equal(1); }); it('should add video property when adUnit includes a renderer', function () { @@ -256,7 +258,7 @@ describe('AppNexusAdapter', function () { expect(payload.user).to.exist; expect(payload.user).to.deep.equal({ - externalUid: '123', + external_uid: '123', }); }); @@ -417,6 +419,44 @@ describe('AppNexusAdapter', function () { expect(payload3.tags.length).to.equal(15); }); + it('should contain hb_source value for adpod', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 300, + durationRangeSec: [15, 30], + } + } + } + ); + const request = spec.buildRequests([bidRequest])[0]; + const payload = JSON.parse(request.data); + expect(payload.tags[0].hb_source).to.deep.equal(7); + }); + + it('should contain hb_source value for other media', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'banner', + params: { + sizes: [[300, 250], [300, 600]], + placementId: 13144370 + } + } + ); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + it('adds brand_category_exclusion to request when set', function() { let bidRequest = Object.assign({}, bidRequests[0]); sinon @@ -480,6 +520,7 @@ describe('AppNexusAdapter', function () { saleprice: {required: true}, privacy_supported: true }); + expect(payload.tags[0].hb_source).to.equal(1); }); it('should always populated tags[].sizes with 1,1 for native if otherwise not defined', function () { @@ -583,6 +624,7 @@ describe('AppNexusAdapter', function () { bidderRequest.bids = bidRequests; const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.options).to.be.empty; const payload = JSON.parse(request.data); expect(payload.gdpr_consent).to.exist; @@ -733,6 +775,32 @@ describe('AppNexusAdapter', function () { config.getConfig.restore(); }); + + it('should set withCredentials to false if purpose 1 consent is not given', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'appnexus', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true, + apiVersion: 2, + vendorData: { + purpose: { + consents: { + 1: false + } + } + } + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.options).to.deep.equal({withCredentials: false}); + }); }) describe('interpretResponse', function () { @@ -1028,17 +1096,29 @@ describe('AppNexusAdapter', function () { it('should add deal_priority and deal_code', function() { let responseWithDeal = deepClone(response); - responseWithDeal.tags[0].ads[0].deal_priority = 'high'; + responseWithDeal.tags[0].ads[0].ad_type = 'video'; + responseWithDeal.tags[0].ads[0].deal_priority = 5; responseWithDeal.tags[0].ads[0].deal_code = '123'; + responseWithDeal.tags[0].ads[0].rtb.video = { + duration_ms: 1500, + player_width: 640, + player_height: 340, + }; let bidderRequest = { bids: [{ bidId: '3db3773286ee59', - adUnitCode: 'code' + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'adpod' + } + } }] } let result = spec.interpretResponse({ body: responseWithDeal }, {bidderRequest}); expect(Object.keys(result[0].appnexus)).to.include.members(['buyerMemberId', 'dealPriority', 'dealCode']); + expect(result[0].video.dealTier).to.equal(5); }); it('should add advertiser id', function() { diff --git a/test/spec/modules/atsAnalyticsAdapter_spec.js b/test/spec/modules/atsAnalyticsAdapter_spec.js index 10fe7de7203..3dd6f261c11 100644 --- a/test/spec/modules/atsAnalyticsAdapter_spec.js +++ b/test/spec/modules/atsAnalyticsAdapter_spec.js @@ -80,7 +80,7 @@ describe('ats analytics adapter', function () { 'user_browser': (browserIsFirefox() || browserIsEdge() || browserIsChrome() || browserIsSafari()), 'user_platform': navigator.platform, 'auction_start': '2020-02-03T14:14:25.161Z', - 'domain': 'https://example.com/dev', + 'domain': window.location.hostname, 'pid': '10433394', 'response_time_stamp': '2020-02-03T14:23:11.978Z', 'currency': 'USD', diff --git a/test/spec/modules/automatadBidAdapter_spec.js b/test/spec/modules/automatadBidAdapter_spec.js new file mode 100644 index 00000000000..788fd3aefc4 --- /dev/null +++ b/test/spec/modules/automatadBidAdapter_spec.js @@ -0,0 +1,144 @@ +import { expect } from 'chai' +import { spec } from 'modules/automatadBidAdapter.js' +import { newBidder } from 'src/adapters/bidderFactory.js' + +describe('automatadBidAdapter', function () { + const adapter = newBidder(spec) + + let bidRequest = { + bidder: 'automatad', + params: {siteId: '123ad', placementId: '123abc345'}, + mediaTypes: { + banner: { + sizes: [[300, 600]], + } + }, + adUnitCode: 'some-ad-unit-code', + transactionId: '1465569e-52cc-4c36-88a1-7174cfef4b44', + sizes: [[300, 600]], + bidId: '123abc', + bidderRequestId: '3213887463c059', + auctionId: 'abc-123', + src: 'client', + bidRequestsCount: 1 + } + + let expectedResponse = [{ + 'body': { + 'id': 'abc-123', + 'seatbid': [ + { + 'bid': [ + { + 'adm': '', + 'adomain': [ + 'someAdDomain' + ], + 'crid': 123, + 'h': 600, + 'id': 'bid1', + 'impid': '1', + 'nurl': 'https://example/win', + 'price': 0.5, + 'w': 300 + } + ] + } + ] + } + }] + + describe('codes', function () { + it('should return a bidder code of automatad', function () { + expect(spec.code).to.equal('automatad') + }) + it('should alias atd', function () { + expect(spec.aliases.length > 0 && spec.aliases[0] === 'atd').to.be.true + }) + }) + + describe('isBidRequestValid', function () { + let inValidBid = Object.assign({}, bidRequest) + delete inValidBid.params + it('should return true if all params present', function () { + expect(spec.isBidRequestValid(bidRequest)).to.equal(true) + }) + + it('should return false if any parameter missing', function () { + expect(spec.isBidRequestValid(inValidBid)).to.be.false + }) + }) + + describe('buildRequests', function () { + let req = spec.buildRequests([ bidRequest ], { refererInfo: { } }) + let rdata + + it('should return request object', function () { + expect(req).to.not.be.null + }) + + it('should build request data', function () { + expect(req.data).to.not.be.null + }) + + it('should include one request', function () { + rdata = JSON.parse(req.data) + expect(rdata.imp.length).to.equal(1) + }) + + it('should include media types', function () { + let r = rdata.imp[0] + expect(r.media_types !== null).to.be.true + }) + + it('should include all publisher params', function () { + let r = rdata.imp[0] + expect(r.siteID !== null && r.placementID !== null).to.be.true + }) + }) + + describe('interpretResponse', function () { + it('should get the correct bid response', function () { + let result = spec.interpretResponse(expectedResponse[0]) + expect(result).to.be.an('array').that.is.not.empty + }) + + it('handles empty bid response', function () { + let response = { + body: '' + } + let result = spec.interpretResponse(response) + expect(result.length).to.equal(0) + }) + }) + + describe('getUserSyncs', function () { + it('should return iframe sync', function () { + let sync = spec.getUserSyncs() + expect(sync.length).to.equal(1) + expect(sync[0].type === 'iframe') + expect(typeof sync[0].url === 'string') + }) + }) + + describe('onBidWon', function () { + let serverResponses = spec.interpretResponse(expectedResponse[0]) + let wonbid = serverResponses[0] + let ajaxStub + + beforeEach(() => { + ajaxStub = sinon.stub(spec, 'ajaxCall') + }) + + afterEach(() => { + ajaxStub.restore() + }) + + it('Returns true is nurl is good/not blank', function () { + expect(wonbid.nurl).to.not.equal('') + expect(spec.onBidWon(wonbid)).to.be.true + expect(ajaxStub.calledOnce).to.equal(true) + expect(ajaxStub.firstCall.args[0].indexOf('https://')).to.equal(0) + }) + }) +}) diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index 7879ed37dc0..f7dcc1305e5 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import { spec, VIDEO_ENDPOINT, BANNER_ENDPOINT, OUTSTREAM_SRC, DEFAULT_MIMES } from 'modules/beachfrontBidAdapter.js'; -import { parse as parseUrl } from 'src/url.js'; +import { parseUrl } from 'src/utils.js'; describe('BeachfrontAdapter', function () { let bidRequests; diff --git a/test/spec/modules/betweenBidAdapter_spec.js b/test/spec/modules/betweenBidAdapter_spec.js index 3a6be953d7b..5dff32ca4d8 100644 --- a/test/spec/modules/betweenBidAdapter_spec.js +++ b/test/spec/modules/betweenBidAdapter_spec.js @@ -201,4 +201,24 @@ describe('betweenBidAdapterTests', function () { expect(syncs).to.be.an('array').that.to.have.lengthOf(1); expect(syncs[0]).to.deep.equal({type: 'iframe', url: 'https://ads.betweendigital.com/sspmatch-iframe'}); }); + + it('check sizes', function() { + let bidRequestData = [{ + bidId: 'bid1234', + bidder: 'between', + mediaTypes: { + banner: { + sizes: [[970, 250], [240, 400], [728, 90]] + } + }, + params: { + s: 1112, + }, + }]; + + let request = spec.buildRequests(bidRequestData); + let req_data = request[0].data; + + expect(req_data.sizes).to.deep.equal(['970x250', '240x400', '728x90']); + }); }); diff --git a/test/spec/modules/bidfluenceBidAdapter_spec.js b/test/spec/modules/bidfluenceBidAdapter_spec.js index ff33715a176..6b3a0c2b044 100644 --- a/test/spec/modules/bidfluenceBidAdapter_spec.js +++ b/test/spec/modules/bidfluenceBidAdapter_spec.js @@ -51,30 +51,30 @@ describe('Bidfluence Adapter test', () => { }); describe('buildRequests', function () { - const request = spec.buildRequests(validBidRequests, bidderRequest); - it('sends bid request to our endpoint via POST', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); expect(request.method).to.equal('POST'); - }); + const payload = JSON.parse(request.data); - const payload = JSON.parse(request.data); - - expect(payload.bids[0].bid).to.equal(validBidRequests[0].bidId); - expect(payload.azr).to.equal(true); - expect(payload.ck).to.not.be.undefined; - expect(payload.bids[0].tid).to.equal(PLACEMENT_ID); - expect(payload.bids[0].pid).to.equal(PUB_ID); - expect(payload.bids[0].rp).to.be.a('number'); - expect(payload.re).to.not.be.undefined; - expect(payload.st).to.not.be.undefined; - expect(payload.tz).to.not.be.undefined; - expect(payload.sr).to.not.be.undefined; - expect(payload.vp).to.not.be.undefined; - expect(payload.sdt).to.not.be.undefined; - expect(payload.bids[0].w).to.equal('300'); - expect(payload.bids[0].h).to.equal('250'); + expect(payload.bids[0].bid).to.equal(validBidRequests[0].bidId); + expect(payload.azr).to.equal(true); + expect(payload.ck).to.not.be.undefined; + expect(payload.bids[0].tid).to.equal(PLACEMENT_ID); + expect(payload.bids[0].pid).to.equal(PUB_ID); + expect(payload.bids[0].rp).to.be.a('number'); + expect(payload.re).to.not.be.undefined; + expect(payload.st).to.not.be.undefined; + expect(payload.tz).to.not.be.undefined; + expect(payload.sr).to.not.be.undefined; + expect(payload.vp).to.not.be.undefined; + expect(payload.sdt).to.not.be.undefined; + expect(payload.bids[0].w).to.equal('300'); + expect(payload.bids[0].h).to.equal('250'); + }); it('sends gdpr info if exists', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); expect(payload.gdpr).to.equal(true); expect(payload.gdprc).to.equal(CONSENT_STRING); }); diff --git a/test/spec/modules/bidlabBidAdapter_spec.js b/test/spec/modules/bidlabBidAdapter_spec.js new file mode 100644 index 00000000000..cffd43ae6ca --- /dev/null +++ b/test/spec/modules/bidlabBidAdapter_spec.js @@ -0,0 +1,235 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/bidlabBidAdapter.js'; + +describe('BidlabBidAdapter', function () { + let bid = { + bidId: '23fhj33i987f', + bidder: 'bidlab', + params: { + placementId: 0, + traffic: 'banner' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and placementId parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://service.bidlab.ai/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes'); + expect(placement.placementId).to.equal(0); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal('banner'); + }); + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + describe('getUserSyncs', function () { + let userSync = spec.getUserSyncs(); + it('Returns valid URL and type', function () { + if (spec.noSync) { + expect(userSync).to.be.equal(false); + } else { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync[0].type).to.be.equal('image'); + expect(userSync[0].url).to.be.equal('https://service.bidlab.ai/?c=o&m=sync'); + } + }); + }); +}); diff --git a/test/spec/modules/categoryTranslation_spec.js b/test/spec/modules/categoryTranslation_spec.js index f8684c0d7c0..555fe3d6357 100644 --- a/test/spec/modules/categoryTranslation_spec.js +++ b/test/spec/modules/categoryTranslation_spec.js @@ -1,4 +1,4 @@ -import { getAdserverCategoryHook, initTranslation } from 'modules/categoryTranslation.js'; +import { getAdserverCategoryHook, initTranslation, storage } from 'modules/categoryTranslation.js'; import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; import { expect } from 'chai'; @@ -9,7 +9,7 @@ describe('category translation', function () { beforeEach(function () { fakeTranslationServer = sinon.fakeServer.create(); - getLocalStorageStub = sinon.stub(utils, 'getDataFromLocalStorage'); + getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); afterEach(function() { diff --git a/test/spec/modules/cedatoBidAdapter_spec.js b/test/spec/modules/cedatoBidAdapter_spec.js index bb71f0ff3ea..a7f4875afff 100644 --- a/test/spec/modules/cedatoBidAdapter_spec.js +++ b/test/spec/modules/cedatoBidAdapter_spec.js @@ -50,12 +50,12 @@ describe('the cedato adapter', function () { }); it('should build a very basic request', function() { - var request = spec.buildRequests([bid], bidRequestObj); + var [request] = spec.buildRequests([bid], bidRequestObj); expect(request.method).to.equal('POST'); }); it('should pass gdpr and usp strings to server', function() { - var request = spec.buildRequests([bid], bidRequestObj); + var [request] = spec.buildRequests([bid], bidRequestObj); var payload = JSON.parse(request.data); expect(payload.gdpr_consent).to.not.be.undefined; expect(payload.gdpr_consent.consent_string).to.equal(bidRequestObj.gdprConsent.consentString); diff --git a/test/spec/modules/clickforceBidAdapter_spec.js b/test/spec/modules/clickforceBidAdapter_spec.js index d0fb9439534..dad00f94641 100644 --- a/test/spec/modules/clickforceBidAdapter_spec.js +++ b/test/spec/modules/clickforceBidAdapter_spec.js @@ -175,7 +175,7 @@ describe('ClickforceAdapter', function () { } let userSync = spec.getUserSyncs(syncOptions); expect(userSync[0].type).to.equal('iframe'); - expect(userSync[0].url).to.equal('https://cdn.doublemax.net/js/capmapping.htm'); + expect(userSync[0].url).to.equal('https://cdn.holmesmind.com/js/capmapping.htm'); }); it('should register type is image', function () { @@ -184,7 +184,7 @@ describe('ClickforceAdapter', function () { } let userSync = spec.getUserSyncs(syncOptions); expect(userSync[0].type).to.equal('image'); - expect(userSync[0].url).to.equal('https://c.doublemax.net/cm'); + expect(userSync[0].url).to.equal('https://c.holmesmind.com/cm'); }); }); }); diff --git a/test/spec/modules/colombiaBidAdapter_spec.js b/test/spec/modules/colombiaBidAdapter_spec.js new file mode 100644 index 00000000000..4e80c6b1d9d --- /dev/null +++ b/test/spec/modules/colombiaBidAdapter_spec.js @@ -0,0 +1,152 @@ +import { expect } from 'chai'; +import { spec } from 'modules/colombiaBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; + +const HOST_NAME = document.location.protocol + '//' + window.location.host; +const ENDPOINT = 'https://ade.clmbtech.com/cde/prebid.htm'; + +describe('colombiaBidAdapter', function() { + const adapter = newBidder(spec); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'colombia', + 'params': { + placementId: '307466' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250] + ], + 'bidId': '23beaa6af6cdde', + 'bidderRequestId': '19c0c1efdf37e7', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when placementId not passed correctly', function () { + bid.params.placementId = ''; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when require params are not passed', function () { + let bid = Object.assign({}, bid); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests = [ + { + 'bidder': 'colombia', + 'params': { + placementId: '307466' + }, + 'adUnitCode': 'adunit-code1', + 'sizes': [ + [300, 250] + ], + 'bidId': '23beaa6af6cdde', + 'bidderRequestId': '19c0c1efdf37e7', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + }, + { + 'bidder': 'colombia', + 'params': { + placementId: '307466' + }, + 'adUnitCode': 'adunit-code2', + 'sizes': [ + [300, 250] + ], + 'bidId': '382091349b149f"', + 'bidderRequestId': '"1f9c98192de251"', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + } + ]; + + const request = spec.buildRequests(bidRequests); + + it('sends bid request to our endpoint via POST', function () { + expect(request[0].method).to.equal('POST'); + expect(request[1].method).to.equal('POST'); + }); + + it('attaches source and version to endpoint URL as query params', function () { + expect(request[0].url).to.equal(ENDPOINT); + expect(request[1].url).to.equal(ENDPOINT); + }); + }); + + describe('interpretResponse', function () { + let bidRequest = [ + { + 'method': 'POST', + 'url': 'https://ade.clmbtech.com/cde/prebid.htm', + 'data': { + 'v': 'hb1', + 'p': '307466', + 'w': '300', + 'h': '250', + 'cb': 12892917383, + 'r': 'http%3A%2F%2Flocalhost%3A9876%2F%3Fid%3D74552836', + 'uid': '23beaa6af6cdde', + 't': 'i', + } + } + ]; + + let serverResponse = { + body: { + 'ad': '
This is test case for colombia adapter
', + 'cpm': 3.14, + 'creativeId': '6b958110-612c-4b03-b6a9-7436c9f746dc-1sk24', + 'currency': 'USD', + 'uid': '23beaa6af6cdde', + 'width': 728, + 'height': 90, + 'netRevenue': true, + 'ttl': 600, + 'dealid': '', + 'referrer': 'http%3A%2F%2Flocalhost%3A9876%2F%3Fid%3D74552836' + } + }; + + it('should get the correct bid response', function () { + let expectedResponse = [{ + 'requestId': '23beaa6af6cdde', + 'cpm': 3.14, + 'width': 728, + 'height': 90, + 'creativeId': '6b958110-612c-4b03-b6a9-7436c9f746dc-1sk24', + 'dealId': '', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'referrer': 'http%3A%2F%2Flocalhost%3A9876%2F%3Fid%3D74552836', + 'ad': '
This is test case for colombia adapter
' + }]; + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + }); + + it('handles empty bid response', function () { + let response = { + body: { + 'uid': '23beaa6af6cdde', + 'height': 0, + 'creativeId': '', + 'statusMessage': 'Bid returned empty or error response', + 'width': 0, + 'cpm': 0 + } + }; + let result = spec.interpretResponse(response, bidRequest[0]); + expect(result.length).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js index d2343516b6a..df9bdcbd47b 100644 --- a/test/spec/modules/colossussspBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -88,7 +88,7 @@ describe('ColossussspAdapter', function () { let placements = data['placements']; for (let i = 0; i < placements.length; i++) { let placement = placements[i]; - expect(placement).to.have.all.keys('placementId', 'bidId', 'traffic', 'sizes', 'schain'); + expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'sizes', 'schain'); expect(placement.schain).to.be.an('object') expect(placement.placementId).to.be.a('number'); expect(placement.bidId).to.be.a('string'); @@ -102,6 +102,36 @@ describe('ColossussspAdapter', function () { expect(data.placements).to.be.an('array').that.is.empty; }); }); + + describe('buildRequests with user ids', function () { + bid.userId = {} + bid.userId.britepoolid = 'britepoolid123'; + bid.userId.idl_env = 'idl_env123'; + bid.userId.tdid = 'tdid123'; + bid.userId.id5id = 'id5id123' + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + let placements = data['placements']; + expect(data).to.be.an('object'); + for (let i = 0; i < placements.length; i++) { + let placement = placements[i]; + expect(placement).to.have.property('eids') + expect(placement.eids).to.be.an('array') + expect(placement.eids.length).to.be.equal(4) + for (let index in placement.eids) { + let v = placement.eids[index]; + expect(v).to.have.all.keys('source', 'uids') + expect(v.source).to.be.oneOf(['britepool.com', 'identityLink', 'adserver.org', 'id5-sync.com']) + expect(v.uids).to.be.an('array'); + expect(v.uids.length).to.be.equal(1) + expect(v.uids[0]).to.have.property('id') + expect(v.uids[0].id).to.be.oneOf(['britepoolid123', 'idl_env123', 'tdid123', 'id5id123']) + } + } + }); + }); + describe('interpretResponse', function () { let resObject = { body: [ { diff --git a/test/spec/modules/connectadBidAdapter_spec.js b/test/spec/modules/connectadBidAdapter_spec.js new file mode 100644 index 00000000000..626018241c4 --- /dev/null +++ b/test/spec/modules/connectadBidAdapter_spec.js @@ -0,0 +1,453 @@ +import {expect} from 'chai'; +import {spec} from 'modules/connectadBidAdapter.js'; +import { config } from 'src/config.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; + +describe('ConnectAd Adapter', function () { + let bidRequests; + let bidderRequest; + let bidRequestsUserIds; + + beforeEach(function () { + bidRequests = [ + { + bidder: 'conntectad', + params: { + siteId: 123456, + networkId: 123456 + }, + adUnitCode: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bidId: '2f95c00074b931', + auctionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df' + } + ]; + + bidRequestsUserIds = [{ + bidder: 'conntectad', + params: { + siteId: 123456, + networkId: 123456 + }, + adUnitCode: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bidId: '2f95c00074b931', + auctionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', + userId: { + tdid: '123456', + digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}} + } + }]; + + bidderRequest = { + timeout: 3000, + gdprConsent: { + gdprApplies: true, + consentString: 'consentDataString', + vendorData: {} + } + } + }); + + describe('inherited functions', function () { + it('should exists and is a function', function () { + const adapter = newBidder(spec); + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('implementation', function () { + describe('for requests', function () { + it('should accept bid', function () { + let validBid = { + bidder: 'connectad', + params: { + siteId: 123456, + networkId: 123456 + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + } + }; + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + + it('should reject if missing sizes', function () { + let invalidBid = { + bidder: 'connectad', + params: { + siteId: 123456, + } + }; + const isValid = spec.isBidRequestValid(invalidBid); + expect(isValid).to.equal(false); + }); + + it('should return true when optional bidFloor params found for an ad', function () { + let validBid = { + bidder: 'connectad', + params: { + siteId: 123456, + networkId: 123456, + bidfloor: 0.20 + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + } + }; + const isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true) + }); + + it('should reject if missing siteId/networkId', function () { + let invalidBid = { + bidder: 'connectad', + params: {}, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + } + }; + const isValid = spec.isBidRequestValid(invalidBid); + expect(isValid).to.equal(false); + }); + + it('should reject if missing networkId', function () { + let invalidBid = { + bidder: 'connectad', + params: { + siteId: 123456 + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + } + }; + const isValid = spec.isBidRequestValid(invalidBid); + expect(isValid).to.equal(false); + }); + + it('should contain SiteId and NetworkId', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.placements[0].siteId).to.equal(123456); + expect(requestparse.placements[0].networkId).to.equal(123456); + }); + + it('should contain gdpr info', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.user.ext.gdpr).to.equal(1); + expect(requestparse.user.ext.consent).to.equal('consentDataString'); + }); + + it('should build a request if Consent but no gdprApplies', function () { + let bidderRequest = { + timeout: 3000, + gdprConsent: { + gdprApplies: false, + consentString: 'consentDataString', + }, + } + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.placements[0].adTypes).to.be.an('array'); + expect(requestparse.placements[0].siteId).to.equal(123456); + expect(requestparse.user.ext.consent).to.equal('consentDataString'); + }); + + it('should build a request if gdprConsent empty', function () { + let bidderRequest = { + timeout: 3000, + gdprConsent: {} + } + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.placements[0].adTypes).to.be.an('array'); + expect(requestparse.placements[0].siteId).to.equal(123456); + }); + + it('should have CCPA Consent if defined', function () { + const uspConsent = '1YYN' + bidderRequest.uspConsent = uspConsent + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.user.ext.us_privacy).to.equal(uspConsent); + }); + + it('should not have CCPA Consent if not defined', function () { + bidderRequest.uspConsent = undefined + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.user.ext.us_privacy).to.be.undefined; + }); + + it('should not include schain when not provided', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.source).to.not.exist; + }); + + it('should submit coppa if set in config', function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.user.coppa).to.equal(1); + config.getConfig.restore(); + }); + + it('should send all UserData data', function () { + const request = spec.buildRequests(bidRequestsUserIds, bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.user.ext.eids).to.be.an('array'); + expect(requestparse.user.ext.eids[0].uids[0].id).to.equal('123456'); + expect(requestparse.user.ext.digitrust.id).to.equal('DTID'); + }); + + it('should add referer info', function () { + const bidRequest = Object.assign({}, bidRequests[0]) + const bidderRequ = { + refererInfo: { + referer: 'https://connectad.io/page.html', + reachedTop: true, + numIframes: 2, + stack: [ + 'https://connectad.io/page.html', + 'https://connectad.io/iframe1.html', + 'https://connectad.io/iframe2.html' + ] + } + } + const request = spec.buildRequests([bidRequest], bidderRequ); + const requestparse = JSON.parse(request.data); + + expect(requestparse.referrer_info).to.exist; + }); + + it('should populate schain', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'reseller1.com', + 'sid': 'absc1', + 'hp': 1 + } + ] + } + }); + + const request = spec.buildRequests([bidRequest], bidderRequest); + const requestparse = JSON.parse(request.data); + expect(requestparse.source.ext.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'reseller1.com', + 'sid': 'absc1', + 'hp': 1 + } + ] + }); + }); + }); + + describe('bid responses', function () { + it('should return complete bid response', function () { + let serverResponse = { + body: { + decisions: { + '2f95c00074b931': { + adId: '0', + contents: [ + { + body: '<<<---- Creative --->>>' + } + ], + height: '250', + width: '300', + pricing: { + clearPrice: 11.899999999999999 + } + } + } + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(1); + expect(bids[0].cpm).to.equal(11.899999999999999); + expect(bids[0].width).to.equal('300'); + expect(bids[0].height).to.equal('250'); + expect(bids[0].ad).to.have.length.above(1); + }); + + it('should return empty bid response', function () { + let serverResponse = { + body: { + decisions: [] + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response on incorrect size', function () { + let serverResponse = { + body: { + decisions: { + '2f95c00074b931': { + adId: '0', + contents: [ + { + body: '<<<---- Creative --->>>' + } + ], + height: '160', + width: '600', + pricing: { + clearPrice: 0 + } + } + } + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response on 0 cpm', function () { + let serverResponse = { + body: { + decisions: { + '2f95c00074b931': { + adId: '0', + contents: [ + { + body: '<<<---- Creative --->>>' + } + ], + height: '300', + width: '250', + pricing: { + clearPrice: 0 + } + } + } + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.lengthOf(0); + }); + + it('should process a deal id', function () { + let serverResponse = { + body: { + decisions: { + '2f95c00074b931': { + adId: '0', + dealid: 'ABC90210', + contents: [ + { + body: '<<<---- Creative --->>>' + } + ], + height: '300', + width: '250', + pricing: { + clearPrice: 11.899999999999999 + } + } + } + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.lengthOf(1); + expect(bids[0].dealid).to.equal('ABC90210'); + }); + }); + }); + + describe('getUserSyncs', () => { + let testParams = [ + { + name: 'iframe/no gdpr or ccpa', + arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, null], + expect: { + type: 'iframe', + pixels: ['https://cdn.connectad.io/connectmyusers.php?'] + } + }, + { + name: 'iframe/gdpr', + arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, {gdprApplies: true, consentString: '234234'}], + expect: { + type: 'iframe', + pixels: ['https://cdn.connectad.io/connectmyusers.php?gdpr=1&gdpr_consent=234234&'] + } + }, + { + name: 'iframe/ccpa', + arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, null, 'YN12'], + expect: { + type: 'iframe', + pixels: ['https://cdn.connectad.io/connectmyusers.php?us_privacy=YN12&'] + } + }, + { + name: 'iframe/ccpa & gdpr', + arguments: [{ iframeEnabled: true, pixelEnabled: true }, {}, {gdprApplies: true, consentString: '234234'}, 'YN12'], + expect: { + type: 'iframe', + pixels: ['https://cdn.connectad.io/connectmyusers.php?gdpr=1&gdpr_consent=234234&us_privacy=YN12&'] + } + } + ]; + + for (let i = 0; i < testParams.length; i++) { + let currParams = testParams[i]; + it(currParams.name, function () { + const result = spec.getUserSyncs.apply(this, currParams.arguments); + expect(result).to.have.lengthOf(currParams.expect.pixels.length); + for (let ix = 0; ix < currParams.expect.pixels.length; ix++) { + expect(result[ix].url).to.equal(currParams.expect.pixels[ix]); + expect(result[ix].type).to.equal(currParams.expect.type); + } + }); + } + }); +}); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index fd6973a29d8..fb33094e151 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -1,4 +1,4 @@ -import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, allowAuction, staticConsentData } from 'modules/consentManagement.js'; +import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, allowAuction, staticConsentData, gdprScope } from 'modules/consentManagement.js'; import { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; @@ -25,6 +25,7 @@ describe('consentManagement', function () { expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(10000); expect(allowAuction).to.be.true; + expect(gdprScope).to.be.equal(false); sinon.assert.callCount(utils.logInfo, 4); }); @@ -50,13 +51,15 @@ describe('consentManagement', function () { let allConfig = { cmpApi: 'iab', timeout: 7500, - allowAuctionWithoutConsent: false + allowAuctionWithoutConsent: false, + defaultGdprScope: true }; setConsentConfig(allConfig); expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(7500); expect(allowAuction).to.be.false; + expect(gdprScope).to.be.true; }); it('should use new consent manager config structure for gdpr', function () { @@ -108,6 +111,7 @@ describe('consentManagement', function () { expect(userCMP).to.be.equal('iab'); expect(consentTimeout).to.be.equal(3333); expect(allowAuction).to.be.equal(false); + expect(gdprScope).to.be.equal(false); }); }); @@ -241,6 +245,7 @@ describe('consentManagement', function () { expect(userCMP).to.be.equal('static'); expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used expect(allowAuction).to.be.false; + expect(gdprScope).to.be.equal(false); expect(staticConsentData).to.be.equal(staticConfig.consentData); }); }); @@ -353,6 +358,31 @@ describe('consentManagement', function () { expect(consent.gdprApplies).to.be.true; sinon.assert.notCalled(cmpStub); }); + + it('should not set consent.gdprApplies to true if defaultGdprScope is true', function () { + let testConsentData = { + gdprApplies: false, + consentData: 'xyz' + }; + + cmpStub = sinon.stub(window, '__cmp').callsFake((...args) => { + args[2](testConsentData); + }); + + setConsentConfig({ + cmpApi: 'iab', + timeout: 7500, + defaultGdprScope: true + }); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + + let consent = gdprDataHandler.getConsentData(); + + expect(consent.gdprApplies).to.be.false; + }); }); describe('iframe tests', function () { @@ -642,7 +672,7 @@ describe('consentManagement', function () { sinon.assert.calledOnce(utils.logWarn); expect(didHookReturn).to.be.true; expect(consent.consentString).to.be.undefined; - expect(consent.gdprApplies).to.be.undefined; + expect(consent.gdprApplies).to.be.false; expect(consent.apiVersion).to.equal(2); }); }); diff --git a/test/spec/modules/convergeBidAdapter_spec.js b/test/spec/modules/convergeBidAdapter_spec.js new file mode 100644 index 00000000000..e92ed475497 --- /dev/null +++ b/test/spec/modules/convergeBidAdapter_spec.js @@ -0,0 +1,899 @@ +import { expect } from 'chai'; +import { spec, resetUserSync, getSyncUrl } from 'modules/convergeBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('ConvergeAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'converge', + 'params': { + 'uid': '1' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'uid': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + function parseRequest(url) { + const res = {}; + url.split('&').forEach((it) => { + const couple = it.split('='); + res[couple[0]] = decodeURIComponent(couple[1]); + }); + return res; + } + + const bidderRequest = { + refererInfo: { + referer: 'https://example.com' + } + }; + const referrer = bidderRequest.refererInfo.referer; + + let bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90], [300, 250]], + 'bidId': '3150ccb55da321', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '42dbe3a7168a6a', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('should attach valid params to the tag', function () { + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '59'); + expect(payload).to.have.property('sizes', '300x250,300x600'); + expect(payload).to.have.property('r', '22edbae2733bf6'); + expect(payload).to.have.property('wrapperType', 'Prebid_js'); + expect(payload).to.have.property('wrapperVersion', '$prebid.version$'); + }); + + it('sizes must not be duplicated', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '59,59,60'); + expect(payload).to.have.property('sizes', '300x250,300x600,728x90'); + expect(payload).to.have.property('r', '22edbae2733bf6'); + }); + + it('pt parameter must be "gross" if params.priceType === "gross"', function () { + bidRequests[1].params.priceType = 'gross'; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('pt', 'gross'); + expect(payload).to.have.property('auids', '59,59,60'); + expect(payload).to.have.property('sizes', '300x250,300x600,728x90'); + expect(payload).to.have.property('r', '22edbae2733bf6'); + delete bidRequests[1].params.priceType; + }); + + it('pt parameter must be "net" or "gross"', function () { + bidRequests[1].params.priceType = 'some'; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '59,59,60'); + expect(payload).to.have.property('sizes', '300x250,300x600,728x90'); + expect(payload).to.have.property('r', '22edbae2733bf6'); + delete bidRequests[1].params.priceType; + }); + + it('if gdprConsent is present payload must have gdpr params', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {consentString: 'AAA', gdprApplies: true}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '1'); + }); + + it('if gdprApplies is false gdpr_applies must be 0', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {consentString: 'AAA', gdprApplies: false}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '0'); + }); + + it('if gdprApplies is undefined gdpr_applies must be 1', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {consentString: 'AAA'}}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '1'); + }); + + it('if usPrivacy is present payload must have us_privacy param', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('us_privacy', '1YNN'); + }); + + it('should convert keyword params to proper form and attaches to request', function () { + const bidRequestWithKeywords = [].concat(bidRequests); + bidRequestWithKeywords[1] = Object.assign({}, + bidRequests[1], + { + params: { + uid: '59', + keywords: { + single: 'val', + singleArr: ['val'], + singleArrNum: [5], + multiValMixed: ['value1', 2, 'value3'], + singleValNum: 123, + emptyStr: '', + emptyArr: [''], + badValue: {'foo': 'bar'} // should be dropped + } + } + } + ); + + const request = spec.buildRequests(bidRequestWithKeywords, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.keywords).to.be.an('string'); + payload.keywords = JSON.parse(payload.keywords); + + expect(payload.keywords).to.deep.equal([{ + 'key': 'single', + 'value': ['val'] + }, { + 'key': 'singleArr', + 'value': ['val'] + }, { + 'key': 'singleArrNum', + 'value': ['5'] + }, { + 'key': 'multiValMixed', + 'value': ['value1', '2', 'value3'] + }, { + 'key': 'singleValNum', + 'value': ['123'] + }, { + 'key': 'emptyStr' + }, { + 'key': 'emptyArr' + }]); + }); + }); + + describe('interpretResponse', function () { + const responses = [ + {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 59, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 60, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.15, 'adm': '
test content 3
', 'auid': 59, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'price': 0, 'auid': 61, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0, 'adm': '
test content 5
', 'h': 250, 'w': 300}], 'seat': '1'}, + undefined, + {'bid': [], 'seat': '1'}, + {'seat': '1'}, + ]; + + it('should get correct bid response', function () { + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '659423fff799cb', + 'bidderRequestId': '5f2009617a7c0a', + 'auctionId': '1cbd2feafe5e8b', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '659423fff799cb', + 'cpm': 1.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 1
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': [responses[0]]}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('should get correct multi bid response', function () { + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d71a5b', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '4dff80cc4ee346', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '5703af74d0472a', + 'bidderRequestId': '2c2bb1972df9a', + 'auctionId': '1fa09aee5c8c99', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '300bfeb0d71a5b', + 'cpm': 1.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 1
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '4dff80cc4ee346', + 'cpm': 0.5, + 'creativeId': 60, + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'ad': '
test content 2
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '5703af74d0472a', + 'cpm': 0.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 728, + 'height': 90, + 'ad': '
test content 3
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': responses.slice(0, 3)}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('handles wrong and nobid responses', function () { + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '61' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d7190gf', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '65' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '300bfeb0d71321', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '70' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '300bfeb0d7183bb', + 'bidderRequestId': '2c2bb1972d23af', + 'auctionId': '1fa09aee5c84d34', + } + ]; + const request = spec.buildRequests(bidRequests); + const result = spec.interpretResponse({'body': {'seatbid': responses.slice(3)}}, request); + expect(result.length).to.equal(0); + }); + + it('complicated case', function () { + const fullResponse = [ + {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 59, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 60, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.15, 'adm': '
test content 3
', 'auid': 59, 'h': 90, 'w': 728}], 'seat': '1'}, + {'bid': [{'price': 0.15, 'adm': '
test content 4
', 'auid': 59, 'h': 600, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.5, 'adm': '
test content 5
', 'auid': 60, 'h': 600, 'w': 350}], 'seat': '1'}, + ]; + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '2164be6358b9', + 'bidderRequestId': '106efe3247', + 'auctionId': '32a1f276cb87cb8', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '326bde7fbf69', + 'bidderRequestId': '106efe3247', + 'auctionId': '32a1f276cb87cb8', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '4e111f1b66e4', + 'bidderRequestId': '106efe3247', + 'auctionId': '32a1f276cb87cb8', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '26d6f897b516', + 'bidderRequestId': '106efe3247', + 'auctionId': '32a1f276cb87cb8', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '1751cd90161', + 'bidderRequestId': '106efe3247', + 'auctionId': '32a1f276cb87cb8', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '2164be6358b9', + 'cpm': 1.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 1
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '4e111f1b66e4', + 'cpm': 0.5, + 'creativeId': 60, + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'ad': '
test content 2
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '26d6f897b516', + 'cpm': 0.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 728, + 'height': 90, + 'ad': '
test content 3
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '326bde7fbf69', + 'cpm': 0.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'ad': '
test content 4
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': fullResponse}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('dublicate uids and sizes in one slot', function () { + const fullResponse = [ + {'bid': [{'price': 1.15, 'adm': '
test content 1
', 'auid': 59, 'h': 250, 'w': 300}], 'seat': '1'}, + {'bid': [{'price': 0.5, 'adm': '
test content 2
', 'auid': 59, 'h': 250, 'w': 300}], 'seat': '1'}, + ]; + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '5126e301f4be', + 'bidderRequestId': '171c5405a390', + 'auctionId': '35bcbc0f7e79c', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '57b2ebe70e16', + 'bidderRequestId': '171c5405a390', + 'auctionId': '35bcbc0f7e79c', + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '59' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '225fcd44b18c', + 'bidderRequestId': '171c5405a390', + 'auctionId': '35bcbc0f7e79c', + } + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '5126e301f4be', + 'cpm': 1.15, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 1
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + }, + { + 'requestId': '57b2ebe70e16', + 'cpm': 0.5, + 'creativeId': 59, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '
test content 2
', + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'banner', + 'netRevenue': true, + 'ttl': 360, + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': fullResponse}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + }); + + it('should get correct video bid response', function () { + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '58' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '57dfefb80eca', + 'bidderRequestId': '20394420a762a2', + 'auctionId': '140132d07b031', + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': 'e893c787c22dd', + 'bidderRequestId': '20394420a762a2', + 'auctionId': '140132d07b031', + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + } + ]; + const response = [ + {'bid': [{'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 58, content_type: 'video', w: 300, h: 600}], 'seat': '2'}, + {'bid': [{'price': 1.00, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 60, content_type: 'video'}], 'seat': '2'} + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '57dfefb80eca', + 'cpm': 1.15, + 'creativeId': 58, + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'video', + 'netRevenue': true, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + } + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': response}}, request); + expect(result).to.deep.equal(expectedResponse); + }); + + it('should have right renderer in the bid response', function () { + const spySetRenderer = sinon.spy(); + const stubRenderer = { + setRender: spySetRenderer + }; + const spyRendererInstall = sinon.spy(function() { return stubRenderer; }); + const stubRendererConst = { + install: spyRendererInstall + }; + const bidRequests = [ + { + 'bidder': 'converge', + 'params': { + 'uid': '58' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': 'e6e65553fc8', + 'bidderRequestId': '1380f393215dc7', + 'auctionId': '10b8d2f3c697e3', + 'mediaTypes': { + 'video': { + 'context': 'outstream' + } + } + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '60' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': 'c8fdcb3f269f', + 'bidderRequestId': '1380f393215dc7', + 'auctionId': '10b8d2f3c697e3' + }, + { + 'bidder': 'converge', + 'params': { + 'uid': '61' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '1de036c37685', + 'bidderRequestId': '1380f393215dc7', + 'auctionId': '10b8d2f3c697e3', + 'renderer': {} + } + ]; + const response = [ + {'bid': [{'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 58, content_type: 'video', w: 300, h: 600}], 'seat': '2'}, + {'bid': [{'price': 1.00, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 60, content_type: 'video', w: 300, h: 250}], 'seat': '2'}, + {'bid': [{'price': 1.20, 'adm': '\n<\/Ad>\n<\/VAST>', 'auid': 61, content_type: 'video', w: 300, h: 250}], 'seat': '2'} + ]; + const request = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': 'e6e65553fc8', + 'cpm': 1.15, + 'creativeId': 58, + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'video', + 'netRevenue': true, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + }, + 'renderer': stubRenderer + }, + { + 'requestId': 'c8fdcb3f269f', + 'cpm': 1.00, + 'creativeId': 60, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'video', + 'netRevenue': true, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + }, + 'renderer': stubRenderer + }, + { + 'requestId': '1de036c37685', + 'cpm': 1.20, + 'creativeId': 61, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'bidderCode': 'converge', + 'currency': 'EUR', + 'mediaType': 'video', + 'netRevenue': true, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + } + } + ]; + + const result = spec.interpretResponse({'body': {'seatbid': response}}, request, stubRendererConst); + + expect(spySetRenderer.calledTwice).to.equal(true); + expect(spySetRenderer.getCall(0).args[0]).to.be.a('function'); + expect(spySetRenderer.getCall(1).args[0]).to.be.a('function'); + + expect(spyRendererInstall.calledTwice).to.equal(true); + expect(spyRendererInstall.getCall(0).args[0]).to.deep.equal({ + id: 'e6e65553fc8', + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + loaded: false + }); + expect(spyRendererInstall.getCall(1).args[0]).to.deep.equal({ + id: 'c8fdcb3f269f', + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + loaded: false + }); + + expect(result).to.deep.equal(expectedResponse); + }); + + describe('user sync', function () { + const syncUrl = getSyncUrl(); + + beforeEach(function () { + resetUserSync(); + }); + + it('should register sync image', function () { + let syncs = spec.getUserSyncs({ + pixelEnabled: true + }); + + expect(syncs).to.deep.equal({type: 'image', url: syncUrl}); + }); + + it('should not register sync image more than once', function () { + let syncs = spec.getUserSyncs({ + pixelEnabled: true + }); + expect(syncs).to.deep.equal({type: 'image', url: syncUrl}); + + // when called again, should still have only been called once + syncs = spec.getUserSyncs(); + expect(syncs).to.equal(undefined); + }); + + it('should pass gdpr params if consent is true', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + gdprApplies: true, consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr=1&gdpr_consent=foo` + }); + }); + + it('should pass gdpr params if consent is false', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + gdprApplies: false, consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr=0&gdpr_consent=foo` + }); + }); + + it('should pass gdpr param gdpr_consent only when gdprApplies is undefined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr_consent=foo` + }); + }); + + it('should pass no params if gdpr consentString is not defined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {})).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is a number', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: 0 + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is null', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: null + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is a object', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: {} + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr is not defined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined)).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass usPrivacy param if it is available', function() { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {}, '1YNN')).to.deep.equal({ + type: 'image', url: `${syncUrl}&us_privacy=1YNN` + }); + }); + }); +}); diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index 7ad37a91afe..7c6d6e5dd6c 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec} from 'modules/conversantBidAdapter.js'; +import {spec, storage} from 'modules/conversantBidAdapter.js'; import * as utils from 'src/utils.js'; import { createEidsArray } from 'modules/userId/eids.js'; @@ -321,6 +321,12 @@ describe('Conversant adapter tests', function() { expect(payload).to.not.have.property('user'); // there should be no user by default }); + it('Verify override url', function() { + const testUrl = 'https://someurl?name=value'; + const request = spec.buildRequests([{params: {white_label_url: testUrl}}]); + expect(request.url).to.equal(testUrl); + }); + it('Verify interpretResponse', function() { const request = spec.buildRequests(bidRequests); const response = spec.interpretResponse(bidResponses, request); @@ -470,14 +476,14 @@ describe('Conversant adapter tests', function() { // add pubcid to every entry requests.forEach((unit) => { - Object.assign(unit, {userId: {pubcid: 112233, tdid: 223344, idl_env: 334455}}); + Object.assign(unit, {userId: {pubcid: '112233', tdid: '223344', idl_env: '334455'}}); Object.assign(unit, {userIdAsEids: createEidsArray(unit.userId)}); }); // construct http post payload const payload = spec.buildRequests(requests).data; expect(payload).to.have.deep.nested.property('user.ext.eids', [ - {source: 'adserver.org', uids: [{id: 223344, atype: 1, ext: {rtiPartner: 'TDID'}}]}, - {source: 'liveramp.com', uids: [{id: 334455, atype: 1}]} + {source: 'adserver.org', uids: [{id: '223344', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'liveramp.com', uids: [{id: '334455', atype: 1}]} ]); }); }); @@ -508,7 +514,7 @@ describe('Conversant adapter tests', function() { const requests = utils.deepClone(bidRequests); // add a pubcid cookie - utils.setCookie(ID_NAME, '12345', expStr(TIMEOUT)); + storage.setCookie(ID_NAME, '12345', expStr(TIMEOUT)); // construct http post payload const payload = spec.buildRequests(requests).data; @@ -521,7 +527,7 @@ describe('Conversant adapter tests', function() { requests[0].params.pubcid_name = CUSTOM_ID_NAME; // add a pubcid cookie - utils.setCookie(CUSTOM_ID_NAME, '12345', expStr(TIMEOUT)); + storage.setCookie(CUSTOM_ID_NAME, '12345', expStr(TIMEOUT)); // construct http post payload const payload = spec.buildRequests(requests).data; @@ -533,8 +539,8 @@ describe('Conversant adapter tests', function() { const requests = utils.deepClone(bidRequests); // add a pubcid in local storage - utils.setDataInLocalStorage(ID_NAME + EXP, ''); - utils.setDataInLocalStorage(ID_NAME, 'abcde'); + storage.setDataInLocalStorage(ID_NAME + EXP, ''); + storage.setDataInLocalStorage(ID_NAME, 'abcde'); // construct http post payload const payload = spec.buildRequests(requests).data; @@ -546,8 +552,8 @@ describe('Conversant adapter tests', function() { const requests = utils.deepClone(bidRequests); // add a pubcid in local storage - utils.setDataInLocalStorage(ID_NAME + EXP, expStr(TIMEOUT)); - utils.setDataInLocalStorage(ID_NAME, 'fghijk'); + storage.setDataInLocalStorage(ID_NAME + EXP, expStr(TIMEOUT)); + storage.setDataInLocalStorage(ID_NAME, 'fghijk'); // construct http post payload const payload = spec.buildRequests(requests).data; @@ -559,8 +565,8 @@ describe('Conversant adapter tests', function() { const requests = utils.deepClone(bidRequests); // add a pubcid in local storage - utils.setDataInLocalStorage(ID_NAME + EXP, expStr(-TIMEOUT)); - utils.setDataInLocalStorage(ID_NAME, 'lmnopq'); + storage.setDataInLocalStorage(ID_NAME + EXP, expStr(-TIMEOUT)); + storage.setDataInLocalStorage(ID_NAME, 'lmnopq'); // construct http post payload const payload = spec.buildRequests(requests).data; @@ -573,8 +579,8 @@ describe('Conversant adapter tests', function() { requests[0].params.pubcid_name = CUSTOM_ID_NAME; // add a pubcid in local storage - utils.setDataInLocalStorage(CUSTOM_ID_NAME + EXP, expStr(TIMEOUT)); - utils.setDataInLocalStorage(CUSTOM_ID_NAME, 'fghijk'); + storage.setDataInLocalStorage(CUSTOM_ID_NAME + EXP, expStr(TIMEOUT)); + storage.setDataInLocalStorage(CUSTOM_ID_NAME, 'fghijk'); // construct http post payload const payload = spec.buildRequests(requests).data; diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index 9c96cfa3a05..042f1a4d61d 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { tryGetCriteoFastBid, spec, PROFILE_ID_PUBLISHERTAG, ADAPTER_VERSION, PUBLISHER_TAG_URL } from 'modules/criteoBidAdapter.js'; +import { tryGetCriteoFastBid, spec, PROFILE_ID_PUBLISHERTAG, ADAPTER_VERSION } from 'modules/criteoBidAdapter.js'; import { createBid } from 'src/bidfactory.js'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; @@ -7,17 +7,20 @@ import { config } from '../../../src/config.js'; import { NATIVE, VIDEO } from '../../../src/mediaTypes.js'; describe('The Criteo bidding adapter', function () { - let utilsMock; + let utilsMock, sandbox; beforeEach(function () { // Remove FastBid to avoid side effects localStorage.removeItem('criteo_fast_bid'); utilsMock = sinon.mock(utils); + + sandbox = sinon.sandbox.create(); }); afterEach(function() { global.Criteo = undefined; utilsMock.restore(); + sandbox.restore(); }); describe('isBidRequestValid', function () { @@ -382,6 +385,7 @@ describe('The Criteo bidding adapter', function () { '91': 1 }, }, + apiVersion: 1, }, }; @@ -433,7 +437,7 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.slots[0].zoneid).to.equal(123); expect(ortbRequest.gdprConsent.consentData).to.equal('concentDataString'); expect(ortbRequest.gdprConsent.gdprApplies).to.equal(true); - expect(ortbRequest.gdprConsent.consentGiven).to.equal(true); + expect(ortbRequest.gdprConsent.version).to.equal(1); }); it('should properly build a networkId request', function () { @@ -481,7 +485,6 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.slots[0].sizes[1]).to.equal('728x90'); expect(ortbRequest.gdprConsent.consentData).to.equal(undefined); expect(ortbRequest.gdprConsent.gdprApplies).to.equal(false); - expect(ortbRequest.gdprConsent.consentGiven).to.equal(undefined); }); it('should properly build a mixed request', function () { @@ -530,7 +533,7 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.gdprConsent).to.equal(undefined); }); - it('should properly build request with undefined gdpr consent fields when they are not provided', function () { + it('should properly build a request with undefined gdpr consent fields when they are not provided', function () { const bidRequests = [ { bidder: 'criteo', @@ -550,7 +553,6 @@ describe('The Criteo bidding adapter', function () { const ortbRequest = spec.buildRequests(bidRequests, bidderRequest).data; expect(ortbRequest.gdprConsent.consentData).to.equal(undefined); expect(ortbRequest.gdprConsent.gdprApplies).to.equal(undefined); - expect(ortbRequest.gdprConsent.consentGiven).to.equal(undefined); }); it('should properly build a request with ccpa consent field', function () { @@ -705,6 +707,115 @@ describe('The Criteo bidding adapter', function () { expect(request.data.user).to.not.be.null; expect(request.data.user.ceh).to.equal('hashedemail'); }); + + it('should properly build a request without first party data', function () { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + sizes: [[728, 90]], + params: { + zoneId: 123 + } + }, + ]; + + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + }; + return utils.deepAccess(config, key); + }); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.publisher.ext).to.equal(undefined); + expect(request.data.user.ext).to.equal(undefined); + expect(request.data.slots[0].ext).to.equal(undefined); + }); + + it('should properly build a request with criteo specific ad unit first party data', function () { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + sizes: [[728, 90]], + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + } + }, + ]; + + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + }; + return utils.deepAccess(config, key); + }); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.slots[0].ext).to.deep.equal({ + bidfloor: 0.75, + }); + }); + + it('should properly build a request with first party data', function () { + const contextData = { + keywords: ['power tools'], + data: { + pageType: 'article' + } + }; + const userData = { + gender: 'M', + data: { + registered: true + } + }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + sizes: [[728, 90]], + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + }, + fpd: { + context: { + data: { + someContextAttribute: 'abc' + } + } + } + }, + ]; + + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + fpd: { + context: contextData, + user: userData + } + }; + return utils.deepAccess(config, key); + }); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.publisher.ext).to.deep.equal(contextData); + expect(request.data.user.ext).to.deep.equal(userData); + expect(request.data.slots[0].ext).to.deep.equal({ + bidfloor: 0.75, + data: { + someContextAttribute: 'abc' + } + }); + }); }); describe('interpretResponse', function () { @@ -1054,6 +1165,132 @@ describe('The Criteo bidding adapter', function () { }); }); + describe('when pubtag prebid adapter is not available', function () { + it('should not warn if sendId is provided on required fields for native bidRequest', () => { + const bidderRequest = { }; + const bidRequestsWithSendId = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + sizes: [[728, 90]], + params: { + zoneId: 123, + publisherSubId: '123', + nativeCallback: function() {} + }, + nativeParams: { + image: { + sendId: true + }, + icon: { + sendId: true + }, + clickUrl: { + sendId: true + }, + displayUrl: { + sendId: true + }, + privacyLink: { + sendId: true + }, + privacyIcon: { + sendId: true + } + } + } + ]; + + utilsMock.expects('logWarn').withArgs('Criteo: all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)').never(); + const request = spec.buildRequests(bidRequestsWithSendId, bidderRequest); + utilsMock.verify(); + }); + + it('should warn only once if sendId is not provided on required fields for native bidRequest', () => { + const bidderRequest = { }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + sizes: [[728, 90]], + params: { + zoneId: 123, + publisherSubId: '123', + nativeCallback: function() {} + }, + }, + { + bidder: 'criteo', + adUnitCode: 'bid-456', + transactionId: 'transaction-456', + sizes: [[728, 90]], + params: { + zoneId: 456, + publisherSubId: '456', + nativeCallback: function() {} + }, + }, + ]; + + const nativeParamsWithoutSendId = [ + { + nativeParams: { + image: { + sendId: false + }, + } + }, + { + nativeParams: { + icon: { + sendId: false + }, + } + }, + { + nativeParams: { + clickUrl: { + sendId: false + }, + } + }, + { + nativeParams: { + displayUrl: { + sendId: false + }, + } + }, + { + nativeParams: { + privacyLink: { + sendId: false + }, + } + }, + { + nativeParams: { + privacyIcon: { + sendId: false + }, + } + } + ]; + + utilsMock.expects('logWarn') + .withArgs('Criteo: all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)') + .exactly(nativeParamsWithoutSendId.length * bidRequests.length); + nativeParamsWithoutSendId.forEach(nativeParams => { + let transformedBidRequests = {...bidRequests}; + transformedBidRequests = [Object.assign(transformedBidRequests[0], nativeParams), Object.assign(transformedBidRequests[1], nativeParams)]; + spec.buildRequests(transformedBidRequests, bidderRequest); + }); + utilsMock.verify(); + }); + }); + describe('when pubtag prebid adapter is available', function () { it('should forward response to pubtag when calling interpretResponse', () => { const response = {}; diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index c36852251f8..aa5807da0da 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -1,7 +1,6 @@ -import { criteoIdSubmodule } from 'modules/criteoIdSystem.js'; +import { criteoIdSubmodule, storage } from 'modules/criteoIdSystem.js'; import * as utils from 'src/utils.js'; import * as ajaxLib from 'src/ajax.js'; -import * as urlLib from 'src/url.js'; const pastDateString = new Date(0).toString() @@ -27,14 +26,14 @@ describe('CriteoId module', function () { let triggerPixelStub; beforeEach(function (done) { - getCookieStub = sinon.stub(utils, 'getCookie'); - setCookieStub = sinon.stub(utils, 'setCookie'); - getLocalStorageStub = sinon.stub(utils, 'getDataFromLocalStorage'); - setLocalStorageStub = sinon.stub(utils, 'setDataInLocalStorage'); - removeFromLocalStorageStub = sinon.stub(utils, 'removeDataFromLocalStorage'); + getCookieStub = sinon.stub(storage, 'getCookie'); + setCookieStub = sinon.stub(storage, 'setCookie'); + getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + setLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); + removeFromLocalStorageStub = sinon.stub(storage, 'removeDataFromLocalStorage'); timeStampStub = sinon.stub(utils, 'timestamp').returns(nowTimestamp); ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(mockResponse('{}')); - parseUrlStub = sinon.stub(urlLib, 'parse').returns({protocol: 'https', hostname: 'testdev.com'}) + parseUrlStub = sinon.stub(utils, 'parseUrl').returns({protocol: 'https', hostname: 'testdev.com'}) triggerPixelStub = sinon.stub(utils, 'triggerPixel'); done(); }); @@ -132,4 +131,26 @@ describe('CriteoId module', function () { } }); })); + + const gdprConsentTestCases = [ + { consentData: { gdprApplies: true, consentString: 'expectedConsentString' }, expected: 'expectedConsentString' }, + { consentData: { gdprApplies: false, consentString: 'expectedConsentString' }, expected: undefined }, + { consentData: { gdprApplies: true, consentString: undefined }, expected: undefined }, + { consentData: { gdprApplies: 'oui', consentString: 'expectedConsentString' }, expected: undefined }, + { consentData: undefined, expected: undefined } + ]; + + gdprConsentTestCases.forEach(testCase => it('should call user sync url with the gdprConsent', function () { + const emptyObj = '{}'; + let ajaxStub = sinon.stub().callsFake((url, callback) => callback(emptyObj)); + ajaxBuilderStub.callsFake(mockResponse(undefined, ajaxStub)) + + criteoIdSubmodule.getId(undefined, testCase.consentData); + + if (testCase.expected) { + expect(ajaxStub.calledWithMatch(`gdprString=${testCase.expected}`)).to.be.true; + } else { + expect(ajaxStub.calledWithMatch('gdprString')).not.to.be.true; + } + })); }); diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index 577d36e9215..ccd205964a9 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -3,6 +3,8 @@ import { getCurrencyRates } from 'test/fixtures/fixtures.js'; +import { getGlobal } from 'src/prebidGlobal.js'; + import { setConfig, addBidResponseHook, @@ -43,8 +45,12 @@ describe('currency', function () { it('results in currencySupportEnabled = false when currency not configured', function () { setConfig({}); expect(currencySupportEnabled).to.equal(false); + expect(getGlobal().convertCurrency).to.be.undefined; + }); + it('adds conversion function onto pbjs global var once initiated', function () { + setConfig({ 'adServerCurrency': 'JPY' }); + expect(getGlobal().convertCurrency).to.be.a('function'); }); - it('results in currencySupportEnabled = true and currencyRates being loaded when configured', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); @@ -124,6 +130,38 @@ describe('currency', function () { }); }); + describe('global currency function', function () { + it('still returns conversion if default rates present with fetch not returned yet', function () { + setConfig({ + 'adServerCurrency': 'USD', + 'defaultRates': { + 'USD': { EUR: 2, JPY: 100 } + } + }); + // first conversion should use default rates + expect(getGlobal().convertCurrency(1.0, 'USD', 'EUR')).to.equal(2); + expect(getGlobal().convertCurrency(1.0, 'USD', 'JPY')).to.equal(100); + fakeCurrencyFileServer.respond(); + }); + it('uses fetch rates if returned', function () { + fakeCurrencyFileServer.respondWith(JSON.stringify({ + 'dataAsOf': '2017-04-25', + 'conversions': { + 'USD': { EUR: 4, JPY: 200 } + } + })); + setConfig({ + 'adServerCurrency': 'USD', + 'defaultRates': { + 'USD': { EUR: 2, JPY: 100 } + } + }); + // now respond and should use updates rates + fakeCurrencyFileServer.respond(); + expect(getGlobal().convertCurrency(1.0, 'USD', 'EUR')).to.equal(4); + expect(getGlobal().convertCurrency(1.0, 'USD', 'JPY')).to.equal(200); + }); + }); describe('bidder override', function () { it('allows setConfig to set bidder currency', function () { setConfig({}); diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 562c7eb297b..895416bae2b 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -2,7 +2,6 @@ import { expect } from 'chai'; import parse from 'url-parse'; import { buildDfpVideoUrl, buildAdpodVideoUrl } from 'modules/dfpAdServerVideo.js'; -import { parseQS } from 'src/url.js'; import adUnit from 'test/fixtures/video/adUnit.json'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; @@ -33,7 +32,7 @@ describe('The DFP video support module', function () { expect(url.protocol).to.equal('https:'); expect(url.host).to.equal('securepubads.g.doubleclick.net'); - const queryParams = parseQS(url.query); + const queryParams = utils.parseQS(url.query); expect(queryParams).to.have.property('correlator'); expect(queryParams).to.have.property('description_url', 'someUrl.com'); expect(queryParams).to.have.property('env', 'vp'); @@ -57,7 +56,7 @@ describe('The DFP video support module', function () { expect(url.host).to.equal('video.adserver.example'); - const queryObject = parseQS(url.query); + const queryObject = utils.parseQS(url.query); expect(queryObject.description_url).to.equal('vastUrl.example'); }); @@ -78,7 +77,7 @@ describe('The DFP video support module', function () { params: { iu: 'my/adUnit' } })); - const queryObject = parseQS(url.query); + const queryObject = utils.parseQS(url.query); expect(queryObject.iu).to.equal('my/adUnit'); }); @@ -92,7 +91,7 @@ describe('The DFP video support module', function () { } })); - expect(parseQS(url.query)).to.have.property('output', 'vast'); + expect(utils.parseQS(url.query)).to.have.property('output', 'vast'); }); it('should include the cache key and adserver targeting in cust_params', function () { @@ -108,8 +107,8 @@ describe('The DFP video support module', function () { 'iu': 'my/adUnit' } })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('hb_uuid', bid.videoCacheKey); @@ -178,8 +177,8 @@ describe('The DFP video support module', function () { 'iu': 'my/adUnit' } })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('hb_uuid', bid.videoCacheKey); @@ -205,8 +204,8 @@ describe('The DFP video support module', function () { }, }, })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('my_targeting', 'foo'); @@ -224,8 +223,8 @@ describe('The DFP video support module', function () { url: 'https://video.adserver.example/ads?sz=640x480&iu=/123/aduniturl&impl=s&cust_params=section%3dblog%26mykey%3dmyvalue' })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('section', 'blog'); @@ -247,7 +246,7 @@ describe('The DFP video support module', function () { } })); - const queryObject = parseQS(url.query); + const queryObject = utils.parseQS(url.query); expect(queryObject.description_url).to.equal('descriptionurl.example'); }); @@ -272,8 +271,8 @@ describe('The DFP video support module', function () { 'iu': 'my/adUnit' } })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_uuid', bid.videoCacheKey); expect(customParams).to.have.property('hb_cache_id', bid.videoCacheKey); @@ -293,8 +292,8 @@ describe('The DFP video support module', function () { 'iu': 'my/adUnit' } })); - const queryObject = parseQS(url.query); - const customParams = parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); expect(customParams).to.have.property('hb_uuid', 'def'); expect(customParams).to.have.property('hb_cache_id', 'def'); @@ -370,7 +369,7 @@ describe('The DFP video support module', function () { expect(url.protocol).to.equal('https:'); expect(url.host).to.equal('securepubads.g.doubleclick.net'); - const queryParams = parseQS(url.query); + const queryParams = utils.parseQS(url.query); expect(queryParams).to.have.property('correlator'); expect(queryParams).to.have.property('description_url', 'someUrl.com'); expect(queryParams).to.have.property('env', 'vp'); @@ -382,7 +381,7 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('url'); expect(queryParams).to.have.property('cust_params'); - const custParams = parseQS(decodeURIComponent(queryParams.cust_params)); + const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); expect(custParams).to.have.property('hb_cache_id', '123'); expect(custParams).to.have.property('hb_pb_cat_dur', '15.00_395_15s,15.00_406_30s,10.00_395_15s'); } @@ -424,7 +423,7 @@ describe('The DFP video support module', function () { expect(url.protocol).to.equal('https:'); expect(url.host).to.equal('securepubads.g.doubleclick.net'); - const queryParams = parseQS(url.query); + const queryParams = utils.parseQS(url.query); expect(queryParams).to.have.property('correlator'); expect(queryParams).to.have.property('description_url', 'someUrl.com'); expect(queryParams).to.have.property('env', 'vp'); @@ -436,7 +435,7 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('url'); expect(queryParams).to.have.property('cust_params'); - const custParams = parseQS(decodeURIComponent(queryParams.cust_params)); + const custParams = utils.parseQS(decodeURIComponent(queryParams.cust_params)); expect(custParams).to.have.property('hb_cache_id', '123'); expect(custParams).to.have.property('hb_pb_cat_dur', '10.00_15s,15.00_15s,15.00_30s'); } diff --git a/test/spec/modules/districtmDmxBidAdapter_spec.js b/test/spec/modules/districtmDmxBidAdapter_spec.js index e7a8e8c3064..d8f0beb9a36 100644 --- a/test/spec/modules/districtmDmxBidAdapter_spec.js +++ b/test/spec/modules/districtmDmxBidAdapter_spec.js @@ -40,7 +40,7 @@ const bidRequest = [{ 'bidder': 'districtmDMX', 'params': { 'dmxid': 100001, - 'memberid': 100003 + 'memberid': 100003, }, 'adUnitCode': 'div-gpt-ad-12345678-1', 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', @@ -53,6 +53,22 @@ const bidRequest = [{ 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' }]; +const bidRequestNoCoppa = [{ + 'bidder': 'districtmDMX', + 'params': { + 'dmxid': 100001, + 'memberid': 100003 + }, + 'adUnitCode': 'div-gpt-ad-12345678-1', + 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', + 'sizes': [ + [300, 250], + [300, 600] + ], + 'bidId': '29a28a1bbc8a8d', + 'bidderRequestId': '124b579a136515', + 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' +}]; const bidderRequest = { 'bidderCode': 'districtmDMX', 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf', @@ -61,7 +77,7 @@ const bidderRequest = { 'bidder': 'districtmDMX', 'params': { 'dmxid': 100001, - 'memberid': 100003 + 'memberid': 100003, }, 'adUnitCode': 'div-gpt-ad-12345678-1', 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', @@ -420,6 +436,32 @@ const bidderRequest = { 'doneCbCallCount': 0 }; +const bidderRequestNoCoppa = { + 'bidderCode': 'districtmDMX', + 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf', + 'bidderRequestId': '124b579a136515', + 'bids': [{ + 'bidder': 'districtmDMX', + 'params': { + 'dmxid': 100001, + 'memberid': 100003, + }, + 'adUnitCode': 'div-gpt-ad-12345678-1', + 'transactionId': 'f6d13fa6-ebc1-41ac-9afa-d8171d22d2c2', + 'sizes': [ + [300, 250], + [300, 600] + ], + 'bidId': '29a28a1bbc8a8d', + 'bidderRequestId': '124b579a136515', + 'auctionId': '3d62f2d3-56a2-4991-888e-f7754619ddcf' + }], + 'auctionStart': 1529511035677, + 'timeout': 700, + 'start': 1529511035686, + 'doneCbCallCount': 0 +}; + const responses = { 'body': { 'id': '1f45b37c-5298-4934-b517-4d911aadabfd', @@ -522,6 +564,7 @@ describe('DistrictM Adaptor', function () { describe(`buildRequests test usage`, function () { const buildRequestResults = districtm.buildRequests(bidRequest, bidderRequest); + const buildRequestResultsNoCoppa = districtm.buildRequests(bidRequestNoCoppa, bidderRequestNoCoppa); it(`the function should return an array`, function () { expect(buildRequestResults).to.be.an('object'); }); @@ -531,6 +574,16 @@ describe('DistrictM Adaptor', function () { expect(bidr.regs.ext.us_privacy).to.be.equal('1NY'); expect(bidr.user.ext.consent).to.be.an('string'); }); + it(`test contain COPPA`, function() { + const bidr = JSON.parse(buildRequestResults.data) + bidr.regs = bidr.regs || {}; + bidr.regs.coppa = 1; + expect(bidr.regs.coppa).to.be.equal(1) + }) + it(`test should not contain COPPA`, function() { + const bidr = JSON.parse(buildRequestResultsNoCoppa.data) + expect(bidr.regs.coppa).to.be.equal(0) + }) it(`the function should return array length of 1`, function () { expect(buildRequestResults.data).to.be.a('string'); }); diff --git a/test/spec/modules/dspxBidAdapter_spec.js b/test/spec/modules/dspxBidAdapter_spec.js index 1156eb8d169..2513a6174cd 100644 --- a/test/spec/modules/dspxBidAdapter_spec.js +++ b/test/spec/modules/dspxBidAdapter_spec.js @@ -108,6 +108,7 @@ describe('dspxAdapter', function () { 'crid': 100500, 'width': '300', 'height': '250', + 'type': 'sspHTML', 'tag': '', 'requestId': '220ed41385952a', 'currency': 'EUR', @@ -127,6 +128,7 @@ describe('dspxAdapter', function () { currency: 'EUR', netRevenue: true, ttl: 300, + type: 'sspHTML', ad: '' }]; diff --git a/test/spec/modules/evolution_techBidAdapter_spec.js b/test/spec/modules/e_volutionBidAdapter_spec.js similarity index 99% rename from test/spec/modules/evolution_techBidAdapter_spec.js rename to test/spec/modules/e_volutionBidAdapter_spec.js index 54e92db4847..447420616d1 100644 --- a/test/spec/modules/evolution_techBidAdapter_spec.js +++ b/test/spec/modules/e_volutionBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec} from '../../../modules/evolution_techBidAdapter.js'; +import {spec} from '../../../modules/e_volutionBidAdapter.js'; describe('EvolutionTechBidAdapter', function () { let bid = { diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 99c34d28a94..ed32ecc51d2 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -1,8 +1,8 @@ import {createEidsArray} from 'modules/userId/eids.js'; import {expect} from 'chai'; -// Note: In unit tets cases for bidders, call the createEidsArray function over userId object that is used for calling fetchBids -// this way the request will stay consistent and unit test cases will not need lots of changes. +// Note: In unit tets cases for bidders, call the createEidsArray function over userId object that is used for calling fetchBids +// this way the request will stay consistent and unit test cases will not need lots of changes. describe('eids array generation for known sub-modules', function() { it('pubCommonId', function() { @@ -158,12 +158,24 @@ describe('Negative case', function() { expect(newEids.length).to.equal(0); }); - it('eids array generation for known sub-module with undefined value', function() { + it('eids array generation for known sub-module with non-string value', function() { // pubCommonId - const userId = { + let userId = { pubcid: undefined }; - const newEids = createEidsArray(userId); + let newEids = createEidsArray(userId); + expect(newEids.length).to.equal(0); + userId.pubcid = 123; + newEids = createEidsArray(userId); + expect(newEids.length).to.equal(0); + userId.pubcid = []; + newEids = createEidsArray(userId); + expect(newEids.length).to.equal(0); + userId.pubcid = {}; + newEids = createEidsArray(userId); + expect(newEids.length).to.equal(0); + userId.pubcid = null; + newEids = createEidsArray(userId); expect(newEids.length).to.equal(0); }); }); diff --git a/test/spec/modules/emoteevBidAdapter_spec.js b/test/spec/modules/emoteevBidAdapter_spec.js index b967eee33b6..43ae62f1eb9 100644 --- a/test/spec/modules/emoteevBidAdapter_spec.js +++ b/test/spec/modules/emoteevBidAdapter_spec.js @@ -47,8 +47,8 @@ import { validateExternalId, VENDOR_ID, WALLPAPER, + storage } from 'modules/emoteevBidAdapter.js'; -import * as url from '../../../src/url.js'; import * as utils from '../../../src/utils.js'; import {config} from '../../../src/config.js'; @@ -329,108 +329,108 @@ describe('emoteevBidAdapter', function () { }); describe('eventsUrl', function () { - expect(eventsUrl(null)).to.deep.equal(url.format({ + expect(eventsUrl(null)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: EVENTS_PATH })); - expect(eventsUrl('anything')).to.deep.equal(url.format({ + expect(eventsUrl('anything')).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: EVENTS_PATH })); - expect(eventsUrl(PRODUCTION)).to.deep.equal(url.format({ + expect(eventsUrl(PRODUCTION)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(PRODUCTION), pathname: EVENTS_PATH })); - expect(eventsUrl(STAGING)).to.deep.equal(url.format({ + expect(eventsUrl(STAGING)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(STAGING), pathname: EVENTS_PATH })); - expect(eventsUrl(DEVELOPMENT)).to.deep.equal(url.format({ + expect(eventsUrl(DEVELOPMENT)).to.deep.equal(utils.buildUrl({ hostname: domain(DEVELOPMENT), pathname: EVENTS_PATH })); }); describe('bidderUrl', function () { - expect(bidderUrl(null)).to.deep.equal(url.format({ + expect(bidderUrl(null)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: BIDDER_PATH })); - expect(bidderUrl('anything')).to.deep.equal(url.format({ + expect(bidderUrl('anything')).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: BIDDER_PATH })); - expect(bidderUrl(PRODUCTION)).to.deep.equal(url.format({ + expect(bidderUrl(PRODUCTION)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(PRODUCTION), pathname: BIDDER_PATH })); - expect(bidderUrl(STAGING)).to.deep.equal(url.format({ + expect(bidderUrl(STAGING)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(STAGING), pathname: BIDDER_PATH })); - expect(bidderUrl(DEVELOPMENT)).to.deep.equal(url.format({ + expect(bidderUrl(DEVELOPMENT)).to.deep.equal(utils.buildUrl({ hostname: domain(DEVELOPMENT), pathname: BIDDER_PATH })); }); describe('userSyncIframeUrl', function () { - expect(userSyncIframeUrl(null)).to.deep.equal(url.format({ + expect(userSyncIframeUrl(null)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: USER_SYNC_IFRAME_PATH })); - expect(userSyncIframeUrl('anything')).to.deep.equal(url.format({ + expect(userSyncIframeUrl('anything')).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: USER_SYNC_IFRAME_PATH })); - expect(userSyncIframeUrl(PRODUCTION)).to.deep.equal(url.format({ + expect(userSyncIframeUrl(PRODUCTION)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(PRODUCTION), pathname: USER_SYNC_IFRAME_PATH })); - expect(userSyncIframeUrl(STAGING)).to.deep.equal(url.format({ + expect(userSyncIframeUrl(STAGING)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(STAGING), pathname: USER_SYNC_IFRAME_PATH })); - expect(userSyncIframeUrl(DEVELOPMENT)).to.deep.equal(url.format({ + expect(userSyncIframeUrl(DEVELOPMENT)).to.deep.equal(utils.buildUrl({ hostname: domain(DEVELOPMENT), pathname: USER_SYNC_IFRAME_PATH })); }); describe('userSyncImageUrl', function () { - expect(userSyncImageUrl(null)).to.deep.equal(url.format({ + expect(userSyncImageUrl(null)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: USER_SYNC_IMAGE_PATH })); - expect(userSyncImageUrl('anything')).to.deep.equal(url.format({ + expect(userSyncImageUrl('anything')).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(DEFAULT_ENV), pathname: USER_SYNC_IMAGE_PATH })); - expect(userSyncImageUrl(PRODUCTION)).to.deep.equal(url.format({ + expect(userSyncImageUrl(PRODUCTION)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(PRODUCTION), pathname: USER_SYNC_IMAGE_PATH })); - expect(userSyncImageUrl(STAGING)).to.deep.equal(url.format({ + expect(userSyncImageUrl(STAGING)).to.deep.equal(utils.buildUrl({ protocol: 'https', hostname: domain(STAGING), pathname: USER_SYNC_IMAGE_PATH })); - expect(userSyncImageUrl(DEVELOPMENT)).to.deep.equal(url.format({ + expect(userSyncImageUrl(DEVELOPMENT)).to.deep.equal(utils.buildUrl({ hostname: domain(DEVELOPMENT), pathname: USER_SYNC_IMAGE_PATH })); @@ -749,7 +749,7 @@ describe('emoteevBidAdapter', function () { }); beforeEach(function () { triggerPixelStub = sinon.stub(utils, 'triggerPixel'); - getCookieSpy = sinon.spy(utils, 'getCookie'); + getCookieSpy = sinon.spy(storage, 'getCookie'); getConfigSpy = sinon.spy(config, 'getConfig'); getParameterByNameSpy = sinon.spy(utils, 'getParameterByName'); }); @@ -776,7 +776,7 @@ describe('emoteevBidAdapter', function () { }; spec.isBidRequestValid(validBidRequest); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // sinon.assert.notCalled(config.getConfig); sinon.assert.notCalled(utils.getParameterByName); }); @@ -786,7 +786,7 @@ describe('emoteevBidAdapter', function () { const invalidBidRequest = {}; spec.isBidRequestValid(invalidBidRequest); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // disabling these getConfig tests as they have been flaky in browserstack testing // sinon.assert.notCalled(config.getConfig); sinon.assert.notCalled(utils.getParameterByName); @@ -796,7 +796,7 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.buildRequests(cannedValidBidRequests, cannedBidderRequest); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // sinon.assert.callCount(config.getConfig, 3); sinon.assert.callCount(utils.getParameterByName, 2); }); @@ -805,7 +805,7 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.interpretResponse(serverResponse); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // sinon.assert.notCalled(config.getConfig); sinon.assert.notCalled(utils.getParameterByName); }); @@ -815,7 +815,7 @@ describe('emoteevBidAdapter', function () { const bidObject = serverResponse.body[0]; spec.onBidWon(bidObject); sinon.assert.calledOnce(utils.triggerPixel); - sinon.assert.calledOnce(utils.getCookie); + sinon.assert.calledOnce(storage.getCookie); // sinon.assert.calledOnce(config.getConfig); sinon.assert.calledOnce(utils.getParameterByName); }); @@ -824,7 +824,7 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.onTimeout(cannedValidBidRequests[0]); sinon.assert.calledOnce(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // sinon.assert.calledOnce(config.getConfig); sinon.assert.calledOnce(utils.getParameterByName); }); @@ -833,7 +833,7 @@ describe('emoteevBidAdapter', function () { it('has intended side-effects', function () { spec.getUserSyncs({}); sinon.assert.notCalled(utils.triggerPixel); - sinon.assert.notCalled(utils.getCookie); + sinon.assert.notCalled(storage.getCookie); // sinon.assert.calledOnce(config.getConfig); sinon.assert.calledOnce(utils.getParameterByName); }); diff --git a/test/spec/modules/eplanningAnalyticsAdapter_spec.js b/test/spec/modules/eplanningAnalyticsAdapter_spec.js index e900f7ca31c..9bb71e7033d 100644 --- a/test/spec/modules/eplanningAnalyticsAdapter_spec.js +++ b/test/spec/modules/eplanningAnalyticsAdapter_spec.js @@ -1,7 +1,7 @@ import eplAnalyticsAdapter from 'modules/eplanningAnalyticsAdapter.js'; import includes from 'core-js/library/fn/array/includes.js'; import { expect } from 'chai'; -import {parse as parseURL} from 'src/url.js'; +import { parseUrl } from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; let adapterManager = require('src/adapterManager').default; let events = require('src/events'); @@ -118,7 +118,7 @@ describe('eplanning analytics adapter', function () { expect(includes(['https://ads.ar.e-planning.net/hba/1/12345?d='], requests[0].url)); let info = requests[0].url; - let purl = parseURL(info); + let purl = parseUrl(info); let eplData = JSON.parse(decodeURIComponent(purl.search.d)); // Step 8 check that 6 events were sent diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index e9d478f911f..ff03bf033af 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; -import { spec } from 'modules/eplanningBidAdapter.js'; +import { spec, storage } from 'modules/eplanningBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import * as utils from 'src/utils.js'; describe('E-Planning Adapter', function () { const adapter = newBidder('spec'); @@ -559,10 +558,10 @@ describe('E-Planning Adapter', function () { }); } beforeEach(function () { - getLocalStorageSpy = sandbox.spy(utils, 'getDataFromLocalStorage'); - setDataInLocalStorageSpy = sandbox.spy(utils, 'setDataInLocalStorage'); + getLocalStorageSpy = sandbox.spy(storage, 'getDataFromLocalStorage'); + setDataInLocalStorageSpy = sandbox.spy(storage, 'setDataInLocalStorage'); - hasLocalStorageStub = sandbox.stub(utils, 'hasLocalStorage'); + hasLocalStorageStub = sandbox.stub(storage, 'hasLocalStorage'); hasLocalStorageStub.returns(true); clock = sandbox.useFakeTimers(); @@ -603,7 +602,7 @@ describe('E-Planning Adapter', function () { sinon.assert.calledWith(getLocalStorageSpy, storageIdRender); sinon.assert.calledWith(setDataInLocalStorageSpy, storageIdRender); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); }); context('when element is fully in view', function() { @@ -617,29 +616,29 @@ describe('E-Planning Adapter', function () { expect(respuesta.data.vs).to.equal('F'); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal('1'); }); it('when you have more than four render', function() { - utils.setDataInLocalStorage(storageIdRender, 4); + storage.setDataInLocalStorage(storageIdRender, 4); respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); expect(respuesta.data.vs).to.equal('0'); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('5'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('5'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal('1'); }); it('when you have more than four render and already record visibility', function() { - utils.setDataInLocalStorage(storageIdRender, 4); - utils.setDataInLocalStorage(storageIdView, 4); + storage.setDataInLocalStorage(storageIdRender, 4); + storage.setDataInLocalStorage(storageIdView, 4); respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); expect(respuesta.data.vs).to.equal('a'); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('5'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal('5'); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('5'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal('5'); }); }); @@ -654,17 +653,17 @@ describe('E-Planning Adapter', function () { clock.tick(1005); expect(respuesta.data.vs).to.equal('F'); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); it('when you have more than four render', function() { - utils.setDataInLocalStorage(storageIdRender, 4); + storage.setDataInLocalStorage(storageIdRender, 4); respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); expect(respuesta.data.vs).to.equal('0'); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('5'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('5'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); @@ -675,16 +674,16 @@ describe('E-Planning Adapter', function () { respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal('1'); }); it('you should not register visibility with less than 50%', function() { createPartiallyInvisibleElement(); respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); context('when width or height of the element is zero', function() { @@ -696,16 +695,16 @@ describe('E-Planning Adapter', function () { spec.buildRequests(bidRequests, bidderRequest) clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); it('if the height is zero but the width is within the range', function() { element.style.height = '0px'; spec.buildRequests(bidRequests, bidderRequest) clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); it('if both are zero', function() { element.style.height = '0px'; @@ -713,8 +712,8 @@ describe('E-Planning Adapter', function () { spec.buildRequests(bidRequests, bidderRequest) clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); context('when tab is inactive', function() { @@ -723,8 +722,8 @@ describe('E-Planning Adapter', function () { focusStub.returns(false); spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); context('segmentBeginsBeforeTheVisibleRange', function() { @@ -732,16 +731,16 @@ describe('E-Planning Adapter', function () { createElementOutOfRange(); spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); - expect(utils.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(utils.getDataFromLocalStorage(storageIdView)).to.equal(null); + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); context('when there are multiple adunit', function() { let respuesta; beforeEach(function () { [ADUNIT_CODE_VIEW, ADUNIT_CODE_VIEW2, ADUNIT_CODE_VIEW3].forEach(ac => { - utils.setDataInLocalStorage('pbsr_' + ac, 5); - utils.setDataInLocalStorage('pbvi_' + ac, 5); + storage.setDataInLocalStorage('pbsr_' + ac, 5); + storage.setDataInLocalStorage('pbvi_' + ac, 5); }); }); afterEach(function () { @@ -761,8 +760,8 @@ describe('E-Planning Adapter', function () { respuesta = spec.buildRequests(bidRequestMultiple, bidderRequest); clock.tick(1005); [ADUNIT_CODE_VIEW, ADUNIT_CODE_VIEW2, ADUNIT_CODE_VIEW3].forEach(ac => { - expect(utils.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); - expect(utils.getDataFromLocalStorage('pbvi_' + ac)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbvi_' + ac)).to.equal('6'); }); expect('aaa').to.equal(respuesta.data.vs); }); @@ -774,8 +773,8 @@ describe('E-Planning Adapter', function () { respuesta = spec.buildRequests(bidRequestMultiple, bidderRequest); clock.tick(1005); [ADUNIT_CODE_VIEW, ADUNIT_CODE_VIEW2, ADUNIT_CODE_VIEW3].forEach(ac => { - expect(utils.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); - expect(utils.getDataFromLocalStorage('pbvi_' + ac)).to.equal('5'); + expect(storage.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbvi_' + ac)).to.equal('5'); }); expect('aaa').to.equal(respuesta.data.vs); @@ -787,11 +786,11 @@ describe('E-Planning Adapter', function () { respuesta = spec.buildRequests(bidRequestMultiple, bidderRequest); clock.tick(1005); - expect(utils.getDataFromLocalStorage('pbsr_' + ADUNIT_CODE_VIEW)).to.equal('6'); - expect(utils.getDataFromLocalStorage('pbvi_' + ADUNIT_CODE_VIEW)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbsr_' + ADUNIT_CODE_VIEW)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbvi_' + ADUNIT_CODE_VIEW)).to.equal('6'); [ADUNIT_CODE_VIEW2, ADUNIT_CODE_VIEW3].forEach(ac => { - expect(utils.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); - expect(utils.getDataFromLocalStorage('pbvi_' + ac)).to.equal('5'); + expect(storage.getDataFromLocalStorage('pbsr_' + ac)).to.equal('6'); + expect(storage.getDataFromLocalStorage('pbvi_' + ac)).to.equal('5'); }); expect('aaa').to.equal(respuesta.data.vs); }); diff --git a/test/spec/modules/etargetBidAdapter_spec.js b/test/spec/modules/etargetBidAdapter_spec.js index 0d8ad7bcf11..4f5e0c224ec 100644 --- a/test/spec/modules/etargetBidAdapter_spec.js +++ b/test/spec/modules/etargetBidAdapter_spec.js @@ -1,5 +1,4 @@ import {assert, expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/etargetBidAdapter.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; diff --git a/test/spec/modules/fidelityBidAdapter_spec.js b/test/spec/modules/fidelityBidAdapter_spec.js index 15bb5a1b59e..1232c20b0d7 100644 --- a/test/spec/modules/fidelityBidAdapter_spec.js +++ b/test/spec/modules/fidelityBidAdapter_spec.js @@ -67,7 +67,19 @@ describe('FidelityAdapter', function () { bidId: '2ffb201a808da7', bidderRequestId: '178e34bad3658f', requestId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', - transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'exchange1.com', + sid: '1234', + hp: 1, + rid: 'bid-request-1', + name: 'publisher', + domain: 'publisher.com' + }] + } } ], start: 1472239426002, @@ -78,7 +90,8 @@ describe('FidelityAdapter', function () { } }; - it('should add source and verison to the tag', function () { + it('should add params to the request', function () { + let schainString = '1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com'; const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); const payload = request.data; expect(payload.from).to.exist; @@ -92,9 +105,11 @@ describe('FidelityAdapter', function () { expect(payload.flashver).to.exist; expect(payload.tmax).to.exist; expect(payload.defloc).to.exist; + expect(payload.schain).to.exist.and.to.be.a('string'); + expect(payload.schain).to.equal(schainString); }); - it('should add gdpr consent information to the request', function () { + it('should add consent information to the request', function () { let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; let uspConsentString = '1YN-'; bidderRequest.gdprConsent = { diff --git a/test/spec/modules/fintezaAnalyticsAdapter_spec.js b/test/spec/modules/fintezaAnalyticsAdapter_spec.js index 4b58761d8a6..ae9ceceecc3 100644 --- a/test/spec/modules/fintezaAnalyticsAdapter_spec.js +++ b/test/spec/modules/fintezaAnalyticsAdapter_spec.js @@ -1,7 +1,7 @@ import fntzAnalyticsAdapter from 'modules/fintezaAnalyticsAdapter.js'; import includes from 'core-js/library/fn/array/includes.js'; import { expect } from 'chai'; -import { parse as parseURL } from 'src/url.js'; +import { parseUrl } from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; let adapterManager = require('src/adapterManager').default; @@ -81,7 +81,7 @@ describe('finteza analytics adapter', function () { expect(server.requests[0].method).to.equal('GET'); expect(server.requests[0].withCredentials).to.equal(true); - const url = parseURL(server.requests[0].url); + const url = parseUrl(server.requests[0].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -124,7 +124,7 @@ describe('finteza analytics adapter', function () { expect(server.requests[0].method).to.equal('GET'); expect(server.requests[0].withCredentials).to.equal(true); - let url = parseURL(server.requests[0].url); + let url = parseUrl(server.requests[0].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -138,7 +138,7 @@ describe('finteza analytics adapter', function () { expect(server.requests[1].method).to.equal('GET'); expect(server.requests[1].withCredentials).to.equal(true); - url = parseURL(server.requests[1].url); + url = parseUrl(server.requests[1].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -178,7 +178,7 @@ describe('finteza analytics adapter', function () { expect(server.requests[0].method).to.equal('GET'); expect(server.requests[0].withCredentials).to.equal(true); - const url = parseURL(server.requests[0].url); + const url = parseUrl(server.requests[0].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -217,7 +217,7 @@ describe('finteza analytics adapter', function () { expect(server.requests[0].method).to.equal('GET'); expect(server.requests[0].withCredentials).to.equal(true); - const url = parseURL(server.requests[0].url); + const url = parseUrl(server.requests[0].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index 69e42c1b2e6..4b80cce8017 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -103,7 +103,7 @@ describe('freewheelSSP BidAdapter Test', () => { it('should add parameters to the tag', () => { const request = spec.buildRequests(bidRequests); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -113,8 +113,8 @@ describe('freewheelSSP BidAdapter Test', () => { it('sends bid request to ENDPOINT via GET', () => { const request = spec.buildRequests(bidRequests); - expect(request.url).to.contain(ENDPOINT); - expect(request.method).to.equal('GET'); + expect(request[0].url).to.contain(ENDPOINT); + expect(request[0].method).to.equal('GET'); }); it('should add usp consent to the request', () => { @@ -122,7 +122,7 @@ describe('freewheelSSP BidAdapter Test', () => { let bidderRequest = {}; bidderRequest.uspConsent = uspConsentString; const request = spec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -141,7 +141,7 @@ describe('freewheelSSP BidAdapter Test', () => { }; const request = spec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -174,7 +174,7 @@ describe('freewheelSSP BidAdapter Test', () => { it('should add parameters to the tag', () => { const request = spec.buildRequests(bidRequests); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -184,8 +184,8 @@ describe('freewheelSSP BidAdapter Test', () => { it('sends bid request to ENDPOINT via GET', () => { const request = spec.buildRequests(bidRequests); - expect(request.url).to.contain(ENDPOINT); - expect(request.method).to.equal('GET'); + expect(request[0].url).to.contain(ENDPOINT); + expect(request[0].method).to.equal('GET'); }); it('should add usp consent to the request', () => { @@ -193,7 +193,7 @@ describe('freewheelSSP BidAdapter Test', () => { let bidderRequest = {}; bidderRequest.uspConsent = uspConsentString; const request = spec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -212,7 +212,7 @@ describe('freewheelSSP BidAdapter Test', () => { }; const request = spec.buildRequests(bidRequests, bidderRequest); - const payload = request.data; + const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); expect(payload.protocolVersion).to.equal('2.0'); expect(payload.zoneId).to.equal('277225'); @@ -327,7 +327,7 @@ describe('freewheelSSP BidAdapter Test', () => { } ]; - let result = spec.interpretResponse(response, request); + let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); @@ -348,15 +348,15 @@ describe('freewheelSSP BidAdapter Test', () => { } ]; - let result = spec.interpretResponse(response, request); + let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); it('handles nobid responses', () => { - var reqest = spec.buildRequests(formattedBidRequests); + var request = spec.buildRequests(formattedBidRequests); let response = ''; - let result = spec.interpretResponse(response, reqest); + let result = spec.interpretResponse(response, request[0]); expect(result.length).to.equal(0); }); }); @@ -454,11 +454,13 @@ describe('freewheelSSP BidAdapter Test', () => { currency: 'EUR', netRevenue: true, ttl: 360, + vastXml: response, + mediaType: 'video', ad: ad } ]; - let result = spec.interpretResponse(response, request); + let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); @@ -475,19 +477,21 @@ describe('freewheelSSP BidAdapter Test', () => { currency: 'EUR', netRevenue: true, ttl: 360, + vastXml: response, + mediaType: 'video', ad: formattedAd } ]; - let result = spec.interpretResponse(response, request); + let result = spec.interpretResponse(response, request[0]); expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); it('handles nobid responses', () => { - var reqest = spec.buildRequests(formattedBidRequests); + var request = spec.buildRequests(formattedBidRequests); let response = ''; - let result = spec.interpretResponse(response, reqest); + let result = spec.interpretResponse(response, request[0]); expect(result.length).to.equal(0); }); }); diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js new file mode 100644 index 00000000000..5b46441cbbb --- /dev/null +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -0,0 +1,418 @@ +import { deviceAccessHook, setEnforcementConfig, userSyncHook, userIdHook } from 'modules/gdprEnforcement.js'; +import { config } from 'src/config.js'; +import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; +import * as utils from 'src/utils.js'; +import { validateStorageEnforcement } from 'src/storageManager.js'; +import { executeStorageCallbacks } from 'src/prebid.js'; + +describe('gdpr enforcement', function() { + let nextFnSpy; + let logWarnSpy; + let gdprDataHandlerStub; + let staticConfig = { + cmpApi: 'static', + timeout: 7500, + allowAuctionWithoutConsent: false, + consentData: { + getTCData: { + 'tcString': 'COuqj-POu90rDBcBkBENAZCgAPzAAAPAACiQFwwBAABAA1ADEAbQC4YAYAAgAxAG0A', + 'cmpId': 92, + 'cmpVersion': 100, + 'tcfPolicyVersion': 2, + 'gdprApplies': true, + 'isServiceSpecific': true, + 'useNonStandardStacks': false, + 'purposeOneTreatment': false, + 'publisherCC': 'US', + 'cmpStatus': 'loaded', + 'eventStatus': 'tcloaded', + 'outOfBand': { + 'allowedVendors': {}, + 'discloseVendors': {} + }, + 'purpose': { + 'consents': { + '1': true, + '2': true, + '3': true + }, + 'legitimateInterests': { + '1': false, + '2': false, + '3': false + } + }, + 'vendor': { + 'consents': { + '1': true, + '2': true, + '3': false + }, + 'legitimateInterests': { + '1': false, + '2': true, + '3': false, + '4': false, + '5': false + } + }, + 'specialFeatureOptins': { + '1': false, + '2': false + }, + 'restrictions': {}, + 'publisher': { + 'consents': { + '1': false, + '2': false, + '3': false + }, + 'legitimateInterests': { + '1': false, + '2': false, + '3': false + }, + 'customPurpose': { + 'consents': {}, + 'legitimateInterests': {} + } + } + } + } + }; + + after(function() { + validateStorageEnforcement.getHooks({hook: deviceAccessHook}).remove(); + $$PREBID_GLOBAL$$.requestBids.getHooks({hook: executeStorageCallbacks}).remove(); + }) + + describe('deviceAccessHook', function() { + beforeEach(function() { + nextFnSpy = sinon.spy(); + gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + logWarnSpy = sinon.spy(utils, 'logWarn'); + }); + afterEach(function() { + config.resetConfig(); + gdprDataHandler.getConsentData.restore(); + logWarnSpy.restore(); + }); + it('should not allow device access when device access flag is set to false', function() { + config.setConfig({ + deviceAccess: false, + consentManagement: { + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: false, + vendorExceptions: ['appnexus', 'rubicon'] + }] + } + } + }); + + deviceAccessHook(nextFnSpy); + expect(nextFnSpy.calledOnce).to.equal(true); + let result = { + hasEnforcementHook: true, + valid: false + } + expect(nextFnSpy.calledWith(undefined, result)); + }); + + it('should only check for consent for vendor exceptions when enforcePurpose and enforceVendor are false', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: false, + vendorExceptions: ['appnexus'] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.gdprApplies = true; + consentData.apiVersion = 2; + gdprDataHandlerStub.returns(consentData); + + deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, 5, 'rubicon'); + expect(logWarnSpy.callCount).to.equal(0); + }); + + it('should check consent for all vendors when enforcePurpose and enforceVendor are true', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.gdprApplies = true; + consentData.apiVersion = 2; + gdprDataHandlerStub.returns(consentData); + + deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, 3, 'rubicon'); + expect(logWarnSpy.callCount).to.equal(1); + }); + + it('should allow device access when gdprApplies is false and hasDeviceAccess flag is true', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.gdprApplies = false; + consentData.apiVersion = 2; + gdprDataHandlerStub.returns(consentData); + + deviceAccessHook(nextFnSpy, 1, 'appnexus'); + expect(nextFnSpy.calledOnce).to.equal(true); + let result = { + hasEnforcementHook: true, + valid: true + } + expect(nextFnSpy.calledWith(undefined, result)); + }); + }); + + describe('userSyncHook', function() { + let curBidderStub; + let adapterManagerStub; + + beforeEach(function() { + gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); + logWarnSpy = sinon.spy(utils, 'logWarn'); + curBidderStub = sinon.stub(config, 'getCurrentBidder'); + adapterManagerStub = sinon.stub(adapterManager, 'getBidAdapter'); + nextFnSpy = sinon.spy(); + }); + + afterEach(function() { + config.getCurrentBidder.restore(); + config.resetConfig(); + gdprDataHandler.getConsentData.restore(); + adapterManager.getBidAdapter.restore(); + logWarnSpy.restore(); + }); + + it('should allow bidder to do user sync if consent is true', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: true, + vendorExceptions: ['sampleBidder2'] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.gdprApplies = true; + consentData.apiVersion = 2; + gdprDataHandlerStub.returns(consentData); + + curBidderStub.returns('sampleBidder1'); + adapterManagerStub.withArgs('sampleBidder1').returns({ + getSpec: function() { + return { + 'gvlid': 1 + } + } + }); + userSyncHook(nextFnSpy); + + curBidderStub.returns('sampleBidder2'); + adapterManagerStub.withArgs('sampleBidder2').returns({ + getSpec: function() { + return { + 'gvlid': 3 + } + } + }); + userSyncHook(nextFnSpy); + expect(nextFnSpy.calledTwice).to.equal(true); + }); + + it('should not allow bidder to do user sync if user has denied consent', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: true, + vendorExceptions: [] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.apiVersion = 2; + consentData.gdprApplies = true; + gdprDataHandlerStub.returns(consentData); + + curBidderStub.returns('sampleBidder1'); + adapterManagerStub.withArgs('sampleBidder1').returns({ + getSpec: function() { + return { + 'gvlid': 1 + } + } + }); + userSyncHook(nextFnSpy); + + curBidderStub.returns('sampleBidder2'); + adapterManagerStub.withArgs('sampleBidder2').returns({ + getSpec: function() { + return { + 'gvlid': 3 + } + } + }); + userSyncHook(nextFnSpy); + expect(nextFnSpy.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(1); + }); + + it('should not check vendor consent when enforceVendor is false', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: true, + enforceVendor: false, + vendorExceptions: ['sampleBidder1'] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.apiVersion = 2; + consentData.gdprApplies = true; + gdprDataHandlerStub.returns(consentData); + + curBidderStub.returns('sampleBidder1'); + adapterManagerStub.withArgs('sampleBidder1').returns({ + getSpec: function() { + return { + 'gvlid': 1 + } + } + }); + userSyncHook(nextFnSpy); + + curBidderStub.returns('sampleBidder2'); + adapterManagerStub.withArgs('sampleBidder2').returns({ + getSpec: function() { + return { + 'gvlid': 3 + } + } + }); + userSyncHook(nextFnSpy); + expect(nextFnSpy.calledTwice).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + }); + }); + + describe('userIdHook', function() { + beforeEach(function() { + logWarnSpy = sinon.spy(utils, 'logWarn'); + nextFnSpy = sinon.spy(); + }); + afterEach(function() { + config.resetConfig(); + logWarnSpy.restore(); + }); + it('should allow user id module if consent is given', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: true, + vendorExceptions: [] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.apiVersion = 2; + consentData.gdprApplies = true; + let submodules = [{ + submodule: { + gvlid: 1, + name: 'sampleUserId' + } + }] + userIdHook(nextFnSpy, submodules, consentData); + expect(nextFnSpy.calledOnce).to.equal(true); + }); + + it('should allow userId module if gdpr not in scope', function() { + let submodules = [{ + submodule: { + gvlid: 1, + name: 'sampleUserId' + } + }]; + let consentData = null; + userIdHook(nextFnSpy, submodules, consentData); + expect(nextFnSpy.calledOnce).to.equal(true); + expect(nextFnSpy.calledWith(undefined, submodules, consentData)); + }); + + it('should not allow user id module if user denied consent', function() { + setEnforcementConfig({ + gdpr: { + rules: [{ + purpose: 'storage', + enforcePurpose: false, + enforceVendor: true, + vendorExceptions: [] + }] + } + }); + let consentData = {} + consentData.vendorData = staticConfig.consentData.getTCData; + consentData.apiVersion = 2; + consentData.gdprApplies = true; + let submodules = [{ + submodule: { + gvlid: 1, + name: 'sampleUserId' + } + }, { + submodule: { + gvlid: 3, + name: 'sampleUserId1' + } + }] + userIdHook(nextFnSpy, submodules, consentData); + expect(logWarnSpy.callCount).to.equal(1); + let expectedSubmodules = [{ + submodule: { + gvlid: 1, + name: 'sampleUserId' + } + }] + expect(nextFnSpy.calledWith(undefined, expectedSubmodules, consentData)); + }); + }); +}); diff --git a/test/spec/modules/gridNMBidAdapter_spec.js b/test/spec/modules/gridNMBidAdapter_spec.js new file mode 100644 index 00000000000..0dbaac0c526 --- /dev/null +++ b/test/spec/modules/gridNMBidAdapter_spec.js @@ -0,0 +1,461 @@ +import { expect } from 'chai'; +import { spec, resetUserSync, getSyncUrl } from 'modules/gridNMBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('TheMediaGridNM Adapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + const paramsList = [ + { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + } + }, + { + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + { + 'source': 'jwp', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + { + 'source': 'jwp', + 'secid': '11', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + } + ]; + paramsList.forEach((params) => { + const invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = params; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + it('should return false when required params has invalid values', function () { + const paramsList = [ + { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': '1,2,3,4,5' + } + }, + { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': [1, 2], + 'protocols': [1, 2, 3, 4, 5] + } + }, + { + 'source': 'jwp', + 'secid': 11, + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5] + } + }, + { + 'source': 111, + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5] + } + } + ]; + + paramsList.forEach((params) => { + const invalidBid = Object.assign({}, bid); + delete invalidBid.params; + invalidBid.params = params; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + }); + + describe('buildRequests', function () { + function parseRequestUrl(url) { + const res = {}; + url.replace(/^[^\?]+\?/, '').split('&').forEach((it) => { + const couple = it.split('='); + res[couple[0]] = decodeURIComponent(couple[1]); + }); + return res; + } + const bidderRequest = {refererInfo: {referer: 'https://example.com'}}; + const referrer = bidderRequest.refererInfo.referer; + let bidRequests = [ + { + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3], + 'skip': 1 + } + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '3150ccb55da321', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('should attach valid params to the tag', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + const requestsSizes = ['300x250,300x600', '728x90']; + requests.forEach((req, i) => { + expect(req.url).to.be.an('string'); + const payload = parseRequestUrl(req.url); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('r', '22edbae2733bf6'); + expect(payload).to.have.property('wrapperType', 'Prebid_js'); + expect(payload).to.have.property('wrapperVersion', '$prebid.version$'); + expect(payload).to.have.property('sizes', requestsSizes[i]); + expect(req.data).to.deep.equal(bidRequests[i].params); + }); + }); + + it('if gdprConsent is present payload must have gdpr params', function () { + const [request] = spec.buildRequests([bidRequests[0]], {gdprConsent: {consentString: 'AAA', gdprApplies: true}, refererInfo: bidderRequest.refererInfo}); + expect(request.url).to.be.an('string'); + const payload = parseRequestUrl(request.url); + expect(payload).to.have.property('u', referrer); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '1'); + }); + + it('if gdprApplies is false gdpr_applies must be 0', function () { + const [request] = spec.buildRequests([bidRequests[0]], {gdprConsent: {consentString: 'AAA', gdprApplies: false}}); + expect(request.url).to.be.an('string'); + const payload = parseRequestUrl(request.url); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '0'); + }); + + it('if gdprApplies is undefined gdpr_applies must be 1', function () { + const [request] = spec.buildRequests([bidRequests[0]], {gdprConsent: {consentString: 'AAA'}}); + expect(request.url).to.be.an('string'); + const payload = parseRequestUrl(request.url); + expect(payload).to.have.property('gdpr_consent', 'AAA'); + expect(payload).to.have.property('gdpr_applies', '1'); + }); + + it('if usPrivacy is present payload must have us_privacy param', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const [request] = spec.buildRequests([bidRequests[0]], bidderRequestWithUSP); + expect(request.url).to.be.an('string'); + const payload = parseRequestUrl(request.url); + expect(payload).to.have.property('us_privacy', '1YNN'); + }); + }); + + describe('interpretResponse', function () { + const responses = [ + {'bid': [{'price': 1.15, 'adm': '\n<\/Ad>\n<\/VAST>', 'content_type': 'video', 'h': 250, 'w': 300, 'dealid': 11}], 'seat': '2'}, + {'bid': [{'price': 0.5, 'adm': '\n<\/Ad>\n<\/VAST>', 'content_type': 'video', 'h': 600, 'w': 300}], 'seat': '2'}, + {'bid': [{'price': 0, 'h': 250, 'w': 300}], 'seat': '2'}, + {'bid': [{'price': 0, 'adm': '\n<\/Ad>\n<\/VAST>', 'h': 250, 'w': 300}], 'seat': '2'}, + undefined, + {'bid': [], 'seat': '2'}, + {'seat': '2'}, + ]; + + it('should get correct video bid response', function () { + const bidRequests = [ + { + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4', 'video/x-ms-wmv'], + 'protocols': [1, 2, 3, 4, 5, 6] + } + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '659423fff799cb', + 'bidderRequestId': '5f2009617a7c0a', + 'auctionId': '1cbd2feafe5e8b', + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + }, + { + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5], + 'skip': 1 + } + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '2bc598e42b6a', + 'bidderRequestId': '1e8b5a465f404', + 'auctionId': '1cbd2feafe5e8b', + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + } + ]; + const requests = spec.buildRequests(bidRequests); + const expectedResponse = [ + { + 'requestId': '659423fff799cb', + 'cpm': 1.15, + 'creativeId': '5f2009617a7c0a', + 'dealId': 11, + 'width': 300, + 'height': 250, + 'bidderCode': 'gridNM', + 'currency': 'USD', + 'mediaType': 'video', + 'netRevenue': false, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + } + }, + { + 'requestId': '2bc598e42b6a', + 'cpm': 0.5, + 'creativeId': '1e8b5a465f404', + 'dealId': undefined, + 'width': 300, + 'height': 600, + 'bidderCode': 'gridNM', + 'currency': 'USD', + 'mediaType': 'video', + 'netRevenue': false, + 'ttl': 360, + 'vastXml': '\n<\/Ad>\n<\/VAST>', + 'adResponse': { + 'content': '\n<\/Ad>\n<\/VAST>' + } + } + ]; + + requests.forEach((req, i) => { + const result = spec.interpretResponse({'body': {'seatbid': [responses[i]]}}, req); + expect(result[0]).to.deep.equal(expectedResponse[i]); + }); + }); + + it('handles wrong and nobid responses', function () { + responses.slice(2).forEach((resp) => { + const request = spec.buildRequests([{ + 'bidder': 'gridNM', + 'params': { + 'source': 'jwp', + 'secid': '11', + 'pubid': '22', + 'video': { + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5], + 'skip': 1 + } + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '2bc598e42b6a', + 'bidderRequestId': '39d74f5b71464', + 'auctionId': '1cbd2feafe5e8b', + 'mediaTypes': { + 'video': { + 'context': 'instream' + } + } + }]); + const result = spec.interpretResponse({'body': {'seatbid': [resp]}}, request[0]); + expect(result.length).to.equal(0); + }); + }); + }); + + describe('user sync', function () { + const syncUrl = getSyncUrl(); + + beforeEach(function () { + resetUserSync(); + }); + + it('should register the Emily iframe', function () { + let syncs = spec.getUserSyncs({ + pixelEnabled: true + }); + + expect(syncs).to.deep.equal({type: 'image', url: syncUrl}); + }); + + it('should not register the Emily iframe more than once', function () { + let syncs = spec.getUserSyncs({ + pixelEnabled: true + }); + expect(syncs).to.deep.equal({type: 'image', url: syncUrl}); + + // when called again, should still have only been called once + syncs = spec.getUserSyncs(); + expect(syncs).to.equal(undefined); + }); + + it('should pass gdpr params if consent is true', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + gdprApplies: true, consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr=1&gdpr_consent=foo` + }); + }); + + it('should pass gdpr params if consent is false', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + gdprApplies: false, consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr=0&gdpr_consent=foo` + }); + }); + + it('should pass gdpr param gdpr_consent only when gdprApplies is undefined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: 'foo' + })).to.deep.equal({ + type: 'image', url: `${syncUrl}&gdpr_consent=foo` + }); + }); + + it('should pass no params if gdpr consentString is not defined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {})).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is a number', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: 0 + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is null', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: null + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr consentString is a object', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { + consentString: {} + })).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass no params if gdpr is not defined', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined)).to.deep.equal({ + type: 'image', url: syncUrl + }); + }); + + it('should pass usPrivacy param if it is available', function() { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {}, '1YNN')).to.deep.equal({ + type: 'image', url: `${syncUrl}&us_privacy=1YNN` + }); + }); + }); +}); diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index 9073ad3bfda..a2fbc2cf029 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -228,6 +228,13 @@ describe('gumgumAdapter', function () { expect(bidRequest.data).to.not.include.any.keys('ns'); } }); + it('has jcsi param correctly encoded', function () { + const jcsi = JSON.stringify({ t: 0, rq: 8 }); + const encodedJCSI = encodeURIComponent(jcsi); + const bidRequest = spec.buildRequests(bidRequests)[0]; + expect(bidRequest.data.jcsi).to.not.contain(/\{.*\}/); + expect(bidRequest.data.jcsi).to.eq(encodedJCSI); + }); }) describe('interpretResponse', function () { diff --git a/test/spec/modules/hybridBidAdapter_spec.js b/test/spec/modules/hybridBidAdapter_spec.js new file mode 100644 index 00000000000..e5ad878c9b1 --- /dev/null +++ b/test/spec/modules/hybridBidAdapter_spec.js @@ -0,0 +1,275 @@ +import { expect } from 'chai' +import { spec } from 'modules/hybridBidAdapter.js' + +function getSlotConfigs(mediaTypes, params) { + return { + params: params, + sizes: [], + bidId: '2df8c0733f284e', + bidder: 'hybrid', + mediaTypes: mediaTypes, + transactionId: '31a58515-3634-4e90-9c96-f86196db1459' + } +} + +describe('Hybrid.ai Adapter', function() { + const PLACE_ID = '5af45ad34d506ee7acad0c26'; + const bidderRequest = { + refererInfo: { referer: 'referer' } + } + const bannerMandatoryParams = { + placeId: PLACE_ID, + placement: 'banner' + } + const videoMandatoryParams = { + placeId: PLACE_ID, + placement: 'video' + } + const validBidRequests = [ + getSlotConfigs({ banner: {} }, bannerMandatoryParams), + getSlotConfigs({ video: {playerSize: [[640, 480]]} }, videoMandatoryParams) + ] + describe('isBidRequestValid method', function() { + describe('returns true', function() { + describe('when banner slot config has all mandatory params', () => { + describe('and placement has the correct value', function() { + const slotConfig = getSlotConfigs( + { banner: {} }, + { + placeId: PLACE_ID, + placement: 'banner' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) + describe('when video slot has all mandatory params.', function() { + it('should return true, when video mediatype object are correct.', function() { + const slotConfig = getSlotConfigs( + { + video: { + context: 'instream', + playerSize: [[640, 480]] + } + }, + { + placeId: PLACE_ID, + placement: 'video' + } + ) + const isBidRequestValid = spec.isBidRequestValid(slotConfig) + expect(isBidRequestValid).to.equal(true) + }) + }) + }) + }) + describe('returns false', function() { + describe('when params are not correct', function() { + function createSlotconfig(params) { + return getSlotConfigs({ banner: {} }, params) + } + it('does not have the placeId.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placement: 'banner' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have the placement.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placeId: PLACE_ID + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have a the correct placement.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createSlotconfig({ + placeId: PLACE_ID, + placement: 'something' + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + }) + describe('when video mediaType object is not correct.', function() { + function createVideoSlotconfig(mediaType) { + return getSlotConfigs(mediaType, { + placeId: PLACE_ID, + placement: 'video' + }) + } + it('is a void object', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ video: {} }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have playerSize.', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ video: { context: 'instream' } }) + ) + expect(isBidRequestValid).to.equal(false) + }) + it('does not have context', function() { + const isBidRequestValid = spec.isBidRequestValid( + createVideoSlotconfig({ + video: { + playerSize: [[640, 480]] + } + }) + ) + expect(isBidRequestValid).to.equal(false) + }) + }) + }) + }) + it('Url params should be correct ', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + expect(request.method).to.equal('POST') + expect(request.url).to.equal('https://hbe198.hybrid.ai/prebidhb') + }) + + describe('buildRequests method', function() { + it('Common data request should be correct', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(Array.isArray(data.bidRequests)).to.equal(true) + expect(data.url).to.equal('referer') + data.bidRequests.forEach(bid => { + expect(bid.bidId).to.equal('2df8c0733f284e') + expect(bid.placeId).to.equal(PLACE_ID) + expect(bid.transactionId).to.equal('31a58515-3634-4e90-9c96-f86196db1459') + }) + }) + + describe('GDPR params', function() { + describe('when there are not consent management platform', function() { + it('cmp should be false', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(false) + }) + }) + describe('when there are consent management platform', function() { + it('cmps should be true and ga should not sended, when gdprApplies is undefined', function() { + bidderRequest['gdprConsent'] = { + gdprApplies: undefined, + consentString: 'consentString' + } + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(true) + expect(Object.keys(data).indexOf('data')).to.equal(-1) + expect(data.cs).to.equal('consentString') + }) + it('cmps should be true and all gdpr parameters should be sended, when there are gdprApplies', function() { + bidderRequest['gdprConsent'] = { + gdprApplies: true, + consentString: 'consentString' + } + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expect(data.cmp).to.equal(true) + expect(data.ga).to.equal(true) + expect(data.cs).to.equal('consentString') + }) + }) + }) + + describe('BidRequests params', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + const bidRequests = data.bidRequests + it('should request a Banner', function() { + const bannerBid = bidRequests[0] + expect(bannerBid.placement).to.equal(spec.placementTypes[bannerMandatoryParams.placement]) + }) + it('should request a Video', function() { + const bannerBid = bidRequests[1] + expect(bannerBid.placement).to.equal(spec.placementTypes[videoMandatoryParams.placement]) + }) + }) + }) + + describe('interpret response method', function() { + it('should return a void array, when the server response are not correct.', function() { + const request = { data: JSON.stringify({}) } + const serverResponse = { + body: {} + } + const bids = spec.interpretResponse(serverResponse, request) + expect(typeof bids).to.equal('object') + expect(bids.length).to.equal(0) + }) + it('should return a void array, when the server response have not got bids.', function() { + const request = { data: JSON.stringify({}) } + const serverResponse = { body: { bids: [] } } + const bids = spec.interpretResponse(serverResponse, request) + expect(typeof bids).to.equal('object') + expect(bids.length).to.equal(0) + }) + describe('when the server response return a bid', function() { + describe('the bid is a banner', function() { + it('should return a banner bid', function() { + const request = spec.buildRequests([validBidRequests[0]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: 'html', + width: 100, + height: 100 + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].mediaType).to.equal(spec.supportedMediaTypes[0]) + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].width).to.equal(100) + expect(bids[0].height).to.equal(100) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].ad).to.equal('string') + }) + }) + describe('the bid is a video', function() { + it('should return a video bid', function() { + const request = spec.buildRequests([validBidRequests[1]], bidderRequest) + const serverResponse = { + body: { + bids: [ + { + bidId: '2df8c0733f284e', + price: 0.5, + currency: 'USD', + content: 'html', + width: 100, + height: 100 + } + ] + } + } + const bids = spec.interpretResponse(serverResponse, request) + expect(bids.length).to.equal(1) + expect(bids[0].requestId).to.equal('2df8c0733f284e') + expect(bids[0].mediaType).to.equal(spec.supportedMediaTypes[1]) + expect(bids[0].cpm).to.equal(0.5) + expect(bids[0].width).to.equal(100) + expect(bids[0].height).to.equal(100) + expect(bids[0].currency).to.equal('USD') + expect(bids[0].netRevenue).to.equal(true) + expect(typeof bids[0].vastXml).to.equal('string') + }) + }) + }) + }) +}) diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 676f6ab0fd0..1466b509c54 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -1,10 +1,10 @@ import { expect } from 'chai'; import { ImproveDigitalAdServerJSClient, spec } from 'modules/improvedigitalBidAdapter.js'; import { config } from 'src/config.js'; -import { userSync } from 'src/userSync.js'; +import * as utils from 'src/utils.js'; describe('Improve Digital Adapter Tests', function () { - let idClient = new ImproveDigitalAdServerJSClient('hb'); + const idClient = new ImproveDigitalAdServerJSClient('hb'); const METHOD = 'GET'; const URL = 'https://ice.360yield.com/hb'; @@ -20,9 +20,41 @@ describe('Improve Digital Adapter Tests', function () { bidId: '33e9500b21129f', bidderRequestId: '2772c1e566670b', auctionId: '192721e36a0239', + mediaTypes: { + banner: { + sizes: [[300, 250], [160, 600], ['blah', 150], [-1, 300], [300, -5]] + } + }, sizes: [[300, 250], [160, 600], ['blah', 150], [-1, 300], [300, -5]] }; + const instreamBidRequest = utils.deepClone(simpleBidRequest); + instreamBidRequest.mediaTypes = { + video: { + context: 'instream', + playerSize: [640, 480] + } + }; + + const outstreamBidRequest = utils.deepClone(simpleBidRequest); + outstreamBidRequest.mediaTypes = { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }; + + const multiFormatBidRequest = utils.deepClone(simpleBidRequest); + multiFormatBidRequest.mediaTypes = { + banner: { + sizes: [[300, 250], [160, 600], ['blah', 150], [-1, 300], [300, -5]] + }, + video: { + context: 'outstream', + playerSize: [640, 480] + } + }; + const simpleSmartTagBidRequest = { bidder: 'improvedigital', bidId: '1a2b3c', @@ -33,7 +65,24 @@ describe('Improve Digital Adapter Tests', function () { } }; + const bidderRequest = { + bids: [simpleBidRequest] + }; + + const instreamBidderRequest = { + bids: [instreamBidRequest] + }; + + const outstreamBidderRequest = { + bids: [outstreamBidRequest] + }; + + const multiFormatBidderRequest = { + bids: [multiFormatBidRequest] + }; + const bidderRequestGdpr = { + bids: [simpleBidRequest], gdprConsent: { consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', vendorData: {}, @@ -42,6 +91,7 @@ describe('Improve Digital Adapter Tests', function () { }; const bidderRequestReferrer = { + bids: [simpleBidRequest], refererInfo: { referer: 'https://blah.com/test.html', }, @@ -53,12 +103,12 @@ describe('Improve Digital Adapter Tests', function () { }); it('should return false when no bid.params', function () { - let bid = {}; + const bid = {}; expect(spec.isBidRequestValid(bid)).to.equal(false); }); it('should return false when both placementId and placementKey + publisherId are missing', function () { - let bid = { 'params': {} }; + const bid = { 'params': {} }; expect(spec.isBidRequestValid(bid)).to.equal(false); }); @@ -78,25 +128,51 @@ describe('Improve Digital Adapter Tests', function () { }); it('should return true when placementId is passed', function () { - let bid = { 'params': {} }; expect(spec.isBidRequestValid(simpleBidRequest)).to.equal(true); }); it('should return true when both placementKey and publisherId are passed', function () { - let bid = { 'params': {} }; expect(spec.isBidRequestValid(simpleSmartTagBidRequest)).to.equal(true); }); }); describe('buildRequests', function () { it('should make a well-formed request objects', function () { - const requests = spec.buildRequests([simpleBidRequest]); + const requests = spec.buildRequests([simpleBidRequest], bidderRequest); expect(requests).to.be.an('array'); expect(requests.length).to.equal(1); const request = requests[0]; expect(request.method).to.equal(METHOD); expect(request.url).to.equal(URL); + expect(request.bidderRequest).to.deep.equal(bidderRequest); + expect(request.data.substring(0, PARAM_PREFIX.length)).to.equal(PARAM_PREFIX); + + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request).to.be.an('object'); + expect(params.bid_request.id).to.be.a('string'); + expect(params.bid_request.version).to.equal(`${spec.version}-${idClient.CONSTANTS.CLIENT_VERSION}`); + expect(params.bid_request.gdpr).to.not.exist; + expect(params.bid_request.us_privacy).to.not.exist; + expect(params.bid_request.imp).to.deep.equal([ + { + id: '33e9500b21129f', + pid: 1053688, + tid: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', + banner: {} + } + ]); + }); + + it('should make a well-formed request object for multi-format ad unit', function () { + const requests = spec.buildRequests([multiFormatBidRequest], multiFormatBidderRequest); + expect(requests).to.be.an('array'); + expect(requests.length).to.equal(1); + + const request = requests[0]; + expect(request.method).to.equal(METHOD); + expect(request.url).to.equal(URL); + expect(request.bidderRequest).to.deep.equal(multiFormatBidderRequest); expect(request.data.substring(0, PARAM_PREFIX.length)).to.equal(PARAM_PREFIX); const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); @@ -116,33 +192,33 @@ describe('Improve Digital Adapter Tests', function () { }); it('should set placementKey and publisherId for smart tags', function () { - const requests = spec.buildRequests([simpleSmartTagBidRequest]); + const requests = spec.buildRequests([simpleSmartTagBidRequest], bidderRequest); const params = JSON.parse(decodeURIComponent(requests[0].data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].pubid).to.equal(1032); expect(params.bid_request.imp[0].pkey).to.equal('data_team_test_hb_smoke_test'); }); it('should add keyValues', function () { - let bidRequest = Object.assign({}, simpleBidRequest); + const bidRequest = Object.assign({}, simpleBidRequest); const keyValues = { testKey: [ 'testValue' ] }; bidRequest.params.keyValues = keyValues; - const request = spec.buildRequests([bidRequest])[0]; + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].kvw).to.deep.equal(keyValues); }); it('should add single size filter', function () { - let bidRequest = Object.assign({}, simpleBidRequest); + const bidRequest = Object.assign({}, simpleBidRequest); const size = { w: 800, h: 600 }; bidRequest.params.size = size; - const request = spec.buildRequests([bidRequest])[0]; + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].banner).to.deep.equal(size); // When single size filter is set, format shouldn't be populated. This @@ -153,7 +229,7 @@ describe('Improve Digital Adapter Tests', function () { it('should add currency', function () { const bidRequest = Object.assign({}, simpleBidRequest); const getConfigStub = sinon.stub(config, 'getConfig').returns('JPY'); - const request = spec.buildRequests([bidRequest])[0]; + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].currency).to.equal('JPY'); getConfigStub.restore(); @@ -161,14 +237,14 @@ describe('Improve Digital Adapter Tests', function () { it('should add bid floor', function () { const bidRequest = Object.assign({}, simpleBidRequest); - let request = spec.buildRequests([bidRequest])[0]; + let request = spec.buildRequests([bidRequest], bidderRequest)[0]; let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); // Floor price currency shouldn't be populated without a floor price expect(params.bid_request.imp[0].bidfloorcur).to.not.exist; // Default floor price currency bidRequest.params.bidFloor = 0.05; - request = spec.buildRequests([bidRequest])[0]; + request = spec.buildRequests([bidRequest], bidderRequest)[0]; params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].bidfloor).to.equal(0.05); expect(params.bid_request.imp[0].bidfloorcur).to.equal('USD'); @@ -205,7 +281,7 @@ describe('Improve Digital Adapter Tests', function () { it('should add ad type for instream video', function () { let bidRequest = Object.assign({}, simpleBidRequest); bidRequest.mediaType = 'video'; - let request = spec.buildRequests([bidRequest])[0]; + let request = spec.buildRequests([bidRequest], bidderRequest)[0]; let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].ad_types).to.deep.equal(['video']); @@ -216,38 +292,48 @@ describe('Improve Digital Adapter Tests', function () { playerSize: [640, 480] } }; - request = spec.buildRequests([bidRequest])[0]; + request = spec.buildRequests([bidRequest], bidderRequest)[0]; params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].ad_types).to.deep.equal(['video']); }); + it('should not set ad type for outstream video', function() { + const request = spec.buildRequests([outstreamBidRequest])[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.imp[0].ad_types).to.not.exist; + }); + + it('should not set ad type for multi-format bids', function() { + const request = spec.buildRequests([multiFormatBidRequest], bidderRequest)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.imp[0].ad_types).to.not.exist; + }); + it('should not set Prebid sizes in bid request for instream video', function () { const getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); - const bidRequest = Object.assign({}, simpleBidRequest); - bidRequest.mediaTypes = { - video: { - context: 'instream', - playerSize: [640, 480] - } - }; - const request = spec.buildRequests([bidRequest])[0]; + const request = spec.buildRequests([instreamBidRequest], bidderRequest)[0]; const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].banner.format).to.not.exist; getConfigStub.restore(); }); - it('should not set ad type for outstream video', function() { - const bidRequest = Object.assign({}, simpleBidRequest); - bidRequest.mediaTypes = { - video: { - context: 'outstream', - playerSize: [640, 480] - } - }; - const request = spec.buildRequests([bidRequest])[0]; + it('should not set Prebid sizes in bid request for outstream video', function () { + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); + const request = spec.buildRequests([outstreamBidRequest], bidderRequest)[0]; const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].ad_types).to.not.exist; + expect(params.bid_request.imp[0].banner.format).to.not.exist; + getConfigStub.restore(); + }); + + it('should not set Prebid sizes in multi-format bid request', function () { + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); + const request = spec.buildRequests([multiFormatBidRequest], bidderRequest)[0]; + const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + expect(params.bid_request.imp[0].banner.format).to.not.exist; + getConfigStub.restore(); }); it('should add schain', function () { @@ -263,9 +349,11 @@ describe('Improve Digital Adapter Tests', function () { const requests = spec.buildRequests([ simpleBidRequest, simpleSmartTagBidRequest - ]); + ], bidderRequest); expect(requests).to.be.an('array'); expect(requests.length).to.equal(2); + expect(requests[0].bidderRequest).to.deep.equal(bidderRequest); + expect(requests[1].bidderRequest).to.deep.equal(bidderRequest); }); it('should return one request in a single request mode', function () { @@ -274,7 +362,7 @@ describe('Improve Digital Adapter Tests', function () { const requests = spec.buildRequests([ simpleBidRequest, simpleSmartTagBidRequest - ]); + ], bidderRequest); expect(requests).to.be.an('array'); expect(requests.length).to.equal(1); getConfigStub.restore(); @@ -283,7 +371,7 @@ describe('Improve Digital Adapter Tests', function () { it('should set Prebid sizes in bid request', function () { const getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); - const request = spec.buildRequests([simpleBidRequest])[0]; + const request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].banner).to.deep.equal({ format: [ @@ -303,7 +391,7 @@ describe('Improve Digital Adapter Tests', function () { h: 600 }; bidRequest.params.size = size; - const request = spec.buildRequests([bidRequest])[0]; + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); expect(params.bid_request.imp[0].banner).to.deep.equal({ format: [ @@ -555,7 +643,7 @@ describe('Improve Digital Adapter Tests', function () { ]; describe('interpretResponse', function () { - let expectedBid = [ + const expectedBid = [ { 'ad': '', 'adId': '33e9500b21129f', @@ -571,7 +659,7 @@ describe('Improve Digital Adapter Tests', function () { } ]; - let expectedTwoBids = [ + const expectedTwoBids = [ expectedBid[0], { 'ad': '', @@ -588,7 +676,7 @@ describe('Improve Digital Adapter Tests', function () { } ]; - let expectedBidNative = [ + const expectedBidNative = [ { mediaType: 'native', adId: '33e9500b21129f', @@ -637,7 +725,7 @@ describe('Improve Digital Adapter Tests', function () { } ]; - let expectedBidVideo = [ + const expectedBidInstreamVideo = [ { 'vastXml': '', 'adId': '33e9500b21129f', @@ -653,60 +741,72 @@ describe('Improve Digital Adapter Tests', function () { } ]; - it('should return a well-formed bid', function () { - const bids = spec.interpretResponse(serverResponse); + const expectedBidOutstreamVideo = utils.deepClone(expectedBidInstreamVideo); + expectedBidOutstreamVideo[0].adResponse = { + content: expectedBidOutstreamVideo[0].vastXml, + height: expectedBidOutstreamVideo[0].height, + width: expectedBidOutstreamVideo[0].width + }; + + it('should return a well-formed display bid', function () { + const bids = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(bids).to.deep.equal(expectedBid); + }); + + it('should return a well-formed display bid for multi-format ad unit', function () { + const bids = spec.interpretResponse(serverResponse, {bidderRequest: multiFormatBidderRequest}); expect(bids).to.deep.equal(expectedBid); }); it('should return two bids', function () { - const bids = spec.interpretResponse(serverResponseTwoBids); + const bids = spec.interpretResponse(serverResponseTwoBids, {bidderRequest}); expect(bids).to.deep.equal(expectedTwoBids); }); it('should set dealId correctly', function () { - let response = JSON.parse(JSON.stringify(serverResponse)); + const response = JSON.parse(JSON.stringify(serverResponse)); let bids; delete response.body.bid[0].lid; response.body.bid[0].buying_type = 'deal_id'; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.not.exist; response.body.bid[0].lid = 268515; delete response.body.bid[0].buying_type; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.not.exist; response.body.bid[0].lid = 268515; response.body.bid[0].buying_type = 'classic'; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.not.exist; response.body.bid[0].lid = 268515; response.body.bid[0].buying_type = 'deal_id'; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.equal(268515); response.body.bid[0].lid = [ 268515, 12456, 34567 ]; response.body.bid[0].buying_type = 'deal_id'; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.not.exist; response.body.bid[0].lid = [ 268515, 12456, 34567 ]; response.body.bid[0].buying_type = [ 'deal_id', 'classic' ]; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.not.exist; response.body.bid[0].lid = [ 268515, 12456, 34567 ]; response.body.bid[0].buying_type = [ 'classic', 'deal_id', 'deal_id' ]; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.equal(12456); }); it('should set currency', function () { - let response = JSON.parse(JSON.stringify(serverResponse)); + const response = JSON.parse(JSON.stringify(serverResponse)); response.body.bid[0].currency = 'eur'; - const bids = spec.interpretResponse(response); + const bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].currency).to.equal('EUR'); }); @@ -716,41 +816,41 @@ describe('Improve Digital Adapter Tests', function () { // Price missing or 0 response.body.bid[0].price = 0; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); delete response.body.bid[0].price; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); response.body.bid[0].price = null; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); // errorCode present response = JSON.parse(JSON.stringify(serverResponse)); response.body.bid[0].errorCode = undefined; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); // adm and native missing response = JSON.parse(JSON.stringify(serverResponse)); delete response.body.bid[0].adm; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); response.body.bid[0].adm = null; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); }); it('should set netRevenue', function () { - let response = JSON.parse(JSON.stringify(serverResponse)); + const response = JSON.parse(JSON.stringify(serverResponse)); response.body.bid[0].isNet = true; - const bids = spec.interpretResponse(response); + const bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].netRevenue).to.equal(true); }); // Native ads it('should return a well-formed native ad bid', function () { - let bids = spec.interpretResponse(serverResponseNative); + let bids = spec.interpretResponse(serverResponseNative, {bidderRequest}); expect(bids[0].ortbNative).to.deep.equal(serverResponseNative.body.bid[0].native); delete bids[0].ortbNative; expect(bids).to.deep.equal(expectedBidNative); @@ -764,15 +864,29 @@ describe('Improve Digital Adapter Tests', function () { 'https://www.mytracker.com/imptracker' ]; expectedBids[0].native.javascriptTrackers = ''; - bids = spec.interpretResponse(response); + bids = spec.interpretResponse(response, {bidderRequest}); delete bids[0].ortbNative; expect(bids).to.deep.equal(expectedBids); }); // Video - it('should return a well-formed video bid', function () { - const bids = spec.interpretResponse(serverResponseVideo); - expect(bids).to.deep.equal(expectedBidVideo); + it('should return a well-formed instream video bid', function () { + const bids = spec.interpretResponse(serverResponseVideo, {bidderRequest: instreamBidderRequest}); + expect(bids).to.deep.equal(expectedBidInstreamVideo); + }); + + it('should return a well-formed outstream video bid', function () { + const bids = spec.interpretResponse(serverResponseVideo, {bidderRequest: outstreamBidderRequest}); + expect(bids[0].renderer).to.exist; + delete (bids[0].renderer); + expect(bids).to.deep.equal(expectedBidOutstreamVideo); + }); + + it('should return a well-formed outstream video bid for multi-format ad unit', function () { + const bids = spec.interpretResponse(serverResponseVideo, {bidderRequest: multiFormatBidderRequest}); + expect(bids[0].renderer).to.exist; + delete (bids[0].renderer); + expect(bids).to.deep.equal(expectedBidOutstreamVideo); }); }); diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index fe18947b37a..114a2226770 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -197,6 +197,26 @@ describe('IndexexchangeAdapter', function () { }); }); + describe('getUserSync tests', function () { + it('UserSync test : check type = iframe, check usermatch URL', function () { + const syncOptions = { + 'iframeEnabled': true + } + let userSync = spec.getUserSyncs(syncOptions); + expect(userSync[0].type).to.equal('iframe'); + const USER_SYNC_URL = 'https://js-sec.indexww.com/um/ixmatch.html'; + expect(userSync[0].url).to.equal(USER_SYNC_URL); + }); + + it('When iframeEnabled is false, no userSync should be returned', function () { + const syncOptions = { + 'iframeEnabled': false + } + let userSync = spec.getUserSyncs(syncOptions); + expect(userSync).to.be.an('array').that.is.empty; + }); + }); + describe('isBidRequestValid', function () { it('should return true when required params found for a banner or video ad', function () { expect(spec.isBidRequestValid(DEFAULT_BANNER_VALID_BID[0])).to.equal(true); @@ -337,19 +357,19 @@ describe('IndexexchangeAdapter', function () { let query; let testCopy; - beforeEach(function() { + beforeEach(function () { window.headertag = {}; - window.headertag.getIdentityInfo = function() { + window.headertag.getIdentityInfo = function () { return testCopy; }; request = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION)[0]; query = request.data; }); - afterEach(function() { + afterEach(function () { delete window.headertag; }); - describe('buildRequestSingleRTI', function() { - before(function() { + describe('buildRequestSingleRTI', function () { + before(function () { testCopy = JSON.parse(JSON.stringify(DEFAULT_IDENTITY_RESPONSE)); }); it('payload should have correct format and value (single identity partner)', function () { @@ -368,15 +388,17 @@ describe('IndexexchangeAdapter', function () { }); }); - describe('buildRequestMultipleIds', function() { - before(function() { + describe('buildRequestMultipleIds', function () { + before(function () { testCopy = JSON.parse(JSON.stringify(DEFAULT_IDENTITY_RESPONSE)); - testCopy.IdentityIp.data.uids.push({ - id: '1234567' - }, - { - id: '2019-04-01TF2:34:41' - }); + testCopy.IdentityIp.data.uids.push( + { + id: '1234567' + }, + { + id: '2019-04-01TF2:34:41' + } + ); }); it('payload should have correct format and value (single identity w/ multi ids)', function () { const payload = JSON.parse(query.r); @@ -395,8 +417,8 @@ describe('IndexexchangeAdapter', function () { }); }); - describe('buildRequestMultipleRTI', function() { - before(function() { + describe('buildRequestMultipleRTI', function () { + before(function () { testCopy = JSON.parse(JSON.stringify(DEFAULT_IDENTITY_RESPONSE)); testCopy.JackIp = { responsePending: false, @@ -444,13 +466,13 @@ describe('IndexexchangeAdapter', function () { }); }); - describe('buildRequestNoData', function() { - beforeEach(function() { + describe('buildRequestNoData', function () { + beforeEach(function () { testCopy = JSON.parse(JSON.stringify(DEFAULT_IDENTITY_RESPONSE)); }); it('payload should not have any user eids with an undefined identity data response', function () { - window.headertag.getIdentityInfo = function() { + window.headertag.getIdentityInfo = function () { return undefined; }; request = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION)[0]; @@ -960,7 +982,7 @@ describe('IndexexchangeAdapter', function () { }); }); - describe('bidrequest consent', function() { + describe('bidrequest consent', function () { it('should have consent info if gdprApplies and consentString exist', function () { const validBidWithConsent = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION); const requestWithConsent = JSON.parse(validBidWithConsent[0].data.r); @@ -1006,7 +1028,7 @@ describe('IndexexchangeAdapter', function () { expect(requestWithConsent.user).to.be.undefined; }); - it('should have us_privacy if uspConsent is defined', function() { + it('should have us_privacy if uspConsent is defined', function () { const options = { uspConsent: '1YYN' }; @@ -1016,7 +1038,7 @@ describe('IndexexchangeAdapter', function () { expect(requestWithUspConsent.regs.ext.us_privacy).to.equal('1YYN'); }); - it('should not have us_privacy if uspConsent undefined', function() { + it('should not have us_privacy if uspConsent undefined', function () { const options = {}; const validBidWithUspConsent = spec.buildRequests(DEFAULT_BANNER_VALID_BID, options); const requestWithUspConsent = JSON.parse(validBidWithUspConsent[0].data.r); @@ -1024,7 +1046,7 @@ describe('IndexexchangeAdapter', function () { expect(requestWithUspConsent.regs).to.be.undefined; }); - it('should have both gdpr and us_privacy if both are defined', function() { + it('should have both gdpr and us_privacy if both are defined', function () { const options = { gdprConsent: { gdprApplies: true, diff --git a/test/spec/modules/konduitWrapper_spec.js b/test/spec/modules/konduitWrapper_spec.js index bfae5711d32..4a0c627e885 100644 --- a/test/spec/modules/konduitWrapper_spec.js +++ b/test/spec/modules/konduitWrapper_spec.js @@ -2,7 +2,7 @@ import { expect } from 'chai'; import parse from 'url-parse'; import { buildVastUrl } from 'modules/konduitWrapper.js'; -import { parseQS } from 'src/url.js'; +import { parseQS } from 'src/utils.js'; import { config } from 'src/config.js'; describe('The Konduit vast wrapper module', function () { diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index 3df1853c9f9..c3c9c8dc38d 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,4 +1,4 @@ -import {liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule} from 'modules/liveIntentIdSystem.js'; +import {liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage} from 'modules/liveIntentIdSystem.js'; import * as utils from 'src/utils.js'; import {uspDataHandler} from '../../../src/adapterManager.js'; import {server} from 'test/mocks/xhr.js'; @@ -17,8 +17,8 @@ describe('LiveIntentId', function () { beforeEach(function () { imgStub = sinon.stub(window, 'Image').returns(pixel); - getCookieStub = sinon.stub(utils, 'getCookie'); - getDataFromLocalStorageStub = sinon.stub(utils, 'getDataFromLocalStorage'); + getCookieStub = sinon.stub(storage, 'getCookie'); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); logErrorStub = sinon.stub(utils, 'logError'); consentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); }); diff --git a/test/spec/modules/livewrappedBidAdapter_spec.js b/test/spec/modules/livewrappedBidAdapter_spec.js index a48c00b6890..b82bf8db160 100644 --- a/test/spec/modules/livewrappedBidAdapter_spec.js +++ b/test/spec/modules/livewrappedBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec} from 'modules/livewrappedBidAdapter.js'; +import {spec, storage} from 'modules/livewrappedBidAdapter.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import { BANNER, NATIVE } from 'src/mediaTypes.js'; @@ -88,7 +88,7 @@ describe('Livewrapped adapter tests', function () { describe('buildRequests', function() { it('should make a well-formed single request object', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let result = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = JSON.parse(result.data); @@ -100,7 +100,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -116,7 +118,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed multiple request object', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let multiplebidRequest = clone(bidderRequest); multiplebidRequest.bids.push(clone(bidderRequest.bids[0])); multiplebidRequest.bids[1].adUnitCode = 'box_d_1'; @@ -135,7 +137,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -156,7 +160,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with AdUnitName', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); testbidRequest.bids[0].params.adUnitName = 'caller id 1'; delete testbidRequest.bids[0].params.adUnitId; @@ -171,7 +175,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'caller id 1', @@ -186,7 +192,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with less parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -200,7 +206,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'panorama_d_1', @@ -215,7 +223,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with less parameters, no publisherId', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -228,7 +236,9 @@ describe('Livewrapped adapter tests', function () { let expectedQuery = { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -244,7 +254,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with app parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -258,7 +268,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, deviceId: 'deviceid', ifa: 'ifa', cookieSupport: true, @@ -275,7 +287,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with debug parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -289,7 +301,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, tid: 'tracking id', test: true, cookieSupport: true, @@ -306,7 +320,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with optional parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -319,7 +333,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'panorama_d_1', @@ -336,7 +352,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with ad blocker revovered parameter', function() { sandbox.stub(utils, 'getWindowTop').returns({ I12C: { Morph: 1 } }); sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -348,7 +364,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, rcv: true, adRequests: [{ @@ -364,7 +382,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with native only parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -377,7 +395,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'panorama_d_1', @@ -393,7 +413,7 @@ describe('Livewrapped adapter tests', function () { it('should make a well-formed single request object with native and banner parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -406,7 +426,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'panorama_d_1', @@ -421,9 +443,56 @@ describe('Livewrapped adapter tests', function () { expect(data).to.deep.equal(expectedQuery); }); + it('should use app objects', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + + let testbidRequest = clone(bidderRequest); + delete testbidRequest.bids[0].params.url; + + let origGetConfig = config.getConfig; + sandbox.stub(config, 'getConfig').callsFake(function (key) { + if (key === 'app') { + return {bundle: 'bundle', domain: 'https://appdomain.com'}; + } + if (key === 'device') { + return {ifa: 'ifa', width: 300, height: 200}; + } + return origGetConfig.apply(config, arguments); + }); + + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://appdomain.com', + seats: {'dsp': ['seat 1']}, + version: '1.3', + width: 300, + height: 200, + ifa: 'ifa', + bundle: 'bundle', + cookieSupport: true, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + it('should use mediaTypes.banner.sizes before legacy sizes', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; delete testbidRequest.bids[0].params.seats; @@ -436,7 +505,9 @@ describe('Livewrapped adapter tests', function () { auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', publisherId: '26947112-2289-405D-88C1-A7340C57E63E', url: 'https://www.domain.com', - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ callerAdUnitId: 'panorama_d_1', @@ -451,7 +522,7 @@ describe('Livewrapped adapter tests', function () { it('should pass gdpr true parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testRequest = clone(bidderRequest); testRequest.gdprConsent = { gdprApplies: true, @@ -468,7 +539,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, gdprApplies: true, gdprConsent: 'test', @@ -486,7 +559,7 @@ describe('Livewrapped adapter tests', function () { it('should pass gdpr false parameters', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testRequest = clone(bidderRequest); testRequest.gdprConsent = { gdprApplies: false @@ -502,7 +575,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, gdprApplies: false, adRequests: [{ @@ -518,7 +593,7 @@ describe('Livewrapped adapter tests', function () { }); it('should pass no cookie support', function() { - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => false); sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); let result = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = JSON.parse(result.data); @@ -531,7 +606,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: false, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -546,7 +623,7 @@ describe('Livewrapped adapter tests', function () { }); it('should pass no cookie support Safari', function() { - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); sandbox.stub(utils, 'isSafariBrowser').callsFake(() => true); let result = spec.buildRequests(bidderRequest.bids, bidderRequest); let data = JSON.parse(result.data); @@ -559,7 +636,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: false, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -605,7 +684,7 @@ describe('Livewrapped adapter tests', function () { it('should make use of pubcid if available', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; testbidRequest.bids[0].crumbs = {pubcid: 'pubcid 123'}; @@ -620,7 +699,9 @@ describe('Livewrapped adapter tests', function () { userId: 'pubcid 123', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -636,7 +717,7 @@ describe('Livewrapped adapter tests', function () { it('should make userId take precedence over pubcid', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); testbidRequest.bids[0].crumbs = {pubcid: 'pubcid 123'}; let result = spec.buildRequests(testbidRequest.bids, testbidRequest); @@ -650,7 +731,9 @@ describe('Livewrapped adapter tests', function () { userId: 'user id', url: 'https://www.domain.com', seats: {'dsp': ['seat 1']}, - version: '1.2', + version: '1.3', + width: 100, + height: 100, cookieSupport: true, adRequests: [{ adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', @@ -667,7 +750,7 @@ describe('Livewrapped adapter tests', function () { it('should make use of Id5-Id if available', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; testbidRequest.bids[0].userId = {}; @@ -686,7 +769,7 @@ describe('Livewrapped adapter tests', function () { it('should make use of publisher common Id if available', function() { sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); - sandbox.stub(utils, 'cookiesAreEnabled').callsFake(() => true); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); let testbidRequest = clone(bidderRequest); delete testbidRequest.bids[0].params.userId; testbidRequest.bids[0].userId = {}; diff --git a/test/spec/modules/lunamediaBidAdapter_spec.js b/test/spec/modules/lunamediaBidAdapter_spec.js new file mode 100755 index 00000000000..fc8648bf8a0 --- /dev/null +++ b/test/spec/modules/lunamediaBidAdapter_spec.js @@ -0,0 +1,137 @@ +import { expect } from 'chai'; +import { spec } from 'modules/lunamediaBidAdapter.js'; +import { BANNER, VIDEO } from 'src/mediaTypes.js'; + +describe('lunamediaBidAdapter', function () { + let bidRequests; + let bidRequestsVid; + + beforeEach(function () { + bidRequests = [{'bidder': 'lunamedia', 'params': {'pubid': '0cf8d6d643e13d86a5b6374148a4afac', 'floor': 0.5, 'placement': 1234}, 'crumbs': {'pubcid': '979fde13-c71e-4ac2-98b7-28c90f99b449'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': 'f72931e6-2b0e-4e37-a2bc-1ea912141f81', 'sizes': [[300, 250]], 'bidId': '2aa73f571eaf29', 'bidderRequestId': '1bac84515a7af3', 'auctionId': '5dbc60fa-1aa1-41ce-9092-e6bbd4d478f7', 'src': 'client', 'bidRequestsCount': 1, 'pageurl': 'http://google.com'}]; + + bidRequestsVid = [{'bidder': 'lunamedia', 'params': {'pubid': '8537f00948fc37cc03c5f0f88e198a76', 'floor': 1.0, 'placement': 1234, 'video': {'id': 123, 'skip': 1, 'mimes': ['video/mp4', 'application/javascript'], 'playbackmethod': [2, 6], 'maxduration': 30}}, 'crumbs': {'pubcid': '979fde13-c71e-4ac2-98b7-28c90f99b449'}, 'mediaTypes': {'video': {'playerSize': [[320, 480]], 'context': 'instream'}}, 'adUnitCode': 'video1', 'transactionId': '8b060952-93f7-4863-af44-bb8796b97c42', 'sizes': [], 'bidId': '25c6ab92aa0e81', 'bidderRequestId': '1d420b73a013fc', 'auctionId': '9a69741c-34fb-474c-83e1-cfa003aaee17', 'src': 'client', 'bidRequestsCount': 1, 'pageurl': 'http://google.com'}]; + }); + + describe('spec.isBidRequestValid', function () { + it('should return true when the required params are passed for banner', function () { + const bidRequest = bidRequests[0]; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true when the required params are passed for video', function () { + const bidRequests = bidRequestsVid[0]; + expect(spec.isBidRequestValid(bidRequests)).to.equal(true); + }); + + it('should return false when no pub id params are passed', function () { + const bidRequest = bidRequests[0]; + bidRequest.params.pubid = ''; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when no placement params are passed', function () { + const bidRequest = bidRequests[0]; + bidRequest.params.placement = ''; + 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); + }); + }); + + describe('spec.buildRequests', function () { + it('should create a POST request for each bid', function () { + const bidRequest = bidRequests[0]; + const requests = spec.buildRequests([ bidRequest ]); + expect(requests[0].method).to.equal('POST'); + }); + + it('should create a POST request for each bid in video request', function () { + const bidRequest = bidRequestsVid[0]; + const requests = spec.buildRequests([ bidRequest ]); + expect(requests[0].method).to.equal('POST'); + }); + + it('should have domain in request', function () { + const bidRequest = bidRequests[0]; + const requests = spec.buildRequests([ bidRequest ]); + expect(requests[0].data.site.domain).length !== 0; + }); + }); + + describe('spec.interpretResponse', function () { + describe('for banner bids', function () { + it('should return no bids if the response is not valid', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + const bidResponse = spec.interpretResponse({ body: null }, { bidRequest }); + + if (typeof bidResponse !== 'undefined') { + expect(bidResponse.length).to.equal(0); + } else { + expect(true).to.equal(true); + } + }); + + it('should return no bids if the response is empty', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + const bidResponse = spec.interpretResponse({ body: [] }, { bidRequest }); + if (typeof bidResponse !== 'undefined') { + expect(bidResponse.length).to.equal(0); + } else { expect(true).to.equal(true); } + }); + + it('should return valid video bid responses', function () { + let _mediaTypes = VIDEO; + const lunamediabidreqVid = {'bidRequest': {'mediaTypes': {'video': {'w': 320, 'h': 480}}}}; + const serverResponseVid = {'cur': 'USD', 'id': '25c6ab92aa0e81', 'seatbid': [{'seat': '3', 'bid': [{'crid': '1855', 'h': 480, 'protocol': 2, 'nurl': 'http://api.lunamedia.io/xp/evt?pp=1MO1wiaMhhq7wLRzZZwwwPkJxxKpYEnM5k5MH4qSGm1HR8rp3Nl7vDocvzZzSAvE4pnREL9mQ1kf5PDjk6E8em6DOk7vVrYUH1TYQyqCucd58PFpJNN7h30RXKHHFg3XaLuQ3PKfMuI1qZATBJ6WHcu875y0hqRdiewn0J4JsCYF53M27uwmcV0HnQxARQZZ72mPqrW95U6wgkZljziwKrICM3aBV07TU6YK5R5AyzJRuD6mtrQ2xtHlQ3jXVYKE5bvWFiUQd90t0jOGhPtYBNoOfP7uQ4ZZj4pyucxbr96orHe9PSOn9UpCSWArdx7s8lOfDpwOvbMuyGxynbStDWm38sDgd4bMHnIt762m5VMDNJfiUyX0vWzp05OsufJDVEaWhAM62i40lQZo7mWP4ipoOWLkmlaAzFIMsTcNaHAHiKKqGEOZLkCEhFNM0SLcvgN2HFRULOOIZvusq7TydOKxuXgCS91dLUDxDDDFUK83BFKlMkTxnCzkLbIR1bd9GKcr1TRryOrulyvRWAKAIhEsUzsc5QWFUhmI2dZ1eqnBQJ0c89TaPcnoaP2WipF68UgyiOstf2CBy0M34858tC5PmuQwQYwXscg6zyqDwR0i9MzGH4FkTyU5yeOlPcsA0ht6UcoCdFpHpumDrLUwAaxwGk1Nj8S6YlYYT5wNuTifDGbg22QKXzZBkUARiyVvgPn9nRtXnrd7WmiMYq596rya9RQj7LC0auQW8bHVQLEe49shsZDnAwZTWr4QuYKqgRGZcXteG7RVJe0ryBZezOq11ha9C0Lv0siNVBahOXE35Wzoq4c4BDaGpqvhaKN7pjeWLGlQR04ufWekwxiMWAvjmfgAfexBJ7HfbYNZpq__', 'adid': '61_1855', 'adomain': ['chevrolet.com.ar'], 'price': 2, 'w': 320, 'iurl': 'https://daf37cpxaja7f.cloudfront.net/c61/creative_url_14922301369663_1.png', 'cat': ['IAB2'], 'id': '7f570b40-aca1-4806-8ea8-818ea679c82b_0', 'attr': [], 'impid': '0', 'cid': '61'}]}], 'bidid': '7f570b40-aca1-4806-8ea8-818ea679c82b'} + const bidResponseVid = spec.interpretResponse({ body: serverResponseVid }, lunamediabidreqVid); + delete bidResponseVid['vastUrl']; + delete bidResponseVid['ad']; + expect(bidResponseVid).to.deep.equal({ + requestId: bidRequestsVid[0].bidId, + bidderCode: 'lunamedia', + creativeId: serverResponseVid.seatbid[0].bid[0].crid, + cpm: serverResponseVid.seatbid[0].bid[0].price, + width: serverResponseVid.seatbid[0].bid[0].w, + height: serverResponseVid.seatbid[0].bid[0].h, + mediaType: 'video', + currency: 'USD', + netRevenue: true, + ttl: 60 + }); + }); + + it('should return valid banner bid responses', function () { + const lunamediabidreq = {bids: {}}; + bidRequests.forEach(bid => { + let _mediaTypes = (bid.mediaTypes && bid.mediaTypes.video ? VIDEO : BANNER); + lunamediabidreq.bids[bid.bidId] = {mediaTypes: _mediaTypes, + w: _mediaTypes == BANNER ? bid.mediaTypes[_mediaTypes].sizes[0][0] : bid.mediaTypes[_mediaTypes].playerSize[0], + h: _mediaTypes == BANNER ? bid.mediaTypes[_mediaTypes].sizes[0][1] : bid.mediaTypes[_mediaTypes].playerSize[1] + + }; + }); + const serverResponse = {'id': '2aa73f571eaf29', 'seatbid': [{'bid': [{'id': '2c5e8a1a84522d', 'impid': '2c5e8a1a84522d', 'price': 0.81, 'adid': 'abcde-12345', 'nurl': '', 'adm': '
', 'adomain': ['advertiserdomain.com'], 'iurl': '', 'cid': 'campaign1', 'crid': 'abcde-12345', 'w': 300, 'h': 250}], 'seat': '19513bcfca8006'}], 'bidid': '19513bcfca8006', 'cur': 'USD', 'w': 300, 'h': 250}; + + const bidResponse = spec.interpretResponse({ body: serverResponse }, lunamediabidreq); + expect(bidResponse).to.deep.equal({ + requestId: bidRequests[0].bidId, + ad: serverResponse.seatbid[0].bid[0].adm, + bidderCode: 'lunamedia', + creativeId: serverResponse.seatbid[0].bid[0].crid, + cpm: serverResponse.seatbid[0].bid[0].price, + width: serverResponse.seatbid[0].bid[0].w, + height: serverResponse.seatbid[0].bid[0].h, + mediaType: 'banner', + currency: 'USD', + netRevenue: true, + ttl: 60 + }); + }); + }); + }); +}); diff --git a/test/spec/modules/luponmediaBidAdapter_spec.js b/test/spec/modules/luponmediaBidAdapter_spec.js new file mode 100644 index 00000000000..39c915e38b8 --- /dev/null +++ b/test/spec/modules/luponmediaBidAdapter_spec.js @@ -0,0 +1,367 @@ +import { resetUserSync, spec, hasValidSupplyChainParams } from 'modules/luponmediaBidAdapter.js'; +const ENDPOINT_URL = 'https://rtb.adxpremium.services/openrtb2/auction'; + +describe('luponmediaBidAdapter', function () { + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'luponmedia', + 'params': { + 'siteId': 12345, + 'keyId': '4o2c4' + }, + 'adUnitCode': 'test-div', + 'sizes': [[300, 250]], + 'bidId': 'g1987234bjkads', + 'bidderRequestId': '290348ksdhkas89324', + 'auctionId': '20384rlek235', + }; + + it('should return true when required params are found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'siteId': 12345 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests = [ + { + 'bidder': 'luponmedia', + 'params': { + 'siteId': 303522, + 'keyId': '4o2c4' + }, + 'crumbs': { + 'pubcid': '8d8b16cb-1383-4a0f-b4bb-0be28464d974' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1533155193780-2', + 'transactionId': '585d96a5-bd93-4a89-b8ea-0f546f3aaa82', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '268a30af10dd6f', + 'bidderRequestId': '140411b5010a2a', + 'auctionId': '7376c117-b7aa-49f5-a661-488543deeefd', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'novi.ba', + 'sid': '199424', + 'hp': 1 + } + ] + } + } + ]; + + let bidderRequest = { + 'bidderCode': 'luponmedia', + 'auctionId': '7376c117-b7aa-49f5-a661-488543deeefd', + 'bidderRequestId': '140411b5010a2a', + 'bids': [ + { + 'bidder': 'luponmedia', + 'params': { + 'siteId': 303522, + 'keyId': '4o2c4' + }, + 'crumbs': { + 'pubcid': '8d8b16cb-1383-4a0f-b4bb-0be28464d974' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1533155193780-2', + 'transactionId': '585d96a5-bd93-4a89-b8ea-0f546f3aaa82', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '268a30af10dd6f', + 'bidderRequestId': '140411b5010a2a', + 'auctionId': '7376c117-b7aa-49f5-a661-488543deeefd', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'novi.ba', + 'sid': '199424', + 'hp': 1 + } + ] + } + } + ], + 'auctionStart': 1587413920820, + 'timeout': 2000, + 'refererInfo': { + 'referer': 'https://novi.ba/clanak/176067/fast-car-beginner-s-guide-to-tuning-turbo-engines', + 'reachedTop': true, + 'numIframes': 0, + 'stack': [ + 'https://novi.ba/clanak/176067/fast-car-beginner-s-guide-to-tuning-turbo-engines' + ] + }, + 'start': 1587413920835 + }; + + it('sends bid request to ENDPOINT via POST', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + let dynRes = JSON.parse(requests.data); + expect(requests.url).to.equal(ENDPOINT_URL); + expect(requests.method).to.equal('POST'); + expect(requests.data).to.equal('{"id":"585d96a5-bd93-4a89-b8ea-0f546f3aaa82","test":0,"source":{"tid":"585d96a5-bd93-4a89-b8ea-0f546f3aaa82","ext":{"schain":{"ver":"1.0","complete":1,"nodes":[{"asi":"novi.ba","sid":"199424","hp":1}]}}},"tmax":1500,"imp":[{"id":"268a30af10dd6f","secure":1,"ext":{"luponmedia":{"siteId":303522,"keyId":"4o2c4"}},"banner":{"format":[{"w":300,"h":250}]}}],"ext":{"prebid":{"targeting":{"includewinners":true,"includebidderkeys":false}}},"user":{"id":"' + dynRes.user.id + '","buyeruid":"8d8b16cb-1383-4a0f-b4bb-0be28464d974"},"site":{"page":"https://novi.ba/clanak/176067/fast-car-beginner-s-guide-to-tuning-turbo-engines"}}'); + }); + }); + + describe('interpretResponse', function () { + it('should get correct banner bid response', function () { + let response = { + 'id': '4776d680-15a2-45c3-bad5-db6bebd94a06', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2a122246ef72ea', + 'impid': '2a122246ef72ea', + 'price': 0.43, + 'adm': ' ', + 'adid': '56380110', + 'cid': '44724710', + 'crid': '443801010', + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'targeting': { + 'hb_bidder': 'luponmedia', + 'hb_pb': '0.40', + 'hb_size': '300x250' + }, + 'type': 'banner' + } + } + } + ], + 'seat': 'luponmedia' + } + ], + 'cur': 'USD', + 'ext': { + 'responsetimemillis': { + 'luponmedia': 233 + }, + 'tmaxrequest': 1500, + 'usersyncs': { + 'status': 'ok', + 'bidder_status': [] + } + } + }; + + let expectedResponse = [ + { + 'requestId': '2a122246ef72ea', + 'cpm': '0.43', + 'width': 300, + 'height': 250, + 'creativeId': '443801010', + 'currency': 'USD', + 'dealId': '23425', + 'netRevenue': false, + 'ttl': 300, + 'referrer': '', + 'ad': ' ' + } + ]; + + let bidderRequest = { + 'data': '{"site":{"page":"https://novi.ba/clanak/176067/fast-car-beginner-s-guide-to-tuning-turbo-engines"}}' + }; + + let result = spec.interpretResponse({ body: response }, bidderRequest); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('handles nobid responses', function () { + let noBidResponse = []; + + let noBidBidderRequest = { + 'data': '{"site":{"page":""}}' + } + let noBidResult = spec.interpretResponse({ body: noBidResponse }, noBidBidderRequest); + expect(noBidResult.length).to.equal(0); + }); + }); + + describe('getUserSyncs', function () { + const bidResponse1 = { + 'body': { + 'ext': { + 'responsetimemillis': { + 'luponmedia': 233 + }, + 'tmaxrequest': 1500, + 'usersyncs': { + 'status': 'ok', + 'bidder_status': [ + { + 'bidder': 'luponmedia', + 'no_cookie': true, + 'usersync': { + 'url': 'https://adxpremium.services/api/usersync', + 'type': 'redirect' + } + }, + { + 'bidder': 'luponmedia', + 'no_cookie': true, + 'usersync': { + 'url': 'https://adxpremium.services/api/iframeusersync', + 'type': 'iframe' + } + } + ] + } + } + } + }; + + const bidResponse2 = { + 'body': { + 'ext': { + 'responsetimemillis': { + 'luponmedia': 233 + }, + 'tmaxrequest': 1500, + 'usersyncs': { + 'status': 'no_cookie', + 'bidder_status': [] + } + } + } + }; + + it('should use a sync url from first response (pixel and iframe)', function () { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [bidResponse1, bidResponse2]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://adxpremium.services/api/usersync' + }, + { + type: 'iframe', + url: 'https://adxpremium.services/api/iframeusersync' + } + ]); + }); + + it('handle empty response (e.g. timeout)', function () { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('returns empty syncs when not pixel enabled and not iframe enabled', function () { + const syncs = spec.getUserSyncs({ pixelEnabled: false, iframeEnabled: false }, [bidResponse1]); + expect(syncs).to.deep.equal([]); + }); + + it('returns pixel syncs when pixel enabled and not iframe enabled', function() { + resetUserSync(); + + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: false }, [bidResponse1]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://adxpremium.services/api/usersync' + } + ]); + }); + + it('returns iframe syncs when not pixel enabled and iframe enabled', function() { + resetUserSync(); + + const syncs = spec.getUserSyncs({ pixelEnabled: false, iframeEnabled: true }, [bidResponse1]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://adxpremium.services/api/iframeusersync' + } + ]); + }); + }); + + describe('hasValidSupplyChainParams', function () { + it('returns true if schain is valid', function () { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'novi.ba', + 'sid': '199424', + 'hp': 1 + } + ] + }; + + const checkSchain = hasValidSupplyChainParams(schain); + expect(checkSchain).to.equal(true); + }); + + it('returns false if schain is invalid', function () { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'invalid': 'novi.ba' + } + ] + }; + + const checkSchain = hasValidSupplyChainParams(schain); + expect(checkSchain).to.equal(false); + }); + }); +}); diff --git a/test/spec/modules/marsmediaBidAdapter_spec.js b/test/spec/modules/marsmediaBidAdapter_spec.js index 553a3b19c55..c0399e5d0a2 100644 --- a/test/spec/modules/marsmediaBidAdapter_spec.js +++ b/test/spec/modules/marsmediaBidAdapter_spec.js @@ -38,7 +38,7 @@ describe('marsmedia adapter tests', function () { var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); - expect(bidRequest.url).to.have.string('https://bid306.rtbsrv.com/bidder/?bid=3mhdom&zoneId=9999&hbv='); + expect(bidRequest.url).to.have.string('https://hb.go2speed.media/bidder/?bid=3mhdom&zoneId=9999&hbv='); expect(bidRequest.method).to.equal('POST'); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.site).to.not.equal(null); @@ -108,7 +108,7 @@ describe('marsmedia adapter tests', function () { var bidRequest = r1adapter.buildRequests(bidRequestList, this.defaultBidderRequest); - expect(bidRequest.url).to.have.string('https://bid306.rtbsrv.com/bidder/?bid=3mhdom&zoneId=9999&hbv='); + expect(bidRequest.url).to.have.string('https://hb.go2speed.media/bidder/?bid=3mhdom&zoneId=9999&hbv='); expect(bidRequest.method).to.equal('POST'); const openrtbRequest = JSON.parse(bidRequest.data); expect(openrtbRequest.site).to.not.equal(null); @@ -127,7 +127,7 @@ describe('marsmedia adapter tests', function () { expect(openrtbRequest.imp[0].video.api).to.eql([1, 2, 5]); }); - it('interpretResponse works', function() { + it('interpretResponse with vast url works', function() { var bidList = { 'body': [ { @@ -159,6 +159,39 @@ describe('marsmedia adapter tests', function () { expect(bid.cpm).to.equal(1.0); expect(bid.ttl).to.equal(600); }); + + it('interpretResponse with xml works', function() { + var bidList = { + 'body': [ + { + 'impid': 'div-gpt-ad-1438287399331-1', + 'price': 1, + 'adm': '', + 'adomain': [ + 'test.com' + ], + 'cid': '467415', + 'crid': 'cr-vid', + 'w': 800, + 'h': 600 + } + ] + }; + + var videoBids = r1adapter.interpretResponse(bidList); + + expect(videoBids.length).to.equal(1); + const bid = videoBids[0]; + expect(bid.width).to.equal(800); + expect(bid.height).to.equal(600); + expect(bid.vastXml).to.equal(''); + expect(bid.mediaType).to.equal('video'); + expect(bid.creativeId).to.equal('cr-vid'); + expect(bid.currency).to.equal('USD'); + expect(bid.netRevenue).to.equal(true); + expect(bid.cpm).to.equal(1.0); + expect(bid.ttl).to.equal(600); + }); }); describe('misc buildRequests', function() { diff --git a/test/spec/modules/medianetAnalyticsAdapter_spec.js b/test/spec/modules/medianetAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..dcec1050652 --- /dev/null +++ b/test/spec/modules/medianetAnalyticsAdapter_spec.js @@ -0,0 +1,185 @@ +import { expect } from 'chai'; +import medianetAnalytics from 'modules/medianetAnalyticsAdapter.js'; +import * as utils from 'src/utils.js'; +import CONSTANTS from 'src/constants.json'; +import events from 'src/events.js'; + +const { + EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, NO_BID, BID_TIMEOUT, AUCTION_END, SET_TARGETING, BID_WON } +} = CONSTANTS; + +const MOCK = { + AUCTION_INIT: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'timestamp': 1584563605739}, + BID_REQUESTED: {'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, + BID_RESPONSE: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, + AUCTION_END: {'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'auctionEnd': 1584563605739}, + SET_TARGETING: {'div-gpt-ad-1460505748561-0': {'prebid_test': '1', 'hb_format': 'banner', 'hb_source': 'client', 'hb_size': '300x250', 'hb_pb': '2.00', 'hb_adid': '3e6e4bce5c8fb3', 'hb_bidder': 'medianet', 'hb_format_medianet': 'banner', 'hb_source_medianet': 'client', 'hb_size_medianet': '300x250', 'hb_pb_medianet': '2.00', 'hb_adid_medianet': '3e6e4bce5c8fb3', 'hb_bidder_medianet': 'medianet'}}, + BID_WON: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'statusMessage': 'Bid available', 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, + NO_BID: {'bidder': 'medianet', 'params': {'cid': 'test123', 'crid': '451466393', 'site': {}}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '303fa0c6-682f-4aea-8e4a-dc68f0d5c7d5', 'sizes': [[300, 250], [300, 600]], 'bidId': '28248b0e6aece2', 'bidderRequestId': '13fccf3809fe43', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}, + BID_TIMEOUT: [{'bidId': '28248b0e6aece2', 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'params': [{'cid': 'test123', 'crid': '451466393', 'site': {}}, {'cid': '8CUX0H51P', 'crid': '451466393', 'site': {}}], 'timeout': 6}] +} + +function performStandardAuctionWithWinner() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON); +} + +function performStandardAuctionWithNoBid() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(NO_BID, MOCK.NO_BID); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); +} + +function performStandardAuctionWithTimeout() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); +} + +function getQueryData(url) { + const queryArgs = url.split('?')[1].split('&'); + return queryArgs.reduce((data, arg) => { + const [key, val] = arg.split('='); + if (data[key] !== undefined) { + if (!Array.isArray(data[key])) { + data[key] = [data[key]]; + } + data[key].push(val); + } else { + data[key] = val; + } + return data; + }, {}); +} + +describe('Media.net Analytics Adapter', function() { + let sandbox; + let CUSTOMER_ID = 'test123'; + let VALID_CONFIGURATION = { + options: { + cid: CUSTOMER_ID + } + } + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('Configuration', function() { + it('should log error if publisher id is not passed', function() { + sandbox.stub(utils, 'logError'); + + medianetAnalytics.enableAnalytics(); + expect( + utils.logError.calledWith( + 'Media.net Analytics adapter: cid is required.' + ) + ).to.be.true; + }); + + it('should not log error if valid config is passed', function() { + sandbox.stub(utils, 'logError'); + + medianetAnalytics.enableAnalytics(VALID_CONFIGURATION); + expect(utils.logError.called).to.equal(false); + medianetAnalytics.disableAnalytics(); + }); + }); + + describe('Events', function() { + beforeEach(function () { + medianetAnalytics.enableAnalytics({ + options: { + cid: 'test123' + } + }); + }); + afterEach(function () { + medianetAnalytics.disableAnalytics(); + }); + + it('should not log if only Auction Init', function() { + medianetAnalytics.clearlogsQueue(); + medianetAnalytics.track({ AUCTION_INIT }) + expect(medianetAnalytics.getlogsQueue().length).to.equal(0); + }); + + it('should have winner log in standard auction', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithWinner(); + let winnerLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter((log) => log.winner); + medianetAnalytics.clearlogsQueue(); + + expect(winnerLog.length).to.equal(1); + }); + + it('should have correct values in winner log', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithWinner(); + let winnerLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter((log) => log.winner); + medianetAnalytics.clearlogsQueue(); + + expect(winnerLog[0]).to.include({ + winner: '1', + pvnm: 'medianet', + curr: 'USD', + src: 'client', + size: '300x250', + mtype: 'banner', + cid: 'test123', + lper: '1', + ogbdp: '1.1495', + flt: '1', + supcrid: 'div-gpt-ad-1460505748561-0', + mpvid: '123' + }); + }); + + it('should have no bid status', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithNoBid(); + let noBidLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); + noBidLog = noBidLog[0]; + + medianetAnalytics.clearlogsQueue(); + expect(noBidLog.pvnm).to.have.ordered.members(['-2', 'medianet']); + expect(noBidLog.iwb).to.have.ordered.members(['0', '0']); + expect(noBidLog.status).to.have.ordered.members(['1', '2']); + expect(noBidLog.src).to.have.ordered.members(['client', 'client']); + expect(noBidLog.curr).to.have.ordered.members(['', '']); + expect(noBidLog.mtype).to.have.ordered.members(['', '']); + expect(noBidLog.ogbdp).to.have.ordered.members(['', '']); + expect(noBidLog.mpvid).to.have.ordered.members(['', '']); + expect(noBidLog.crid).to.have.ordered.members(['', '451466393']); + }); + + it('should have timeout status', function() { + medianetAnalytics.clearlogsQueue(); + performStandardAuctionWithTimeout(); + let timeoutLog = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)); + timeoutLog = timeoutLog[0]; + + medianetAnalytics.clearlogsQueue(); + expect(timeoutLog.pvnm).to.have.ordered.members(['-2', 'medianet']); + expect(timeoutLog.iwb).to.have.ordered.members(['0', '0']); + expect(timeoutLog.status).to.have.ordered.members(['1', '3']); + expect(timeoutLog.src).to.have.ordered.members(['client', 'client']); + expect(timeoutLog.curr).to.have.ordered.members(['', '']); + expect(timeoutLog.mtype).to.have.ordered.members(['', '']); + expect(timeoutLog.ogbdp).to.have.ordered.members(['', '']); + expect(timeoutLog.mpvid).to.have.ordered.members(['', '']); + expect(timeoutLog.crid).to.have.ordered.members(['', '451466393']); + }); + }); +}); diff --git a/test/spec/modules/mgidBidAdapter_spec.js b/test/spec/modules/mgidBidAdapter_spec.js index b96d718ab36..16f4f0b4607 100644 --- a/test/spec/modules/mgidBidAdapter_spec.js +++ b/test/spec/modules/mgidBidAdapter_spec.js @@ -1,7 +1,6 @@ import {assert, expect} from 'chai'; import {spec} from 'modules/mgidBidAdapter.js'; import * as utils from '../../../src/utils.js'; -import * as urlUtils from '../../../src/url.js'; describe('Mgid bid adapter', function () { let sandbox; @@ -361,7 +360,7 @@ describe('Mgid bid adapter', function () { }; let bidRequests = [bid]; const page = top.location.href; - const domain = urlUtils.parse(page).hostname; + const domain = utils.parseUrl(page).hostname; const request = spec.buildRequests(bidRequests); expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); expect(request.method).deep.equal('POST'); @@ -409,7 +408,7 @@ describe('Mgid bid adapter', function () { let bidRequests = [bid]; const page = top.location.href; - const domain = urlUtils.parse(page).hostname; + const domain = utils.parseUrl(page).hostname; const request = spec.buildRequests(bidRequests); expect(request).to.be.a('object'); expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); @@ -446,7 +445,7 @@ describe('Mgid bid adapter', function () { let bidRequests = [bid]; const page = top.location.href; - const domain = urlUtils.parse(page).hostname; + const domain = utils.parseUrl(page).hostname; const request = spec.buildRequests(bidRequests); expect(request).to.be.a('object'); expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); @@ -482,7 +481,7 @@ describe('Mgid bid adapter', function () { let bidRequests = [bid]; const page = top.location.href; - const domain = urlUtils.parse(page).hostname; + const domain = utils.parseUrl(page).hostname; const request = spec.buildRequests(bidRequests); expect(request).to.be.a('object'); expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); @@ -516,7 +515,7 @@ describe('Mgid bid adapter', function () { const request = spec.buildRequests(bidRequests); const page = top.location.href; - const domain = urlUtils.parse(page).hostname; + const domain = utils.parseUrl(page).hostname; expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); expect(request.method).deep.equal('POST'); const data = JSON.parse(request.data); diff --git a/test/spec/modules/newborntownWebBidAdapter_spec.js b/test/spec/modules/newborntownWebBidAdapter_spec.js index 84937d00012..3d3285328fe 100644 --- a/test/spec/modules/newborntownWebBidAdapter_spec.js +++ b/test/spec/modules/newborntownWebBidAdapter_spec.js @@ -54,8 +54,8 @@ describe('NewborntownWebAdapter', function() { 'timeout': 9000, 'start': 1573123289383 } - const request = spec.buildRequests(bidderRequest['bids'], bidderRequest); it('Returns POST method', function () { + const request = spec.buildRequests(bidderRequest['bids'], bidderRequest); expect(request[0].method).to.equal('POST'); expect(request[0].url.indexOf('//us-west.solortb.com/adx/api/rtb?from=4') !== -1).to.equal(true); expect(request[0].data).to.exist; diff --git a/test/spec/modules/nobidBidAdapter_spec.js b/test/spec/modules/nobidBidAdapter_spec.js index 8d119461d6a..afbc46f862f 100644 --- a/test/spec/modules/nobidBidAdapter_spec.js +++ b/test/spec/modules/nobidBidAdapter_spec.js @@ -50,6 +50,95 @@ describe('Nobid Adapter', function () { }); }); + describe('isVideoBidRequestValid', function () { + let bid = { + bidder: 'nobid', + params: { + siteId: 2, + video: { + skippable: true, + playback_methods: ['auto_play_sound_off'], + position: 'atf', + mimes: ['video/x-flv', 'video/mp4', 'video/x-ms-wmv', 'application/x-shockwave-flash', 'application/javascript'], + minduration: 1, + maxduration: 30, + frameworks: [1, 2, 3, 4, 5, 6] + } + }, + adUnitCode: 'adunit-code', + sizes: [[640, 480]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + mediaTypes: { + video: { + context: 'instream' + } + } + }; + const SITE_ID = 2; + const REFERER = 'https://www.examplereferer.com'; + let bidRequests = [ + { + bidder: 'nobid', + params: { + siteId: SITE_ID, + video: { + skippable: true, + playback_methods: ['auto_play_sound_off'], + position: 'atf', + mimes: ['video/x-flv', 'video/mp4', 'video/x-ms-wmv', 'application/x-shockwave-flash', 'application/javascript'], + minduration: 1, + maxduration: 30, + frameworks: [1, 2, 3, 4, 5, 6] + } + }, + adUnitCode: 'adunit-code', + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + } + } + ]; + + let bidderRequest = { + refererInfo: {referer: REFERER} + } + + it('should add source and version to the tag', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.sid).to.equal(SITE_ID); + expect(payload.l).to.exist.and.to.equal(encodeURIComponent(REFERER)); + expect(payload.a).to.exist; + expect(payload.t).to.exist; + expect(payload.tz).to.exist; + expect(payload.r).to.exist; + expect(payload.lang).to.exist; + expect(payload.ref).to.exist; + expect(payload.a[0].d).to.exist.and.to.equal('adunit-code'); + expect(payload.a[0].at).to.exist.and.to.equal('video'); + expect(payload.a[0].params.video).to.exist; + expect(payload.a[0].params.video.skippable).to.exist.and.to.equal(true); + expect(payload.a[0].params.video.playback_methods).to.exist.and.to.contain('auto_play_sound_off'); + expect(payload.a[0].params.video.position).to.exist.and.to.equal('atf'); + expect(payload.a[0].params.video.mimes).to.exist.and.to.contain('video/x-flv'); + expect(payload.a[0].params.video.minduration).to.exist.and.to.equal(1); + expect(payload.a[0].params.video.maxduration).to.exist.and.to.equal(30); + expect(payload.a[0].params.video.frameworks[0]).to.exist.and.to.equal(1); + expect(payload.a[0].params.video.frameworks[1]).to.exist.and.to.equal(2); + expect(payload.a[0].params.video.frameworks[2]).to.exist.and.to.equal(3); + expect(payload.a[0].params.video.frameworks[3]).to.exist.and.to.equal(4); + expect(payload.a[0].params.video.frameworks[4]).to.exist.and.to.equal(5); + expect(payload.a[0].params.video.frameworks[5]).to.exist.and.to.equal(6); + }); + }); + describe('buildRequests', function () { const SITE_ID = 2; const REFERER = 'https://www.examplereferer.com'; diff --git a/test/spec/modules/oneVideoBidAdapter_spec.js b/test/spec/modules/oneVideoBidAdapter_spec.js index fc7c29c7b51..82546f2e96d 100644 --- a/test/spec/modules/oneVideoBidAdapter_spec.js +++ b/test/spec/modules/oneVideoBidAdapter_spec.js @@ -136,7 +136,7 @@ describe('OneVideoBidAdapter', function () { const placement = bidRequest.params.video.placement; const rewarded = bidRequest.params.video.rewarded; const inventoryid = bidRequest.params.video.inventoryid; - const VERSION = '3.0.0'; + const VERSION = '3.0.1'; expect(data.imp[0].video.w).to.equal(width); expect(data.imp[0].video.h).to.equal(height); expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); @@ -292,7 +292,9 @@ describe('OneVideoBidAdapter', function () { placement: 1, inventoryid: 123, sid: 134, - display: 1 + display: 1, + minduration: 10, + maxduration: 30 }, site: { id: 1, @@ -314,6 +316,8 @@ describe('OneVideoBidAdapter', function () { expect(data.imp[0].ext.inventoryid).to.equal(bidRequest.params.video.inventoryid); expect(data.imp[0].banner.mimes).to.equal(bidRequest.params.video.mimes); expect(data.imp[0].banner.placement).to.equal(bidRequest.params.video.placement); + expect(data.imp[0].banner.ext.minduration).to.equal(bidRequest.params.video.minduration); + expect(data.imp[0].banner.ext.maxduration).to.equal(bidRequest.params.video.maxduration); expect(data.site.id).to.equal(bidRequest.params.site.id); }); it('should send video object when display is other than 1', function () { @@ -384,7 +388,9 @@ describe('OneVideoBidAdapter', function () { delivery: [2], playbackmethod: [1, 5], placement: 123, - sid: 134 + sid: 134, + minduration: 10, + maxduration: 30 }, site: { id: 1, @@ -406,6 +412,8 @@ describe('OneVideoBidAdapter', function () { expect(data.imp[0].video.mimes).to.equal(bidRequest.params.video.mimes); expect(data.imp[0].video.protocols).to.equal(bidRequest.params.video.protocols); expect(data.imp[0].video.linearity).to.equal(1); + expect(data.imp[0].video.maxduration).to.equal(bidRequest.params.video.maxduration); + expect(data.imp[0].video.minduration).to.equal(bidRequest.params.video.minduration); }); describe('getUserSyncs', function () { const GDPR_CONSENT_STRING = 'GDPR_CONSENT_STRING'; diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index 791077d15b9..2b31c875502 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -1,40 +1,117 @@ -import { spec } from 'modules/onetagBidAdapter.js'; +import { spec, isValid, hasTypeVideo } from 'modules/onetagBidAdapter.js'; import { expect } from 'chai'; +import { BANNER, VIDEO } from 'src/mediaTypes.js'; describe('onetag', function () { - let bid = { - 'bidder': 'onetag', - 'params': { - 'pubId': '386276e072', - }, - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'transactionId': 'qwerty123' - }; + function createBid() { + return { + 'bidder': 'onetag', + 'params': { + 'pubId': '386276e072', + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': 'qwerty123' + }; + } + + function createBannerBid(bidRequest) { + const bid = bidRequest || createBid(); + bid.mediaTypes = bid.mediaTypes || {}; + bid.mediaTypes.banner = { + sizes: [[300, 250]] + }; + return bid; + } + + function createVideoBid(bidRequest) { + const bid = bidRequest || createBid(); + bid.mediaTypes = bid.mediaTypes || {}; + bid.mediaTypes.video = { + context: 'instream', + mimes: ['video/mp4', 'video/webm', 'application/javascript', 'video/ogg'], + playerSize: [640, 480] + }; + return bid; + } + + function createWrongVideoOutstreamBid(bidRequest) { + const bid = bidRequest || createBid(); + bid.mediaTypes = bid.mediaTypes || {}; + bid.mediaTypes.video = { + context: 'outstream', + mimes: ['video/mp4', 'video/webm', 'application/javascript', 'video/ogg'], + playerSize: [640, 480] + }; + return bid; + } + + function createMultiFormatBid() { + return createVideoBid(createBannerBid()); + } + + const bannerBid = createBannerBid(); + const videoBid = createVideoBid(); + const outstreamVideoBid = createWrongVideoOutstreamBid(); describe('isBidRequestValid', function () { it('Should return true when required params are found', function () { - expect(spec.isBidRequestValid(bid)).to.be.true; + expect(spec.isBidRequestValid(bannerBid)).to.be.true; }); it('Should return false when pubId is not a string', function () { - bid.params.pubId = 30; - expect(spec.isBidRequestValid(bid)).to.be.false; + bannerBid.params.pubId = 30; + expect(spec.isBidRequestValid(bannerBid)).to.be.false; }); it('Should return false when pubId is undefined', function () { - bid.params.pubId = undefined; - expect(spec.isBidRequestValid(bid)).to.be.false; + bannerBid.params.pubId = undefined; + expect(spec.isBidRequestValid(bannerBid)).to.be.false; }); - it('Should return false when the sizes array is empty', function () { - bid.sizes = []; - expect(spec.isBidRequestValid(bid)).to.be.false; + describe('banner bidRequest', function () { + it('Should return false when the sizes array is empty', function () { + bannerBid.sizes = []; + expect(spec.isBidRequestValid(bannerBid)).to.be.false; + }); + }); + describe('video bidRequest', function () { + it('Should return false when the context is undefined', function () { + videoBid.mediaTypes.video.context = undefined; + expect(spec.isBidRequestValid(videoBid)).to.be.false; + }); + it('Should return false when the context is not instream or outstream', function () { + videoBid.mediaTypes.video.context = 'wrong'; + expect(spec.isBidRequestValid(videoBid)).to.be.false; + }); + it('Should return false when playerSize is undefined', function () { + const videoBid = createVideoBid(); + videoBid.mediaTypes.video.playerSize = undefined; + expect(spec.isBidRequestValid(videoBid)).to.be.false; + }); + it('Should return false when playerSize is not an array', function () { + const videoBid = createVideoBid(); + videoBid.mediaTypes.video.playerSize = 30; + expect(spec.isBidRequestValid(videoBid)).to.be.false; + }); + it('Should return false when playerSize is an empty array', function () { + const videoBid = createVideoBid(); + videoBid.mediaTypes.video.playerSize = []; + expect(spec.isBidRequestValid(videoBid)).to.be.false; + }); + it('Should return false when context is outstream but no renderer object is defined', function () { + expect(spec.isBidRequestValid(outstreamVideoBid)).to.be.false; + }); + }); + describe('multi format bidRequest', function () { + const multiFormatBid = createMultiFormatBid(); + it('Should return true when correct multi format bid is passed', function () { + expect(spec.isBidRequestValid(multiFormatBid)).to.be.true; + }); }); }); describe('buildRequests', function () { - let serverRequest = spec.buildRequests([bid]); + let serverRequest = spec.buildRequests([bannerBid, videoBid]); it('Creates a ServerRequest object with method, URL and data', function () { expect(serverRequest).to.exist; expect(serverRequest.method).to.exist; @@ -53,7 +130,7 @@ describe('onetag', function () { const data = JSON.parse(d); it('Should contains all keys', function () { expect(data).to.be.an('object'); - expect(data).to.have.all.keys('location', 'masked', 'referrer', 'sHeight', 'sWidth', 'timeOffset', 'date', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids'); + expect(data).to.have.all.keys('location', 'masked', 'referrer', 'sHeight', 'sWidth', 'timeOffset', 'date', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset'); expect(data.location).to.be.a('string'); expect(data.masked).to.be.a('number'); expect(data.referrer).to.be.a('string'); @@ -75,7 +152,11 @@ describe('onetag', function () { const bids = data['bids']; for (let i = 0; i < bids.length; i++) { const bid = bids[i]; - expect(bid).to.have.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'pubId', 'transactionId', 'sizes'); + if (hasTypeVideo(bid)) { + expect(bid).to.have.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'pubId', 'transactionId', 'context', 'mimes', 'playerSize', 'protocols', 'maxDuration', 'api', 'type'); + } else if (isValid(BANNER, bid)) { + expect(bid).to.have.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'pubId', 'transactionId', 'sizes', 'type'); + } expect(bid.bidId).to.be.a('string'); expect(bid.pubId).to.be.a('string'); } @@ -101,7 +182,7 @@ describe('onetag', function () { gdprApplies: true } }; - let serverRequest = spec.buildRequests([bid], bidderRequest); + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); const payload = JSON.parse(serverRequest.data); expect(payload).to.exist; @@ -118,7 +199,7 @@ describe('onetag', function () { 'timeout': 3000, 'uspConsent': consentString }; - let serverRequest = spec.buildRequests([bid], bidderRequest); + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); const payload = JSON.parse(serverRequest.data); expect(payload.usPrivacy).to.exist; @@ -126,33 +207,61 @@ describe('onetag', function () { }); }); describe('interpretResponse', function () { - const resObject = { - body: { - nobid: false, - bids: [{ - ad: '
Advertising
', - cpm: 13, - width: 300, - height: 250, - creativeId: '1820', - dealId: 'dishfo', - currency: 'USD', - requestId: 'sdiceobxcw' - }] + function getBannerRes() { + return { + ad: '
Advertising
', + cpm: 13, + width: 300, + height: 250, + creativeId: '1820', + dealId: 'dishfo', + currency: 'USD', + requestId: 'sdiceobxcw', + mediaType: BANNER } - }; + } + function getVideoRes() { + return { + ad: '', + cpm: 13, + width: 300, + height: 250, + creativeId: '1820', + dealId: 'dishfo', + currency: 'USD', + requestId: 'sdiceobxcw', + mediaType: VIDEO + } + } + function getBannerAdnVideoRes() { + return { + body: { + nobid: false, + bids: [getBannerRes(), getVideoRes()] + } + }; + } + const responseObj = getBannerAdnVideoRes(); it('Returns an array of valid server responses if response object is valid', function () { - const serverResponses = spec.interpretResponse(resObject); + const serverResponses = spec.interpretResponse(responseObj); expect(serverResponses).to.be.an('array').that.is.not.empty; for (let i = 0; i < serverResponses.length; i++) { let dataItem = serverResponses[i]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'dealId'); + if (dataItem.mediaType === VIDEO) { + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastXml', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'dealId'); + } else if (dataItem.mediaType === BANNER) { + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'dealId'); + } expect(dataItem.requestId).to.be.a('string'); expect(dataItem.cpm).to.be.a('number'); expect(dataItem.width).to.be.a('number'); expect(dataItem.height).to.be.a('number'); - expect(dataItem.ad).to.be.a('string'); + if (dataItem.mediaType === VIDEO) { + expect(dataItem.vastXml).to.be.a('string'); + } else if (dataItem.mediaType === BANNER) { + expect(dataItem.ad).to.be.a('string'); + } expect(dataItem.ttl).to.be.a('number'); expect(dataItem.creativeId).to.be.a('string'); expect(dataItem.netRevenue).to.be.a('boolean'); diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index 66e59ad6f3b..a6fbc9666b9 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec, resetBoPixel} from 'modules/openxBidAdapter.js'; +import {spec, USER_ID_CODE_TO_QUERY_ARG} from 'modules/openxBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; import {userSync} from 'src/userSync.js'; import {config} from 'src/config.js'; @@ -1026,151 +1026,66 @@ describe('OpenxAdapter', function () { }); describe('when there are userid providers', function () { - describe('with publisher common id', function () { - it('should not send a pubcid query param when there is no crumbs.pubcid and no userId.pubcid defined in the bid requests', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].data).to.not.have.any.keys('pubcid'); - }); - - it('should send a pubcid query param when crumbs.pubcid is defined in the bid requests', function () { - const bidRequestsWithPubcid = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain' - }, - crumbs: { - pubcid: 'c4a4c843-2368-4b5e-b3b1-6ee4702b9ad6' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1' - }]; - const request = spec.buildRequests(bidRequestsWithPubcid, mockBidderRequest); - expect(request[0].data.pubcid).to.equal('c4a4c843-2368-4b5e-b3b1-6ee4702b9ad6'); - }); - - it('should send a pubcid query param when userId.pubcid is defined in the bid requests', function () { - const bidRequestsWithPubcid = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain' - }, - userId: { - pubcid: 'c1a4c843-2368-4b5e-b3b1-6ee4702b9ad6' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1' - }]; - const request = spec.buildRequests(bidRequestsWithPubcid, mockBidderRequest); - expect(request[0].data.pubcid).to.equal('c1a4c843-2368-4b5e-b3b1-6ee4702b9ad6'); - }); - }); - - describe('with the trade desk unified id', function () { - it('should not send a tdid query param when there is no userId.tdid defined in the bid requests', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].data).to.not.have.any.keys('ttduuid'); - }); - - it('should send a tdid query param when userId.tdid is defined in the bid requests', function () { - const bidRequestsWithTdid = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain' - }, - userId: { - tdid: '00000000-aaaa-1111-bbbb-222222222222' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1' - }]; - const request = spec.buildRequests(bidRequestsWithTdid, mockBidderRequest); - expect(request[0].data.ttduuid).to.equal('00000000-aaaa-1111-bbbb-222222222222'); - }); - }); - - describe('with the liveRamp identity link envelope', function () { - it('should not send a tdid query param when there is no userId.lre defined in the bid requests', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].data).to.not.have.any.keys('lre'); - }); + const EXAMPLE_DATA_BY_ATTR = { + britepoolid: '1111-britepoolid', + criteoId: '1111-criteoId', + digitrustid: {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}, + id5id: '1111-id5id', + idl_env: '1111-idl_env', + lipb: {lipbid: '1111-lipb'}, + netId: 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + parrableid: 'eidVersion.encryptionKeyReference.encryptedValue', + pubcid: '1111-pubcid', + tdid: '1111-tdid', + }; - it('should send a lre query param when userId.lre is defined in the bid requests', function () { - const bidRequestsWithLiveRampEnvelope = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain' - }, - userId: { - idl_env: '00000000-aaaa-1111-bbbb-222222222222' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1' - }]; - const request = spec.buildRequests(bidRequestsWithLiveRampEnvelope, mockBidderRequest); - expect(request[0].data.lre).to.equal('00000000-aaaa-1111-bbbb-222222222222'); - }); - }); + // generates the same set of tests for each id provider + utils._each(USER_ID_CODE_TO_QUERY_ARG, (userIdQueryArg, userIdProviderKey) => { + describe(`with userId attribute: ${userIdProviderKey}`, function () { + it(`should not send a ${userIdQueryArg} query param when there is no userId.${userIdProviderKey} defined in the bid requests`, function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data).to.not.have.any.keys(userIdQueryArg); + }); - describe('with the criteo id for exchanges', function () { - it('should not send a criteoid query param when there is no userId.criteoId defined in the bid requests', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].data).to.not.have.any.keys('criteoid'); - }); + it(`should send a ${userIdQueryArg} query param when userId.${userIdProviderKey} is defined in the bid requests`, function () { + const bidRequestsWithUserId = [{ + bidder: 'openx', + params: { + unit: '11', + delDomain: 'test-del-domain' + }, + userId: { + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1' + }]; + // enrich bid request with userId key/value + bidRequestsWithUserId[0].userId[userIdProviderKey] = EXAMPLE_DATA_BY_ATTR[userIdProviderKey]; + + const request = spec.buildRequests(bidRequestsWithUserId, mockBidderRequest); + + let userIdValue; + // handle cases where userId key refers to an object + switch (userIdProviderKey) { + case 'digitrustid': + userIdValue = EXAMPLE_DATA_BY_ATTR.digitrustid.data.id; + break; + case 'lipb': + userIdValue = EXAMPLE_DATA_BY_ATTR.lipb.lipbid; + break; + default: + userIdValue = EXAMPLE_DATA_BY_ATTR[userIdProviderKey]; + } - it('should send a criteoid query param when userId.criteoId is defined in the bid requests', function () { - const bidRequestsWithCriteo = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain' - }, - userId: { - criteoId: '00000000-aaaa-1111-bbbb-222222222222' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1' - }]; - const request = spec.buildRequests(bidRequestsWithCriteo, mockBidderRequest); - expect(request[0].data.criteoid).to.equal('00000000-aaaa-1111-bbbb-222222222222'); + expect(request[0].data[USER_ID_CODE_TO_QUERY_ARG[userIdProviderKey]]).to.equal(userIdValue); + }); }); }); }); @@ -1240,7 +1155,7 @@ describe('OpenxAdapter', function () { 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' - } + }; mockBidderRequest = {refererInfo: {}}; }); @@ -1672,32 +1587,31 @@ describe('OpenxAdapter', function () { payload: {'bid': bidsWithMediaType[0], 'startTime': new Date()} }; const bidResponse = { - 'pub_rev': '1', + 'pub_rev': '1000', 'width': '640', 'height': '480', 'adid': '5678', - 'vastUrl': 'https://testvast.com/vastpath?colo=https://test-colo.com&ph=test-ph&ts=test-ts', + 'currency': 'AUD', + 'vastUrl': 'https://testvast.com', 'pixels': 'https://testpixels.net' }; it('should return correct bid response with MediaTypes', function () { - const expectedResponse = [ - { - 'requestId': '30b31c1838de1e', - 'cpm': 1, - 'width': '640', - 'height': '480', - 'mediaType': 'video', - 'creativeId': '5678', - 'vastUrl': 'https://testvast.com', - 'ttl': 300, - 'netRevenue': true, - 'currency': 'USD' - } - ]; + const expectedResponse = { + 'requestId': '30b31c1838de1e', + 'cpm': 1, + 'width': 640, + 'height': 480, + 'mediaType': 'video', + 'creativeId': '5678', + 'vastUrl': 'https://testvast.com', + 'ttl': 300, + 'netRevenue': true, + 'currency': 'AUD' + }; const result = spec.interpretResponse({body: bidResponse}, bidRequestsWithMediaTypes); - expect(JSON.stringify(Object.keys(result[0]).sort())).to.eql(JSON.stringify(Object.keys(expectedResponse[0]).sort())); + expect(result[0]).to.eql(expectedResponse); }); it('should return correct bid response with MediaType', function () { diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 936b4c3a824..93415126a0a 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -3,6 +3,9 @@ import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; import { parrableIdSubmodule } from 'modules/parrableIdSystem.js'; +import { newStorageManager } from 'src/storageManager.js'; + +const storage = newStorageManager(); const EXPIRED_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:01 GMT'; const P_COOKIE_NAME = '_parrable_eid'; @@ -52,7 +55,7 @@ describe('Parrable ID System', function() { it('should append parrableid to bid request', function(done) { // simulate existing browser local storage values - utils.setCookie( + storage.setCookie( P_COOKIE_NAME, P_COOKIE_VALUE, (new Date(Date.now() + 5000).toUTCString()) @@ -69,7 +72,7 @@ describe('Parrable ID System', function() { expect(bid.userId.parrableid).to.equal(P_COOKIE_VALUE); }); }); - utils.setCookie(P_COOKIE_NAME, '', EXPIRED_COOKIE_DATE); + storage.setCookie(P_COOKIE_NAME, '', EXPIRED_COOKIE_DATE); done(); }, { adUnits }); }); diff --git a/test/spec/modules/platformioBidAdapter_spec.js b/test/spec/modules/platformioBidAdapter_spec.js new file mode 100644 index 00000000000..ee753be17a7 --- /dev/null +++ b/test/spec/modules/platformioBidAdapter_spec.js @@ -0,0 +1,348 @@ +import {expect} from 'chai'; +import {spec} from 'modules/platformioBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; + +describe('Platform.io Adapter Tests', function () { + const slotConfigs = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bidId: 'bid12345', + mediaType: 'banner', + params: { + pubId: '29521', + siteId: '26047', + placementId: '123', + bidFloor: '0.001', + ifa: 'IFA', + latitude: '40.712775', + longitude: '-74.005973' + } + }, { + placementCode: '/DfpAccount2/slot2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bidId: 'bid23456', + mediaType: 'banner', + params: { + pubId: '29521', + siteId: '26047', + placementId: '1234', + bidFloor: '0.000001', + } + }]; + const nativeSlotConfig = [{ + placementCode: '/DfpAccount1/slot3', + bidId: 'bid12345', + mediaType: 'native', + nativeParams: { + title: { required: true, len: 200 }, + body: {}, + image: { wmin: 100 }, + sponsoredBy: { }, + icon: { } + }, + params: { + pubId: '29521', + placementId: '123', + siteId: '26047' + } + }]; + const videoSlotConfig = [{ + placementCode: '/DfpAccount1/slot4', + mediaTypes: { + video: { + playerSize: [[640, 480]] + } + }, + bidId: 'bid12345678', + mediaType: 'video', + video: { + skippable: true + }, + params: { + pubId: '29521', + placementId: '1234567', + siteId: '26047', + } + }]; + const appSlotConfig = [{ + placementCode: '/DfpAccount1/slot5', + bidId: 'bid12345', + params: { + pubId: '29521', + placementId: '1234', + app: { + id: '1111', + name: 'app name', + bundle: 'io.platform.apps', + storeUrl: 'https://platform.io/apps', + domain: 'platform.io' + } + } + }]; + + it('Verify build request', function () { + const request = spec.buildRequests(slotConfigs); + expect(request.url).to.equal('https://piohbdisp.hb.adx1.com/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + // site object + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site.publisher).to.not.equal(null); + expect(ortbRequest.site.publisher.id).to.equal('29521'); + expect(ortbRequest.site.ref).to.equal(window.top.document.referrer); + expect(ortbRequest.site.page).to.equal(window.location.href); + expect(ortbRequest.imp).to.have.lengthOf(2); + // device object + expect(ortbRequest.device).to.not.equal(null); + expect(ortbRequest.device.ua).to.equal(navigator.userAgent); + expect(ortbRequest.device.ifa).to.equal('IFA'); + expect(ortbRequest.device.geo.lat).to.equal('40.712775'); + expect(ortbRequest.device.geo.lon).to.equal('-74.005973'); + // slot 1 + expect(ortbRequest.imp[0].tagid).to.equal('123'); + expect(ortbRequest.imp[0].banner).to.not.equal(null); + expect(ortbRequest.imp[0].banner.w).to.equal(300); + expect(ortbRequest.imp[0].banner.h).to.equal(250); + expect(ortbRequest.imp[0].bidfloor).to.equal('0.001'); + // slot 2 + expect(ortbRequest.imp[1].tagid).to.equal('1234'); + expect(ortbRequest.imp[1].banner).to.not.equal(null); + expect(ortbRequest.imp[1].banner.w).to.equal(728); + expect(ortbRequest.imp[1].banner.h).to.equal(90); + expect(ortbRequest.imp[1].bidfloor).to.equal('0.000001'); + }); + + it('Verify parse response', function () { + const request = spec.buildRequests(slotConfigs); + const ortbRequest = JSON.parse(request.data); + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + w: 300, + h: 250 + }] + }], + cur: 'USD' + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.ad).to.equal('This is an Ad'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.adId).to.equal('bid12345'); + expect(bid.creativeId).to.equal('bid12345'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(360); + }); + + it('Verify full passback', function () { + const request = spec.buildRequests(slotConfigs); + const bids = spec.interpretResponse({ body: null }, request) + expect(bids).to.have.lengthOf(0); + }); + + it('Verify Native request', function () { + const request = spec.buildRequests(nativeSlotConfig); + expect(request.url).to.equal('https://piohbdisp.hb.adx1.com/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + // native impression + expect(ortbRequest.imp[0].tagid).to.equal('123'); + const nativePart = ortbRequest.imp[0]['native']; + expect(nativePart).to.not.equal(null); + expect(nativePart.ver).to.equal('1.1'); + expect(nativePart.request).to.not.equal(null); + // native request assets + const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); + expect(nativeRequest).to.not.equal(null); + expect(nativeRequest.assets).to.have.lengthOf(5); + expect(nativeRequest.assets[0].id).to.equal(1); + expect(nativeRequest.assets[1].id).to.equal(2); + expect(nativeRequest.assets[2].id).to.equal(3); + expect(nativeRequest.assets[3].id).to.equal(4); + expect(nativeRequest.assets[4].id).to.equal(5); + expect(nativeRequest.assets[0].required).to.equal(1); + expect(nativeRequest.assets[0].title).to.not.equal(null); + expect(nativeRequest.assets[0].title.len).to.equal(200); + expect(nativeRequest.assets[1].title).to.be.undefined; + expect(nativeRequest.assets[1].data).to.not.equal(null); + expect(nativeRequest.assets[1].data.type).to.equal(2); + expect(nativeRequest.assets[1].data.len).to.equal(200); + expect(nativeRequest.assets[2].required).to.equal(0); + expect(nativeRequest.assets[3].img).to.not.equal(null); + expect(nativeRequest.assets[3].img.wmin).to.equal(50); + expect(nativeRequest.assets[3].img.hmin).to.equal(50); + expect(nativeRequest.assets[3].img.type).to.equal(1); + expect(nativeRequest.assets[4].img).to.not.equal(null); + expect(nativeRequest.assets[4].img.wmin).to.equal(100); + expect(nativeRequest.assets[4].img.hmin).to.equal(150); + expect(nativeRequest.assets[4].img.type).to.equal(3); + }); + + it('Verify Native response', function () { + const request = spec.buildRequests(nativeSlotConfig); + expect(request.url).to.equal('https://piohbdisp.hb.adx1.com/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + const nativeResponse = { + 'native': { + assets: [ + { id: 1, title: { text: 'Ad Title' } }, + { id: 2, data: { value: 'Test description' } }, + { id: 3, data: { value: 'Brand' } }, + { id: 4, img: { url: 'https://adx1public.s3.amazonaws.com/creatives_icon.png', w: 100, h: 100 } }, + { id: 5, img: { url: 'https://adx1public.s3.amazonaws.com/creatives_image.png', w: 300, h: 300 } } + ], + link: { url: 'https://brand.com/' } + } + }; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + nurl: 'https://rtb.adx1.com/log', + adm: JSON.stringify(nativeResponse) + }] + }], + cur: 'USD', + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + // verify bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.adId).to.equal('bid12345'); + expect(bid.ad).to.be.undefined; + expect(bid.mediaType).to.equal('native'); + const nativeBid = bid['native']; + expect(nativeBid).to.not.equal(null); + expect(nativeBid.title).to.equal('Ad Title'); + expect(nativeBid.sponsoredBy).to.equal('Brand'); + expect(nativeBid.icon.url).to.equal('https://adx1public.s3.amazonaws.com/creatives_icon.png'); + expect(nativeBid.image.url).to.equal('https://adx1public.s3.amazonaws.com/creatives_image.png'); + expect(nativeBid.image.width).to.equal(300); + expect(nativeBid.image.height).to.equal(300); + expect(nativeBid.icon.width).to.equal(100); + expect(nativeBid.icon.height).to.equal(100); + expect(nativeBid.clickUrl).to.equal(encodeURIComponent('https://brand.com/')); + expect(nativeBid.impressionTrackers).to.have.lengthOf(1); + expect(nativeBid.impressionTrackers[0]).to.equal('https://rtb.adx1.com/log'); + }); + + it('Verify Video request', function () { + const request = spec.buildRequests(videoSlotConfig); + expect(request.url).to.equal('https://piohbdisp.hb.adx1.com/'); + expect(request.method).to.equal('POST'); + const videoRequest = JSON.parse(request.data); + // site object + expect(videoRequest.site).to.not.equal(null); + expect(videoRequest.site.publisher.id).to.equal('29521'); + expect(videoRequest.site.ref).to.equal(window.top.document.referrer); + expect(videoRequest.site.page).to.equal(window.location.href); + // device object + expect(videoRequest.device).to.not.equal(null); + expect(videoRequest.device.ua).to.equal(navigator.userAgent); + // slot 1 + expect(videoRequest.imp[0].tagid).to.equal('1234567'); + expect(videoRequest.imp[0].video).to.not.equal(null); + expect(videoRequest.imp[0].video.w).to.equal(640); + expect(videoRequest.imp[0].video.h).to.equal(480); + expect(videoRequest.imp[0].banner).to.equal(null); + expect(videoRequest.imp[0].native).to.equal(null); + }); + + it('Verify parse video response', function () { + const request = spec.buildRequests(videoSlotConfig); + const videoRequest = JSON.parse(request.data); + const videoResponse = { + seatbid: [{ + bid: [{ + impid: videoRequest.imp[0].id, + price: 1.90, + adm: 'https://vid.example.com/9876', + crid: '510511_754567308' + }] + }], + cur: 'USD' + }; + const bids = spec.interpretResponse({ body: videoResponse }, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.90); + expect(bid.vastUrl).to.equal('https://vid.example.com/9876'); + expect(bid.crid).to.equal('510511_754567308'); + expect(bid.width).to.equal(640); + expect(bid.height).to.equal(480); + expect(bid.adId).to.equal('bid12345678'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('USD'); + expect(bid.ttl).to.equal(360); + }); + + it('Verifies bidder code', function () { + expect(spec.code).to.equal('platformio'); + }); + + it('Verifies supported media types', function () { + expect(spec.supportedMediaTypes).to.have.lengthOf(3); + expect(spec.supportedMediaTypes[0]).to.equal('banner'); + expect(spec.supportedMediaTypes[1]).to.equal('native'); + expect(spec.supportedMediaTypes[2]).to.equal('video'); + }); + + it('Verifies if bid request valid', function () { + expect(spec.isBidRequestValid(slotConfigs[0])).to.equal(true); + expect(spec.isBidRequestValid(slotConfigs[1])).to.equal(true); + expect(spec.isBidRequestValid(nativeSlotConfig[0])).to.equal(true); + expect(spec.isBidRequestValid(videoSlotConfig[0])).to.equal(true); + }); + + it('Verify app requests', function () { + const request = spec.buildRequests(appSlotConfig); + const ortbRequest = JSON.parse(request.data); + expect(ortbRequest.site).to.equal(null); + expect(ortbRequest.app).to.not.be.null; + expect(ortbRequest.app.publisher).to.not.equal(null); + expect(ortbRequest.app.publisher.id).to.equal('29521'); + expect(ortbRequest.app.id).to.equal('1111'); + expect(ortbRequest.app.name).to.equal('app name'); + expect(ortbRequest.app.bundle).to.equal('io.platform.apps'); + expect(ortbRequest.app.storeurl).to.equal('https://platform.io/apps'); + expect(ortbRequest.app.domain).to.equal('platform.io'); + }); + + it('Verify GDPR', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'serialized_gpdr_data' + } + }; + const request = spec.buildRequests(slotConfigs, bidderRequest); + expect(request.url).to.equal('https://piohbdisp.hb.adx1.com/'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); + expect(ortbRequest.user).to.not.equal(null); + expect(ortbRequest.user.ext).to.not.equal(null); + expect(ortbRequest.user.ext.consent).to.equal('serialized_gpdr_data'); + expect(ortbRequest.regs).to.not.equal(null); + expect(ortbRequest.regs.ext).to.not.equal(null); + expect(ortbRequest.regs.ext.gdpr).to.equal(1); + }); +}); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index cad4c6b819e..4744bef0ee3 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -899,6 +899,15 @@ describe('S2S Adapter', function () { const _config = { s2sConfig: s2sConfig, + site: { + publisher: { + id: '1234', + domain: 'test.com' + }, + content: { + language: 'en' + } + } }; config.setConfig(_config); @@ -906,7 +915,21 @@ describe('S2S Adapter', function () { const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.site).to.exist.and.to.be.a('object'); expect(requestBid.site.publisher).to.exist.and.to.be.a('object'); + expect(requestBid.site.publisher.id).to.exist.and.to.be.a('string'); + expect(requestBid.site.publisher.domain).to.exist.and.to.be.a('string'); expect(requestBid.site.page).to.exist.and.to.be.a('string'); + expect(requestBid.site.content).to.exist.and.to.be.a('object'); + expect(requestBid.site.content.language).to.exist.and.to.be.a('string'); + expect(requestBid.site).to.deep.equal({ + publisher: { + id: '1', + domain: 'test.com' + }, + content: { + language: 'en' + }, + page: 'http://mytestpage.com' + }); }); it('adds appnexus aliases to request', function () { diff --git a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js index 7c9173c14ab..e87be40314c 100644 --- a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js +++ b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js @@ -101,4 +101,38 @@ describe('Prebid Manager Analytics Adapter', function () { sinon.assert.callCount(prebidmanagerAnalytics.track, 6); }); }); + + describe('build utm tag data', function () { + beforeEach(function () { + localStorage.setItem('pm_utm_source', 'utm_source'); + localStorage.setItem('pm_utm_medium', 'utm_medium'); + localStorage.setItem('pm_utm_campaign', 'utm_camp'); + localStorage.setItem('pm_utm_term', ''); + localStorage.setItem('pm_utm_content', ''); + }); + afterEach(function () { + localStorage.removeItem('pm_utm_source'); + localStorage.removeItem('pm_utm_medium'); + localStorage.removeItem('pm_utm_campaign'); + localStorage.removeItem('pm_utm_term'); + localStorage.removeItem('pm_utm_content'); + prebidmanagerAnalytics.disableAnalytics() + }); + it('should build utm data from local storage', function () { + prebidmanagerAnalytics.enableAnalytics({ + provider: 'prebidmanager', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.utmTags.utm_source).to.equal('utm_source'); + expect(pmEvents.utmTags.utm_medium).to.equal('utm_medium'); + expect(pmEvents.utmTags.utm_campaign).to.equal('utm_camp'); + expect(pmEvents.utmTags.utm_term).to.equal(''); + expect(pmEvents.utmTags.utm_content).to.equal(''); + }); + }); }); diff --git a/test/spec/modules/priceFloorsSchema.json b/test/spec/modules/priceFloorsSchema.json new file mode 100644 index 00000000000..7b524da381e --- /dev/null +++ b/test/spec/modules/priceFloorsSchema.json @@ -0,0 +1,86 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "description": "A Price Floors object which is parsed and used to enforce dynamic floors depending on the properties of the json", + "type": "object", + "title": "Price Floors Enforcement", + "required": [ + "schema", + "values" + ], + "properties": { + "schema": { + "type": "object", + "description": "Defines the schema of the rules", + "required": [ + "fields" + ], + "properties": { + "fields": { + "type": "array", + "description": "The list of fields to be used for matching criteria of a bid response with a price floor", + "minItems": 1, + "uniqueItems": true, + "additionalItems": false, + "items": { + "type": "string" + } + }, + "delimiter": { + "type": "string", + "description": "The character used to differentiate the fields inside a single rule", + "examples": [ + "|", + "^", + "~" + ] + } + } + }, + "values": { + "type": "object", + "description": "A object with key : value pairs which constitutes a rule and floor", + "additionalProperties": { + "type": "number" + }, + "minProperties": 1, + "examples": [ + { + "123456/someSlot|300x250|www.prebid.org": 1.5, + "123456/someSlot|300x600|www.prebid.org": 2.5, + "123456/someSlot|300x600|*": 1.2, + "123456/someSlot|*|*": 0.8 + } + ] + }, + "currency": { + "type": "string", + "description": "The three digit Currency Code which the floors are provided in", + "examples": [ + "USD", + "EUR", + "JPY" + ], + "pattern": "^[a-zA-Z]{3}$" + }, + "modelVersion": { + "type": "string", + "description": "The floor modeling name to be used for tracking", + "examples": [ + "Prebid-Floor-Model-1.2" + ] + }, + "skipRate": { + "type": "integer", + "description": "The skip rate as to which flooring will be 'turned off' for a given auction", + "minimum": 0, + "maximum": 100 + }, + "default": { + "type": "number", + "description": "The default floor to use if no entry in the value matches a bid response", + "examples": [ + 0.75 + ] + } + } +} \ No newline at end of file diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js new file mode 100644 index 00000000000..55aa7900252 --- /dev/null +++ b/test/spec/modules/priceFloors_spec.js @@ -0,0 +1,972 @@ +import {expect} from 'chai'; +import * as utils from 'src/utils.js'; +import { getGlobal } from 'src/prebidGlobal.js'; +import CONSTANTS from 'src/constants.json'; +import { + _floorDataForAuction, + getFloorsDataForAuction, + getFirstMatchingFloor, + getFloor, + handleSetFloorsConfig, + requestBidsHook, + isFloorsDataValid, + addBidResponseHook, + fieldMatchingFunctions, + allowedFields +} from 'modules/priceFloors.js'; + +describe('the price floors module', function () { + let logErrorSpy; + let logWarnSpy; + let sandbox; + const basicFloorData = { + modelVersion: 'basic model', + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType'] + }, + values: { + 'banner': 1.0, + 'video': 5.0, + '*': 2.5 + } + }; + const basicFloorConfig = { + enabled: true, + auctionDelay: 0, + endpoint: {}, + enforcement: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + data: basicFloorData + } + const basicBidRequest = { + bidder: 'rubicon', + adUnitCode: 'test_div_1', + auctionId: '1234-56-789', + }; + + function getAdUnitMock(code = 'adUnit-code') { + return { + code, + mediaTypes: {banner: { sizes: [[300, 200], [300, 600]] }, native: {}}, + bids: [{bidder: 'someBidder'}, {bidder: 'someOtherBidder'}] + }; + } + beforeEach(function() { + sandbox = sinon.sandbox.create(); + logErrorSpy = sinon.spy(utils, 'logError'); + logWarnSpy = sinon.spy(utils, 'logWarn'); + }); + + afterEach(function() { + handleSetFloorsConfig({enabled: false}); + sandbox.restore(); + utils.logError.restore(); + utils.logWarn.restore(); + // reset global bidder settings so no weird test side effects + getGlobal().bidderSettings = {}; + }); + + describe('getFloorsDataForAuction', function () { + it('converts basic input floor data into a floorData map for the auction correctly', function () { + // basic input where nothing needs to be updated + expect(getFloorsDataForAuction(basicFloorData)).to.deep.equal(basicFloorData); + + // if cur and delim not defined then default to correct ones (usd and |) + let inputFloorData = utils.deepClone(basicFloorData); + delete inputFloorData.currency; + delete inputFloorData.schema.delimiter; + expect(getFloorsDataForAuction(inputFloorData)).to.deep.equal(basicFloorData); + + // should not use defaults if differing values + inputFloorData.currency = 'EUR' + inputFloorData.schema.delimiter = '^' + let resultingData = getFloorsDataForAuction(inputFloorData); + expect(resultingData.currency).to.equal('EUR'); + expect(resultingData.schema.delimiter).to.equal('^'); + }); + + it('converts more complex floor data correctly', function () { + let inputFloorData = { + schema: { + fields: ['mediaType', 'size', 'domain'] + }, + values: { + 'banner|300x250|prebid.org': 1.0, + 'video|640x480|prebid.org': 5.0, + 'banner|728x90|rubicon.com': 3.5, + 'video|600x300|appnexus.com': 3.5, + '*|*|prebid.org': 3.5, + } + }; + let resultingData = getFloorsDataForAuction(inputFloorData); + expect(resultingData).to.deep.equal({ + currency: 'USD', + schema: { + delimiter: '|', + fields: ['mediaType', 'size', 'domain'] + }, + values: { + 'banner|300x250|prebid.org': 1.0, + 'video|640x480|prebid.org': 5.0, + 'banner|728x90|rubicon.com': 3.5, + 'video|600x300|appnexus.com': 3.5, + '*|*|prebid.org': 3.5, + } + }); + }); + + it('adds adUnitCode to the schema if the floorData comes from adUnit level to maintain scope', function () { + let inputFloorData = utils.deepClone(basicFloorData); + let resultingData = getFloorsDataForAuction(inputFloorData, 'test_div_1'); + expect(resultingData).to.deep.equal({ + modelVersion: 'basic model', + currency: 'USD', + schema: { + delimiter: '|', + fields: ['adUnitCode', 'mediaType'] + }, + values: { + 'test_div_1|banner': 1.0, + 'test_div_1|video': 5.0, + 'test_div_1|*': 2.5 + } + }); + + // uses the right delim if not | + inputFloorData.schema.delimiter = '^'; + resultingData = getFloorsDataForAuction(inputFloorData, 'this_is_a_div'); + expect(resultingData).to.deep.equal({ + modelVersion: 'basic model', + currency: 'USD', + schema: { + delimiter: '^', + fields: ['adUnitCode', 'mediaType'] + }, + values: { + 'this_is_a_div^banner': 1.0, + 'this_is_a_div^video': 5.0, + 'this_is_a_div^*': 2.5 + } + }); + }); + }); + + describe('getFirstMatchingFloor', function () { + it('selects the right floor for different mediaTypes', function () { + // banner with * size (not in rule file so does not do anything) + expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + matchingFloor: 1.0, + matchingData: 'banner', + matchingRule: 'banner' + }); + // video with * size (not in rule file so does not do anything) + expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'video', size: '*'})).to.deep.equal({ + matchingFloor: 5.0, + matchingData: 'video', + matchingRule: 'video' + }); + // native (not in the rule list) with * size (not in rule file so does not do anything) + expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'native', size: '*'})).to.deep.equal({ + matchingFloor: 2.5, + matchingData: 'native', + matchingRule: '*' + }); + }); + it('selects the right floor for different sizes', function () { + let inputFloorData = { + currency: 'USD', + schema: { + delimiter: '|', + fields: ['size'] + }, + values: { + '300x250': 1.1, + '640x480': 2.2, + '728x90': 3.3, + '600x300': 4.4, + '*': 5.5, + } + } + // banner with 300x250 size + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + matchingFloor: 1.1, + matchingData: '300x250', + matchingRule: '300x250' + }); + // video with 300x250 size + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + matchingFloor: 1.1, + matchingData: '300x250', + matchingRule: '300x250' + }); + // native (not in the rule list) with 300x600 size + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'native', size: [600, 300]})).to.deep.equal({ + matchingFloor: 4.4, + matchingData: '600x300', + matchingRule: '600x300' + }); + // n/a mediaType with a size not in file should go to catch all + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: undefined, size: [1, 1]})).to.deep.equal({ + matchingFloor: 5.5, + matchingData: '1x1', + matchingRule: '*' + }); + }); + it('selects the right floor for more complex rules', function () { + let inputFloorData = { + currency: 'USD', + schema: { + delimiter: '^', + fields: ['adUnitCode', 'mediaType', 'size'] + }, + values: { + 'test_div_1^banner^300x250': 1.1, + 'test_div_1^video^640x480': 2.2, + 'test_div_2^*^*': 3.3, + '*^banner^300x250': 4.4, + 'weird_div^*^300x250': 5.5 + }, + default: 0.5 + }; + // banner with 300x250 size + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ + matchingFloor: 1.1, + matchingData: 'test_div_1^banner^300x250', + matchingRule: 'test_div_1^banner^300x250' + }); + // video with 300x250 size -> No matching rule so should use default + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + matchingFloor: 0.5, + matchingData: 'test_div_1^video^300x250', + matchingRule: undefined + }); + // remove default and should still return the same floor as above since matches are cached + delete inputFloorData.default; + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'video', size: [300, 250]})).to.deep.equal({ + matchingFloor: 0.5, + matchingData: 'test_div_1^video^300x250', + matchingRule: undefined + }); + // update adUnitCode to test_div_2 with weird other params + let newBidRequest = { ...basicBidRequest, adUnitCode: 'test_div_2' } + expect(getFirstMatchingFloor(inputFloorData, newBidRequest, {mediaType: 'badmediatype', size: [900, 900]})).to.deep.equal({ + matchingFloor: 3.3, + matchingData: 'test_div_2^badmediatype^900x900', + matchingRule: 'test_div_2^*^*' + }); + }); + it('it does not break if floorData has bad values', function () { + let inputFloorData = {}; + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + matchingFloor: undefined + }); + // if default is there use it + inputFloorData = { default: 5.0 }; + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + matchingFloor: 5.0 + }); + }); + }); + describe('pre-auction tests', function () { + let exposedAdUnits; + const validateBidRequests = (getFloorExpected, FloorDataExpected) => { + exposedAdUnits.forEach(adUnit => adUnit.bids.forEach(bid => { + expect(bid.hasOwnProperty('getFloor')).to.equal(getFloorExpected); + expect(bid.floorData).to.deep.equal(FloorDataExpected); + })); + }; + const runStandardAuction = (adUnits = [getAdUnitMock('test_div_1')]) => { + requestBidsHook(config => exposedAdUnits = config.adUnits, { + auctionId: basicBidRequest.auctionId, + adUnits, + }); + }; + let fakeFloorProvider; + let clock; + let actualAllowedFields = allowedFields; + let actualFieldMatchingFunctions = fieldMatchingFunctions; + const defaultAllowedFields = [...allowedFields]; + const defaultMatchingFunctions = {...fieldMatchingFunctions}; + before(function () { + clock = sinon.useFakeTimers(); + }); + after(function () { + clock.restore(); + }); + beforeEach(function() { + fakeFloorProvider = sinon.fakeServer.create(); + }); + afterEach(function() { + fakeFloorProvider.restore(); + exposedAdUnits = undefined; + actualAllowedFields = [...defaultAllowedFields]; + actualFieldMatchingFunctions = {...defaultMatchingFunctions}; + }); + it('should not do floor stuff if no resulting floor object can be resolved for auciton', function () { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + runStandardAuction(); + validateBidRequests(false, undefined); + }); + it('should use adUnit level data if not setConfig or fetch has occured', function () { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + // attach floor data onto an adUnit and run an auction + let adUnitWithFloors1 = { + ...getAdUnitMock('adUnit-Div-1'), + floors: { + ...basicFloorData, + modelVersion: 'adUnit Model Version', // change the model name + } + }; + let adUnitWithFloors2 = { + ...getAdUnitMock('adUnit-Div-2'), + floors: { + ...basicFloorData, + values: { + 'banner': 5.0, + '*': 10.4 + } + } + }; + runStandardAuction([adUnitWithFloors1, adUnitWithFloors2]); + validateBidRequests(true, { + skipped: false, + modelVersion: 'adUnit Model Version', + location: 'adUnit', + }); + }); + it('bidRequests should have getFloor function and flooring meta data when setConfig occurs', function () { + handleSetFloorsConfig({...basicFloorConfig}); + runStandardAuction(); + validateBidRequests(true, { + skipped: false, + modelVersion: 'basic model', + location: 'setConfig', + }); + }); + it('should not overwrite previous data object if the new one is bad', function () { + handleSetFloorsConfig({...basicFloorConfig}); + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + handleSetFloorsConfig({ + ...basicFloorConfig, + data: 5 + }); + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + schema: {fields: ['thisIsNotAllowedSoShouldFail']}, + values: {'*': 1.2}, + modelVersion: 'FAIL' + } + }); + runStandardAuction(); + validateBidRequests(true, { + skipped: false, + modelVersion: 'basic model', + location: 'setConfig', + }); + }); + it('should dynamically add new schema fileds and functions if added via setConfig', function () { + let deviceSpoof; + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + schema: {fields: ['deviceType']}, + values: { + 'mobile': 1.0, + 'desktop': 2.0, + 'tablet': 3.0, + '*': 4.0 + } + }, + additionalSchemaFields: { + deviceType: () => deviceSpoof + } + }); + expect(allowedFields).to.contain('deviceType'); + expect(fieldMatchingFunctions['deviceType']).to.be.a('function'); + + // run getFloor to make sure it selcts right stuff! (other params do not matter since we only are testing deviceType) + runStandardAuction(); + + // set deviceType to mobile; + deviceSpoof = 'mobile'; + exposedAdUnits[0].bids[0].auctionId = basicBidRequest.auctionId + expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.0 // 'mobile': 1.0, + }); + + // set deviceType to desktop; + deviceSpoof = 'desktop'; + expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({ + currency: 'USD', + floor: 2.0 // 'desktop': 2.0, + }); + + // set deviceType to tablet; + deviceSpoof = 'tablet'; + expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({ + currency: 'USD', + floor: 3.0 // 'tablet': 3.0 + }); + + // set deviceType to unknown; + deviceSpoof = 'unknown'; + expect(exposedAdUnits[0].bids[0].getFloor()).to.deep.equal({ + currency: 'USD', + floor: 4.0 // '*': 4.0 + }); + }); + it('Should continue auction of delay is hit without a response from floor provider', function () { + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + + // start the auction it should delay and not immediately call `continueAuction` + runStandardAuction(); + + // exposedAdUnits should be undefined if the auction has not continued + expect(exposedAdUnits).to.be.undefined; + + // hit the delay + clock.tick(250); + + // log warn should be called and adUnits not undefined + expect(logWarnSpy.calledOnce).to.equal(true); + expect(exposedAdUnits).to.not.be.undefined; + + // the exposedAdUnits should be from the fetch not setConfig level data + validateBidRequests(true, { + skipped: false, + modelVersion: 'basic model', + location: 'setConfig', + }); + fakeFloorProvider.respond(); + }); + it('It should fetch if config has url and bidRequests have fetch level flooring meta data', function () { + // init the fake server with response stuff + let fetchFloorData = { + ...basicFloorData, + modelVersion: 'fetch model name', // change the model name + }; + fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + + // run setConfig indicating fetch + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + + // floor provider should be called + expect(fakeFloorProvider.requests.length).to.equal(1); + expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + + // start the auction it should delay and not immediately call `continueAuction` + runStandardAuction(); + + // exposedAdUnits should be undefined if the auction has not continued + expect(exposedAdUnits).to.be.undefined; + + // make the fetch respond + fakeFloorProvider.respond(); + expect(exposedAdUnits).to.not.be.undefined; + + // the exposedAdUnits should be from the fetch not setConfig level data + validateBidRequests(true, { + skipped: false, + modelVersion: 'fetch model name', + location: 'fetch', + }); + }); + it('Should not break if floor provider returns non json', function () { + fakeFloorProvider.respondWith('Not valid response'); + + // run setConfig indicating fetch + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + + // run the auction and make server respond + fakeFloorProvider.respond(); + runStandardAuction(); + + // should have caught the response error and still used setConfig data + validateBidRequests(true, { + skipped: false, + modelVersion: 'basic model', + location: 'setConfig', + }); + }); + it('should handle not using fetch correctly', function () { + // run setConfig twice indicating fetch + fakeFloorProvider.respondWith(JSON.stringify(basicFloorData)); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + + // log warn should be called and server only should have one request + expect(logWarnSpy.calledOnce).to.equal(true); + expect(fakeFloorProvider.requests.length).to.equal(1); + expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + + // now we respond and then run again it should work and make another request + fakeFloorProvider.respond(); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + fakeFloorProvider.respond(); + + // now warn still only called once and server called twice + expect(logWarnSpy.calledOnce).to.equal(true); + expect(fakeFloorProvider.requests.length).to.equal(2); + + // should log error if method is not GET for now + expect(logErrorSpy.calledOnce).to.equal(false); + handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakeFloorProvider.json', method: 'POST'}}); + expect(logErrorSpy.calledOnce).to.equal(true); + }); + describe('isFloorsDataValid', function () { + it('should work correctly for fields array', function () { + let inputFloorData = utils.deepClone(basicFloorData); + expect(isFloorsDataValid(inputFloorData)).to.to.equal(true); + + // no fields array + delete inputFloorData.schema.fields; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + + // Fields is not an array + inputFloorData.schema.fields = {}; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + inputFloorData.schema.fields = undefined; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + inputFloorData.schema.fields = 'adUnitCode'; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + + // fields has a value that is not one of the "allowed" fields + inputFloorData.schema.fields = ['adUnitCode', 'notValidMapping']; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + }); + it('should work correctly for values object', function () { + let inputFloorData = utils.deepClone(basicFloorData); + expect(isFloorsDataValid(inputFloorData)).to.to.equal(true); + + // no values object + delete inputFloorData.values; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + + // values is not correct type + inputFloorData.values = []; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + inputFloorData.values = '123455/slot'; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + + // is an object but structure is wrong + inputFloorData.values = { + 'banner': 'not a floor value' + }; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + inputFloorData.values = { + 'banner': undefined + }; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(false); + + // should be true if at least one rule is valid + inputFloorData.schema.fields = ['adUnitCode', 'mediaType']; + inputFloorData.values = { + 'banner': 1.0, + 'test-div-1|native': 1.0, // only valid rule should still work and delete the other rules + 'video': 1.0, + '*': 1.0 + }; + expect(isFloorsDataValid(inputFloorData)).to.to.equal(true); + expect(inputFloorData.values).to.deep.equal({ 'test-div-1|native': 1.0 }); + }); + }); + describe('getFloor', function () { + let bidRequest = { + ...basicBidRequest, + getFloor + }; + it('returns empty if no matching data for auction is found', function () { + expect(bidRequest.getFloor({})).to.deep.equal({}); + }); + it('picks the right rule depending on input', function () { + _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + + // empty params into getFloor should use default of banner * FloorData Curr + let inputParams = {}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 1.0 + }); + + // ask for banner + inputParams = {mediaType: 'banner'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 1.0 + }); + + // ask for video + inputParams = {mediaType: 'video'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 5.0 + }); + + // ask for * + inputParams = {mediaType: '*'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 2.5 + }); + }); + it('picks the right rule with more complex rules', function () { + _floorDataForAuction[bidRequest.auctionId] = { + ...basicFloorConfig, + data: { + currency: 'USD', + schema: { fields: ['mediaType', 'size'], delimiter: '|' }, + values: { + 'banner|300x250': 0.5, + 'banner|300x600': 1.5, + 'banner|728x90': 2.5, + 'banner|*': 3.5, + 'video|640x480': 4.5, + 'video|*': 5.5 + }, + default: 10.0 + } + }; + + // assumes banner * + let inputParams = {}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 3.5 + }); + + // ask for banner with a size + inputParams = {mediaType: 'banner', size: [300, 600]}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 1.5 + }); + + // ask for video with a size + inputParams = {mediaType: 'video', size: [640, 480]}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 4.5 + }); + + // ask for video with a size not in rules (should pick rule which has video and *) + inputParams = {mediaType: 'video', size: [111, 222]}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 5.5 + }); + + // ask for native * but no native rule so should use default value if there + inputParams = {mediaType: 'native', size: '*'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 10.0 + }); + }); + it('should round up to 4 decimal places', function () { + _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidRequest.auctionId].data.values = { + 'banner': 1.777777, + 'video': 1.1111111, + }; + + // assumes banner * + let inputParams = {mediaType: 'banner'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 1.7778 + }); + + // assumes banner * + inputParams = {mediaType: 'video'}; + expect(bidRequest.getFloor(inputParams)).to.deep.equal({ + currency: 'USD', + floor: 1.1112 + }); + }); + it('should return the adjusted floor if bidder has cpm adjustment function', function () { + getGlobal().bidderSettings = { + rubicon: { + bidCpmAdjustment: function (bidCpm) { + return bidCpm * 0.5; + }, + }, + appnexus: { + bidCpmAdjustment: function (bidCpm) { + return bidCpm * 0.75; + }, + } + }; + _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 }; + let appnexusBid = { + ...bidRequest, + bidder: 'appnexus' + }; + + // the conversion should be what the bidder would need to return in order to match the actual floor + // rubicon + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 2.0 // a 2.0 bid after rubicons cpm adjustment would be 1.0 and thus is the floor after adjust + }); + + // appnexus + expect(appnexusBid.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.3334 // 1.3334 * 0.75 = 1.000005 which is the floor (we cut off getFloor at 4 decimal points) + }); + }); + it('should correctly pick the right attributes if * is passed in and context can be assumed', function () { + let inputBidReq = { + bidder: 'rubicon', + adUnitCode: 'test_div_2', + auctionId: '987654321', + mediaTypes: { + video: {} + }, + getFloor + }; + _floorDataForAuction[inputBidReq.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[inputBidReq.auctionId].data.values = { + '*': 1.0, + 'banner': 3.0, + 'video': 5.0 + }; + + // because bid req only has video, if a bidder asks for a floor for * we can actually give them the right mediaType + expect(inputBidReq.getFloor({mediaType: '*'})).to.deep.equal({ + currency: 'USD', + floor: 5.0 // 'video': 5.0 + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // Same for if only banner is in the input bid + inputBidReq.mediaTypes = {banner: {}}; + expect(inputBidReq.getFloor({mediaType: '*'})).to.deep.equal({ + currency: 'USD', + floor: 3.0 // 'banner': 3.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // if both are present then it will really use the * + inputBidReq.mediaTypes = {banner: {}, video: {}}; + expect(inputBidReq.getFloor({mediaType: '*'})).to.deep.equal({ + currency: 'USD', + floor: 1.0 // '*': 1.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // now if size can be inferred (meaning only one size is in the specified mediaType, it will use it) + _floorDataForAuction[inputBidReq.auctionId].data.schema.fields = ['mediaType', 'size']; + _floorDataForAuction[inputBidReq.auctionId].data.values = { + '*|*': 1.0, + 'banner|300x250': 2.0, + 'banner|728x90': 3.0, + 'banner|*': 4.0, + 'video|300x250': 5.0, + 'video|728x90': 6.0, + 'video|*': 7.0 + }; + // mediaType is banner and only one size, so if someone asks for banner * we should give them banner 300x250 + // instead of banner|* + inputBidReq.mediaTypes = {banner: {sizes: [[300, 250]]}}; + expect(inputBidReq.getFloor({mediaType: 'banner', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 2.0 // 'banner|300x250': 2.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // now for video it should look at playersize (prebid core translates playersize into typical array of size arrays) + inputBidReq.mediaTypes = {video: {playerSize: [[728, 90]]}}; + expect(inputBidReq.getFloor({mediaType: 'video', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 6.0 // 'video|728x90': 6.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // Now if multiple sizes are there, it will actually use * since can't infer + inputBidReq.mediaTypes = {banner: {sizes: [[300, 250], [728, 90]]}}; + expect(inputBidReq.getFloor({mediaType: 'banner', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 4.0 // 'banner|*': 4.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // lastly, if you pass in * mediaType and * size it should resolve both if possble + inputBidReq.mediaTypes = {banner: {sizes: [[300, 250]]}}; + expect(inputBidReq.getFloor({mediaType: '*', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 2.0 // 'banner|300x250': 2.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + inputBidReq.mediaTypes = {video: {playerSize: [[300, 250]]}}; + expect(inputBidReq.getFloor({mediaType: '*', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 5.0 // 'video|300x250': 5.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + + // now it has both mediaTypes so will use * mediaType and thus not use sizes either + inputBidReq.mediaTypes = {video: {playerSize: [[300, 250]]}, banner: {sizes: [[300, 250]]}}; + expect(inputBidReq.getFloor({mediaType: '*', size: '*'})).to.deep.equal({ + currency: 'USD', + floor: 1.0 // '*|*': 1.0, + }); + delete _floorDataForAuction[inputBidReq.auctionId].data.matchingInputs; + }); + }); + }); + describe('bidResponseHook tests', function () { + let returnedBidResponse; + let bidderRequest = { + bidderCode: 'appnexus', + auctionId: '123456', + bids: [{ + bidder: 'appnexus', + adUnitCode: 'test_div_1', + auctionId: '123456', + bidId: '1111' + }] + }; + let basicBidResponse = { + bidderCode: 'appnexus', + width: 300, + height: 250, + cpm: 0.5, + mediaType: 'banner', + requestId: '1111', + }; + beforeEach(function () { + returnedBidResponse = {}; + }); + function runBidResponse(bidResp = basicBidResponse) { + let next = (adUnitCode, bid) => { + returnedBidResponse = bid; + }; + addBidResponseHook.bind({ bidderRequest })(next, bidResp.adUnitCode, bidResp); + }; + it('continues with the auction if not floors data is present without any flooring', function () { + runBidResponse(); + expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); + }); + it('if no matching rule it should not floor and should call log warn', function () { + _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidderRequest.auctionId].data.values = { 'video': 1.0 }; + runBidResponse(); + expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); + expect(logWarnSpy.calledOnce).to.equal(true); + }); + it('if it finds a rule and floors should update the bid accordingly', function () { + _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidderRequest.auctionId].data.values = { 'banner': 1.0 }; + runBidResponse(); + expect(returnedBidResponse).to.haveOwnProperty('floorData'); + expect(returnedBidResponse.status).to.equal(CONSTANTS.BID_STATUS.BID_REJECTED); + expect(returnedBidResponse.cpm).to.equal(0); + }); + it('if it finds a rule and does not floor should update the bid accordingly', function () { + _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[bidderRequest.auctionId].data.values = { 'banner': 0.3 }; + runBidResponse(); + expect(returnedBidResponse).to.haveOwnProperty('floorData'); + expect(returnedBidResponse.floorData).to.deep.equal({ + floorValue: 0.3, + floorCurrency: 'USD', + floorRule: 'banner', + cpmAfterAdjustments: 0.5, + enforcements: { + bidAdjustment: true, + enforceJS: true, + enforcePBS: false, + floorDeals: false + }, + matchedFields: { + mediaType: 'banner' + } + }); + expect(returnedBidResponse.cpm).to.equal(0.5); + }); + it('if should work with more complex rules and update accordingly', function () { + _floorDataForAuction[bidderRequest.auctionId] = { + ...basicFloorConfig, + data: { + currency: 'USD', + schema: { fields: ['mediaType', 'size'], delimiter: '|' }, + values: { + 'banner|300x250': 0.5, + 'banner|300x600': 1.5, + 'banner|728x90': 2.5, + 'banner|*': 3.5, + 'video|640x480': 4.5, + 'video|*': 5.5 + }, + default: 10.0 + } + }; + runBidResponse(); + expect(returnedBidResponse).to.haveOwnProperty('floorData'); + expect(returnedBidResponse.floorData).to.deep.equal({ + floorValue: 0.5, + floorCurrency: 'USD', + floorRule: 'banner|300x250', + cpmAfterAdjustments: 0.5, + enforcements: { + bidAdjustment: true, + enforceJS: true, + enforcePBS: false, + floorDeals: false + }, + matchedFields: { + mediaType: 'banner', + size: '300x250' + } + }); + expect(returnedBidResponse.cpm).to.equal(0.5); + + // update bidResponse to have different combinations (should pick video|*) + runBidResponse({ + width: 300, + height: 250, + cpm: 7.5, + mediaType: 'video', + requestId: '1111', + }); + expect(returnedBidResponse).to.haveOwnProperty('floorData'); + expect(returnedBidResponse.floorData).to.deep.equal({ + floorValue: 5.5, + floorCurrency: 'USD', + floorRule: 'video|*', + cpmAfterAdjustments: 7.5, + enforcements: { + bidAdjustment: true, + enforceJS: true, + enforcePBS: false, + floorDeals: false + }, + matchedFields: { + mediaType: 'video', + size: '300x250' + } + }); + expect(returnedBidResponse.cpm).to.equal(7.5); + }); + }); +}); diff --git a/test/spec/modules/proxistoreBidAdapter_spec.js b/test/spec/modules/proxistoreBidAdapter_spec.js index 579ef5fdb3b..e18262ae797 100644 --- a/test/spec/modules/proxistoreBidAdapter_spec.js +++ b/test/spec/modules/proxistoreBidAdapter_spec.js @@ -34,7 +34,7 @@ describe('ProxistoreBidAdapter', function () { }); describe('buildRequests', function () { - const url = 'https://abs.proxistore.com/fr/v3/rtb/prebid'; + const url = 'https://abs.proxistore.com/fr/v3/rtb/prebid/multi'; const request = spec.buildRequests([bid], bidderRequest); it('should return a valid object', function () { expect(request).to.be.an('object'); @@ -55,10 +55,14 @@ describe('ProxistoreBidAdapter', function () { expect(data.gdpr.applies).to.be.true; expect(data.gdpr.consentGiven).to.be.true; }); - it('should have a property bidId if there is only one bid', function () { + it('should have a property a length of bids equal to one if there is only one bid', function () { const data = JSON.parse(request.data); - expect(data.hasOwnProperty('bidId')).to.be.true; - }) + expect(data.hasOwnProperty('bids')).to.be.true; + expect(data.bids).to.be.an('array'); + expect(data.bids.length).equal(1); + expect(data.bids[0].hasOwnProperty('id')).to.be.true; + expect(data.bids[0].sizes).to.be.an('array'); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js new file mode 100755 index 00000000000..e9d23692d23 --- /dev/null +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -0,0 +1,556 @@ +import pubmaticAnalyticsAdapter from 'modules/pubmaticAnalyticsAdapter.js'; +import CONSTANTS from 'src/constants.json'; +import { config } from 'src/config.js'; +import { + setConfig, + addBidResponseHook, +} from 'modules/currency.js'; + +// using es6 "import * as events from 'src/events'" causes the events.getEvents stub not to work... +let events = require('src/events'); +let ajax = require('src/ajax'); +let utils = require('src/utils'); + +const { + EVENTS: { + AUCTION_INIT, + AUCTION_END, + BID_REQUESTED, + BID_RESPONSE, + BIDDER_DONE, + BID_WON, + BID_TIMEOUT, + SET_TARGETING + } +} = CONSTANTS; + +const BID = { + 'bidder': 'pubmatic', + 'width': 640, + 'height': 480, + 'mediaType': 'video', + 'statusMessage': 'Bid available', + 'bidId': '2ecff0db240757', + 'adId': 'fake_ad_id', + 'source': 's2s', + 'requestId': '2ecff0db240757', + 'currency': 'USD', + 'creativeId': '3571560', + 'cpm': 1.22752, + 'originalCpm': 1.22752, + 'originalCurrency': 'USD', + 'ttl': 300, + 'netRevenue': false, + 'ad': '', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'responseTimestamp': 1519149629415, + 'requestTimestamp': 1519149628471, + 'adUnitCode': '/19968336/header-bid-tag-0', + 'timeToRespond': 944, + 'pbLg': '1.00', + 'pbMg': '1.20', + 'pbHg': '1.22', + 'pbAg': '1.20', + 'pbDg': '1.22', + 'pbCg': '', + 'size': '640x480', + 'adserverTargeting': { + 'hb_bidder': 'pubmatic', + 'hb_adid': '2ecff0db240757', + 'hb_pb': 1.20, + 'hb_size': '640x480', + 'hb_source': 'server' + }, + getStatusCode() { + return 1; + } +}; + +const BID2 = Object.assign({}, BID, { + adUnitCode: '/19968336/header-bid-tag-1', + bidId: '3bd4ebb1c900e2', + adId: 'fake_ad_id_2', + requestId: '3bd4ebb1c900e2', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 1.52, + originalCpm: 1.52, + dealId: 'the-deal-id', + dealChannel: 'PMP', + mi: 'matched-impression', + seatBidId: 'aaaa-bbbb-cccc-dddd', + adserverTargeting: { + 'hb_bidder': 'pubmatic', + 'hb_adid': '3bd4ebb1c900e2', + 'hb_pb': '1.500', + 'hb_size': '728x90', + 'hb_source': 'server' + } +}); + +const MOCK = { + SET_TARGETING: { + [BID.adUnitCode]: BID.adserverTargeting, + [BID2.adUnitCode]: BID2.adserverTargeting + }, + AUCTION_INIT: { + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'pubmatic', + 'params': { + 'publisherId': '1001' + } + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } + ], + 'adUnitCodes': ['/19968336/header-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'pubmatic', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'pubmatic', + 'params': { + 'publisherId': '1001', + 'kgpv': 'this-is-a-kgpv' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'referer': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 + }, + BID_REQUESTED: { + 'bidder': 'pubmatic', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'pubmatic', + 'params': { + 'publisherId': '1001', + 'video': { + 'minduration': 30, + 'skippable': true + } + }, + 'mediaType': 'video', + 'adUnitCode': '/19968336/header-bid-tag-0', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + }, + { + 'bidder': 'pubmatic', + 'params': { + 'publisherId': '1001', + 'kgpv': 'this-is-a-kgpv' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[1000, 300], [970, 250], [728, 90]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'c116413c-9e3f-401a-bee1-d56aec29a1d4', + 'sizes': [[1000, 300], [970, 250], [728, 90]], + 'bidId': '3bd4ebb1c900e2', + 'seatBidId': 'aaaa-bbbb-cccc-dddd', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + } + ], + 'auctionStart': 1519149536560, + 'timeout': 5000, + 'start': 1519149562216, + 'refererInfo': { + 'referer': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + }, + 'gdprConsent': { + 'consentString': 'here-goes-gdpr-consent-string', + 'gdprApplies': true + } + }, + BID_RESPONSE: [ + BID, + BID2 + ], + AUCTION_END: { + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + }, + BID_WON: [ + Object.assign({}, BID, { + 'status': 'rendered' + }), + Object.assign({}, BID2, { + 'status': 'rendered' + }) + ], + BIDDER_DONE: { + 'bidderCode': 'pubmatic', + 'bids': [ + BID, + Object.assign({}, BID2, { + 'serverResponseTimeMs': 42, + }) + ] + }, + BID_TIMEOUT: [ + { + 'bidId': '3bd4ebb1c900e2', + 'bidder': 'pubmatic', + 'adUnitCode': '/19968336/header-bid-tag-1', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + } + ] +}; + +function getLoggerJsonFromRequest(requestBody) { + return JSON.parse(decodeURIComponent(requestBody.split('json=')[1])); +} + +describe('pubmatic analytics adapter', function () { + let sandbox; + let xhr; + let requests; + let oldScreen; + let clock; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + + xhr = sandbox.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = request => requests.push(request); + + sandbox.stub(events, 'getEvents').returns([]); + + clock = sandbox.useFakeTimers(1519767013781); + + config.setConfig({ + s2sConfig: { + timeout: 1000, + accountId: 10000, + bidders: ['pubmatic'] + } + }) + }); + + afterEach(function () { + sandbox.restore(); + config.resetConfig(); + }); + + it('should require publisherId', function () { + sandbox.stub(utils, 'logError'); + pubmaticAnalyticsAdapter.enableAnalytics({ + options: {} + }); + expect(utils.logError.called).to.equal(true); + }); + + describe('when handling events', function() { + beforeEach(function () { + pubmaticAnalyticsAdapter.enableAnalytics({ + options: { + publisherId: 9999, + profileId: 1111, + profileVersionId: 20 + } + }); + }); + + afterEach(function () { + pubmaticAnalyticsAdapter.disableAnalytics(); + }); + + it('Logger: best case + win tracker', function() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999&gdEn=1'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.pubid).to.equal('9999'); + expect(data.pid).to.equal('1111'); + expect(data.pdvid).to.equal('20'); + expect(data.iid).to.equal('25c6d7f5-699a-4bfc-87c9-996f915341fa'); + expect(data.to).to.equal('3000'); + expect(data.purl).to.equal('http://www.test.com/page.html'); + expect(data.orig).to.equal('www.test.com'); + expect(data.tst).to.equal(1519767016); + expect(data.cns).to.equal('here-goes-gdpr-consent-string'); + expect(data.gdpr).to.equal(1); + expect(data.s).to.be.an('array'); + expect(data.s.length).to.equal(2); + // slot 1 + expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].sz).to.deep.equal(['640x480']); + expect(data.s[0].ps).to.be.an('array'); + expect(data.s[0].ps.length).to.equal(1); + expect(data.s[0].ps[0].pn).to.equal('pubmatic'); + expect(data.s[0].ps[0].bidid).to.equal('2ecff0db240757'); + expect(data.s[0].ps[0].db).to.equal(0); + expect(data.s[0].ps[0].kgpv).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].ps[0].kgpsv).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].ps[0].psz).to.equal('640x480'); + expect(data.s[0].ps[0].eg).to.equal(1.23); + expect(data.s[0].ps[0].en).to.equal(1.23); + expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].dc).to.equal(''); + expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l2).to.equal(0); + expect(data.s[0].ps[0].ss).to.equal(1); + expect(data.s[0].ps[0].t).to.equal(0); + expect(data.s[0].ps[0].wb).to.equal(1); + expect(data.s[0].ps[0].af).to.equal('video'); + expect(data.s[0].ps[0].ocpm).to.equal(1.23); + expect(data.s[0].ps[0].ocry).to.equal('USD'); + // slot 2 + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(1); + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + + // tracker slot1 + let firstTracker = requests[0].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + data = {}; + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.pubid).to.equal('9999'); + expect(decodeURIComponent(data.purl)).to.equal('http://www.test.com/page.html'); + expect(data.tst).to.equal('1519767014'); + expect(data.iid).to.equal('25c6d7f5-699a-4bfc-87c9-996f915341fa'); + expect(data.bidid).to.equal('2ecff0db240757'); + expect(data.pid).to.equal('1111'); + expect(data.pdvid).to.equal('20'); + expect(decodeURIComponent(data.slot)).to.equal('/19968336/header-bid-tag-0'); + expect(decodeURIComponent(data.kgpv)).to.equal('/19968336/header-bid-tag-0'); + expect(data.pn).to.equal('pubmatic'); + expect(data.eg).to.equal('1.23'); + expect(data.en).to.equal('1.23'); + }); + + it('Logger: when bid is not submitted, default bid status 1 check: pubmatic set as s2s', function() { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + 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); + events.emit(BID_WON, MOCK.BID_WON[0]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(2); // 1 logger and 1 win-tracker + let request = requests[1]; // logger is executed late, trackers execute first + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(1); + expect(data.s[1].ps[0].kgpv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('0x0'); + expect(data.s[1].ps[0].eg).to.equal(0); + expect(data.s[1].ps[0].en).to.equal(0); + expect(data.s[1].ps[0].di).to.equal(''); + expect(data.s[1].ps[0].dc).to.equal(''); + expect(data.s[1].ps[0].mi).to.equal(undefined); + expect(data.s[1].ps[0].l1).to.equal(0); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); + expect(data.s[1].ps[0].af).to.equal(undefined); + expect(data.s[1].ps[0].ocpm).to.equal(0); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + }); + + it('Logger: post-timeout check without bid response', function() { + // db = 1 and t = 1 means bidder did NOT respond with a bid but we got a timeout notification + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); + events.emit(AUCTION_END, MOCK.AUCTION_END); + clock.tick(2000 + 1000); + + expect(requests.length).to.equal(1); // 1 logger and 0 win-tracker + let request = requests[0]; + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(1); + expect(data.s[1].ps[0].kgpv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('0x0'); + expect(data.s[1].ps[0].eg).to.equal(0); + expect(data.s[1].ps[0].en).to.equal(0); + expect(data.s[1].ps[0].di).to.equal(''); + expect(data.s[1].ps[0].dc).to.equal(''); + expect(data.s[1].ps[0].mi).to.equal(undefined); + expect(data.s[1].ps[0].l1).to.equal(0); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(1); + expect(data.s[1].ps[0].wb).to.equal(0); + expect(data.s[1].ps[0].af).to.equal(undefined); + expect(data.s[1].ps[0].ocpm).to.equal(0); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + }); + + it('Logger: post-timeout check with bid response', function() { + // db = 1 and t = 1 means bidder did NOT respond with a bid but we got a timeout notification + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BID_TIMEOUT, MOCK.BID_TIMEOUT); + events.emit(AUCTION_END, MOCK.AUCTION_END); + clock.tick(2000 + 1000); + + expect(requests.length).to.equal(1); // 1 logger and 0 win-tracker + let request = requests[0]; + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(1.52); + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(1); + expect(data.s[1].ps[0].wb).to.equal(1); + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + }); + + it('Logger: currency conversion check', function() { + setConfig({ + adServerCurrency: 'JPY', + rates: { + USD: { + JPY: 100 + } + } + }); + const bidCopy = utils.deepClone(BID2); + bidCopy.currency = 'JPY'; + bidCopy.cpm = 100; + bidCopy.originalCpm = 100; + bidCopy.originalCurrency = 'JPY'; + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, bidCopy); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999&gdEn=1'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(undefined); // bidPriceUSD is not getting set as currency module is not added + expect(data.s[1].ps[0].en).to.equal(undefined); // bidPriceUSD is not getting set as currency module is not added + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(0); // bidPriceUSD is not getting set as currency module is not added, so unable to set wb to 1 + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(100); + expect(data.s[1].ps[0].ocry).to.equal('JPY'); + }); + }) +}); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 5c2f1f76b2e..817661ef51f 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -533,6 +533,10 @@ describe('PubMatic adapter', function () { 'body': { 'id': '93D3BAD6-E2E2-49FB-9D89-920B1761C865', 'seatbid': [{ + 'seat': 'seat-id', + 'ext': { + 'buyid': 'BUYER-ID-987' + }, 'bid': [{ 'id': '74858439-49D7-4169-BA5D-44A046315B2F', 'impid': '22bddb28db77d', @@ -548,6 +552,9 @@ describe('PubMatic adapter', function () { } }] }, { + 'ext': { + 'buyid': 'BUYER-ID-789' + }, 'bid': [{ 'id': '74858439-49D7-4169-BA5D-44A046315BEF', 'impid': '22bddb28db77e', @@ -725,7 +732,9 @@ describe('PubMatic adapter', function () { expect(data.test).to.equal(undefined); }); - it('test flag set to 1 when pubmaticTest=true is present in page url', function() { + // disabled this test case as it refreshes the whole suite when in karma watch mode + // todo: needs a fix + xit('test flag set to 1 when pubmaticTest=true is present in page url', function() { window.location.href += '#pubmaticTest=true'; // now all the test cases below will have window.location.href with #pubmaticTest=true let request = spec.buildRequests(bidRequests); @@ -766,6 +775,70 @@ describe('PubMatic adapter', function () { expect(data.source.ext.schain).to.deep.equal(bidRequests[0].schain); }); + it('Merge the device info from config', function() { + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + device: { + 'newkey': 'new-device-data' + } + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.device.js).to.equal(1); + expect(data.device.dnt).to.equal((navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0); + expect(data.device.h).to.equal(screen.height); + expect(data.device.w).to.equal(screen.width); + expect(data.device.language).to.equal(navigator.language); + expect(data.device.newkey).to.equal('new-device-data');// additional data from config + sandbox.restore(); + }); + + it('Merge the device info from config; data from config overrides the info we have gathered', function() { + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + device: { + newkey: 'new-device-data', + language: 'MARATHI' + } + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.device.js).to.equal(1); + expect(data.device.dnt).to.equal((navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0); + expect(data.device.h).to.equal(screen.height); + expect(data.device.w).to.equal(screen.width); + expect(data.device.language).to.equal('MARATHI');// // data overriding from config + expect(data.device.newkey).to.equal('new-device-data');// additional data from config + sandbox.restore(); + }); + + it('Set app from config, copy publisher and ext from site, unset site', function() { + let sandbox = sinon.sandbox.create(); + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + app: { + bundle: 'org.prebid.mobile.demoapp', + domain: 'prebid.org' + } + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.app.bundle).to.equal('org.prebid.mobile.demoapp'); + expect(data.app.domain).to.equal('prebid.org'); + expect(data.app.publisher.id).to.equal(bidRequests[0].params.publisherId); + expect(data.app.ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); + expect(data.site).to.not.exist; + sandbox.restore(); + }); + it('Request params check: without adSlot', function () { delete bidRequests[0].params.adSlot; let request = spec.buildRequests(bidRequests); @@ -1856,6 +1929,42 @@ describe('PubMatic adapter', function () { expect(data.user.eids).to.equal(undefined); }); }); + + describe('NetId', function() { + it('send the NetId if it is present', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.netId = 'netid-user-id'; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.deep.equal([{ + 'source': 'netid.de', + 'uids': [{ + 'id': 'netid-user-id', + 'atype': 1 + }] + }]); + }); + + it('do not pass if not string', function() { + bidRequests[0].userId = {}; + bidRequests[0].userId.netId = 1; + let request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.netId = []; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.netId = null; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + bidRequests[0].userId.netId = {}; + request = spec.buildRequests(bidRequests, {}); + data = JSON.parse(request.data); + expect(data.user.eids).to.equal(undefined); + }); + }); }); it('Request params check for video ad', function () { @@ -2519,10 +2628,13 @@ describe('PubMatic adapter', function () { expect(response[0].netRevenue).to.equal(false); expect(response[0].ttl).to.equal(300); expect(response[0].meta.networkId).to.equal(123); + expect(response[0].adserverTargeting.hb_buyid_pubmatic).to.equal('BUYER-ID-987'); expect(response[0].meta.buyerId).to.equal(976); expect(response[0].meta.clickUrl).to.equal('blackrock.com'); expect(response[0].referrer).to.include(data.site.ref); expect(response[0].ad).to.equal(bidResponses.body.seatbid[0].bid[0].adm); + expect(response[0].pm_seat).to.equal(bidResponses.body.seatbid[0].seat); + expect(response[0].pm_dspid).to.equal(bidResponses.body.seatbid[0].bid[0].ext.dspid); expect(response[1].requestId).to.equal(bidResponses.body.seatbid[1].bid[0].impid); expect(response[1].cpm).to.equal((bidResponses.body.seatbid[1].bid[0].price).toFixed(2)); @@ -2538,10 +2650,13 @@ describe('PubMatic adapter', function () { expect(response[1].netRevenue).to.equal(false); expect(response[1].ttl).to.equal(300); expect(response[1].meta.networkId).to.equal(422); + expect(response[1].adserverTargeting.hb_buyid_pubmatic).to.equal('BUYER-ID-789'); expect(response[1].meta.buyerId).to.equal(832); expect(response[1].meta.clickUrl).to.equal('hivehome.com'); expect(response[1].referrer).to.include(data.site.ref); expect(response[1].ad).to.equal(bidResponses.body.seatbid[1].bid[0].adm); + expect(response[1].pm_seat).to.equal(bidResponses.body.seatbid[1].seat || null); + expect(response[1].pm_dspid).to.equal(bidResponses.body.seatbid[1].bid[0].ext.dspid); }); it('should check for dealChannel value selection', function () { @@ -2604,37 +2719,53 @@ describe('PubMatic adapter', function () { }); describe('getUserSyncs', function() { - const syncurl = 'https://ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p=5670'; + const syncurl_iframe = 'https://ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p=5670'; + const syncurl_image = 'https://image8.pubmatic.com/AdServer/ImgSync?p=5670'; let sandbox; beforeEach(function () { sandbox = sinon.sandbox.create(); }); afterEach(function() { sandbox.restore(); - }) + }); - it('execute only if iframeEnabled', function() { + it('execute as per config', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ - type: 'iframe', url: syncurl + type: 'iframe', url: syncurl_iframe + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ + type: 'image', url: syncurl_image }]); - expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.equal(undefined); }); it('CCPA/USP', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&us_privacy=1NYN` + type: 'iframe', url: `${syncurl_iframe}&us_privacy=1NYN` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, '1NYN')).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&us_privacy=1NYN` }]); }); it('GDPR', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&gdpr=1&gdpr_consent=foo` + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo` }]); expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&gdpr=0&gdpr_consent=foo` + type: 'iframe', url: `${syncurl_iframe}&gdpr=0&gdpr_consent=foo` }]); expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&gdpr=1&gdpr_consent=` + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=` + }]); + + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&gdpr=1&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&gdpr=0&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&gdpr=1&gdpr_consent=` }]); }); @@ -2646,7 +2777,10 @@ describe('PubMatic adapter', function () { return config[key]; }); expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&coppa=1` + type: 'iframe', url: `${syncurl_iframe}&coppa=1` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&coppa=1` }]); }); @@ -2658,7 +2792,10 @@ describe('PubMatic adapter', function () { return config[key]; }); expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ - type: 'iframe', url: `${syncurl}` + type: 'iframe', url: `${syncurl_iframe}` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ + type: 'image', url: `${syncurl_image}` }]); }); @@ -2670,7 +2807,10 @@ describe('PubMatic adapter', function () { return config[key]; }); expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ - type: 'iframe', url: `${syncurl}&gdpr=1&gdpr_consent=foo&us_privacy=1NYN&coppa=1` + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo&us_privacy=1NYN&coppa=1` + }]); + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&gdpr=1&gdpr_consent=foo&us_privacy=1NYN&coppa=1` }]); }); }); diff --git a/test/spec/modules/pubstackAnalyticsAdapter_spec.js b/test/spec/modules/pubstackAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..e3db334c888 --- /dev/null +++ b/test/spec/modules/pubstackAnalyticsAdapter_spec.js @@ -0,0 +1,38 @@ +import * as utils from 'src/utils.js'; +import pubstackAnalytics from '../../../modules/pubstackAnalyticsAdapter.js'; +import adapterManager from 'src/adapterManager'; +import events from 'src/events'; +import constants from 'src/constants.json' + +describe('Pubstack Analytics Adapter', () => { + const scope = utils.getWindowSelf(); + let queue = []; + + beforeEach(() => { + scope.PubstackAnalytics = (...args) => queue.push(args); + adapterManager.enableAnalytics({ + provider: 'pubstack' + }); + queue = [] + }); + + afterEach(() => { + pubstackAnalytics.disableAnalytics(); + }); + + it('should forward all events to the queue', () => { + // Given + const args = 'any-args' + + // When + events.emit(constants.EVENTS.AUCTION_END, args) + events.emit(constants.EVENTS.BID_REQUESTED, args) + events.emit(constants.EVENTS.BID_ADJUSTMENT, args) + events.emit(constants.EVENTS.BID_RESPONSE, args) + events.emit(constants.EVENTS.BID_WON, args) + events.emit(constants.EVENTS.NO_BID, args) + + // Then + expect(queue.length).to.eql(6); + }); +}); diff --git a/test/spec/modules/pulsepointBidAdapter_spec.js b/test/spec/modules/pulsepointBidAdapter_spec.js index ea81aa5d962..4b21856b68e 100644 --- a/test/spec/modules/pulsepointBidAdapter_spec.js +++ b/test/spec/modules/pulsepointBidAdapter_spec.js @@ -6,6 +6,11 @@ import {deepClone} from 'src/utils.js'; describe('PulsePoint Adapter Tests', function () { const slotConfigs = [{ placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[728, 90], [160, 600]] + } + }, bidId: 'bid12345', params: { cp: 'p10000', @@ -655,4 +660,45 @@ describe('PulsePoint Adapter Tests', function () { userVerify(ortbRequest.user.ext.eids[4], 'parrable.com', 'parrable_id234'); userVerify(ortbRequest.user.ext.eids[5], 'liveintent.com', 'liveintent_id123'); }); + it('Verify multiple adsizes', function () { + const bidRequests = deepClone(slotConfigs); + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request).to.be.not.null; + expect(request.data).to.be.not.null; + const ortbRequest = request.data; + expect(ortbRequest.imp).to.have.lengthOf(2); + // first impression has multi sizes + expect(ortbRequest.imp[0].banner).to.not.be.null; + expect(ortbRequest.imp[0].banner.w).to.equal(300); + expect(ortbRequest.imp[0].banner.h).to.equal(250); + expect(ortbRequest.imp[0].banner.format).to.not.be.null; + expect(ortbRequest.imp[0].banner.format).to.have.lengthOf(2); + expect(ortbRequest.imp[0].banner.format[0].w).to.equal(728); + expect(ortbRequest.imp[0].banner.format[0].h).to.equal(90); + expect(ortbRequest.imp[0].banner.format[1].w).to.equal(160); + expect(ortbRequest.imp[0].banner.format[1].h).to.equal(600); + // slot 2 + expect(ortbRequest.imp[1].banner).to.not.be.null; + expect(ortbRequest.imp[1].banner.w).to.equal(728); + expect(ortbRequest.imp[1].banner.h).to.equal(90); + expect(ortbRequest.imp[1].banner.format).to.be.null; + // adsize on response + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + crid: 'Creative#123', + w: 728, + h: 90 + }] + }] + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.width).to.equal(728); + expect(bid.height).to.equal(90); + }); }); diff --git a/test/spec/modules/quantcastBidAdapter_spec.js b/test/spec/modules/quantcastBidAdapter_spec.js index 19d53674793..cd168ec61e6 100644 --- a/test/spec/modules/quantcastBidAdapter_spec.js +++ b/test/spec/modules/quantcastBidAdapter_spec.js @@ -10,7 +10,8 @@ import { spec as qcSpec } from '../../../modules/quantcastBidAdapter.js'; import { newBidder } from '../../../src/adapters/bidderFactory.js'; -import { parse } from 'src/url.js'; +import { parseUrl } from 'src/utils.js'; +import { config } from 'src/config.js'; describe('Quantcast adapter', function () { const quantcastAdapter = newBidder(qcSpec); @@ -89,13 +90,13 @@ describe('Quantcast adapter', function () { describe('`buildRequests`', function () { it('sends secure bid requests', function () { const requests = qcSpec.buildRequests([bidRequest]); - const url = parse(requests[0]['url']); + const url = parseUrl(requests[0]['url']); expect(url.protocol).to.equal('https'); }); it('sends bid requests to Quantcast Canary Endpoint if `publisherId` is `test-publisher`', function () { const requests = qcSpec.buildRequests([bidRequest]); - const url = parse(requests[0]['url']); + const url = parseUrl(requests[0]['url']); expect(url.hostname).to.equal(QUANTCAST_TEST_DOMAIN); }); @@ -138,6 +139,7 @@ describe('Quantcast adapter', function () { bidId: '2f7b179d443f14', gdprSignal: 0, uspSignal: 0, + coppa: 0, prebidJsVersion: '$prebid.version$' }; @@ -205,6 +207,7 @@ describe('Quantcast adapter', function () { bidId: '2f7b179d443f14', gdprSignal: 0, uspSignal: 0, + coppa: 0, prebidJsVersion: '$prebid.version$' }; @@ -240,6 +243,7 @@ describe('Quantcast adapter', function () { bidId: '2f7b179d443f14', gdprSignal: 0, uspSignal: 0, + coppa: 0, prebidJsVersion: '$prebid.version$' }; @@ -271,6 +275,7 @@ describe('Quantcast adapter', function () { bidId: '2f7b179d443f14', gdprSignal: 0, uspSignal: 0, + coppa: 0, prebidJsVersion: '$prebid.version$' }; @@ -334,6 +339,7 @@ describe('Quantcast adapter', function () { bidId: '2f7b179d443f14', gdprSignal: 0, uspSignal: 0, + coppa: 0, prebidJsVersion: '$prebid.version$' }; @@ -342,13 +348,254 @@ describe('Quantcast adapter', function () { }); it('propagates GDPR consent string and signal', function () { - const bidderRequest = { gdprConsent: { gdprApplies: true, consentString: 'consentString' } } + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString' + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + const parsed = JSON.parse(requests[0].data); + + expect(parsed.gdprSignal).to.equal(1); + expect(parsed.gdprConsent).to.equal('consentString'); + }); + + it('allows TCF v1 request with consent for purpose 1', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendorConsents: { + '11': true + }, + purposeConsents: { + '1': true + } + }, + apiVersion: 1 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + const parsed = JSON.parse(requests[0].data); + + expect(parsed.gdprSignal).to.equal(1); + expect(parsed.gdprConsent).to.equal('consentString'); + }); + + it('blocks TCF v1 request without vendor consent', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendorConsents: { + '11': false + }, + purposeConsents: { + '1': true + } + }, + apiVersion: 1 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.equal(undefined); + }); + + it('blocks TCF v1 request without consent for purpose 1', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendorConsents: { + '11': true + }, + purposeConsents: { + '1': false + } + }, + apiVersion: 1 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.equal(undefined); + }); + + it('allows TCF v2 request from Germany for purpose 1', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + publisherCC: 'DE', + purposeOneTreatment: true + }, + apiVersion: 2 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + const parsed = JSON.parse(requests[0].data); + + expect(parsed.gdprSignal).to.equal(1); + expect(parsed.gdprConsent).to.equal('consentString'); + }); + + it('allows TCF v2 request when Quantcast has consent for purpose 1', function() { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendor: { + consents: { + '11': true + } + }, + purpose: { + consents: { + '1': true + } + } + }, + apiVersion: 2 + } + }; + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); const parsed = JSON.parse(requests[0].data); + expect(parsed.gdprSignal).to.equal(1); expect(parsed.gdprConsent).to.equal('consentString'); }); + it('blocks TCF v2 request when no consent for Quantcast', function() { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendor: { + consents: { + '11': false + } + }, + purpose: { + consents: { + '1': true + } + } + }, + apiVersion: 2 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.equal(undefined); + }); + + it('blocks TCF v2 request when no consent for purpose 1', function() { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendor: { + consents: { + '11': true + } + }, + purpose: { + consents: { + '1': false + } + } + }, + apiVersion: 2 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.equal(undefined); + }); + + it('blocks TCF v2 request when Quantcast not allowed by publisher', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendor: { + consents: { + '11': true + } + }, + purpose: { + consents: { + '1': true + } + }, + publisher: { + restrictions: { + '1': { + '11': 0 + } + } + } + }, + apiVersion: 2 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.equal(undefined); + }); + + it('blocks TCF v2 request when legitimate interest required', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + vendor: { + consents: { + '11': true + } + }, + purpose: { + consents: { + '1': true + } + }, + publisher: { + restrictions: { + '1': { + '11': 2 + } + } + } + }, + apiVersion: 2 + } + }; + + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + + expect(requests).to.equal(undefined); + }); + it('propagates US Privacy/CCPA consent information', function () { const bidderRequest = { uspConsent: 'consentString' } const requests = qcSpec.buildRequests([bidRequest], bidderRequest); @@ -357,6 +604,50 @@ describe('Quantcast adapter', function () { expect(parsed.uspConsent).to.equal('consentString'); }); + describe('propagates coppa', function() { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('propagates coppa as 1 if coppa param is set to true in the bid request', function () { + bidRequest.params = { + publisherId: 'test_publisher_id', + coppa: true + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + const config = { + 'coppa': true + }; + return config[key]; + }); + const requests = qcSpec.buildRequests([bidRequest], bidderRequest); + expect(JSON.parse(requests[0].data).coppa).to.equal(1); + }); + + it('propagates coppa as 0 if there is no coppa param or coppa is set to false in the bid request', function () { + const requestsWithoutCoppa = qcSpec.buildRequests([bidRequest], bidderRequest); + expect(JSON.parse(requestsWithoutCoppa[0].data).coppa).to.equal(0); + + bidRequest.params = { + publisherId: 'test_publisher_id', + coppa: false + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + const config = { + 'coppa': false + }; + return config[key]; + }); + const requestsWithFalseCoppa = qcSpec.buildRequests([bidRequest], bidderRequest); + expect(JSON.parse(requestsWithFalseCoppa[0].data).coppa).to.equal(0); + }); + }); + describe('`interpretResponse`', function () { // The sample response is from https://wiki.corp.qc/display/adinf/QCX const body = { @@ -492,5 +783,67 @@ describe('Quantcast adapter', function () { expect(interpretedResponse.length).to.equal(0); }); + + it('should return pixel url when available userSync available', function () { + const syncOptions = { + pixelEnabled: true + }; + const serverResponses = [ + { + body: { + userSync: { + url: 'http://quantcast.com/pixelUrl' + } + } + }, + { + body: { + + } + } + ]; + + const actualSyncs = qcSpec.getUserSyncs(syncOptions, serverResponses); + const expectedSync = { + type: 'image', + url: 'http://quantcast.com/pixelUrl' + }; + expect(actualSyncs.length).to.equal(1); + expect(actualSyncs[0]).to.deep.equal(expectedSync); + qcSpec.resetUserSync(); + }); + + it('should not return user syncs if done already', function () { + const syncOptions = { + pixelEnabled: true + }; + const serverResponses = [ + { + body: { + userSync: { + url: 'http://quantcast.com/pixelUrl' + } + } + }, + { + body: { + + } + } + ]; + + let actualSyncs = qcSpec.getUserSyncs(syncOptions, serverResponses); + const expectedSync = { + type: 'image', + url: 'http://quantcast.com/pixelUrl' + }; + expect(actualSyncs.length).to.equal(1); + expect(actualSyncs[0]).to.deep.equal(expectedSync); + + actualSyncs = qcSpec.getUserSyncs(syncOptions, serverResponses); + expect(actualSyncs.length).to.equal(0); + + qcSpec.resetUserSync(); + }); }); }); diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js index a0cff8a3464..eb9077fac39 100644 --- a/test/spec/modules/readpeakBidAdapter_spec.js +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { spec, ENDPOINT } from 'modules/readpeakBidAdapter.js'; import { config } from 'src/config.js'; -import { parse as parseUrl } from 'src/url.js'; +import { parseUrl } from 'src/utils.js'; describe('ReadPeakAdapter', function() { let bidRequest; diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js new file mode 100644 index 00000000000..f0d3a2fb6d8 --- /dev/null +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -0,0 +1,323 @@ +import { expect } from 'chai'; +import { spec } from 'modules/relaidoBidAdapter.js'; +import * as utils from 'src/utils.js'; + +const UUID_KEY = 'relaido_uuid'; +const DEFAULT_USER_AGENT = window.navigator.userAgent; +const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1'; + +const setUADefault = () => { window.navigator.__defineGetter__('userAgent', function () { return DEFAULT_USER_AGENT }) }; +const setUAMobile = () => { window.navigator.__defineGetter__('userAgent', function () { return MOBILE_USER_AGENT }) }; + +describe('RelaidoAdapter', function () { + const relaido_uuid = 'hogehoge'; + let bidRequest; + let bidderRequest; + let serverResponse; + let serverRequest; + + beforeEach(function () { + bidRequest = { + bidder: 'relaido', + params: { + placementId: '100000', + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [ + [640, 360] + ] + } + }, + adUnitCode: 'test', + bidId: '2ed93003f7bb99', + bidderRequestId: '1c50443387a1f2', + auctionId: '413ed000-8c7a-4ba1-a1fa-9732e006f8c3', + transactionId: '5c2d064c-7b76-42e8-a383-983603afdc45', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0 + }; + bidderRequest = { + timeout: 1000, + refererInfo: { + referer: 'https://publisher.com/home' + } + }; + serverResponse = { + body: { + status: 'ok', + price: 500, + model: 'vcpm', + currency: 'JPY', + creativeId: 1000, + uuid: relaido_uuid, + vast: '', + playerUrl: 'https://relaido/player.js', + syncUrl: 'https://relaido/sync.html' + } + }; + serverRequest = { + method: 'GET', + bidId: bidRequest.bidId, + width: bidRequest.mediaTypes.video.playerSize[0][0], + height: bidRequest.mediaTypes.video.playerSize[0][1], + mediaType: 'video', + }; + localStorage.setItem(UUID_KEY, relaido_uuid); + }); + + describe('spec.isBidRequestValid', function () { + it('should return true when the required params are passed by video', function () { + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true when the required params are passed by banner', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: { + sizes: [ + [300, 250] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + setUADefault(); + }); + + it('should return false when the uuid are missing', function () { + localStorage.removeItem(UUID_KEY); + const result = !!(utils.isSafariBrowser()); + expect(spec.isBidRequestValid(bidRequest)).to.equal(result); + }); + + it('should return false when the placementId params are missing', function () { + bidRequest.params.placementId = undefined; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when the mediaType video params are missing', function () { + bidRequest.mediaTypes = { + video: {} + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when the mediaType banner params are missing', function () { + setUAMobile(); + bidRequest.mediaTypes = { + banner: {} + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + setUADefault(); + }); + + it('should return false when the non-mobile', function () { + bidRequest.mediaTypes = { + banner: { + sizes: [ + [300, 250] + ] + } + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false when the mediaTypes params are missing', function () { + bidRequest.mediaTypes = {}; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + + describe('spec.buildRequests', function () { + it('should build bid requests by video', function () { + const bidRequests = spec.buildRequests([bidRequest], bidderRequest); + expect(bidRequests).to.have.lengthOf(1); + const request = bidRequests[0]; + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://api.relaido.jp/vast/v1/out/bid/100000'); + expect(request.bidId).to.equal(bidRequest.bidId); + expect(request.width).to.equal(bidRequest.mediaTypes.video.playerSize[0][0]); + expect(request.height).to.equal(bidRequest.mediaTypes.video.playerSize[0][1]); + expect(request.mediaType).to.equal('video'); + expect(request.data.ref).to.equal(bidderRequest.refererInfo.referer); + expect(request.data.timeout_ms).to.equal(bidderRequest.timeout); + expect(request.data.ad_unit_code).to.equal(bidRequest.adUnitCode); + expect(request.data.auction_id).to.equal(bidRequest.auctionId); + expect(request.data.bidder).to.equal(bidRequest.bidder); + expect(request.data.bidder_request_id).to.equal(bidRequest.bidderRequestId); + expect(request.data.bid_requests_count).to.equal(bidRequest.bidRequestsCount); + expect(request.data.bid_id).to.equal(bidRequest.bidId); + expect(request.data.transaction_id).to.equal(bidRequest.transactionId); + expect(request.data.media_type).to.equal('video'); + expect(request.data.uuid).to.equal(relaido_uuid); + expect(request.data.width).to.equal(bidRequest.mediaTypes.video.playerSize[0][0]); + expect(request.data.height).to.equal(bidRequest.mediaTypes.video.playerSize[0][1]); + }); + + it('should build bid requests by banner', function () { + bidRequest.mediaTypes = { + banner: { + sizes: [ + [640, 360] + ] + } + }; + const bidRequests = spec.buildRequests([bidRequest], bidderRequest); + expect(bidRequests).to.have.lengthOf(1); + const request = bidRequests[0]; + expect(request.mediaType).to.equal('banner'); + }); + }); + + describe('spec.interpretResponse', function () { + it('should build bid response by video', function () { + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(1); + const response = bidResponses[0]; + expect(response.requestId).to.equal(serverRequest.bidId); + expect(response.width).to.equal(serverRequest.width); + expect(response.height).to.equal(serverRequest.height); + expect(response.cpm).to.equal(serverResponse.body.price); + expect(response.currency).to.equal(serverResponse.body.currency); + expect(response.creativeId).to.equal(serverResponse.body.creativeId); + expect(response.vastXml).to.equal(serverResponse.body.vast); + expect(response.ad).to.be.undefined; + }); + + it('should build bid response by banner', function () { + serverRequest.mediaType = 'banner'; + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(1); + const response = bidResponses[0]; + expect(response.requestId).to.equal(serverRequest.bidId); + expect(response.width).to.equal(serverRequest.width); + expect(response.height).to.equal(serverRequest.height); + expect(response.cpm).to.equal(serverResponse.body.price); + expect(response.currency).to.equal(serverResponse.body.currency); + expect(response.creativeId).to.equal(serverResponse.body.creativeId); + expect(response.vastXml).to.be.undefined; + expect(response.ad).to.include(`
`); + expect(response.ad).to.include(``); + expect(response.ad).to.include(`window.RelaidoPlayer.renderAd`); + }); + + it('should not build bid response', function () { + serverResponse = {}; + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(0); + }); + + it('should not build bid response', function () { + serverResponse = { + body: { + status: 'no_ad', + } + }; + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(0); + }); + }); + + describe('spec.getUserSyncs', function () { + it('should choose iframe sync urls', function () { + let userSyncs = spec.getUserSyncs({iframeEnabled: true}, [serverResponse]); + expect(userSyncs).to.deep.equal([{ + type: 'iframe', + url: serverResponse.body.syncUrl + }]); + }); + + it('should choose iframe sync urls if serverResponse are empty', function () { + let userSyncs = spec.getUserSyncs({iframeEnabled: true}, []); + expect(userSyncs).to.deep.equal([{ + type: 'iframe', + url: 'https://api.relaido.jp/tr/v1/prebid/sync.html' + }]); + }); + + it('should choose iframe sync urls if syncUrl are undefined', function () { + serverResponse.body.syncUrl = undefined; + let userSyncs = spec.getUserSyncs({iframeEnabled: true}, [serverResponse]); + expect(userSyncs).to.deep.equal([{ + type: 'iframe', + url: 'https://api.relaido.jp/tr/v1/prebid/sync.html' + }]); + }); + + it('should return empty if iframeEnabled are false', function () { + let userSyncs = spec.getUserSyncs({iframeEnabled: false}, [serverResponse]); + expect(userSyncs).to.have.lengthOf(0); + }); + }); + + describe('spec.onBidWon', function () { + let stub; + beforeEach(() => { + stub = sinon.stub(utils, 'triggerPixel'); + }); + afterEach(() => { + stub.restore(); + }); + + it('Should create nurl pixel if bid nurl', function () { + let bid = { + bidder: bidRequest.bidder, + creativeId: serverResponse.body.creativeId, + cpm: serverResponse.body.price, + params: [bidRequest.params], + auctionId: bidRequest.auctionId, + requestId: bidRequest.bidId, + adId: '3b286a4db7031f', + adUnitCode: bidRequest.adUnitCode, + ref: window.location.href, + } + spec.onBidWon(bid); + const parser = utils.parseUrl(stub.getCall(0).args[0]); + const query = parser.search; + expect(parser.hostname).to.equal('api.relaido.jp'); + expect(parser.pathname).to.equal('/tr/v1/prebid/win.gif'); + expect(query.placement_id).to.equal('100000'); + expect(query.creative_id).to.equal('1000'); + expect(query.price).to.equal('500'); + expect(query.auction_id).to.equal('413ed000-8c7a-4ba1-a1fa-9732e006f8c3'); + expect(query.bid_id).to.equal('2ed93003f7bb99'); + expect(query.ad_id).to.equal('3b286a4db7031f'); + expect(query.ad_unit_code).to.equal('test'); + expect(query.ref).to.include(window.location.href); + }); + }); + + describe('spec.onTimeout', function () { + let stub; + beforeEach(() => { + stub = sinon.stub(utils, 'triggerPixel'); + }); + afterEach(() => { + stub.restore(); + }); + + it('Should create nurl pixel if bid nurl', function () { + const data = [{ + bidder: bidRequest.bidder, + bidId: bidRequest.bidId, + adUnitCode: bidRequest.adUnitCode, + auctionId: bidRequest.auctionId, + params: [bidRequest.params], + timeout: bidderRequest.timeout, + }]; + spec.onTimeout(data); + const parser = utils.parseUrl(stub.getCall(0).args[0]); + const query = parser.search; + expect(parser.hostname).to.equal('api.relaido.jp'); + expect(parser.pathname).to.equal('/tr/v1/prebid/timeout.gif'); + expect(query.placement_id).to.equal('100000'); + expect(query.timeout).to.equal('1000'); + expect(query.auction_id).to.equal('413ed000-8c7a-4ba1-a1fa-9732e006f8c3'); + expect(query.bid_id).to.equal('2ed93003f7bb99'); + expect(query.ad_unit_code).to.equal('test'); + expect(query.ref).to.include(window.location.href); + }); + }); +}); diff --git a/test/spec/modules/revcontentBidAdapter_spec.js b/test/spec/modules/revcontentBidAdapter_spec.js index 548f3019a4f..1aa08d9469e 100644 --- a/test/spec/modules/revcontentBidAdapter_spec.js +++ b/test/spec/modules/revcontentBidAdapter_spec.js @@ -1,6 +1,5 @@ // jshint esversion: 6, es3: false, node: true import {assert, expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/revcontentBidAdapter.js'; import { NATIVE } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 1842c29861d..9dc228ed288 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -659,6 +659,102 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(ANALYTICS_MESSAGE); }); + it('should capture price floor information correctly', function () { + let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); + auctionInit.bidderRequests[0].bids[0].floorData = { + skipped: false, + modelVersion: 'someModelName', + location: 'setConfig' + }; + let flooredResponse = { + ...BID, + floorData: { + floorValue: 4, + floorRule: '12345/sports|video', + floorCurrency: 'USD', + cpmAfterAdjustments: 2.1, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + matchedFields: { + gptSlot: '12345/sports', + mediaType: 'video' + } + }, + status: 'bidRejected', + cpm: 0, + getStatusCode() { + return 2; + } + }; + + let notFlooredResponse = { + ...BID2, + floorData: { + floorValue: 1, + floorRule: '12345/news|banner', + floorCurrency: 'USD', + cpmAfterAdjustments: 1.55, + enforcements: { + enforceJS: true, + enforcePBS: false, + floorDeals: false, + bidAdjustment: true + }, + matchedFields: { + gptSlot: '12345/news', + mediaType: 'banner' + } + } + }; + + // spoof the auction with just our duplicates + events.emit(AUCTION_INIT, auctionInit); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, flooredResponse); + events.emit(BID_RESPONSE, notFlooredResponse); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[1]); + clock.tick(SEND_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + + // verify our floor stuff is passed + // top level floor info + expect(message.auctions[0].floors).to.deep.equal({ + location: 'setConfig', + modelName: 'someModelName', + skipped: false, + enforcement: true, + dealsEnforced: false + }); + // first adUnit's adSlot + expect(message.auctions[0].adUnits[0].adSlot).to.equal('12345/sports'); + // since no other bids, we set adUnit status to no-bid + expect(message.auctions[0].adUnits[0].status).to.equal('no-bid'); + // first adUnits bid is rejected + expect(message.auctions[0].adUnits[0].bids[0].status).to.equal('rejected'); + expect(message.auctions[0].adUnits[0].bids[0].bidResponse.floorValue).to.equal(4); + // if bid rejected should take cpmAfterAdjustments val + expect(message.auctions[0].adUnits[0].bids[0].bidResponse.bidPriceUSD).to.equal(2.1); + + // second adUnit's adSlot + expect(message.auctions[0].adUnits[1].adSlot).to.equal('12345/news'); + // top level adUnit status is success + expect(message.auctions[0].adUnits[1].status).to.equal('success'); + // second adUnits bid is success + expect(message.auctions[0].adUnits[1].bids[0].status).to.equal('success'); + expect(message.auctions[0].adUnits[1].bids[0].bidResponse.floorValue).to.equal(1); + expect(message.auctions[0].adUnits[1].bids[0].bidResponse.bidPriceUSD).to.equal(1.52); + }); + it('should correctly overwrite bidId if seatBidId is on the bidResponse', function () { // Only want one bid request in our mock auction let bidRequested = utils.deepClone(MOCK.BID_REQUESTED); diff --git a/test/spec/modules/rubiconAnalyticsSchema.json b/test/spec/modules/rubiconAnalyticsSchema.json index 686aced840f..16cca629d8c 100644 --- a/test/spec/modules/rubiconAnalyticsSchema.json +++ b/test/spec/modules/rubiconAnalyticsSchema.json @@ -292,7 +292,8 @@ "enum": [ "success", "no-bid", - "error" + "error", + "rejected" ] }, "error": { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 7adedbcfdde..29cc6cb2c0a 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -15,7 +15,8 @@ const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be s describe('the rubicon adapter', function () { let sandbox, bidderRequest, - sizeMap; + sizeMap, + getFloorResponse; /** * @typedef {Object} sizeMapConverted @@ -274,7 +275,7 @@ describe('the rubicon adapter', function () { beforeEach(function () { sandbox = sinon.sandbox.create(); - + getFloorResponse = {}; bidderRequest = { bidderCode: 'rubicon', auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', @@ -411,6 +412,43 @@ describe('the rubicon adapter', function () { }); }); + it('should correctly send hard floors when getFloor function is present and returns valid floor', function () { + // default getFloor response is empty object so should not break and not send hard_floor + bidderRequest.bids[0].getFloor = () => getFloorResponse; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); + expect(data.rp_hard_floor).to.be.undefined; + + // not an object should work and not send + getFloorResponse = undefined; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + data = parseQuery(request.data); + expect(data.rp_hard_floor).to.be.undefined; + + // make it respond with a non USD floor should not send it + getFloorResponse = {currency: 'EUR', floor: 1.0}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + data = parseQuery(request.data); + expect(data.rp_hard_floor).to.be.undefined; + + // make it respond with a non USD floor should not send it + getFloorResponse = {currency: 'EUR'}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + data = parseQuery(request.data); + expect(data.rp_hard_floor).to.be.undefined; + + // make it respond with USD floor and string floor + getFloorResponse = {currency: 'USD', floor: '1.23'}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + data = parseQuery(request.data); + expect(data.rp_hard_floor).to.equal('1.23'); + + // make it respond with USD floor and num floor + getFloorResponse = {currency: 'USD', floor: 1.23}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + data = parseQuery(request.data); + expect(data.rp_hard_floor).to.equal('1.23'); + }); it('should not send p_pos to AE if not params.position specified', function() { var noposRequest = utils.deepClone(bidderRequest); delete noposRequest.bids[0].params.position; @@ -1450,6 +1488,39 @@ describe('the rubicon adapter', function () { expect(post.ext.prebid.cache.vastxml.returnCreative).to.equal(false) }); + it('should correctly set bidfloor on imp when getfloor in scope', function () { + createVideoBidderRequest(); + // default getFloor response is empty object so should not break and not send hard_floor + bidderRequest.bids[0].getFloor = () => getFloorResponse; + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); + + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // not an object should work and not send + expect(request.data.imp[0].bidfloor).to.be.undefined; + + // make it respond with a non USD floor should not send it + getFloorResponse = {currency: 'EUR', floor: 1.0}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.be.undefined; + + // make it respond with a non USD floor should not send it + getFloorResponse = {currency: 'EUR'}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.be.undefined; + + // make it respond with USD floor and string floor + getFloorResponse = {currency: 'USD', floor: '1.23'}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.equal(1.23); + + // make it respond with USD floor and num floor + getFloorResponse = {currency: 'USD', floor: 1.23}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.equal(1.23); + }); it('should add alias name to PBS Request', function() { createVideoBidderRequest(); @@ -1853,6 +1924,12 @@ describe('the rubicon adapter', function () { } }); }); + + it('should not fail if keywords param is not an array', function () { + bidderRequest.bids[0].params.keywords = 'a,b,c'; + const slotParams = spec.createSlotParams(bidderRequest.bids[0], bidderRequest); + expect(slotParams.kw).to.equal(''); + }); }); describe('hasVideoMediaType', function () { diff --git a/test/spec/modules/seedingAllianceAdapter_spec.js b/test/spec/modules/seedingAllianceAdapter_spec.js index c5c2a5d682e..e6f96c92fd9 100755 --- a/test/spec/modules/seedingAllianceAdapter_spec.js +++ b/test/spec/modules/seedingAllianceAdapter_spec.js @@ -1,6 +1,5 @@ // jshint esversion: 6, es3: false, node: true import {assert, expect} from 'chai'; -import * as url from 'src/url.js'; import {spec} from 'modules/seedingAllianceBidAdapter.js'; import { NATIVE } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; diff --git a/test/spec/modules/showheroes-bsBidAdapter_spec.js b/test/spec/modules/showheroes-bsBidAdapter_spec.js index c82042469ef..4f2fd9d641a 100644 --- a/test/spec/modules/showheroes-bsBidAdapter_spec.js +++ b/test/spec/modules/showheroes-bsBidAdapter_spec.js @@ -79,6 +79,22 @@ const bidRequestBanner = { } } +const bidRequestBannerMultiSizes = { + ...bidRequestCommonParams, + ...{ + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 360], [480, 320]] + } + } + } +} + +const bidRequestVideoAndBanner = { + ...bidRequestBanner, + ...bidRequestVideo +} + describe('shBidAdapter', function () { const adapter = newBidder(spec) @@ -120,8 +136,8 @@ describe('shBidAdapter', function () { }], bidderRequest) const payload = request.data.requests[0]; expect(payload).to.be.an('object'); - expect(payload.video).to.have.property('width', 640); - expect(payload.video).to.have.property('height', 480); + expect(payload.size).to.have.property('width', 640); + expect(payload.size).to.have.property('height', 480); const request2 = spec.buildRequests([{ 'params': {}, @@ -130,8 +146,8 @@ describe('shBidAdapter', function () { }], bidderRequest) const payload2 = request2.data.requests[0]; expect(payload).to.be.an('object'); - expect(payload2.video).to.have.property('width', 320); - expect(payload2.video).to.have.property('height', 240); + expect(payload2.size).to.have.property('width', 320); + expect(payload2.size).to.have.property('height', 240); }) it('should get size from mediaTypes when sizes property is empty', function () { @@ -146,8 +162,8 @@ describe('shBidAdapter', function () { }], bidderRequest) const payload = request.data.requests[0]; expect(payload).to.be.an('object'); - expect(payload.video).to.have.property('width', 640); - expect(payload.video).to.have.property('height', 480); + expect(payload.size).to.have.property('width', 640); + expect(payload.size).to.have.property('height', 480); const request2 = spec.buildRequests([{ 'params': {}, @@ -160,8 +176,8 @@ describe('shBidAdapter', function () { }], bidderRequest) const payload2 = request2.data.requests[0]; expect(payload).to.be.an('object'); - expect(payload2.video).to.have.property('width', 320); - expect(payload2.video).to.have.property('height', 240); + expect(payload2.size).to.have.property('width', 320); + expect(payload2.size).to.have.property('height', 240); }) it('should attach valid params to the payload when type is video', function () { @@ -191,6 +207,38 @@ describe('shBidAdapter', function () { expect(payload).to.have.property('type', 5); }) + it('should attach valid params to the payload when type is banner (multi sizes)', function () { + const request = spec.buildRequests([bidRequestBannerMultiSizes], bidderRequest) + const payload = request.data.requests[0]; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); + expect(payload).to.have.property('mediaType', BANNER); + expect(payload).to.have.property('type', 5); + expect(payload).to.have.nested.property('size.width', 640); + expect(payload).to.have.nested.property('size.height', 360); + const payload2 = request.data.requests[1]; + expect(payload2).to.be.an('object'); + expect(payload2).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); + expect(payload2).to.have.property('mediaType', BANNER); + expect(payload2).to.have.property('type', 5); + expect(payload2).to.have.nested.property('size.width', 480); + expect(payload2).to.have.nested.property('size.height', 320); + }) + + it('should attach valid params to the payload when type is banner and video', function () { + const request = spec.buildRequests([bidRequestVideoAndBanner], bidderRequest) + const payload = request.data.requests[0]; + expect(payload).to.be.an('object'); + expect(payload).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); + expect(payload).to.have.property('mediaType', VIDEO); + expect(payload).to.have.property('type', 2); + const payload2 = request.data.requests[1]; + expect(payload2).to.be.an('object'); + expect(payload2).to.have.property('playerId', '47427aa0-f11a-4d24-abca-1295a46a46cd'); + expect(payload2).to.have.property('mediaType', BANNER); + expect(payload2).to.have.property('type', 5); + }) + it('passes gdpr if present', function () { const request = spec.buildRequests([bidRequestVideo], {...bidderRequest, ...gdpr}) const payload = request.data.requests[0]; @@ -208,16 +256,36 @@ describe('shBidAdapter', function () { const vastTag = 'https://video-library.stage.showheroes.com/commercial/wrapper?player_id=47427aa0-f11a-4d24-abca-1295a46a46cd&ad_bidder=showheroes-bs&master_shadt=1&description_url=https%3A%2F%2Fbid-service.stage.showheroes.com%2Fvast%2Fad%2Fcache%2F4840b920-40e1-4e09-9231-60bbf088c8d6' const vastXml = '' - const response = { + const basicResponse = { + 'cpm': 5, + 'currency': 'EUR', + 'mediaType': VIDEO, + 'context': 'instream', + 'bidId': '38b373e1e31c18', + 'size': {'width': 640, 'height': 480}, + 'vastTag': 'https:\/\/video-library.stage.showheroes.com\/commercial\/wrapper?player_id=47427aa0-f11a-4d24-abca-1295a46a46cd&ad_bidder=showheroes-bs&master_shadt=1&description_url=https%3A%2F%2Fbid-service.stage.showheroes.com%2Fvast%2Fad%2Fcache%2F4840b920-40e1-4e09-9231-60bbf088c8d6', + 'vastXml': vastXml, + }; + + const responseVideo = { 'bids': [{ - 'cpm': 5, - 'currency': 'EUR', - 'bidId': '38b373e1e31c18', - 'video': {'width': 640, 'height': 480}, - 'vastTag': 'https:\/\/video-library.stage.showheroes.com\/commercial\/wrapper?player_id=47427aa0-f11a-4d24-abca-1295a46a46cd&ad_bidder=showheroes-bs&master_shadt=1&description_url=https%3A%2F%2Fbid-service.stage.showheroes.com%2Fvast%2Fad%2Fcache%2F4840b920-40e1-4e09-9231-60bbf088c8d6', - 'vastXml': vastXml, + ...basicResponse, }], - } + }; + + const responseVideoOutstream = { + 'bids': [{ + ...basicResponse, + 'context': 'outstream', + }], + }; + + const responseBanner = { + 'bids': [{ + ...basicResponse, + 'mediaType': BANNER, + }], + }; it('should get correct bid response when type is video', function () { const request = spec.buildRequests([bidRequestVideo], bidderRequest) @@ -240,14 +308,14 @@ describe('shBidAdapter', function () { } ] - const result = spec.interpretResponse({'body': response}, request) + const result = spec.interpretResponse({'body': responseVideo}, request) expect(result).to.deep.equal(expectedResponse) }) it('should get correct bid response when type is banner', function () { const request = spec.buildRequests([bidRequestBanner], bidderRequest) - const result = spec.interpretResponse({'body': response}, request) + const result = spec.interpretResponse({'body': responseBanner}, request) expect(result[0]).to.have.property('mediaType', BANNER); expect(result[0].ad).to.include('', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 334553, + 'auction_id': 514667951122925701, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + }, + { + 'id': 'bidId2', + 'impid': 'bidId2', + 'price': 0.1, + 'adm': '', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 386046, + 'auction_id': 517067951122925501, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + } + ], + 'seat': 'yieldlift' + } + ], + 'ext': { + 'usersync': { + 'sovrn': { + 'status': 'none', + 'syncs': [ + { + 'url': 'urlsovrn', + 'type': 'iframe' + } + ] + }, + 'appnexus': { + 'status': 'none', + 'syncs': [ + { + 'url': 'urlappnexus', + 'type': 'pixel' + } + ] + } + }, + 'responsetimemillis': { + 'appnexus': 127 + } + } + } +}; + +describe('YieldLift', function () { + describe('isBidRequestValid', function () { + it('should accept request if only unitId is passed', function () { + let bid = { + bidder: 'yieldlift', + params: { + unitId: 'unitId', + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should accept request if only networkId is passed', function () { + let bid = { + bidder: 'yieldlift', + params: { + networkId: 'networkId', + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should accept request if only publisherId is passed', function () { + let bid = { + bidder: 'yieldlift', + params: { + publisherId: 'publisherId', + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('reject requests without params', function () { + let bid = { + bidder: 'yieldlift', + params: {} + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('creates request data', function () { + let request = spec.buildRequests(REQUEST.bidRequest, REQUEST); + + expect(request).to.exist.and.to.be.a('object'); + const payload = JSON.parse(request.data); + expect(payload.imp[0]).to.have.property('id', REQUEST.bidRequest[0].bidId); + expect(payload.imp[1]).to.have.property('id', REQUEST.bidRequest[1].bidId); + }); + + it('has gdpr data if applicable', function () { + const req = Object.assign({}, REQUEST, { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, + } + }); + let request = spec.buildRequests(REQUEST.bidRequest, req); + + const payload = JSON.parse(request.data); + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + }); + + describe('interpretResponse', function () { + it('have bids', function () { + let bids = spec.interpretResponse(RESPONSE, REQUEST); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + validateBidOnIndex(1); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].impid); + expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); + expect(bids[index]).to.have.property('ttl', 30); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, REQUEST); + + expect(bids).to.be.empty; + }); + }); + + describe('getUserSyncs', function () { + it('handles no parameters', function () { + let opts = spec.getUserSyncs({}); + expect(opts).to.be.an('array').that.is.empty; + }); + it('returns non if sync is not allowed', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + + expect(opts).to.be.an('array').that.is.empty; + }); + + it('iframe sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [RESPONSE]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['sovrn'].syncs[0].url); + }); + + it('pixel sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['appnexus'].syncs[0].url); + }); + + it('all sync enabled should return all results', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]); + + expect(opts.length).to.equal(2); + }); + }); +}); diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 0bbc4b57785..5904113bd42 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -8,44 +8,48 @@ describe('YieldmoAdapter', function () { const ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; let tdid = '8d146286-91d4-4958-aff4-7e489dd1abd6'; + let criteoId = 'aff4'; let bid = { bidder: 'yieldmo', params: { - bidFloor: 0.1 + bidFloor: 0.1, }, adUnitCode: 'adunit-code', mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]] - } + sizes: [ + [300, 250], + [300, 600], + ], + }, }, bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', crumbs: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da' + pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da', }, userId: { tdid, - } + }, }; let bidArray = [bid]; let bidderRequest = { - 'bidderCode': 'yieldmo', - 'auctionId': 'e3a336ad-2761-4a1c-b421-ecc7c5294a34', - 'bidderRequestId': '14c4ede8c693f', - 'bids': bidArray, - 'auctionStart': 1520001292880, - 'timeout': 3000, - 'start': 1520001292884, - 'doneCbCallCount': 0, - 'refererInfo': { - 'numIframes': 1, - 'reachedTop': true, - 'referer': 'yieldmo.com' - } - } + bidderCode: 'yieldmo', + auctionId: 'e3a336ad-2761-4a1c-b421-ecc7c5294a34', + bidderRequestId: '14c4ede8c693f', + bids: bidArray, + auctionStart: 1520001292880, + timeout: 3000, + start: 1520001292884, + doneCbCallCount: 0, + refererInfo: { + numIframes: 1, + reachedTop: true, + referer: 'yieldmo.com', + }, + }; describe('isBidRequestValid', function () { it('should return true when necessary information is found', function () { @@ -77,38 +81,44 @@ describe('YieldmoAdapter', function () { }); it('should not blow up if crumbs is undefined', function () { - let bidArray = [ - { ...bid, crumbs: undefined } - ] - expect(function () { spec.buildRequests(bidArray, bidderRequest) }).not.to.throw() - }) + let bidArray = [{ ...bid, crumbs: undefined }]; + expect(function () { + spec.buildRequests(bidArray, bidderRequest); + }).not.to.throw(); + }); it('should place bid information into the p parameter of data', function () { let placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.equal('[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1}]'); + expect(placementInfo).to.equal( + '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1}]' + ); bidArray.push({ bidder: 'yieldmo', params: { - bidFloor: 0.2 + bidFloor: 0.2, }, adUnitCode: 'adunit-code-1', mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]] - } + sizes: [ + [300, 250], + [300, 600], + ], + }, }, bidId: '123456789', bidderRequestId: '987654321', auctionId: '0246810', crumbs: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da' - } - + pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da', + }, }); // multiple placements placementInfo = spec.buildRequests(bidArray, bidderRequest).data.p; - expect(placementInfo).to.equal('[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1},{"placement_id":"adunit-code-1","callback_id":"123456789","sizes":[[300,250],[300,600]],"bidFloor":0.2}]'); + expect(placementInfo).to.equal( + '[{"placement_id":"adunit-code","callback_id":"30b31c1838de1e","sizes":[[300,250],[300,600]],"bidFloor":0.1},{"placement_id":"adunit-code-1","callback_id":"123456789","sizes":[[300,250],[300,600]],"bidFloor":0.2}]' + ); }); it('should add placement id if given', function () { @@ -145,18 +155,23 @@ describe('YieldmoAdapter', function () { adUnitCode: 'adunit-code', mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]] - } + sizes: [ + [300, 250], + [300, 600], + ], + }, }, bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', userId: { - pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2' - } + pubcid: 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2', + }, }; const data = spec.buildRequests([pubcidBid], bidderRequest).data; - expect(data.pubcid).to.deep.equal('c604130c-0144-4b63-9bf2-c2bd8c8d86da2'); + expect(data.pubcid).to.deep.equal( + 'c604130c-0144-4b63-9bf2-c2bd8c8d86da2' + ); }); it('should add unified id as parameter of request', function () { @@ -166,31 +181,60 @@ describe('YieldmoAdapter', function () { adUnitCode: 'adunit-code', mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]] - } + sizes: [ + [300, 250], + [300, 600], + ], + }, }, bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', userId: { tdid, - } + }, }; const data = spec.buildRequests([unifiedIdBid], bidderRequest).data; expect(data.tdid).to.deep.equal(tdid); }); + it('should add CRITEO RTUS id as parameter of request', function () { + const criteoIdBid = { + bidder: 'yieldmo', + params: {}, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + userId: { + criteoId, + }, + }; + const data = spec.buildRequests([criteoIdBid], bidderRequest).data; + expect(data.cri_prebid).to.deep.equal(criteoId); + }); + it('should add gdpr information to request if available', () => { bidderRequest.gdprConsent = { - 'consentString': 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', - 'vendorData': {'blerp': 1}, - 'gdprApplies': true - } + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: { blerp: 1 }, + gdprApplies: true, + }; const data = spec.buildRequests(bidArray, bidderRequest).data; - expect(data.userConsent).equal(JSON.stringify({ - 'gdprApplies': true, - 'cmp': 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==' - })); + expect(data.userConsent).equal( + JSON.stringify({ + gdprApplies: true, + cmp: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + }) + ); }); it('should add ccpa information to request if available', () => { @@ -201,11 +245,15 @@ describe('YieldmoAdapter', function () { }); it('should add schain if it is in the bidRequest', () => { - const schain = {'ver': '1.0', 'complete': 1, 'nodes': [{'asi': 'indirectseller.com', 'sid': '00001', 'hp': 1}]}; + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; bidArray[0].schain = schain; const request = spec.buildRequests([bidArray[0]], bidderRequest); expect(request.data.schain).equal(JSON.stringify(schain)); - }) + }); }); describe('interpretResponse', function () { @@ -213,17 +261,20 @@ describe('YieldmoAdapter', function () { beforeEach(function () { serverResponse = { - body: [{ - callback_id: '21989fdbef550a', - cpm: 3.45455, - width: 300, - height: 250, - ad: '
', - creative_id: '9874652394875' - }], - header: 'header?' + body: [ + { + callback_id: '21989fdbef550a', + cpm: 3.45455, + width: 300, + height: 250, + ad: + '
', + creative_id: '9874652394875', + }, + ], + header: 'header?', }; - }) + }); it('should correctly reorder the server response', function () { const newResponse = spec.interpretResponse(serverResponse); @@ -237,7 +288,8 @@ describe('YieldmoAdapter', function () { currency: 'USD', netRevenue: true, ttl: 300, - ad: '
' + ad: + '
', }); }); @@ -248,7 +300,7 @@ describe('YieldmoAdapter', function () { serverResponse.body[0].cpm = null; response = spec.interpretResponse(serverResponse); - expect(response).to.deep.equal([]) + expect(response).to.deep.equal([]); }); }); @@ -256,15 +308,17 @@ describe('YieldmoAdapter', function () { const SYNC_ENDPOINT = 'https://static.yieldmo.com/blank.min.html?orig='; let options = { iframeEnabled: true, - pixelEnabled: true + pixelEnabled: true, }; it('should return a tracker with type and url as parameters', function () { if (/iPhone|iPad|iPod/i.test(window.navigator.userAgent)) { - expect(spec.getUserSync(options)).to.deep.equal([{ - type: 'iframe', - url: SYNC_ENDPOINT + utils.getOrigin() - }]); + expect(spec.getUserSync(options)).to.deep.equal([ + { + type: 'iframe', + url: SYNC_ENDPOINT + utils.getOrigin(), + }, + ]); options.iframeEnabled = false; expect(spec.getUserSync(options)).to.deep.equal([]); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 566ebe2e400..8ee345166e1 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -43,16 +43,23 @@ var rubiconAdapterMock = { callBids: sinon.stub() }; +var badAdapterMock = { + bidder: 'badBidder', + callBids: sinon.stub().throws(Error('some fake error')) +}; + describe('adapterManager tests', function () { let orgAppnexusAdapter; let orgAdequantAdapter; let orgPrebidServerAdapter; let orgRubiconAdapter; + let orgBadBidderAdapter; before(function () { orgAppnexusAdapter = adapterManager.bidderRegistry['appnexus']; orgAdequantAdapter = adapterManager.bidderRegistry['adequant']; orgPrebidServerAdapter = adapterManager.bidderRegistry['prebidServer']; orgRubiconAdapter = adapterManager.bidderRegistry['rubicon']; + orgBadBidderAdapter = adapterManager.bidderRegistry['badBidder']; }); after(function () { @@ -60,6 +67,7 @@ describe('adapterManager tests', function () { adapterManager.bidderRegistry['adequant'] = orgAdequantAdapter; adapterManager.bidderRegistry['prebidServer'] = orgPrebidServerAdapter; adapterManager.bidderRegistry['rubicon'] = orgRubiconAdapter; + adapterManager.bidderRegistry['badBidder'] = orgBadBidderAdapter; config.setConfig({s2sConfig: { enabled: false }}); }); @@ -72,11 +80,16 @@ describe('adapterManager tests', function () { sinon.stub(utils, 'logError'); appnexusAdapterMock.callBids.reset(); adapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; + adapterManager.bidderRegistry['rubicon'] = rubiconAdapterMock; + adapterManager.bidderRegistry['badBidder'] = badAdapterMock; }); afterEach(function () { utils.logError.restore(); delete adapterManager.bidderRegistry['appnexus']; + delete adapterManager.bidderRegistry['rubicon']; + delete adapterManager.bidderRegistry['badBidder']; + config.resetConfig(); }); it('should log an error if a bidder is used that does not exist', function () { @@ -95,6 +108,34 @@ describe('adapterManager tests', function () { sinon.assert.called(utils.logError); }); + it('should catch a bidder adapter thrown error and continue with other bidders', function () { + const adUnits = [{ + code: 'adUnit-code', + sizes: [[728, 90]], + bids: [ + {bidder: 'appnexus', params: {placementId: 'id'}}, + {bidder: 'badBidder', params: {placementId: 'id'}}, + {bidder: 'rubicon', params: {account: 1111, site: 2222, zone: 3333}} + ] + }]; + let bidRequests = adapterManager.makeBidRequests(adUnits, 1111, 2222, 1000); + + let doneBidders = []; + function mockDoneCB() { + doneBidders.push(this.bidderCode) + } + adapterManager.callBids(adUnits, bidRequests, () => {}, mockDoneCB); + sinon.assert.calledOnce(appnexusAdapterMock.callBids); + sinon.assert.calledOnce(badAdapterMock.callBids); + sinon.assert.calledOnce(rubiconAdapterMock.callBids); + + expect(utils.logError.calledOnce).to.be.true; + expect(utils.logError.calledWith( + 'badBidder Bid Adapter emitted an uncaught error when parsing their bidRequest' + )).to.be.true; + // done should be called for our bidder! + expect(doneBidders.indexOf('badBidder') === -1).to.be.false; + }); it('should emit BID_REQUESTED event', function () { // function to count BID_REQUESTED events let cnt = 0; @@ -175,7 +216,7 @@ describe('adapterManager tests', function () { buildRequests: { data: 1 }, - test1: { speedy: true }, + test1: { speedy: true, fun: { test: true } }, interpretResponse: 'baseInterpret', afterInterpretResponse: 'anotherBaseInterpret' }); @@ -212,7 +253,7 @@ describe('adapterManager tests', function () { data: 1, test: 2 }, - { fun: { safe: true, cheap: false }, speedy: true }, + { fun: { safe: true, cheap: false, test: true }, speedy: true }, { amazing: true }, 'appnexusInterpret', 'anotherBaseInterpret' @@ -221,14 +262,14 @@ describe('adapterManager tests', function () { { data: 1 }, - { speedy: true }, + { fun: { test: true }, speedy: true }, undefined, 'baseInterpret', 'anotherBaseInterpret' ], 'rubicon': [ 'rubiconBuild', - { speedy: true }, + { fun: { test: true }, speedy: true }, { amazing: true }, null, 'anotherBaseInterpret' diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index c06eb29b6b9..6d0595ba4d8 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -1,4 +1,4 @@ -import { newBidder, registerBidder, preloadBidderMappingFile } from 'src/adapters/bidderFactory.js'; +import { newBidder, registerBidder, preloadBidderMappingFile, storage } from 'src/adapters/bidderFactory.js'; import adapterManager from 'src/adapterManager.js'; import * as ajax from 'src/ajax.js'; import { expect } from 'chai'; @@ -56,15 +56,66 @@ describe('bidders created by newBidder', function () { describe('when the ajax response is irrelevant', function () { let ajaxStub; + let getConfigSpy; beforeEach(function () { ajaxStub = sinon.stub(ajax, 'ajax'); addBidResponseStub.reset(); + getConfigSpy = sinon.spy(config, 'getConfig'); doneStub.reset(); }); afterEach(function () { ajaxStub.restore(); + getConfigSpy.restore(); + }); + + it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); + + it('should let registerSyncs run with valid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); + + it('should let registerSyncs run with invalid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); + + it('should not let registerSyncs run with valid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false + } + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(false); }); it('should handle bad bid requests gracefully', function () { @@ -196,7 +247,7 @@ describe('bidders created by newBidder', function () { bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2&`); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); expect(ajaxStub.firstCall.args[2]).to.be.undefined; expect(ajaxStub.firstCall.args[3]).to.deep.equal({ method: 'GET', @@ -220,7 +271,7 @@ describe('bidders created by newBidder', function () { bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2&`); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); expect(ajaxStub.firstCall.args[2]).to.be.undefined; expect(ajaxStub.firstCall.args[3]).to.deep.equal({ method: 'GET', @@ -793,7 +844,7 @@ describe('preload mapping url hook', function() { beforeEach(function () { fakeTranslationServer = server; - getLocalStorageStub = sinon.stub(utils, 'getDataFromLocalStorage'); + getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); adapterManagerStub = sinon.stub(adapterManager, 'getBidAdapter'); }); diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js new file mode 100644 index 00000000000..0b406242f90 --- /dev/null +++ b/test/spec/unit/core/storageManager_spec.js @@ -0,0 +1,46 @@ +import { resetData, getCoreStorageManager, storageCallbacks, getStorageManager } from 'src/storageManager.js'; +import { config } from 'src/config.js'; +import * as utils from 'src/utils.js'; + +describe('storage manager', function() { + beforeEach(function() { + resetData(); + }); + + afterEach(function() { + config.resetConfig(); + }) + + it('should allow to set cookie for core modules without checking gdpr enforcements', function() { + const coreStorage = getCoreStorageManager(); + let date = new Date(); + date.setTime(date.getTime() + (24 * 60 * 60 * 1000)); + let expires = date.toUTCString(); + coreStorage.setCookie('hello', 'world', expires); + expect(coreStorage.getCookie('hello')).to.equal('world'); + }); + + it('should add done callbacks to storageCallbacks array', function() { + let noop = sinon.spy(); + const coreStorage = getStorageManager(); + + coreStorage.setCookie('foo', 'bar', null, null, null, noop); + coreStorage.getCookie('foo', noop); + coreStorage.localStorageIsEnabled(noop); + coreStorage.cookiesAreEnabled(noop); + coreStorage.setDataInLocalStorage('foo', 'bar', noop); + coreStorage.getDataFromLocalStorage('foo', noop); + coreStorage.removeDataFromLocalStorage('foo', noop); + coreStorage.hasLocalStorage(noop); + + expect(storageCallbacks.length).to.equal(8); + }); + + it('should allow bidder to access device if gdpr enforcement module is not included', function() { + let deviceAccessSpy = sinon.spy(utils, 'hasDeviceAccess'); + const storage = getStorageManager(); + storage.setCookie('foo1', 'baz1'); + expect(deviceAccessSpy.calledOnce).to.equal(true); + deviceAccessSpy.restore(); + }) +}); diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index cfdd39239d3..252c074cd61 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -1223,6 +1223,12 @@ describe('Unit: Prebid Module', function () { let makeRequestsStub; let adUnits; let clock; + before(function () { + clock = sinon.useFakeTimers(); + }); + after(function () { + clock.restore(); + }); let bidsBackHandlerStub = sinon.stub(); const BIDDER_CODE = 'sampleBidder'; @@ -1309,7 +1315,6 @@ describe('Unit: Prebid Module', function () { spec.isBidRequestValid.returns(true); spec.interpretResponse.returns(bids); - clock = sinon.useFakeTimers(); let requestObj = { bidsBackHandler: null, // does not need to be defined because of newAuction mock in beforeEach timeout: 2000, @@ -1363,7 +1368,6 @@ describe('Unit: Prebid Module', function () { auction.getBidsReceived = function() { return [adResponse]; } auction.getAuctionId = () => auctionId; - clock = sinon.useFakeTimers(); let requestObj = { bidsBackHandler: null, // does not need to be defined because of newAuction mock in beforeEach timeout: 2000, diff --git a/test/spec/url_spec.js b/test/spec/url_spec.js deleted file mode 100644 index 842b954c4bc..00000000000 --- a/test/spec/url_spec.js +++ /dev/null @@ -1,94 +0,0 @@ -import {format, parse} from '../../src/url.js'; -import { expect } from 'chai'; - -describe('helpers.url', function () { - describe('parse()', function () { - let parsed; - - beforeEach(function () { - parsed = parse('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash'); - }); - - it('extracts the protocol', function () { - expect(parsed).to.have.property('protocol', 'http'); - }); - - it('extracts the hostname', function () { - expect(parsed).to.have.property('hostname', 'example.com'); - }); - - it('extracts the port', function () { - expect(parsed).to.have.property('port', 3000); - }); - - it('extracts the pathname', function () { - expect(parsed).to.have.property('pathname', '/pathname/'); - }); - - it('extracts the search query', function () { - expect(parsed).to.have.property('search'); - expect(parsed.search).to.eql({ - foo: 'xxx', - search: 'test', - bar: 'foo', - }); - }); - - it('extracts the hash', function () { - expect(parsed).to.have.property('hash', 'hash'); - }); - - it('extracts the host', function () { - expect(parsed).to.have.property('host', 'example.com:3000'); - }); - }); - - describe('parse(url, {noDecodeWholeURL: true})', function () { - let parsed; - - beforeEach(function () { - parsed = parse('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash', {noDecodeWholeURL: true}); - }); - - it('extracts the search query', function () { - expect(parsed).to.have.property('search'); - expect(parsed.search).to.eql({ - foo: 'bar', - search: 'test', - bar: 'foo%26foo%3Dxxx', - }); - }); - }); - - describe('format()', function () { - it('formats an object in to a URL', function () { - expect(format({ - protocol: 'http', - hostname: 'example.com', - port: 3000, - pathname: '/pathname/', - search: {foo: 'bar', search: 'test', bar: 'foo%26foo%3Dxxx'}, - hash: 'hash' - })).to.equal('http://example.com:3000/pathname/?foo=bar&search=test&bar=foo%26foo%3Dxxx#hash'); - }); - - it('will use defaults for missing properties', function () { - expect(format({ - hostname: 'example.com' - })).to.equal('http://example.com'); - }); - }); - - describe('parse(url, {decodeSearchAsString: true})', function () { - let parsed; - - beforeEach(function () { - parsed = parse('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash', {decodeSearchAsString: true}); - }); - - it('extracts the search query', function () { - expect(parsed).to.have.property('search'); - expect(parsed.search).to.equal('?search=test&foo=bar&bar=foo&foo=xxx'); - }); - }); -}); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js old mode 100755 new mode 100644 index d98e7492e52..96df3cf996c --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -18,24 +18,6 @@ describe('Utils', function () { type_array = 'Array', type_function = 'Function'; - describe('replaceTokenInString', function () { - it('should replace all given tokens in a String', function () { - var tokensToReplace = { - foo: 'bar', - zap: 'quux' - }; - - var output = utils.replaceTokenInString('hello %FOO%, I am %ZAP%', tokensToReplace, '%'); - assert.equal(output, 'hello bar, I am quux'); - }); - - it('should ignore tokens it does not see', function () { - var output = utils.replaceTokenInString('hello %FOO%', {}, '%'); - - assert.equal(output, 'hello %FOO%'); - }); - }); - describe('getBidIdParameter', function () { it('should return value of the key in input object', function () { var obj = { @@ -86,7 +68,7 @@ describe('Utils', function () { }; var output = utils.parseQueryStringParameters(obj); - var expectedResult = 'a=' + encodeURIComponent('1') + '&b=' + encodeURIComponent('2') + '&'; + var expectedResult = 'a=' + encodeURIComponent('1') + '&b=' + encodeURIComponent('2'); assert.equal(output, expectedResult); }); @@ -356,14 +338,6 @@ describe('Utils', function () { }); }); - describe('cookie functions', function() { - it('returns an array of cookies in a jar that have a similar name', function() { - utils.setCookie('cookie-a', 'cookie-value-a'); - utils.setCookie('cookie-b', 'cookie-value-b'); - expect(utils.findSimilarCookies('cookie')).to.include.members(['cookie-value-a', 'cookie-value-b']); - }); - }); - describe('isStr', function () { it('should return true with input string', function () { var output = utils.isStr(obj_string); @@ -707,20 +681,6 @@ describe('Utils', function () { }); }); - describe('createContentToExecuteExtScriptInFriendlyFrame', function () { - it('should return empty string if url is not passed', function () { - var output = utils.createContentToExecuteExtScriptInFriendlyFrame(); - assert.equal(output, ''); - }); - - it('should have URL in returned value if url is passed', function () { - var url = 'https://abcd.com/service?a=1&b=2&c=3'; - var output = utils.createContentToExecuteExtScriptInFriendlyFrame(url); - var expected = ``; - assert.equal(output, expected); - }); - }); - describe('getDefinedParams', function () { it('builds an object consisting of defined params', function () { const adUnit = { @@ -841,6 +801,98 @@ describe('Utils', function () { }); }); + describe('URL helpers', function () { + describe('parseUrl()', function () { + let parsed; + + beforeEach(function () { + parsed = utils.parseUrl('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash'); + }); + + it('extracts the protocol', function () { + expect(parsed).to.have.property('protocol', 'http'); + }); + + it('extracts the hostname', function () { + expect(parsed).to.have.property('hostname', 'example.com'); + }); + + it('extracts the port', function () { + expect(parsed).to.have.property('port', 3000); + }); + + it('extracts the pathname', function () { + expect(parsed).to.have.property('pathname', '/pathname/'); + }); + + it('extracts the search query', function () { + expect(parsed).to.have.property('search'); + expect(parsed.search).to.eql({ + foo: 'xxx', + search: 'test', + bar: 'foo', + }); + }); + + it('extracts the hash', function () { + expect(parsed).to.have.property('hash', 'hash'); + }); + + it('extracts the host', function () { + expect(parsed).to.have.property('host', 'example.com:3000'); + }); + }); + + describe('parseUrl(url, {noDecodeWholeURL: true})', function () { + let parsed; + + beforeEach(function () { + parsed = utils.parseUrl('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash', {noDecodeWholeURL: true}); + }); + + it('extracts the search query', function () { + expect(parsed).to.have.property('search'); + expect(parsed.search).to.eql({ + foo: 'bar', + search: 'test', + bar: 'foo%26foo%3Dxxx', + }); + }); + }); + + describe('buildUrl()', function () { + it('formats an object in to a URL', function () { + expect(utils.buildUrl({ + protocol: 'http', + hostname: 'example.com', + port: 3000, + pathname: '/pathname/', + search: {foo: 'bar', search: 'test', bar: 'foo%26foo%3Dxxx'}, + hash: 'hash' + })).to.equal('http://example.com:3000/pathname/?foo=bar&search=test&bar=foo%26foo%3Dxxx#hash'); + }); + + it('will use defaults for missing properties', function () { + expect(utils.buildUrl({ + hostname: 'example.com' + })).to.equal('http://example.com'); + }); + }); + + describe('parseUrl(url, {decodeSearchAsString: true})', function () { + let parsed; + + beforeEach(function () { + parsed = utils.parseUrl('http://example.com:3000/pathname/?search=test&foo=bar&bar=foo%26foo%3Dxxx#hash', {decodeSearchAsString: true}); + }); + + it('extracts the search query', function () { + expect(parsed).to.have.property('search'); + expect(parsed.search).to.equal('?search=test&foo=bar&bar=foo&foo=xxx'); + }); + }); + }); + describe('transformBidderParamKeywords', function () { it('returns an array of objects when keyvalue is an array', function () { let keywords = { @@ -930,4 +982,137 @@ describe('Utils', function () { }); }); }); + + describe('isSafariBrowser', function () { + let userAgentStub; + let userAgent; + + before(function () { + userAgentStub = sinon.stub(navigator, 'userAgent').get(function () { + return userAgent; + }); + }); + + after(function () { + userAgentStub.restore(); + }); + + it('properly detects safari', function () { + userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25'; + expect(utils.isSafariBrowser()).to.equal(true); + }); + it('does not flag Chrome on MacOS', function () { + userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36'; + expect(utils.isSafariBrowser()).to.equal(false); + }); + it('does not flag Chrome iOS', function () { + userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/80.0.3987.95 Mobile/15E148 Safari/604.1'; + expect(utils.isSafariBrowser()).to.equal(false); + }); + it('does not flag Firefox iOS', function () { + userAgent = 'Mozilla/5.0 (iPhone; CPU OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/23.0 Mobile/15E148 Safari/605.1.15'; + expect(utils.isSafariBrowser()).to.equal(false); + }); + it('does not flag Windows Edge', function () { + userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.74 Safari/537.36 Edg/79.0.309.43'; + expect(utils.isSafariBrowser()).to.equal(false); + }); + }); + + describe('mergeDeep', function() { + it('properly merge objects that share same property names', function() { + const object1 = { + propA: { + subPropA: 'abc' + } + }; + const object2 = { + propA: { + subPropB: 'def' + } + }; + + const resultWithoutMergeDeep = Object.assign({}, object1, object2); + expect(resultWithoutMergeDeep).to.deep.equal({ + propA: { + subPropB: 'def' + } + }); + + const resultWithMergeDeep = utils.mergeDeep({}, object1, object2); + expect(resultWithMergeDeep).to.deep.equal({ + propA: { + subPropA: 'abc', + subPropB: 'def' + } + }); + }); + + it('properly merge objects that have different depths', function() { + const object1 = { + depth0_A: { + depth1_A: { + depth2_A: 123 + } + } + }; + const object2 = { + depth0_A: { + depth1_A: { + depth2_B: { + depth3_A: { + depth4_A: 'def' + } + } + }, + depth1_B: 'abc' + } + }; + const object3 = { + depth0_B: 456 + }; + + const result = utils.mergeDeep({}, object1, object2, object3); + expect(result).to.deep.equal({ + depth0_A: { + depth1_A: { + depth2_A: 123, + depth2_B: { + depth3_A: { + depth4_A: 'def' + } + } + }, + depth1_B: 'abc' + }, + depth0_B: 456 + }); + }); + + it('properly merge objects with various property types', function() { + const object1 = { + depth0_A: { + depth1_A: ['a', 'b', 'c'], + depth1_B: 'abc', + depth1_C: 123 + } + }; + const object2 = { + depth0_A: { + depth1_A: ['d', 'e', 'f'], + depth1_D: true, + } + }; + + const result = utils.mergeDeep({}, object1, object2); + expect(result).to.deep.equal({ + depth0_A: { + depth1_A: ['a', 'b', 'c', 'd', 'e', 'f'], + depth1_B: 'abc', + depth1_C: 123, + depth1_D: true, + } + }); + }); + }); });