diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 1152e2942bf..2934a30fb47 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -51,11 +51,16 @@ Follow steps above for general review process. In addition, please verify the fo - If the adapter being submitted is an alias type, check with the bidder contact that is being aliased to make sure it's allowed. - All bidder parameter conventions must be followed: - Video params must be read from AdUnit.mediaTypes.video when available; however bidder config can override the ad unit. - - First party data must be read from [`fpd.context` and `fpd.user`](https://docs.prebid.org/dev-docs/publisher-api-reference.html#setConfig-fpd). + - First party data must be read from [getConfig('ortb2');](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-fpd). - Adapters that accept a floor parameter must also support the [floors module](https://docs.prebid.org/dev-docs/modules/floors.html) -- look for a call to the `getFloor()` function. - Adapters cannot accept an schain parameter. Rather, they must look for the schain parameter at bidRequest.schain. - - The bidRequest page referrer must checked in addition to any bidder-specific parameter. + - The bidderRequest.refererInfo.referer must be checked in addition to any bidder-specific parameter. - If they're getting the COPPA flag, it must come from config.getConfig('coppa'); + - Page position must come from bidrequest.mediaTypes.banner.pos or bidrequest.mediaTypes.video.pos + - Global OpenRTB fields should come from [getConfig('ortb2');](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-fpd): + - bcat, battr, badv + - Impression-specific OpenRTB fields should come from bidrequest.ortb2imp + - instl - Below are some examples of bidder specific updates that should require docs update (in their dev-docs/bidders/BIDDER.md file): - If they support the GDPR consentManagement module and TCF1, add `gdpr_supported: true` - If they support the GDPR consentManagement module and TCF2, add `tcf2_supported: true` diff --git a/gulpfile.js b/gulpfile.js index ef51dacf45e..0c4ebc50653 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -134,7 +134,9 @@ function makeDevpackPkg() { function makeWebpackPkg() { var cloned = _.cloneDeep(webpackConfig); - delete cloned.devtool; + if (!argv.sourceMaps) { + delete cloned.devtool; + } var externalModules = helpers.getArgModules(); @@ -144,10 +146,19 @@ function makeWebpackPkg() { return gulp.src([].concat(moduleSources, analyticsSources, 'src/prebid.js')) .pipe(helpers.nameModules(externalModules)) .pipe(webpackStream(cloned, webpack)) - .pipe(gulpif(file => file.basename === 'prebid-core.js', header(banner, { prebid: prebid }))) .pipe(gulp.dest('build/dist')); } +function addBanner() { + const sm = argv.sourceMaps; + + return gulp.src(['build/dist/prebid-core.js']) + .pipe(gulpif(sm, sourcemaps.init({loadMaps: true}))) + .pipe(header(banner, {prebid})) + .pipe(gulpif(sm, sourcemaps.write('.'))) + .pipe(gulp.dest('build/dist')) +} + function getModulesListToAddInBanner(modules) { return (modules.length > 0) ? modules.join(', ') : 'All available modules in current version.'; } @@ -172,6 +183,7 @@ function nodeBundle(modules) { function bundle(dev, moduleArr) { var modules = moduleArr || helpers.getArgModules(); var allModules = helpers.getModuleNames(modules); + const sm = dev || argv.sourceMaps; if (modules.length === 0) { modules = allModules.filter(module => explicitModules.indexOf(module) === -1); @@ -203,13 +215,13 @@ function bundle(dev, moduleArr) { ) // Need to uodate the "Modules: ..." section in comment with the current modules list .pipe(replace(/(Modules: )(.*?)(\*\/)/, ('$1' + getModulesListToAddInBanner(helpers.getArgModules()) + ' $3'))) - .pipe(gulpif(dev, sourcemaps.init({ loadMaps: true }))) + .pipe(gulpif(sm, sourcemaps.init({ loadMaps: true }))) .pipe(concat(outputFileName)) .pipe(gulpif(!argv.manualEnable, footer('\n<%= global %>.processQueue();', { global: prebid.globalVarName } ))) - .pipe(gulpif(dev, sourcemaps.write('.'))); + .pipe(gulpif(sm, sourcemaps.write('.'))); } // Run the unit tests. @@ -398,7 +410,7 @@ gulp.task(clean); gulp.task(escapePostbidConfig); gulp.task('build-bundle-dev', gulp.series(makeDevpackPkg, gulpBundle.bind(null, true))); -gulp.task('build-bundle-prod', gulp.series(makeWebpackPkg, gulpBundle.bind(null, false))); +gulp.task('build-bundle-prod', gulp.series(makeWebpackPkg, addBanner, gulpBundle.bind(null, false))); // public tasks (dependencies are needed for each task since they can be ran on their own) gulp.task('test-only', test); @@ -417,7 +429,7 @@ gulp.task('serve-fast', gulp.series(clean, gulp.parallel('build-bundle-dev', wat gulp.task('serve-and-test', gulp.series(clean, gulp.parallel('build-bundle-dev', watchFast, testTaskMaker({watch: true})))); gulp.task('serve-fake', gulp.series(clean, gulp.parallel('build-bundle-dev', watch), injectFakeServerEndpointDev, test, startFakeServer)); -gulp.task('default', gulp.series(clean, makeWebpackPkg)); +gulp.task('default', gulp.series(clean, 'build-bundle-prod')); gulp.task('e2e-test', gulp.series(clean, setupE2e, gulp.parallel('build-bundle-prod', watch), injectFakeServerEndpoint, test)); // other tasks diff --git a/integrationExamples/gpt/permutiveRtdProvider_example.html b/integrationExamples/gpt/permutiveRtdProvider_example.html index b6a22096c90..dbb4d2af0d6 100644 --- a/integrationExamples/gpt/permutiveRtdProvider_example.html +++ b/integrationExamples/gpt/permutiveRtdProvider_example.html @@ -45,6 +45,12 @@ } }, bids: [ + { + bidder: 'ix', + params: { + siteId: '123456', + } + }, { bidder: 'appnexus', params: { @@ -135,6 +141,7 @@ pbjs.que.push(function() { pbjs.setConfig({ debug: true, + pageUrl: 'http://www.test.com/test.html', realTimeData: { auctionDelay: 80, // maximum time for RTD modules to respond dataProviders: [ @@ -142,8 +149,20 @@ name: 'permutive', waitForIt: true, params: { - acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx'], + acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'], maxSegs: 500, + transformations: [ + { + id: 'iab', + config: { + segtax: 4, + iabIds: { + 1000001: '777777', + 1000002: '888888' + } + } + } + ], overwrites: { rubicon: function (bid, data, acEnabled, utils, defaultFn) { if (defaultFn){ @@ -160,7 +179,7 @@ } }); pbjs.setBidderConfig({ - bidders: ['appnexus', 'rubicon'], + bidders: ['appnexus', 'rubicon', 'ix'], config: { ortb2: { site: { @@ -180,13 +199,9 @@ gender: 'm', keywords: 'a,b', data: [ - { - name: 'www.dataprovider1.com', - ext: { taxonomyname: 'iab_audience_taxonomy' }, - segment: [{ id: '687' }, { id: '123' }] - }, { name: 'permutive.com', + ext: { segtax: 6 }, segment: [{ id: '1' }] } ] diff --git a/modules/admanBidAdapter.js b/modules/admanBidAdapter.js index 666e9aea309..241864c50fc 100644 --- a/modules/admanBidAdapter.js +++ b/modules/admanBidAdapter.js @@ -5,7 +5,7 @@ import {config} from '../src/config.js'; const BIDDER_CODE = 'adman'; const AD_URL = 'https://pub.admanmedia.com/?c=o&m=multi'; -const URL_SYNC = 'https://pub.admanmedia.com'; +const URL_SYNC = 'https://sync.admanmedia.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || diff --git a/modules/admaruBidAdapter.js b/modules/admaruBidAdapter.js new file mode 100644 index 00000000000..65f62c77e26 --- /dev/null +++ b/modules/admaruBidAdapter.js @@ -0,0 +1,81 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const ADMARU_ENDPOINT = 'https://p1.admaru.net/AdCall'; +const BIDDER_CODE = 'admaru'; + +const DEFAULT_BID_TTL = 360; + +function parseBid(rawBid, currency) { + const bid = {}; + + bid.cpm = rawBid.price; + bid.impid = rawBid.impid; + bid.requestId = rawBid.impid; + bid.netRevenue = true; + bid.dealId = ''; + bid.creativeId = rawBid.crid; + bid.currency = currency; + bid.ad = rawBid.adm; + bid.width = rawBid.w; + bid.height = rawBid.h; + bid.mediaType = BANNER; + bid.ttl = DEFAULT_BID_TTL; + + return bid; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!(bid && bid.params && bid.params.pub_id && bid.params.adspace_id); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + return validBidRequests.map(bid => { + const payload = { + pub_id: bid.params.pub_id, + adspace_id: bid.params.adspace_id, + bidderRequestId: bid.bidderRequestId, + bidId: bid.bidId + }; + + return { + method: 'GET', + url: ADMARU_ENDPOINT, + data: payload, + } + }) + }, + + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + let bid = null; + + if (!serverResponse.hasOwnProperty('body') || !serverResponse.body.hasOwnProperty('seatbid')) { + return bidResponses; + } + + const serverBody = serverResponse.body; + const seatbid = serverBody.seatbid; + + for (let i = 0; i < seatbid.length; i++) { + if (!seatbid[i].hasOwnProperty('bid')) { + continue; + } + + const innerBids = seatbid[i].bid; + for (let j = 0; j < innerBids.length; j++) { + bid = parseBid(innerBids[j], serverBody.cur); + + bidResponses.push(bid); + } + } + + return bidResponses; + } +} + +registerBidder(spec); diff --git a/modules/admaruBidAdapter.md b/modules/admaruBidAdapter.md new file mode 100644 index 00000000000..9985a660ac6 --- /dev/null +++ b/modules/admaruBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: Admaru Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@admaru.com +``` + +# Description + +Module that connects to Admaru demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: "admaru", + params: { + pub_id: '1234', // string - required + adspace_id: '1234' // string - required + } + } + ] + } + ]; +``` diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index f309ed4e96e..d8638c4da47 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -20,6 +20,7 @@ const HOST_GETTERS = { onefiftytwomedia: () => 'ghb.ads.152media.com', bidsxchange: () => 'ghb.hbd.bidsxchange.com', streamkey: () => 'ghb.hb.streamkey.net', + janet: () => 'ghb.bidder.jmgads.com', } const getUri = function (bidderCode) { let bidderWithoutSuffix = bidderCode.split('_')[0]; @@ -35,7 +36,7 @@ const syncsCache = {}; export const spec = { code: BIDDER_CODE, gvlid: 410, - aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa', 'bidsxchange', 'streamkey', + aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa', 'bidsxchange', 'streamkey', 'janet', { code: 'navelix', gvlid: 380 } ], supportedMediaTypes: [VIDEO, BANNER], diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 2758fc2d03a..41fad3caba3 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -21,7 +21,8 @@ import { logInfo, logMessage, logWarn, - transformBidderParamKeywords + transformBidderParamKeywords, + getWindowFromDocument } from '../src/utils.js'; import {Renderer} from '../src/Renderer.js'; import {config} from '../src/config.js'; @@ -271,6 +272,10 @@ export const spec = { rd_ifs: bidderRequest.refererInfo.numIframes, rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',') } + let pubPageUrl = config.getConfig('pageUrl'); + if (isStr(pubPageUrl) && pubPageUrl !== '') { + refererinfo.rd_can = pubPageUrl; + } payload.referrer_detection = refererinfo; } @@ -699,7 +704,10 @@ function newBid(serverBid, rtbBid, bidderRequest) { if (rtbBid.renderer_url) { const videoBid = find(bidderRequest.bids, bid => bid.bidId === serverBid.uuid); - const rendererOptions = deepAccess(videoBid, 'renderer.options'); + let rendererOptions = deepAccess(videoBid, 'mediaTypes.video.renderer.options'); // mediaType definition has preference (shouldn't options be .config?) + if (!rendererOptions) { + rendererOptions = deepAccess(videoBid, 'renderer.options'); // second the adUnit definition has preference (shouldn't options be .config?) + } bid.renderer = newRenderer(bid.adUnitCode, rtbBid, rendererOptions); } break; @@ -802,6 +810,13 @@ function bidToTag(bid) { } if (bid.params.position) { tag.position = { 'above': 1, 'below': 2 }[bid.params.position] || 0; + } else { + let mediaTypePos = deepAccess(bid, `mediaTypes.banner.pos`) || deepAccess(bid, `mediaTypes.video.pos`); + // only support unknown, atf, and btf values for position at this time + if (mediaTypePos === 0 || mediaTypePos === 1 || mediaTypePos === 3) { + // ortb spec treats btf === 3, but our system interprets btf === 2; so converting the ortb value here for consistency + tag.position = (mediaTypePos === 3) ? 2 : mediaTypePos; + } } if (bid.params.trafficSourceCode) { tag.traffic_source_code = bid.params.trafficSourceCode; @@ -1120,9 +1135,13 @@ function buildNativeRequest(params) { * @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'); + try { + const el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']"); + if (el[0]) { + el[0].style.setProperty('display', 'none'); + } + } catch (e) { + // element not found! } } @@ -1138,12 +1157,13 @@ function hideSASIframe(elementId) { } } -function outstreamRender(bid) { +function outstreamRender(bid, doc) { hidedfpContainer(bid.adUnitCode); hideSASIframe(bid.adUnitCode); // push to render queue because ANOutstreamVideo may not be loaded yet bid.renderer.push(() => { - window.ANOutstreamVideo.renderAd({ + const win = getWindowFromDocument(doc) || window; + win.ANOutstreamVideo.renderAd({ tagId: bid.adResponse.tag_id, sizes: [bid.getSize().split('x')], targetId: bid.adUnitCode, // target div id to render video diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index 04dccf563e6..acf574a3fe2 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -118,7 +118,7 @@ export const spec = { mediaType: serverResponse.body[i].mediaType, ttl: serverResponse.body[i].ttl, creativeId: serverResponse.body[i].creativeid, - currency: serverResponse.body[i].currency || 'RUB', + currency: serverResponse.body[i].currency || 'USD', netRevenue: serverResponse.body[i].netRevenue || true, ad: serverResponse.body[i].ad, meta: { @@ -158,10 +158,16 @@ export const spec = { // type: 'iframe', // url: 'https://acdn.adnxs.com/dmp/async_usersync.html' // }); - syncs.push({ - type: 'iframe', - url: 'https://ads.betweendigital.com/sspmatch-iframe' - }); + syncs.push( + { + type: 'iframe', + url: 'https://ads.betweendigital.com/sspmatch-iframe' + }, + { + type: 'image', + url: 'https://ads.betweendigital.com/sspmatch' + } + ); return syncs; } } diff --git a/modules/biddoBidAdapter.js b/modules/biddoBidAdapter.js new file mode 100644 index 00000000000..5512ca60f8e --- /dev/null +++ b/modules/biddoBidAdapter.js @@ -0,0 +1,92 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'biddo'; +const ENDPOINT_URL = 'https://ad.adopx.net/delivery/impress'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bidRequest The bid request params to validate. + * @return boolean True if this is a valid bid request, and false otherwise. + */ + isBidRequestValid: function(bidRequest) { + return !!bidRequest.params.zoneId; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {Array} validBidRequests an array of bid requests + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests) { + let serverRequests = []; + + validBidRequests.forEach(bidRequest => { + const sizes = bidRequest.mediaTypes.banner.sizes; + + sizes.forEach(([width, height]) => { + bidRequest.params.requestedSizes = [width, height]; + + const payload = { + ctype: 'div', + pzoneid: bidRequest.params.zoneId, + width, + height, + }; + + const payloadString = Object.keys(payload).map(k => k + '=' + encodeURIComponent(payload[k])).join('&'); + + serverRequests.push({ + method: 'GET', + url: ENDPOINT_URL, + data: payloadString, + bidderRequest: bidRequest, + }); + }); + }); + + return serverRequests; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidderRequest A matched bid request for this response. + * @return Array An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, {bidderRequest}) { + const response = serverResponse.body; + const bidResponses = []; + + if (response && response.template && response.template.html) { + const {bidId} = bidderRequest; + const [width, height] = bidderRequest.params.requestedSizes; + + const bidResponse = { + requestId: bidId, + cpm: response.hb.cpm, + creativeId: response.banner.hash, + currency: 'USD', + netRevenue: response.hb.netRevenue, + ttl: 600, + ad: response.template.html, + mediaType: 'banner', + meta: { + advertiserDomains: response.hb.adomains || [], + }, + width, + height, + }; + + bidResponses.push(bidResponse); + } + + return bidResponses; + }, +} + +registerBidder(spec); diff --git a/modules/biddoBidAdapter.md b/modules/biddoBidAdapter.md new file mode 100644 index 00000000000..baea44b22f2 --- /dev/null +++ b/modules/biddoBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +``` +Module Name: Biddo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: contact@biddo.net +``` + +# Description + +Module that connects to Invamia demand sources. + +# Test Parameters + +``` + const adUnits = [{ + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [{ + bidder: 'biddo', + params: { + zoneId: 7254, + }, + }], + }]; +``` diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index 94265617d8f..c1b6e31ff2e 100644 --- a/modules/colossussspBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -61,12 +61,33 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { - const winTop = getWindowTop(); - const location = winTop.location; + let deviceWidth = 0; + let deviceHeight = 0; + let winLocation; + + try { + const winTop = getWindowTop(); + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.referer; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + + const location = refferLocation || winLocation; let placements = []; let request = { - deviceWidth: winTop.screen.width, - deviceHeight: winTop.screen.height, + deviceWidth, + deviceHeight, language: (navigator && navigator.language) ? navigator.language : '', secure: location.protocol === 'https:' ? 1 : 0, host: location.host, diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 65e0d6e92eb..f0355749055 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -34,23 +34,22 @@ const cmpCallMap = { /** * This function reads the consent string from the config to obtain the consent information of the user. - * @param {function(string)} cmpSuccess acts as a success callback when the value is read from config; pass along consentObject (string) from CMP - * @param {function(string)} cmpError acts as an error callback while interacting with the config string; pass along an error message (string) - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * @param {function({})} onSuccess acts as a success callback when the value is read from config; pass along consentObject from CMP */ -function lookupStaticConsentData(cmpSuccess, cmpError, hookConfig) { - cmpSuccess(staticConsentData, hookConfig); +function lookupStaticConsentData({onSuccess}) { + onSuccess(staticConsentData); } /** * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function * based on the appropriate result. - * @param {function(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP - * @param {function(string)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP + * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) + * @param width + * @param height size info passed to the SafeFrame API (used only for TCFv1 when Prebid is running within a safeframe) */ -function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { +function lookupIabConsent({onSuccess, onError, width, height}) { function findCMP() { let f = window; let cmpFrame; @@ -100,10 +99,10 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { logInfo('Received a response from CMP', tcfData); if (success) { if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { - cmpSuccess(tcfData, hookConfig); + processCmpData(tcfData, {onSuccess, onError}); } } else { - cmpError('CMP unable to register callback function. Please check CMP setup.', hookConfig); + onError('CMP unable to register callback function. Please check CMP setup.'); } } @@ -113,7 +112,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { function afterEach() { if (cmpResponse.getConsentData && cmpResponse.getVendorConsents) { logInfo('Received all requested responses from CMP', cmpResponse); - cmpSuccess(cmpResponse, hookConfig); + processCmpData(cmpResponse, {onSuccess, onError}); } } @@ -134,7 +133,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { let { cmpFrame, cmpFunction } = findCMP(); if (!cmpFrame) { - return cmpError('CMP not found.', hookConfig); + return onError('CMP not found.'); } // to collect the consent information from the user, we perform two calls to the CMP in parallel: // first to collect the user's consent choices represented in an encoded string (via getConsentData) @@ -181,16 +180,6 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { } } - // find sizes from adUnits object - let adUnits = hookConfig.adUnits; - let width = 1; - let height = 1; - if (Array.isArray(adUnits) && adUnits.length > 0) { - let sizes = getAdUnitSizes(adUnits[0]); - width = sizes[0][0]; - height = sizes[0][1]; - } - window.$sf.ext.register(width, height, sfCallback); window.$sf.ext.cmp(commandName); } @@ -259,6 +248,70 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { } } +/** + * Look up consent data and store it in the `consentData` global as well as `adapterManager.js`' gdprDataHandler. + * + * @param cb A callback that takes: a boolean that is true if the auction should be canceled; an error message and extra + * error arguments that will be undefined if there's no error. + * @param width if we are running in an iframe, the TCFv1 spec requires us to use the SafeFrame API to find the CMP - which + * in turn requires width and height. + * @param height see width above + */ +function loadConsentData(cb, width = 1, height = 1) { + let isDone = false; + let timer = null; + + function done(consentData, shouldCancelAuction, errMsg, ...extraArgs) { + if (timer != null) { + clearTimeout(timer); + } + isDone = true; + gdprDataHandler.setConsentData(consentData); + if (cb != null) { + cb(shouldCancelAuction, errMsg, ...extraArgs); + } + } + + if (!includes(Object.keys(cmpCallMap), userCMP)) { + done(null, false, `CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + return; + } + + const callbacks = { + onSuccess: (data) => done(data, false), + onError: function (msg, ...extraArgs) { + let consentData = null; + let shouldCancelAuction = true; + if (allowAuction.value && cmpVersion === 1) { + // still set the consentData to undefined when there is a problem as per config options + consentData = storeConsentData(undefined); + shouldCancelAuction = false; + } + done(consentData, shouldCancelAuction, msg, ...extraArgs); + } + } + cmpCallMap[userCMP]({ + width, + height, + ...callbacks + }); + + if (!isDone) { + if (consentTimeout === 0) { + processCmpData(undefined, callbacks); + } else { + timer = setTimeout(function () { + if (cmpVersion === 2) { + // for TCFv2, we allow the auction to continue on timeout + done(storeConsentData(undefined), false, `No response from CMP, continuing auction...`) + } else { + callbacks.onError('CMP workflow exceeded timeout threshold.'); + } + }, consentTimeout); + } + } +} + /** * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the * user's encoded consent string from the supported CMP. Once obtained, the module will store this @@ -268,49 +321,60 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { * @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 = { - context: this, - args: [reqBidsConfigObj], - nextFn: fn, - adUnits: reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits, - bidsBackHandler: reqBidsConfigObj.bidsBackHandler, - haveExited: false, - timer: null - }; - - // in case we already have consent (eg during bid refresh) - if (consentData) { - logInfo('User consent information already known. Pulling internally stored information...'); - return exitModule(null, hookConfig); - } + const load = (() => { + if (consentData) { + logInfo('User consent information already known. Pulling internally stored information...'); + return function (cb) { + // eslint-disable-next-line standard/no-callback-literal + cb(false); + } + } else { + // find sizes from adUnits object + let adUnits = reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits; + let width = 1; + let height = 1; + if (Array.isArray(adUnits) && adUnits.length > 0) { + let sizes = getAdUnitSizes(adUnits[0]); + width = sizes[0][0]; + height = sizes[0][1]; + } - if (!includes(Object.keys(cmpCallMap), userCMP)) { - logWarn(`CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); - gdprDataHandler.setConsentData(null); - return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args); - } + return function (cb) { + loadConsentData(cb, width, height); + } + } + })(); - cmpCallMap[userCMP].call(this, processCmpData, cmpFailed, hookConfig); + load(function (shouldCancelAuction, errMsg, ...extraArgs) { + if (errMsg) { + let log = logWarn; + if (cmpVersion === 1 && !shouldCancelAuction) { + errMsg = `${errMsg} 'allowAuctionWithoutConsent' activated.`; + } else if (shouldCancelAuction) { + log = logError; + errMsg = `${errMsg} Canceling auction as per consentManagement config.`; + } + log(errMsg, ...extraArgs); + } - // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished) - if (!hookConfig.haveExited) { - if (consentTimeout === 0) { - processCmpData(undefined, hookConfig); + if (shouldCancelAuction) { + if (typeof reqBidsConfigObj.bidsBackHandler === 'function') { + reqBidsConfigObj.bidsBackHandler(); + } else { + logError('Error executing bidsBackHandler'); + } } else { - hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), consentTimeout); + fn.call(this, reqBidsConfigObj); } - } + }); } /** * This function checks the consent data provided by CMP to ensure it's in an expected state. - * If it's bad, we exit the module depending on config settings. - * If it's good, then we store the value and exits the module. - * @param {object} consentObject required; object returned by CMP that contains user's consent choices - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * If it's bad, we call `onError` + * If it's good, then we store the value and call `onSuccess` */ -function processCmpData(consentObject, hookConfig) { +function processCmpData(consentObject, {onSuccess, onError}) { function checkV1Data(consentObject) { let gdprApplies = consentObject && consentObject.getConsentData && consentObject.getConsentData.gdprApplies; return !!( @@ -346,57 +410,19 @@ function processCmpData(consentObject, hookConfig) { // determine which set of checks to run based on cmpVersion let checkFn = (cmpVersion === 1) ? checkV1Data : (cmpVersion === 2) ? checkV2Data : null; - // Raise deprecation warning if 'allowAuctionWithoutConsent' is used with TCF 2. - if (allowAuction.definedInConfig && cmpVersion === 2) { - logWarn(`'allowAuctionWithoutConsent' ignored for TCF 2`); - } else if (!allowAuction.definedInConfig && cmpVersion === 1) { - logInfo(`'allowAuctionWithoutConsent' using system default: (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); - } - if (isFn(checkFn)) { if (checkFn(consentObject)) { - cmpFailed(`CMP returned unexpected value during lookup process.`, hookConfig, consentObject); + onError(`CMP returned unexpected value during lookup process.`, consentObject); } else { - clearTimeout(hookConfig.timer); - storeConsentData(consentObject); - exitModule(null, hookConfig); + onSuccess(storeConsentData(consentObject)); } } else { - cmpFailed('Unable to derive CMP version to process data. Consent object does not conform to TCF v1 or v2 specs.', hookConfig, consentObject); + onError('Unable to derive CMP version to process data. Consent object does not conform to TCF v1 or v2 specs.', consentObject); } } /** - * General timeout callback when interacting with CMP takes too long. - */ -function cmpTimedOut(hookConfig) { - if (cmpVersion === 2) { - logWarn(`No response from CMP, continuing auction...`) - storeConsentData(undefined); - exitModule(null, hookConfig) - } else { - cmpFailed('CMP workflow exceeded timeout threshold.', hookConfig); - } -} - -/** - * This function contains the controlled steps to perform when there's a problem with CMP. - * @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened. - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) - * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging -*/ -function cmpFailed(errMsg, hookConfig, extraArgs) { - clearTimeout(hookConfig.timer); - - // still set the consentData to undefined when there is a problem as per config options - if (allowAuction.value && cmpVersion === 1) { - storeConsentData(undefined); - } - exitModule(errMsg, hookConfig, extraArgs); -} - -/** - * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanager.js for later in the auction + * Stores CMP data locally in module to make information available in adaptermanager.js for later in the auction * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) */ function storeConsentData(cmpConsentObject) { @@ -417,51 +443,7 @@ function storeConsentData(cmpConsentObject) { }; } consentData.apiVersion = cmpVersion; - gdprDataHandler.setConsentData(consentData); -} - -/** - * This function handles the exit logic for the module. - * While there are several paths in the module's logic to call this function, we only allow 1 of the 3 potential exits to happen before suppressing others. - * - * We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios. - * One scenario could be auction was canceled due to timeout with CMP being reached. - * While the timeout is the accepted exit and runs first, the CMP's callback still tries to process the user's data (which normally leads to a good exit). - * In this case, the good exit will be suppressed since we already decided to cancel the auction. - * - * Three exit paths are: - * 1. good exit where auction runs (CMP data is processed normally). - * 2. bad exit but auction still continues (warning message is logged, CMP data is undefined and still passed along). - * 3. bad exit with auction canceled (error message is logged). - * @param {string} errMsg optional; only to be used when there was a 'bad' exit. String is a descriptive message for the failure/issue encountered. - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) - * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging - */ -function exitModule(errMsg, hookConfig, extraArgs) { - if (hookConfig.haveExited === false) { - hookConfig.haveExited = true; - - let context = hookConfig.context; - let args = hookConfig.args; - let nextFn = hookConfig.nextFn; - - if (errMsg) { - if (allowAuction.value && cmpVersion === 1) { - logWarn(errMsg + ` 'allowAuctionWithoutConsent' activated.`, extraArgs); - nextFn.apply(context, args); - } else { - logError(errMsg + ' Canceling auction as per consentManagement config.', extraArgs); - gdprDataHandler.setConsentData(null); - if (typeof hookConfig.bidsBackHandler === 'function') { - hookConfig.bidsBackHandler(); - } else { - logError('Error executing bidsBackHandler'); - } - } - } else { - nextFn.apply(context, args); - } - } + return consentData; } /** @@ -509,7 +491,6 @@ export function setConsentConfig(config) { gdprScope = config.defaultGdprScope === true; logInfo('consentManagement module has been activated...'); - gdprDataHandler.enable(); if (userCMP === 'static') { if (isPlainObject(config.consentData)) { @@ -523,5 +504,14 @@ export function setConsentConfig(config) { $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); } addedConsentHook = true; + gdprDataHandler.enable(); + loadConsentData(); // immediately look up consent data to make it available without requiring an auction + + // Raise deprecation warning if 'allowAuctionWithoutConsent' is used with TCF 2. + if (allowAuction.definedInConfig && cmpVersion === 2) { + logWarn(`'allowAuctionWithoutConsent' ignored for TCF 2`); + } else if (!allowAuction.definedInConfig && cmpVersion === 1) { + logInfo(`'allowAuctionWithoutConsent' using system default: (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); + } } config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 75462221403..e98b41d5c9e 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -27,23 +27,17 @@ const uspCallMap = { /** * This function reads the consent string from the config to obtain the consent information of the user. - * @param {function(string)} cmpSuccess acts as a success callback when the value is read from config; pass along consentObject (string) from CMP - * @param {function(string)} cmpError acts as an error callback while interacting with the config string; pass along an error message (string) - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) */ -function lookupStaticConsentData(cmpSuccess, cmpError, hookConfig) { - cmpSuccess(staticConsentData, hookConfig); +function lookupStaticConsentData({onSuccess}) { + onSuccess(staticConsentData); } /** * This function handles interacting with an USP compliant consent manager to obtain the consent information of the user. * Given the async nature of the USP's API, we pass in acting success/error callback functions to exit this function * based on the appropriate result. - * @param {function(string)} uspSuccess acts as a success callback when USPAPI returns a value; pass along consentObject (string) from USPAPI - * @param {function(string)} uspError acts as an error callback while interacting with USPAPI; pass along an error message (string) - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) */ -function lookupUspConsent(uspSuccess, uspError, hookConfig) { +function lookupUspConsent({onSuccess, onError}) { function findUsp() { let f = window; let uspapiFrame; @@ -78,9 +72,9 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { function afterEach() { if (uspResponse.usPrivacy) { - uspSuccess(uspResponse, hookConfig); + processUspData(uspResponse, {onSuccess, onError}) } else { - uspError('Unable to get USP consent string.', hookConfig); + onError('Unable to get USP consent string.'); } } @@ -100,7 +94,7 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { let { uspapiFrame, uspapiFunction } = findUsp(); if (!uspapiFrame) { - return uspError('USP CMP not found.', hookConfig); + return onError('USP CMP not found.'); } // to collect the consent information from the user, we perform a call to USPAPI @@ -165,121 +159,92 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { } /** - * If consentManagementUSP module is enabled (ie included in setConfig), this hook function will attempt to fetch the - * user's encoded consent string from the supported USPAPI. Once obtained, the module will store this - * data as part of a uspConsent object which gets transferred to adapterManager's uspDataHandler object. - * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. - * @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 + * Lookup consent data and store it in the `consentData` global as well as `adapterManager.js`' uspDataHanlder. + * + * @param cb a callback that takes an error message and extra error arguments; all args will be undefined if consent + * data was retrieved successfully. */ -export function requestBidsHook(fn, reqBidsConfigObj) { - // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions) - const hookConfig = { - context: this, - args: [reqBidsConfigObj], - nextFn: fn, - adUnits: reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits, - bidsBackHandler: reqBidsConfigObj.bidsBackHandler, - haveExited: false, - timer: null - }; +function loadConsentData(cb) { + let timer = null; + let isDone = false; + + function done(consentData, errMsg, ...extraArgs) { + if (timer != null) { + clearTimeout(timer); + } + isDone = true; + uspDataHandler.setConsentData(consentData); + if (cb != null) { + cb(errMsg, ...extraArgs) + } + } if (!uspCallMap[consentAPI]) { - logWarn(`USP framework (${consentAPI}) is not a supported framework. Aborting consentManagement module and resuming auction.`); - uspDataHandler.setConsentData(null); - return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args); + done(null, `USP framework (${consentAPI}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + return; + } + + const callbacks = { + onSuccess: done, + onError: function (errMsg, ...extraArgs) { + done(null, `${errMsg} Resuming auction without consent data as per consentManagement config.`, ...extraArgs); + } } - uspCallMap[consentAPI].call(this, processUspData, uspapiFailed, hookConfig); + uspCallMap[consentAPI](callbacks); - // only let this code run if module is still active (ie if the callbacks used by USPs haven't already finished) - if (!hookConfig.haveExited) { + if (!isDone) { if (consentTimeout === 0) { - processUspData(undefined, hookConfig); + processUspData(undefined, callbacks); } else { - hookConfig.timer = setTimeout(uspapiTimeout.bind(null, hookConfig), consentTimeout); + timer = setTimeout(callbacks.onError.bind(null, 'USPAPI workflow exceeded timeout threshold.'), consentTimeout) } } } +/** + * If consentManagementUSP module is enabled (ie included in setConfig), this hook function will attempt to fetch the + * user's encoded consent string from the supported USPAPI. Once obtained, the module will store this + * data as part of a uspConsent object which gets transferred to adapterManager's uspDataHandler object. + * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. + * @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) { + loadConsentData((errMsg, ...extraArgs) => { + if (errMsg != null) { + logWarn(errMsg, ...extraArgs); + } + fn.call(this, reqBidsConfigObj); + }); +} + /** * This function checks the consent data provided by USPAPI to ensure it's in an expected state. * If it's bad, we exit the module depending on config settings. * If it's good, then we store the value and exits the module. * @param {object} consentObject required; object returned by USPAPI that contains user's consent choices - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * @param {function(string)} onSuccess callback accepting the resolved consent USP consent string + * @param {function(string, ...{}?)} onError callback accepting error message and any extra error arguments (used purely for logging) */ -function processUspData(consentObject, hookConfig) { +function processUspData(consentObject, {onSuccess, onError}) { const valid = !!(consentObject && consentObject.usPrivacy); if (!valid) { - uspapiFailed(`USPAPI returned unexpected value during lookup process.`, hookConfig, consentObject); + onError(`USPAPI returned unexpected value during lookup process.`, consentObject); return; } - clearTimeout(hookConfig.timer); storeUspConsentData(consentObject); - exitModule(null, hookConfig); -} - -/** - * General timeout callback when interacting with USPAPI takes too long. - */ -function uspapiTimeout(hookConfig) { - uspapiFailed('USPAPI workflow exceeded timeout threshold.', hookConfig); -} - -/** - * This function contains the controlled steps to perform when there's a problem with USPAPI. - * @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened. - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) - * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging -*/ -function uspapiFailed(errMsg, hookConfig, extraArgs) { - clearTimeout(hookConfig.timer); - - exitModule(errMsg, hookConfig, extraArgs); + onSuccess(consentData); } /** * Stores USP data locally in module and then invokes uspDataHandler.setConsentData() to make information available in adaptermanger.js for later in the auction - * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) + * @param {object} consentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) */ function storeUspConsentData(consentObject) { if (consentObject && consentObject.usPrivacy) { consentData = consentObject.usPrivacy; - uspDataHandler.setConsentData(consentData); - } -} - -/** - * This function handles the exit logic for the module. - * There are a couple paths in the module's logic to call this function and we only allow 1 of the 2 potential exits to happen before suppressing others. - * - * We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios. - * One scenario could be auction was canceled due to timeout with USPAPI being reached. - * While the timeout is the accepted exit and runs first, the USP's callback still tries to process the user's data (which normally leads to a good exit). - * In this case, the good exit will be suppressed since we already decided to cancel the auction. - * - * Three exit paths are: - * 1. good exit where auction runs (USPAPI data is processed normally). - * 2. bad exit but auction still continues (warning message is logged, USPAPI data is undefined and still passed along). - * @param {string} errMsg optional; only to be used when there was a 'bad' exit. String is a descriptive message for the failure/issue encountered. - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) - * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging - */ -function exitModule(errMsg, hookConfig, extraArgs) { - if (hookConfig.haveExited === false) { - hookConfig.haveExited = true; - - let context = hookConfig.context; - let args = hookConfig.args; - let nextFn = hookConfig.nextFn; - - if (errMsg) { - logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.', extraArgs); - uspDataHandler.setConsentData(null) // let core know that no consent data is available - } - nextFn.apply(context, args); } } @@ -317,7 +282,6 @@ export function setConsentConfig(config) { } logInfo('USPAPI consentManagement module has been activated...'); - uspDataHandler.enable(); if (consentAPI === 'static') { if (isPlainObject(config.consentData) && isPlainObject(config.consentData.getUSPData)) { @@ -331,5 +295,7 @@ export function setConsentConfig(config) { $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); } addedConsentHook = true; + uspDataHandler.enable(); + loadConsentData(); // immediately look up consent data to make it available without requiring an auction } config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); diff --git a/modules/consumableBidAdapter.js b/modules/consumableBidAdapter.js index 1a2845ba85b..de08fc8677a 100644 --- a/modules/consumableBidAdapter.js +++ b/modules/consumableBidAdapter.js @@ -122,10 +122,30 @@ export const spec = { bid.currency = 'USD'; bid.creativeId = decision.adId; bid.ttl = 30; - bid.meta = { advertiserDomains: decision.adomain ? decision.adomain : [] } bid.netRevenue = true; bid.referrer = bidRequest.bidderRequest.refererInfo.referer; + bid.meta = { + advertiserDomains: decision.adomain || [] + }; + + if (decision.cats) { + if (decision.cats.length > 0) { + bid.meta.primaryCatId = decision.cats[0]; + if (decision.cats.length > 1) { + bid.meta.secondaryCatIds = decision.cats.slice(1); + } + } + } + + if (decision.networkId) { + bid.meta.networkId = decision.networkId; + } + + if (decision.mediaType) { + bid.meta.mediaType = decision.mediaType; + } + bidResponses.push(bid); } } @@ -136,13 +156,15 @@ export const spec = { getUserSyncs: function(syncOptions, serverResponses) { if (syncOptions.iframeEnabled) { - return [{ - type: 'iframe', - url: 'https://sync.serverbid.com/ss/' + siteId + '.html' - }]; + if (!serverResponses || serverResponses.length === 0 || !serverResponses[0].body.bdr || serverResponses[0].body.bdr !== 'cx') { + return [{ + type: 'iframe', + url: 'https://sync.serverbid.com/ss/' + siteId + '.html' + }]; + } } - if (syncOptions.pixelEnabled && serverResponses.length > 0) { + if (syncOptions.pixelEnabled && serverResponses && serverResponses.length > 0) { return serverResponses[0].body.pixels; } else { logWarn(bidder + ': Please enable iframe based user syncing.'); diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 2dfd9ea4386..7f8ad3351fa 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -88,7 +88,14 @@ export function buildDfpVideoUrl(options) { sz: parseSizesInput(deepAccess(adUnit, 'mediaTypes.video.playerSize')).join('|'), url: encodeURIComponent(location.href), }; - const encodedCustomParams = getCustParams(bid, options); + + const urlSearchComponent = urlComponents.search; + const urlSzParam = urlSearchComponent && urlSearchComponent.sz + if (urlSzParam) { + derivedParams.sz = urlSzParam + '|' + derivedParams.sz; + } + + let encodedCustomParams = getCustParams(bid, options, urlSearchComponent && urlSearchComponent.cust_params); const queryParams = Object.assign({}, defaultParamConstants, @@ -111,12 +118,11 @@ export function buildDfpVideoUrl(options) { const uspConsent = uspDataHandler.getConsentData(); if (uspConsent) { queryParams.us_privacy = uspConsent; } - return buildUrl({ + return buildUrl(Object.assign({ protocol: 'https', host: 'securepubads.g.doubleclick.net', - pathname: '/gampad/ads', - search: queryParams - }); + pathname: '/gampad/ads' + }, urlComponents, { search: queryParams })); } export function notifyTranslationModule(fn) { @@ -227,9 +233,7 @@ function buildUrlFromAdserverUrlComponents(components, bid, options) { const descriptionUrl = getDescriptionUrl(bid, components, 'search'); if (descriptionUrl) { components.search.description_url = descriptionUrl; } - const encodedCustomParams = getCustParams(bid, options); - components.search.cust_params = (components.search.cust_params) ? components.search.cust_params + '%26' + encodedCustomParams : encodedCustomParams; - + components.search.cust_params = getCustParams(bid, options, components.search.cust_params); return buildUrl(components); } @@ -258,7 +262,7 @@ function getDescriptionUrl(bid, components, prop) { * @param {Object} options this is the options passed in from the `buildDfpVideoUrl` function * @return {Object} Encoded key value pairs for cust_params */ -function getCustParams(bid, options) { +function getCustParams(bid, options, urlCustParams) { const adserverTargeting = (bid && bid.adserverTargeting) || {}; let allTargetingData = {}; @@ -281,7 +285,12 @@ function getCustParams(bid, options) { // merge the prebid + publisher targeting sets const publisherTargetingSet = deepAccess(options, 'params.cust_params'); const targetingSet = Object.assign({}, prebidTargetingSet, publisherTargetingSet); - return encodeURIComponent(formatQS(targetingSet)); + let encodedParams = encodeURIComponent(formatQS(targetingSet)); + if (urlCustParams) { + encodedParams = urlCustParams + '%26' + encodedParams; + } + + return encodedParams; } registerVideoSupport('dfp', { diff --git a/modules/distroscaleBidAdapter.js b/modules/distroscaleBidAdapter.js new file mode 100644 index 00000000000..822bea3603a --- /dev/null +++ b/modules/distroscaleBidAdapter.js @@ -0,0 +1,262 @@ +import { logWarn, isPlainObject, isStr, isArray, isFn, inIframe, mergeDeep, deepSetValue, logError, deepClone } 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 = 'distroscale'; +const SHORT_CODE = 'ds'; +const LOG_WARN_PREFIX = 'DistroScale: '; +const ENDPOINT = 'https://hb.jsrdn.com/hb?from=pbjs'; +const DEFAULT_CURRENCY = 'USD'; +const AUCTION_TYPE = 1; +const GVLID = 754; +const UNDEF = undefined; + +const SUPPORTED_MEDIATYPES = [ BANNER ]; + +function _getHost(url) { + let a = document.createElement('a'); + a.href = url; + return a.hostname; +} + +function _getBidFloor(bid, mType, sz) { + if (isFn(bid.getFloor)) { + let floor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: mType || '*', + size: sz || '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY) { + return floor.floor; + } + } + return null; +} + +function _createImpressionObject(bid) { + var impObj = UNDEF; + var i; + var sizes = {}; + var sizesCount = 0; + + function addSize(arr) { + var w, h; + if (arr && arr.length > 1) { + w = parseInt(arr[0]); + h = parseInt(arr[1]); + } + sizes[w + 'x' + h] = { + w: w, + h: h, + area: w * h, + idx: + ({ + '970x250': 1, + '300x250': 2 + })[w + 'x' + h] || Math.max(w * h, 200) + }; + sizesCount++; + } + + // Gather all sizes + if (isArray(bid.sizes)) { + for (i = 0; i < bid.sizes.length; i++) { + addSize(bid.sizes[i]); + } + } + if (bid.params && bid.params.width && bid.params.height) { + addSize([bid.params.width, bid.params.height]); + } + if (bid.mediaTypes && BANNER in bid.mediaTypes && bid.mediaTypes[BANNER] && bid.mediaTypes[BANNER].sizes) { + for (i = 0; i < bid.mediaTypes[BANNER].sizes.length; i++) { + addSize(bid.mediaTypes[BANNER].sizes[i]); + } + } + if (sizesCount == 0) { + logWarn(LOG_WARN_PREFIX + 'Error: missing sizes: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); + } else { + // Use the first preferred size + var keys = Object.keys(sizes); + keys.sort(function(a, b) { + return sizes[a].idx - sizes[b].idx + }); + var bannerObj = { + pos: 0, + w: sizes[keys[0]].w, + h: sizes[keys[0]].h, + topframe: inIframe() ? 0 : 1, + format: [{ + 'w': sizes[keys[0]].w, + 'h': sizes[keys[0]].h + }] + }; + + impObj = { + id: bid.bidId, + tagid: bid.params.zoneid || '', + secure: 1, + ext: { + pubid: bid.params.pubid || '', + zoneid: bid.params.zoneid || '' + } + }; + + var floor = _getBidFloor(bid, BANNER, [sizes[keys[0]].w, sizes[keys[0]].h]); + if (floor > 0) { + impObj.bidfloor = floor; + impObj.bidfloorcur = DEFAULT_CURRENCY; + } + + impObj[BANNER] = bannerObj; + } + + return impObj; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: SUPPORTED_MEDIATYPES, + aliases: [SHORT_CODE], + + isBidRequestValid: bid => { + if (bid && bid.params && bid.params.pubid && isStr(bid.params.pubid)) { + return true; + } else { + logWarn(LOG_WARN_PREFIX + 'Error: pubid is mandatory and cannot be numeric'); + } + return false; + }, + + buildRequests: (validBidRequests, bidderRequest) => { + var pageUrl = (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) || window.location.href; + + var payload = { + id: '' + (new Date()).getTime(), + at: AUCTION_TYPE, + cur: [DEFAULT_CURRENCY], + site: { + page: pageUrl + }, + device: { + ua: navigator.userAgent, + js: 1, + h: screen.height, + w: screen.width, + language: (navigator.language && navigator.language.replace(/-.*/, '')) || 'en', + dnt: (navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1' || navigator.doNotTrack == 'yes') ? 1 : 0 + }, + imp: [], + user: {}, + ext: {} + }; + + validBidRequests.forEach(b => { + var bid = deepClone(b); + var impObj = _createImpressionObject(bid); + if (impObj) { + payload.imp.push(impObj); + } + }); + + if (payload.imp.length == 0) { + return; + } + + payload.site.domain = _getHost(payload.site.page); + + // add the content object from config in request + if (typeof config.getConfig('content') === 'object') { + payload.site.content = config.getConfig('content'); + } + + // merge the device from config.getConfig('device') + if (typeof config.getConfig('device') === 'object') { + payload.device = Object.assign(payload.device, config.getConfig('device')); + } + + // adding schain object + if (validBidRequests[0].schain) { + deepSetValue(payload, 'source.schain', validBidRequests[0].schain); + } + + // Attaching GDPR Consent Params + if (bidderRequest && bidderRequest.gdprConsent) { + deepSetValue(payload, 'user.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(payload, 'regs.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest && bidderRequest.uspConsent) { + deepSetValue(payload, 'regs.us_privacy', bidderRequest.uspConsent); + } + + // coppa compliance + if (config.getConfig('coppa') === true) { + deepSetValue(payload, 'regs.coppa', 1); + } + + // First Party Data + const commonFpd = config.getConfig('ortb2') || {}; + if (commonFpd.site) { + mergeDeep(payload, {site: commonFpd.site}); + } + if (commonFpd.user) { + mergeDeep(payload, {user: commonFpd.user}); + } + + // User IDs + if (validBidRequests[0].userIdAsEids && validBidRequests[0].userIdAsEids.length > 0) { + // Standard ORTB structure + deepSetValue(payload, 'user.eids', validBidRequests[0].userIdAsEids); + } else if (validBidRequests[0].userId && Object.keys(validBidRequests[0].userId).length > 0) { + // Fallback to non-ortb structure + deepSetValue(payload, 'user.ext.userId', validBidRequests[0].userId); + } + + return { + method: 'POST', + url: ENDPOINT, + data: payload, + bidderRequest: bidderRequest + }; + }, + + interpretResponse: (response, request) => { + const bidResponses = []; + try { + if (response.body && response.body.seatbid && isArray(response.body.seatbid)) { + // Supporting multiple bid responses for same adSize + response.body.seatbid.forEach(seatbidder => { + seatbidder.bid && + isArray(seatbidder.bid) && + seatbidder.bid.forEach(bid => { + let newBid = { + requestId: bid.impid, + cpm: (parseFloat(bid.price) || 0), + currency: DEFAULT_CURRENCY, + width: parseInt(bid.w), + height: parseInt(bid.h), + creativeId: bid.crid || bid.id, + netRevenue: true, + ttl: 300, + ad: bid.adm, + meta: { + advertiserDomains: [] + } + }; + if (isArray(bid.adomain) && bid.adomain.length > 0) { + newBid.meta.advertiserDomains = bid.adomain; + } + bidResponses.push(newBid); + }); + }); + } + } catch (error) { + logError(error); + } + return bidResponses; + } +}; + +registerBidder(spec); diff --git a/modules/distroscaleBidAdapter.md b/modules/distroscaleBidAdapter.md new file mode 100644 index 00000000000..1d7948b2a02 --- /dev/null +++ b/modules/distroscaleBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +``` +Module Name: DistroScale Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@distroscale.com +``` + +# Description + +Connects to DistroScale exchange for bids. DistroScale bid adapter supports Banner currently. + +# Test Parameters +``` +var adUnits = [{ + code: 'banner-1', + mediaTypes: { + banner: { + sizes: [[970, 250]], + } + }, + bids: [{ + bidder: 'distroscale', + params: { + pubid: '12345' // required, must be a string + ,zoneid: '67890' // optional, must be a string + } + }] +}]; +``` diff --git a/modules/gamoshiBidAdapter.js b/modules/gamoshiBidAdapter.js index 67fc9910825..22a70db0fab 100644 --- a/modules/gamoshiBidAdapter.js +++ b/modules/gamoshiBidAdapter.js @@ -111,17 +111,11 @@ export const spec = { source: {ext: {}}, regs: {ext: {}} }; - const gdprConsent = bidderRequest.gdprConsent; - if (gdprConsent && gdprConsent.consentString && gdprConsent.gdprApplies) { - rtbBidRequest.ext.gdpr_consent = { - consent_string: gdprConsent.consentString, - consent_required: gdprConsent.gdprApplies - }; - - deepSetValue(rtbBidRequest, 'regs.ext.gdpr', gdprConsent.gdprApplies === true ? 1 : 0); - deepSetValue(rtbBidRequest, 'user.ext.consent', gdprConsent.consentString); - } + const gdprConsent = getGdprConsent(bidderRequest); + rtbBidRequest.ext.gdpr_consent = gdprConsent; + deepSetValue(rtbBidRequest, 'regs.ext.gdpr', gdprConsent.consent_required === true ? 1 : 0); + deepSetValue(rtbBidRequest, 'user.ext.consent', gdprConsent.consent_string); if (validBidRequests[0].schain) { deepSetValue(rtbBidRequest, 'source.ext.schain', validBidRequests[0].schain); @@ -197,6 +191,7 @@ export const spec = { if (bidRequest && bidRequest.userId) { addExternalUserId(eids, deepAccess(bidRequest, `userId.id5id.uid`), 'id5-sync.com', 'ID5ID'); addExternalUserId(eids, deepAccess(bidRequest, `userId.tdid`), 'adserver.org', 'TDID'); + addExternalUserId(eids, deepAccess(bidRequest, `userId.idl_env`), 'liveramp.com', 'idl'); } if (eids.length > 0) { rtbBidRequest.user.ext.eids = eids; @@ -373,4 +368,20 @@ function replaceMacros(url, macros) { .replace('[US_PRIVACY]', macros.uspConsent); } +function getGdprConsent(bidderRequest) { + const gdprConsent = bidderRequest.gdprConsent; + + if (gdprConsent && gdprConsent.consentString && gdprConsent.gdprApplies) { + return { + consent_string: gdprConsent.consentString, + consent_required: gdprConsent.gdprApplies + }; + } + + return { + consent_required: false, + consent_string: '', + }; +} + registerBidder(spec); diff --git a/modules/glimpseBidAdapter.js b/modules/glimpseBidAdapter.js index 678e35729da..35aaf56c604 100644 --- a/modules/glimpseBidAdapter.js +++ b/modules/glimpseBidAdapter.js @@ -1,18 +1,27 @@ -import { BANNER } from '../src/mediaTypes.js' -import { config } from '../src/config.js' -import { getStorageManager } from '../src/storageManager.js' -import { isArray } from '../src/utils.js' -import { registerBidder } from '../src/adapters/bidderFactory.js' - -const GVLID = 1012 -const BIDDER_CODE = 'glimpse' -const storageManager = getStorageManager({bidderCode: BIDDER_CODE}) -const ENDPOINT = 'https://api.glimpsevault.io/ads/serving/public/v1/prebid' +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { + isArray, + isEmpty, + isEmptyStr, + isStr, + isPlainObject, +} from '../src/utils.js'; + +const GVLID = 1012; +const BIDDER_CODE = 'glimpse'; +const storageManager = getStorageManager({ + gvlid: GVLID, + bidderCode: BIDDER_CODE, +}); +const ENDPOINT = 'https://market.glimpsevault.io/public/v1/prebid'; const LOCAL_STORAGE_KEY = { vault: { jwt: 'gp_vault_jwt', }, -} +}; export const spec = { gvlid: GVLID, @@ -20,126 +29,121 @@ export const spec = { supportedMediaTypes: [BANNER], /** - * Determines whether or not the given bid request is valid + * Determines if the bid request is valid * @param bid {BidRequest} The bid to validate * @return {boolean} */ isBidRequestValid: (bid) => { - return ( - hasValue(bid) && - hasValue(bid.params) && - hasStringValue(bid.params.placementId) - ) + const pid = bid?.params?.pid; + return isStr(pid) && !isEmptyStr(pid); }, /** - * Builds http request for Glimpse bids + * Builds the http request * @param validBidRequests {BidRequest[]} * @param bidderRequest {BidderRequest} * @returns {ServerRequest} */ buildRequests: (validBidRequests, bidderRequest) => { - const auth = getVaultJwt() - const referer = getReferer(bidderRequest) - const gdprConsent = getGdprConsentChoice(bidderRequest) - const bidRequests = validBidRequests.map(processBidRequest) - const firstPartyData = getFirstPartyData() + const url = buildQuery(bidderRequest); + const auth = getVaultJwt(); + const referer = getReferer(bidderRequest); + const imp = validBidRequests.map(processBidRequest); + const fpd = getFirstPartyData(); const data = { auth, data: { referer, - gdprConsent, - bidRequests, - site: firstPartyData.site, - user: firstPartyData.user, - bidderCode: spec.code, - } - } + imp, + fpd, + }, + }; return { method: 'POST', - url: ENDPOINT, + url, data: JSON.stringify(data), options: {}, - } + }; }, /** - * Parse response from Glimpse server - * @param bidResponse {ServerResponse} + * Parse http response + * @param response {ServerResponse} * @returns {Bid[]} */ - interpretResponse: (bidResponse) => { - const isValidResponse = isValidBidResponse(bidResponse) - - if (isValidResponse) { - const {auth, data} = bidResponse.body - setVaultJwt(auth) - return data.bids + interpretResponse: (response) => { + if (isValidResponse(response)) { + const { auth, data } = response.body; + setVaultJwt(auth); + const bids = data.bids.map(processBidResponse); + return bids; } - - return [] + return []; }, -} +}; function setVaultJwt(auth) { - storageManager.setDataInLocalStorage(LOCAL_STORAGE_KEY.vault.jwt, auth) + storageManager.setDataInLocalStorage(LOCAL_STORAGE_KEY.vault.jwt, auth); } function getVaultJwt() { - return storageManager.getDataFromLocalStorage(LOCAL_STORAGE_KEY.vault.jwt) || '' + return ( + storageManager.getDataFromLocalStorage(LOCAL_STORAGE_KEY.vault.jwt) || '' + ); } function getReferer(bidderRequest) { - const hasReferer = - hasValue(bidderRequest) && - hasValue(bidderRequest.refererInfo) && - hasStringValue(bidderRequest.refererInfo.referer) - - if (hasReferer) { - return bidderRequest.refererInfo.referer - } - - return '' + return bidderRequest?.refererInfo?.referer || ''; } -function getGdprConsentChoice(bidderRequest) { - const hasGdprConsent = - hasValue(bidderRequest) && - hasValue(bidderRequest.gdprConsent) +function buildQuery(bidderRequest) { + let url = appendQueryParam(ENDPOINT, 'ver', '$prebid.version$'); - if (hasGdprConsent) { - const gdprConsent = bidderRequest.gdprConsent - const hasGdprApplies = hasBooleanValue(gdprConsent.gdprApplies) + const timeout = config.getConfig('bidderTimeout'); + url = appendQueryParam(url, 'tmax', timeout); - return { - consentString: gdprConsent.consentString || '', - vendorData: gdprConsent.vendorData || {}, - gdprApplies: hasGdprApplies ? gdprConsent.gdprApplies : true, - } + if (gdprApplies(bidderRequest)) { + const consentString = bidderRequest.gdprConsent.consentString; + url = appendQueryParam(url, 'gdpr', consentString); } - return { - consentString: '', - vendorData: {}, - gdprApplies: false, + if (ccpaApplies(bidderRequest)) { + url = appendQueryParam(url, 'ccpa', bidderRequest.uspConsent); + } + + return url; +} + +function appendQueryParam(url, key, value) { + if (!value) { + return url; } + const prefix = url.includes('?') ? '&' : '?'; + return `${url}${prefix}${key}=${encodeURIComponent(value)}`; +} + +function gdprApplies(bidderRequest) { + return Boolean(bidderRequest?.gdprConsent?.gdprApplies); +} + +function ccpaApplies(bidderRequest) { + return ( + isStr(bidderRequest.uspConsent) && + !isEmptyStr(bidderRequest.uspConsent) && + bidderRequest.uspConsent?.substr(1, 3) !== '---' + ); } -function processBidRequest(bidRequest) { - const demand = bidRequest.params.demand || 'glimpse' - const sizes = normalizeSizes(bidRequest.sizes) - const keywords = bidRequest.params.keywords || {} +function processBidRequest(bid) { + const sizes = normalizeSizes(bid.sizes); return { - demand, + bid: bid.bidId, + pid: bid.params.pid, sizes, - keywords, - bidId: bidRequest.bidId, - placementId: bidRequest.params.placementId, - unitCode: bidRequest.adUnitCode, - } + }; } function normalizeSizes(sizes) { @@ -147,84 +151,51 @@ function normalizeSizes(sizes) { isArray(sizes) && sizes.length === 2 && !isArray(sizes[0]) && - !isArray(sizes[1]) + !isArray(sizes[1]); if (isSingleSize) { - return [sizes] + return [sizes]; } - return sizes + return sizes; } function getFirstPartyData() { - const siteKeywords = parseGlobalKeywords('site') - const userKeywords = parseGlobalKeywords('user') - - const siteAttributes = getConfig('ortb2.site.ext.data', {}) - const userAttributes = getConfig('ortb2.user.ext.data', {}) - - return { - site: { - keywords: siteKeywords, - attributes: siteAttributes, - }, - user: { - keywords: userKeywords, - attributes: userAttributes, - }, - } -} - -function parseGlobalKeywords(scope) { - const keywords = getConfig(`ortb2.${scope}.keywords`, '') - - return keywords - .split(', ') - .filter((keyword) => keyword !== '') -} - -function getConfig(path, defaultValue) { - return config.getConfig(path) || defaultValue -} - -function isValidBidResponse(bidResponse) { - return ( - hasValue(bidResponse) && - hasValue(bidResponse.body) && - hasValue(bidResponse.body.data) && - hasArrayValue(bidResponse.body.data.bids) && - hasStringValue(bidResponse.body.auth) - ) + let fpd = config.getConfig('ortb2') || {}; + optimizeObject(fpd); + return fpd; } -function hasValue(value) { - return ( - value !== undefined && - value !== null - ) +function optimizeObject(obj) { + if (!isPlainObject(obj)) { + return; + } + for (const [key, value] of Object.entries(obj)) { + optimizeObject(value); + // only delete empty object, array, or string + if ( + (isPlainObject(value) || isArray(value) || isStr(value)) && + isEmpty(value) + ) { + delete obj[key]; + } + } } -function hasBooleanValue(value) { - return ( - hasValue(value) && - typeof value === 'boolean' - ) +function isValidResponse(bidResponse) { + const auth = bidResponse?.body?.auth; + const bids = bidResponse?.body?.data?.bids; + return isStr(auth) && isArray(bids) && !isEmpty(bids); } -function hasStringValue(value) { - return ( - hasValue(value) && - typeof value === 'string' && - value.length > 0 - ) -} +function processBidResponse(bid) { + const meta = bid.meta || {}; + meta.advertiserDomains = bid.meta?.advertiserDomains || []; -function hasArrayValue(value) { - return ( - hasValue(value) && - isArray(value) && - value.length > 0 - ) + return { + ...bid, + meta, + }; } -registerBidder(spec) +registerBidder(spec); diff --git a/modules/glimpseBidAdapter.md b/modules/glimpseBidAdapter.md index 767efcecf54..e82c5d8f32e 100644 --- a/modules/glimpseBidAdapter.md +++ b/modules/glimpseBidAdapter.md @@ -24,15 +24,14 @@ const adUnits = [ sizes: [[300, 250]], }, }, - bids: [{ - bidder: 'glimpse', - params: { - placementId: 'e53a7f564f8f44cc913b', - keywords: { - country: 'uk', + bids: [ + { + bidder: 'glimpse', + params: { + pid: 'e53a7f564f8f44cc913b', }, }, - }], + ], }, -] +]; ``` diff --git a/modules/iasRtdProvider.js b/modules/iasRtdProvider.js index 6f7b2d5215d..58899d7a8c0 100644 --- a/modules/iasRtdProvider.js +++ b/modules/iasRtdProvider.js @@ -13,6 +13,28 @@ const FRAUD_FIELD_NAME = 'fr'; const SLOTS_OBJECT_FIELD_NAME = 'slots'; const CUSTOM_FIELD_NAME = 'custom'; const IAS_KW = 'ias-kw'; +const IAS_KEY_MAPPINGS = { + adt: 'adt', + alc: 'alc', + dlm: 'dlm', + hat: 'hat', + off: 'off', + vio: 'vio', + drg: 'drg', + 'ias-kw': 'ias-kw', + fr: 'fr', + vw: 'vw', + grm: 'grm', + pub: 'pub', + vw05: 'vw05', + vw10: 'vw10', + vw15: 'vw15', + vw30: 'vw30', + vw_vv: 'vw_vv', + grm_vv: 'grm_vv', + pub_vv: 'pub_vv', + id: 'id' +}; /** * Module init @@ -26,6 +48,14 @@ export function init(config, userConsent) { utils.logError('missing pubId param for IAS provider'); return false; } + if (params.hasOwnProperty('keyMappings')) { + const keyMappings = params.keyMappings; + for (let prop in keyMappings) { + if (IAS_KEY_MAPPINGS.hasOwnProperty(prop)) { + IAS_KEY_MAPPINGS[prop] = keyMappings[prop] + } + } + } return true; } @@ -62,6 +92,16 @@ function stringifyScreenSize() { return [(window.screen && window.screen.width) || -1, (window.screen && window.screen.height) || -1].join('.'); } +function renameKeyValues(source) { + let result = {}; + for (let prop in IAS_KEY_MAPPINGS) { + if (source.hasOwnProperty(prop)) { + result[IAS_KEY_MAPPINGS[prop]] = source[prop]; + } + } + return result; +} + function formatTargetingData(adUnit) { let result = {}; if (iasTargeting[BRAND_SAFETY_OBJECT_FIELD_NAME]) { @@ -76,7 +116,7 @@ function formatTargetingData(adUnit) { if (iasTargeting[SLOTS_OBJECT_FIELD_NAME] && adUnit in iasTargeting[SLOTS_OBJECT_FIELD_NAME]) { utils.mergeDeep(result, iasTargeting[SLOTS_OBJECT_FIELD_NAME][adUnit]); } - return result; + return renameKeyValues(result); } function constructQueryString(anId, adUnits) { @@ -147,6 +187,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { undefined, { method: 'GET' } ); + callback() } /** @type {RtdSubmodule} */ diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 18016eea530..c696dabc64a 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -1,69 +1,58 @@ import { - _each, - deepAccess, - deepSetValue, - getBidIdParameter, - getBidRequest, - getUniqueIdentifierStr, - isArray, - isEmpty, - isFn, - isInteger, - isNumber, - isPlainObject, - isStr, - logError, - logWarn, mergeDeep + cleanObj, deepAccess, deepClone, deepSetValue, getBidIdParameter, getBidRequest, getDNT, + getUniqueIdentifierStr, isFn, isPlainObject, logWarn, mergeDeep, parseUrl } 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'; import {createEidsArray} from './userId/eids.js'; -import {includes} from '../src/polyfill.js'; const BIDDER_CODE = 'improvedigital'; -const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; -const VIDEO_TARGETING = ['skip', 'skipmin', 'skipafter']; +const REQUEST_URL = 'https://ad.360yield.com/pb'; +const CREATIVE_TTL = 300; + +const VIDEO_PARAMS = { + DEFAULT_MIMES: ['video/mp4'], + SUPPORTED_PROPERTIES: ['mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', + 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', 'delivery', 'pos', 'companionad', + 'api', 'companiontype', 'ext'], + PLACEMENT_TYPE: { + INSTREAM: 1, + OUTSTREAM: 3, + } +}; -const ID_RAZR = { - RENDERER_URL: 'https://razr.improvedigital.com/renderer.js', - addBidData({bid, bidRequest}) { - if (this.isValidBid(bid)) { - bid.renderer = Renderer.install({ - url: this.RENDERER_URL, - config: {bidRequest} - }); - bid.renderer.setRender(this.render); - } +const NATIVE_DATA = { + VERSION: '1.2', + ASSET_TYPES: { + TITLE: 'title', + IMG: 'img', + DATA: 'data', }, - - isValidBid(bid) { - return bid && /razr:\\?\/\\?\//.test(bid.ad); + ASSETS: { + title: {id: 0, name: 'title', assetType: 'title', default: {len: 140}}, + sponsoredBy: {id: 1, name: 'sponsoredBy', assetType: 'data', type: 1}, + icon: {id: 2, name: 'icon', assetType: 'img', type: 2}, + body: {id: 3, name: 'body', assetType: 'data', type: 2}, + image: {id: 4, name: 'image', assetType: 'img', type: 3}, + rating: {id: 5, name: 'rating', assetType: 'data', type: 3}, + likes: {id: 6, name: 'likes', assetType: 'data', type: 4}, + downloads: {id: 7, name: 'downloads', assetType: 'data', type: 5}, + price: {id: 8, name: 'price', assetType: 'data', type: 6}, + salePrice: {id: 9, name: 'salePrice', assetType: 'data', type: 7}, + phone: {id: 10, name: 'phone', assetType: 'data', type: 8}, + address: {id: 11, name: 'address', assetType: 'data', type: 9}, + body2: {id: 12, name: 'body2', assetType: 'data', type: 10}, + displayUrl: {id: 13, name: 'displayUrl', assetType: 'data', type: 11}, + cta: {id: 14, name: 'cta', assetType: 'data', type: 12}, }, - - render(bid) { - const {bidRequest} = bid.renderer.getConfig(); - - const payload = { - type: 'prebid', - bidRequest, - bid, - config: mergeDeep( - {}, - config.getConfig('improvedigital.rendererConfig'), - deepAccess(bidRequest, 'params.rendererConfig') - ) - }; - - const razr = window.razr = window.razr || {}; - razr.queue = razr.queue || []; - razr.queue.push(payload); + getAssetById(id) { + return Object.values(this.ASSETS).find(asset => id === asset.id); } }; export const spec = { - version: '7.7.0', code: BIDDER_CODE, gvlid: 253, aliases: ['id'], @@ -75,7 +64,7 @@ export const spec = { * @param {object} bid The bid to validate. * @return boolean True if this is a valid bid, and false otherwise. */ - isBidRequestValid: function (bid) { + isBidRequestValid(bid) { return !!(bid && bid.params && (bid.params.placementId || (bid.params.placementKey && bid.params.publisherId))); }, @@ -83,179 +72,130 @@ export const spec = { * Make a server request from the list of BidRequests. * * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @param bidderRequest * @return ServerRequest Info describing the request to the server. */ - buildRequests: function (bidRequests, bidderRequest) { - let normalizedBids = bidRequests.map((bidRequest) => { - return getNormalizedBidRequest(bidRequest); - }); - - let idClient = new ImproveDigitalAdServerJSClient('hb'); - let requestParameters = { - singleRequestMode: (config.getConfig('improvedigital.singleRequest') === true), - returnObjType: idClient.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT, - libVersion: this.version - }; - - const gdprConsent = deepAccess(bidderRequest, 'gdprConsent') - if (gdprConsent) { - // GDPR Consent String - if (gdprConsent.consentString) { - requestParameters.gdpr = gdprConsent.consentString; - } - - // Additional Consent String - const additionalConsent = deepAccess(gdprConsent, 'addtlConsent'); - if (additionalConsent && additionalConsent.indexOf('~') !== -1) { - // Google Ad Tech Provider IDs - const atpIds = additionalConsent.substring(additionalConsent.indexOf('~') + 1); - deepSetValue( - requestParameters, - 'user.ext.consented_providers_settings.consented_providers', - atpIds.split('.').map(id => parseInt(id, 10)) - ); + buildRequests(bidRequests, bidderRequest) { + const request = { + id: getUniqueIdentifierStr(), + cur: [config.getConfig('currency.adServerCurrency') || 'USD'], + ext: { + improvedigital: { + sdk: { + name: 'pbjs', + version: '$prebid.version$', + } + } } - } + }; - if (bidderRequest && bidderRequest.uspConsent) { - requestParameters.usPrivacy = bidderRequest.uspConsent; + // Device + request.device = (typeof config.getConfig('device') === 'object') ? config.getConfig('device') : {}; + request.device.w = request.device.w || window.innerWidth; + request.device.h = request.device.h || window.innerHeight; + if (getDNT()) { + request.device.dnt = 1; } - if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { - requestParameters.referrer = bidderRequest.refererInfo.referer; + // Coppa + const coppa = config.getConfig('coppa'); + if (typeof coppa === 'boolean') { + deepSetValue(request, 'regs.coppa', ID_UTIL.toBit(coppa)); } - // Adding first party data - const site = config.getConfig('ortb2.site'); - if (site) { - const pageCategory = site.pagecat || site.cat; - if (pageCategory && isArray(pageCategory)) { - requestParameters.pagecat = pageCategory.filter((category) => { - return category && isStr(category) - }); + if (bidderRequest) { + // GDPR + const gdprConsent = deepAccess(bidderRequest, 'gdprConsent') + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + deepSetValue(request, 'regs.ext.gdpr', ID_UTIL.toBit(gdprConsent.gdprApplies)); + } + deepSetValue(request, 'user.ext.consent', gdprConsent.consentString); + + // Additional Consent String + const additionalConsent = deepAccess(gdprConsent, 'addtlConsent'); + if (additionalConsent && additionalConsent.indexOf('~') !== -1) { + // Google Ad Tech Provider IDs + const atpIds = additionalConsent.substring(additionalConsent.indexOf('~') + 1); + deepSetValue( + request, + 'user.ext.consented_providers_settings.consented_providers', + atpIds.split('.').map(id => parseInt(id, 10)) + ); + } } - const genre = deepAccess(site, 'content.genre'); - if (genre && isStr(genre)) { - requestParameters.genre = genre; + + // Timeout + deepSetValue(request, 'tmax', bidderRequest.timeout); + // US Privacy + if (typeof bidderRequest.uspConsent !== typeof undefined) { + deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); } } - // End of adding first party data - requestParameters.schain = bidRequests[0].schain; - requestParameters.coppa = config.getConfig('coppa') === true; + ID_REQUEST.buildSiteOrApp(request, bidderRequest); - if (bidRequests[0].userId) { - const eids = createEidsArray(bidRequests[0].userId); - if (eids.length) { - deepSetValue(requestParameters, 'user.ext.eids', eids); - } - } + const bidRequest0 = bidRequests[0]; - let requestObj = idClient.createRequest( - normalizedBids, // requestObject - requestParameters - ); + deepSetValue(request, 'source.ext.schain', bidRequest0.schain); + deepSetValue(request, 'source.tid', bidRequest0.transactionId); - if (requestObj.errors && requestObj.errors.length > 0) { - logError('ID WARNING 0x01'); + if (bidRequest0.userId) { + const eids = createEidsArray(bidRequest0.userId); + deepSetValue(request, 'user.ext.eids', eids.length ? eids : undefined); } - requestObj.requests.forEach(request => request.bidderRequest = bidderRequest); - return requestObj.requests; + + return ID_REQUEST.buildServerRequests(request, bidRequests, bidderRequest); }, /** * Unpack the response from the server into a list of bids. * * @param {*} serverResponse A successful response from the server. + * @param bidderRequest * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: function (serverResponse, {bidderRequest}) { + interpretResponse(serverResponse, { bidderRequest }) { + if (!Array.isArray(deepAccess(serverResponse, 'body.seatbid'))) { + return []; + } + const bids = []; - _each(serverResponse.body.bid, function (bidObject) { - if (!bidObject.price || bidObject.price === null || - bidObject.hasOwnProperty('errorCode') || - (!bidObject.adm && !bidObject.native)) { - return; - } - const bidRequest = getBidRequest(bidObject.id, [bidderRequest]); - const bid = {}; - - if (bidObject.native) { - // Native - bid.native = getNormalizedNativeAd(bidObject.native); - // Expose raw oRTB response to the client to allow parsing assets not directly supported by Prebid - bid.ortbNative = bidObject.native; - if (bidObject.nurl) { - bid.native.impressionTrackers.unshift(bidObject.nurl); - } - bid.mediaType = NATIVE; - } 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 = ''; - if (bidObject.nurl && bidObject.nurl.length > 0) { - nurl = ``; - } - bid.ad = `${nurl}`; - bid.mediaType = BANNER; - } - // Common properties - bid.cpm = parseFloat(bidObject.price); - bid.creativeId = bidObject.crid; - bid.currency = bidObject.currency ? bidObject.currency.toUpperCase() : 'USD'; - - // Deal ID. Composite ads can have multiple line items and the ID of the first - // dealID line item will be used. - if (isNumber(bidObject.lid) && bidObject.buying_type && bidObject.buying_type !== 'rtb') { - bid.dealId = bidObject.lid; - } else if (Array.isArray(bidObject.lid) && - Array.isArray(bidObject.buying_type) && - bidObject.lid.length === bidObject.buying_type.length) { - let isDeal = false; - bidObject.buying_type.forEach((bt, i) => { - if (isDeal) return; - if (bt && bt !== 'rtb') { - isDeal = true; - bid.dealId = bidObject.lid[i]; - } - }); - } + serverResponse.body.seatbid.forEach(seatbid => { + if (!Array.isArray(seatbid.bid)) return; - bid.height = bidObject.h; - bid.netRevenue = bidObject.isNet ? bidObject.isNet : false; - bid.requestId = bidObject.id; - bid.ttl = 300; - bid.width = bidObject.w; + seatbid.bid.forEach(bidObject => { + if (!bidObject.adm || !bidObject.price || bidObject.hasOwnProperty('errorCode')) { + return; + } + const bidRequest = getBidRequest(bidObject.impid, [bidderRequest]); + const idExt = deepAccess(bidObject, `ext.${BIDDER_CODE}`); + + const bid = { + requestId: bidObject.impid, + cpm: bidObject.price, + creativeId: bidObject.crid, + currency: serverResponse.body.cur.toUpperCase() || 'USD', + dealId: (typeof idExt.buying_type === 'string' && idExt.buying_type !== 'rtb') ? idExt.line_item_id : undefined, + meta: { + advertiserDomains: bidObject.adomain ? bidObject.adomain : [] + }, + netRevenue: idExt.is_net || false, + ttl: CREATIVE_TTL + } - if (!bid.width || !bid.height) { - bid.width = 1; - bid.height = 1; - } + ID_RESPONSE.buildAd(bid, bidRequest, bidObject); - if (bidObject.adomain) { - bid.meta = { - advertiserDomains: bidObject.adomain - }; - } + ID_RAZR.addBidData({ + bidRequest, + bid + }); - ID_RAZR.addBidData({ - bidRequest, - bid + bids.push(bid); }); - - bids.push(bid); }); + return bids; }, @@ -266,17 +206,14 @@ export const spec = { * @param {ServerResponse[]} serverResponses List of server's responses. * @return {UserSync[]} The user syncs which should be dropped. */ - getUserSyncs: function(syncOptions, serverResponses) { + getUserSyncs(syncOptions, serverResponses) { if (syncOptions.pixelEnabled) { const syncs = []; serverResponses.forEach(response => { - response.body.bid.forEach(bidObject => { - if (isArray(bidObject.sync)) { - bidObject.sync.forEach(syncElement => { - if (syncs.indexOf(syncElement) === -1) { - syncs.push(syncElement); - } - }); + const syncArr = deepAccess(response, `body.ext.${BIDDER_CODE}.sync`, []); + syncArr.forEach(syncElement => { + if (syncs.indexOf(syncElement) === -1) { + syncs.push(syncElement); } }); }); @@ -286,529 +223,374 @@ export const spec = { } }; -function isInstreamVideo(bid) { - const mediaTypes = Object.keys(deepAccess(bid, 'mediaTypes', {})); - const videoMediaType = deepAccess(bid, 'mediaTypes.video'); - const context = deepAccess(bid, 'mediaTypes.video.context'); - return bid.mediaType === 'video' || (mediaTypes.length === 1 && videoMediaType && context !== 'outstream'); -} - -function isOutstreamVideo(bid) { - const videoMediaType = deepAccess(bid, 'mediaTypes.video'); - const context = deepAccess(bid, 'mediaTypes.video.context'); - return videoMediaType && context === 'outstream'; -} - -function getVideoTargetingParams(bid) { - const result = {}; - Object.keys(Object(bid.mediaTypes.video)) - .filter(key => includes(VIDEO_TARGETING, key)) - .forEach(key => { - result[ key ] = bid.mediaTypes.video[ key ]; - }); - Object.keys(Object(bid.params.video)) - .filter(key => includes(VIDEO_TARGETING, key)) - .forEach(key => { - result[ key ] = bid.params.video[ key ]; - }); - return result; -} +registerBidder(spec); -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return null; - } - const floor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { - return floor.floor; - } - return null; -} - -function outstreamRender(bid) { - bid.renderer.push(() => { - window.ANOutstreamVideo.renderAd({ - sizes: [bid.width, bid.height], - targetId: bid.adUnitCode, - adResponse: bid.adResponse, - rendererOptions: bid.renderer.getConfig() - }, handleOutstreamRendererEvents.bind(null, bid)); - }); -} - -function handleOutstreamRendererEvents(bid, id, eventName) { - bid.renderer.handleVideoEvent({ id, eventName }); -} - -function createRenderer(bidRequest) { - const renderer = Renderer.install({ - id: bidRequest.adUnitCode, - url: RENDERER_URL, - loaded: false, - config: deepAccess(bidRequest, 'renderer.options'), - adUnitCode: bidRequest.adUnitCode - }); - try { - renderer.setRender(outstreamRender); - } catch (err) { - logWarn('Prebid Error calling setRender on renderer', err); - } - return renderer; -} - -function getNormalizedBidRequest(bid) { - let adUnitId = getBidIdParameter('adUnitCode', bid) || null; - let placementId = getBidIdParameter('placementId', bid.params) || null; - let publisherId = null; - let placementKey = null; - - if (placementId === null) { - publisherId = getBidIdParameter('publisherId', bid.params) || null; - placementKey = getBidIdParameter('placementKey', bid.params) || null; - } - const keyValues = getBidIdParameter('keyValues', bid.params) || null; - const singleSizeFilter = getBidIdParameter('size', bid.params) || null; - const bidId = getBidIdParameter('bidId', bid); - const transactionId = getBidIdParameter('transactionId', bid); - const currency = config.getConfig('currency.adServerCurrency'); - - let normalizedBidRequest = {}; - if (isInstreamVideo(bid)) { - normalizedBidRequest.adTypes = [ VIDEO ]; - } - if (isInstreamVideo(bid) || isOutstreamVideo(bid)) { - normalizedBidRequest.video = getVideoTargetingParams(bid); - } - if (placementId) { - normalizedBidRequest.placementId = placementId; - } else { - if (publisherId) { - normalizedBidRequest.publisherId = publisherId; - } - if (placementKey) { - normalizedBidRequest.placementKey = placementKey; +const ID_REQUEST = { + buildServerRequests(requestObject, bidRequests, bidderRequest) { + const requests = []; + if (config.getConfig('improvedigital.singleRequest') === true) { + requestObject.imp = bidRequests.map((bidRequest) => this.buildImp(bidRequest)); + requests[0] = this.formatRequest(requestObject, bidderRequest); + } else { + bidRequests.map((bidRequest) => { + const request = deepClone(requestObject); + request.id = bidRequest.bidId || getUniqueIdentifierStr(); + request.imp = [this.buildImp(bidRequest)]; + deepSetValue(request, 'source.tid', bidRequest.transactionId); + requests.push(this.formatRequest(request, bidderRequest)); + }); } - } - if (keyValues) { - normalizedBidRequest.keyValues = keyValues; - } + return requests; + }, - 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 = {}; - normalizedBidRequest.size.h = singleSizeFilter.h; - normalizedBidRequest.size.w = singleSizeFilter.w; - } + formatRequest(request, bidderRequest) { + return { + method: 'POST', + url: REQUEST_URL, + data: JSON.stringify(request), + bidderRequest + } + }, - if (bidId) { - normalizedBidRequest.id = bidId; - } - if (adUnitId) { - normalizedBidRequest.adUnitId = adUnitId; - } - if (transactionId) { - normalizedBidRequest.transactionId = transactionId; - } - if (currency) { - normalizedBidRequest.currency = currency; - } - // Floor - let bidFloor = getBidFloor(bid); - let bidFloorCur = null; - if (!bidFloor) { - bidFloor = getBidIdParameter('bidFloor', bid.params); - bidFloorCur = getBidIdParameter('bidFloorCur', bid.params); - } - if (bidFloor) { - normalizedBidRequest.bidFloor = bidFloor; - normalizedBidRequest.bidFloorCur = bidFloorCur ? bidFloorCur.toUpperCase() : 'USD'; - } - return normalizedBidRequest; -} + buildImp(bidRequest) { + const imp = { + id: getBidIdParameter('bidId', bidRequest) || getUniqueIdentifierStr(), + secure: ID_UTIL.toBit(window.location.protocol === 'https:'), + }; -function getNormalizedNativeAd(rawNative) { - const native = {}; - if (!rawNative || !isArray(rawNative.assets)) { - return null; - } - // Assets - rawNative.assets.forEach(asset => { - if (asset.title) { - native.title = asset.title.text; - } else if (asset.data) { - switch (asset.data.type) { - case 1: - native.sponsoredBy = asset.data.value; - break; - case 2: - native.body = asset.data.value; - break; - case 3: - native.rating = asset.data.value; - break; - case 4: - native.likes = asset.data.value; - break; - case 5: - native.downloads = asset.data.value; - break; - case 6: - native.price = asset.data.value; - break; - case 7: - native.salePrice = asset.data.value; - break; - case 8: - native.phone = asset.data.value; - break; - case 9: - native.address = asset.data.value; - break; - case 10: - native.body2 = asset.data.value; - break; - case 11: - native.displayUrl = asset.data.value; - break; - case 12: - native.cta = asset.data.value; - break; - } - } else if (asset.img) { - switch (asset.img.type) { - case 2: - native.icon = { - url: asset.img.url, - width: asset.img.w, - height: asset.img.h - }; - break; - case 3: - native.image = { - url: asset.img.url, - width: asset.img.w, - height: asset.img.h - }; - break; - } + // Floor + const bidFloor = this.getBidFloor(bidRequest) || getBidIdParameter('bidFloor', bidRequest.params); + if (bidFloor) { + const bidFloorCur = getBidIdParameter('bidFloorCur', bidRequest.params) || 'USD'; + deepSetValue(imp, 'bidfloor', bidFloor); + deepSetValue(imp, 'bidfloorcur', bidFloorCur ? bidFloorCur.toUpperCase() : undefined); } - }); - // Trackers - if (rawNative.eventtrackers) { - native.impressionTrackers = []; - rawNative.eventtrackers.forEach(tracker => { - // Only handle impression event. Viewability events are not supported yet. - if (tracker.event !== 1) return; - switch (tracker.method) { - case 1: // img - native.impressionTrackers.push(tracker.url); - break; - case 2: // js - // javascriptTrackers is a string. If there's more than one JS tracker in bid response, the last script will be used. - native.javascriptTrackers = ``; - break; - } - }); - } else { - native.impressionTrackers = rawNative.imptrackers || []; - native.javascriptTrackers = rawNative.jstracker; - } - if (rawNative.link) { - native.clickUrl = rawNative.link.url; - native.clickTrackers = rawNative.link.clicktrackers; - } - if (rawNative.privacy) { - native.privacyLink = rawNative.privacy; - } - return native; -} -registerBidder(spec); -export function ImproveDigitalAdServerJSClient(endPoint) { - this.CONSTANTS = { - AD_SERVER_BASE_URL: 'ice.360yield.com', - END_POINT: endPoint || 'hb', - AD_SERVER_URL_PARAM: 'jsonp=', - CLIENT_VERSION: 'JS-6.4.0', - MAX_URL_LENGTH: 2083, - ERROR_CODES: { - MISSING_PLACEMENT_PARAMS: 2, - LIB_VERSION_MISSING: 3 - }, - RETURN_OBJ_TYPE: { - DEFAULT: 0, - URL_PARAMS_SPLIT: 1 + const placementId = getBidIdParameter('placementId', bidRequest.params); + if (placementId) { + deepSetValue(imp, 'ext.bidder.placementId', placementId); + } else { + deepSetValue(imp, 'ext.bidder.publisherId', getBidIdParameter('publisherId', bidRequest.params)); + deepSetValue(imp, 'ext.bidder.placementKey', getBidIdParameter('placementKey', bidRequest.params)); } - }; - this.getErrorReturn = function(errorCode) { - return { - idMappings: {}, - requests: {}, - 'errorCode': errorCode - }; - }; + deepSetValue(imp, 'ext.bidder.keyValues', getBidIdParameter('keyValues', bidRequest.params) || undefined); - this.createRequest = function(requestObject, requestParameters, extraRequestParameters) { - if (!requestParameters.libVersion) { - return this.getErrorReturn(this.CONSTANTS.ERROR_CODES.LIB_VERSION_MISSING); - } + // Adding GPID + const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || + deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot') || + deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.adslot'); - requestParameters.returnObjType = requestParameters.returnObjType || this.CONSTANTS.RETURN_OBJ_TYPE.DEFAULT; - requestParameters.adServerBaseUrl = 'https://' + (requestParameters.adServerBaseUrl || this.CONSTANTS.AD_SERVER_BASE_URL); + deepSetValue(imp, 'ext.gpid', gpid); - let impressionObjects = []; - let impressionObject; - if (isArray(requestObject)) { - for (let counter = 0; counter < requestObject.length; counter++) { - impressionObject = this.createImpressionObject(requestObject[counter]); - impressionObjects.push(impressionObject); - } - } else { - impressionObject = this.createImpressionObject(requestObject); - impressionObjects.push(impressionObject); + // Adding Interstitial Signal + if (deepAccess(bidRequest, 'ortb2Imp.instl')) { + imp.instl = 1; } - let returnIdMappings = true; - if (requestParameters.returnObjType === this.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT) { - returnIdMappings = false; + const videoParams = deepAccess(bidRequest, 'mediaTypes.video'); + if (videoParams) { + imp.video = this.buildVideoRequest(bidRequest); + deepSetValue(imp, 'ext.is_rewarded_inventory', (videoParams.rewarded === 1 || deepAccess(videoParams, 'ext.rewarded') === 1) || undefined); } - let returnObject = {}; - returnObject.requests = []; - if (returnIdMappings) { - returnObject.idMappings = []; + if (deepAccess(bidRequest, 'mediaTypes.banner')) { + imp.banner = this.buildBannerRequest(bidRequest); } - let errors = null; - let baseUrl = `${requestParameters.adServerBaseUrl}/${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; + if (deepAccess(bidRequest, 'mediaTypes.native')) { + imp.native = this.buildNativeRequest(bidRequest); + } - let bidRequestObject = { - bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) - }; - for (let counter = 0; counter < impressionObjects.length; counter++) { - impressionObject = impressionObjects[counter]; - - if (impressionObject.errorCode) { - errors = errors || []; - errors.push({ - errorCode: impressionObject.errorCode, - adUnitId: impressionObject.adUnitId - }); - } else { - if (returnIdMappings) { - returnObject.idMappings.push({ - adUnitId: impressionObject.adUnitId, - id: impressionObject.impressionObject.id - }); - } - bidRequestObject.bid_request.imp = bidRequestObject.bid_request.imp || []; - bidRequestObject.bid_request.imp.push(impressionObject.impressionObject); - - let writeLongRequest = false; - const outputUri = baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)); - if (outputUri.length > this.CONSTANTS.MAX_URL_LENGTH) { - writeLongRequest = true; - if (bidRequestObject.bid_request.imp.length > 1) { - // Pop the current request and process it again in the next iteration - bidRequestObject.bid_request.imp.pop(); - if (returnIdMappings) { - returnObject.idMappings.pop(); - } - counter--; - } - } + return imp; + }, - if (writeLongRequest || - !requestParameters.singleRequestMode || - counter === impressionObjects.length - 1) { - returnObject.requests.push(this.formatRequest(requestParameters, bidRequestObject)); - bidRequestObject = { - bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) - }; - } - } + buildVideoRequest(bidRequest) { + const videoParams = deepClone(bidRequest.mediaTypes.video); + const videoImproveParams = deepClone(deepAccess(bidRequest, 'params.video', {})); + const video = {...videoParams, ...videoImproveParams}; + + if (Array.isArray(video.playerSize)) { + // Player size can be defined as [w, h] or [[w, h]] + const size = Array.isArray(video.playerSize[0]) ? video.playerSize[0] : video.playerSize; + video.w = size[0]; + video.h = size[1]; } + video.placement = this.isOutstreamVideo(bidRequest) ? VIDEO_PARAMS.PLACEMENT_TYPE.OUTSTREAM : VIDEO_PARAMS.PLACEMENT_TYPE.INSTREAM; - if (errors) { - returnObject.errors = errors; + // Mimes is required + if (!video.mimes) { + video.mimes = VIDEO_PARAMS.DEFAULT_MIMES; } - return returnObject; - }; + // skip must be 0 or 1 + if (video.skip !== 1) { + delete video.skipmin; + delete video.skipafter; + if (video.skip !== 0) { + logWarn(`video.skip: invalid value '${video.skip}'. Expected 0 or 1`); + delete video.skip; + } + } - this.formatRequest = function(requestParameters, bidRequestObject) { - switch (requestParameters.returnObjType) { - case this.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT: - return { - method: 'GET', - url: `${requestParameters.adServerBaseUrl}/${this.CONSTANTS.END_POINT}`, - data: `${this.CONSTANTS.AD_SERVER_URL_PARAM}${encodeURIComponent(JSON.stringify(bidRequestObject))}` + Object.keys(video).forEach(prop => { + if (VIDEO_PARAMS.SUPPORTED_PROPERTIES.indexOf(prop) === -1) delete video[prop]; + }); + return video; + }, + + buildBannerRequest(bidRequest) { + // Set the desired creative sizes + // Input Format: array of pairs, i.e. [[300, 250], [250, 250]] + // Unless improvedigital.usePrebidSizes == true, no sizes are sent to the server + // and the sizes defined in the server for the placement will be used + const banner = {}; + if (config.getConfig('improvedigital.usePrebidSizes') === true && bidRequest.sizes) { + // Convert sizes from [x, y] to { w: x, h: y} + banner.format = bidRequest.sizes.map(sizePair => ({w: sizePair[0], h: sizePair[1]})); + } + return banner; + }, + + buildNativeRequest(bidRequest) { + const nativeParams = bidRequest.mediaTypes.native; + const request = { + assets: [], + } + for (let i of Object.keys(nativeParams)) { + const assetOrtbParams = NATIVE_DATA.ASSETS[i]; + if (assetOrtbParams) { + const assetParams = nativeParams[i]; + const asset = { + id: assetOrtbParams.id, + required: ID_UTIL.toBit(assetParams.required), }; - default: - const baseUrl = `${requestParameters.adServerBaseUrl}/` + - `${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; - return { - url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) + switch (assetOrtbParams.assetType) { + case NATIVE_DATA.ASSET_TYPES.TITLE: + asset.title = {len: assetParams.len || assetOrtbParams.default.len}; + break; + case NATIVE_DATA.ASSET_TYPES.DATA: + asset.data = cleanObj({type: assetOrtbParams.type, len: assetParams.len}) + break; + case NATIVE_DATA.ASSET_TYPES.IMG: + asset.img = cleanObj({ + type: assetOrtbParams.type, + w: deepAccess(assetParams, 'sizes.0'), + h: deepAccess(assetParams, 'sizes.1'), + wmin: deepAccess(assetParams, 'aspect_ratios.0.min_width'), + hmin: deepAccess(assetParams, 'aspect_ratios.0.min_height') + }); + break; + default: + return; } - } - }; - - this.createBasicBidRequestObject = function(requestParameters, extraRequestParameters) { - let impressionBidRequestObject = {}; - impressionBidRequestObject.secure = 1; - if (requestParameters.requestId) { - impressionBidRequestObject.id = requestParameters.requestId; - } else { - impressionBidRequestObject.id = getUniqueIdentifierStr(); - } - if (requestParameters.domain) { - impressionBidRequestObject.domain = requestParameters.domain; - } - if (requestParameters.page) { - impressionBidRequestObject.page = requestParameters.page; - } - if (requestParameters.ref) { - impressionBidRequestObject.ref = requestParameters.ref; - } - if (requestParameters.callback) { - impressionBidRequestObject.callback = requestParameters.callback; - } - if (requestParameters.libVersion) { - impressionBidRequestObject.version = requestParameters.libVersion + '-' + this.CONSTANTS.CLIENT_VERSION; - } - if (requestParameters.referrer) { - impressionBidRequestObject.referrer = requestParameters.referrer; - } - if (requestParameters.gdpr || requestParameters.gdpr === 0) { - impressionBidRequestObject.gdpr = requestParameters.gdpr; - } - if (requestParameters.usPrivacy) { - impressionBidRequestObject.us_privacy = requestParameters.usPrivacy; - } - if (requestParameters.schain) { - impressionBidRequestObject.schain = requestParameters.schain; - } - if (requestParameters.pagecat) { - impressionBidRequestObject.pagecat = requestParameters.pagecat; - } - if (requestParameters.genre) { - impressionBidRequestObject.genre = requestParameters.genre; - } - if (requestParameters.user) { - impressionBidRequestObject.user = requestParameters.user; - } - if (extraRequestParameters) { - for (let prop in extraRequestParameters) { - impressionBidRequestObject[prop] = extraRequestParameters[prop]; + request.assets.push(asset); } } + return { ver: NATIVE_DATA.VERSION, request: JSON.stringify(request) }; + }, - if (requestParameters.coppa) { - impressionBidRequestObject.coppa = 1; - } + isOutstreamVideo(bidRequest) { + return deepAccess(bidRequest, 'mediaTypes.video.context') === 'outstream'; + }, - return impressionBidRequestObject; - }; + getBidFloor(bidRequest) { + if (!isFn(bidRequest.getFloor)) { + return null; + } + const floor = bidRequest.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; + }, - this.createImpressionObject = function(placementObject) { - let outputObject = {}; - let impressionObject = {}; - outputObject.impressionObject = impressionObject; + buildSiteOrApp(request, bidderRequest) { + const app = {}; + const configAppSettings = config.getConfig('app') || {}; + const fpdAppSettings = config.getConfig('ortb2.app') || {}; + mergeDeep(app, configAppSettings, fpdAppSettings); - if (placementObject.id) { - impressionObject.id = placementObject.id; + if (Object.keys(app).length !== 0) { + request.app = app; } else { - impressionObject.id = getUniqueIdentifierStr(); - } - if (placementObject.adTypes) { - impressionObject.ad_types = placementObject.adTypes; - } - if (placementObject.adUnitId) { - outputObject.adUnitId = placementObject.adUnitId; - } - if (placementObject.currency) { - impressionObject.currency = placementObject.currency.toUpperCase(); - } - if (placementObject.bidFloor) { - impressionObject.bidfloor = placementObject.bidFloor; - } - if (placementObject.bidFloorCur) { - impressionObject.bidfloorcur = placementObject.bidFloorCur.toUpperCase(); - } - if (placementObject.placementId) { - impressionObject.pid = placementObject.placementId; - } - if (placementObject.publisherId) { - impressionObject.pubid = placementObject.publisherId; - } - if (placementObject.placementKey) { - impressionObject.pkey = placementObject.placementKey; - } - if (placementObject.transactionId) { - impressionObject.tid = placementObject.transactionId; - } - if (!isEmpty(placementObject.video)) { - const video = Object.assign({}, placementObject.video); - // skip must be 0 or 1 - if (video.skip !== 1) { - delete video.skipmin; - delete video.skipafter; - if (video.skip !== 0) { - logWarn(`video.skip: invalid value '${video.skip}'. Expected 0 or 1`); - delete video.skip; - } - } - if (!isEmpty(video)) { - impressionObject.video = video; + const site = {}; + const url = config.getConfig('pageUrl') || deepAccess(bidderRequest, 'refererInfo.referer'); + if (url) { + site.page = url; + site.domain = parseUrl(url).hostname; } + const configSiteSettings = config.getConfig('site') || {}; + const fpdSiteSettings = config.getConfig('ortb2.site') || {}; + mergeDeep(site, configSiteSettings, fpdSiteSettings); + request.site = site; } - if (placementObject.keyValues) { - for (let key in placementObject.keyValues) { - for (let valueCounter = 0; valueCounter < placementObject.keyValues[key].length; valueCounter++) { - impressionObject.kvw = impressionObject.kvw || {}; - impressionObject.kvw[key] = impressionObject.kvw[key] || []; - impressionObject.kvw[key].push(placementObject.keyValues[key][valueCounter]); - } + }, +}; + +const ID_RESPONSE = { + buildAd(bid, bidRequest, bidResponse) { + if (bidRequest.mediaTypes && Object.keys(bidRequest.mediaTypes).length === 1) { + if (deepAccess(bidRequest, 'mediaTypes.video')) { + this.buildVideoAd(bid, bidRequest, bidResponse); + } else if (deepAccess(bidRequest, 'mediaTypes.banner')) { + this.buildBannerAd(bid, bidRequest, bidResponse); + } else if (deepAccess(bidRequest, 'mediaTypes.native')) { + this.buildNativeAd(bid, bidRequest, bidResponse) + } + } else { + if (bidResponse.adm.search(/^ sizePair.length === 2 && - isInteger(sizePair[0]) && - isInteger(sizePair[1]) && - sizePair[0] >= 0 && - sizePair[1] >= 0) - .map(sizePair => { - return { w: sizePair[0], h: sizePair[1] } - }); - if (format.length > 0) { - impressionObject.banner.format = format; + buildBannerAd(bid, bidRequest, bidResponse) { + bid.mediaType = BANNER; + bid.ad = bidResponse.adm; + bid.width = bidResponse.w; + bid.height = bidResponse.h; + }, + + buildNativeAd(bid, bidRequest, bidResponse) { + bid.mediaType = NATIVE; + const nativeResponse = JSON.parse(bidResponse.adm); + const nativeAd = { + clickUrl: deepAccess(nativeResponse, 'link.url'), + clickTrackers: deepAccess(nativeResponse, 'link.clicktrackers'), + privacyLink: nativeResponse.privacy + } + // Trackers + if (nativeResponse.eventtrackers) { + nativeAd.impressionTrackers = []; + nativeResponse.eventtrackers.forEach(tracker => { + // Only handle impression event. Viewability events are not supported yet. + if (tracker.event !== 1) return; + switch (tracker.method) { + case 1: // img + nativeAd.impressionTrackers.push(tracker.url); + break; + case 2: // js + // javascriptTrackers is a string. If there's more than one JS tracker in bid response, the last script will be used. + nativeAd.javascriptTrackers = ``; + break; + } + }); + } else { + nativeAd.impressionTrackers = nativeResponse.imptrackers || []; + nativeAd.javascriptTrackers = nativeResponse.jstracker; + } + nativeResponse.assets.map(asset => { + const assetParams = NATIVE_DATA.getAssetById(asset.id); + switch (assetParams.assetType) { + case NATIVE_DATA.ASSET_TYPES.TITLE: + nativeAd.title = asset.title.text; + break; + case NATIVE_DATA.ASSET_TYPES.DATA: + nativeAd[assetParams.name] = asset.data.value; + break; + case NATIVE_DATA.ASSET_TYPES.IMG: + nativeAd[assetParams.name] = { + url: asset.img.url, + width: asset.img.w, + height: asset.img.h, + }; + break; } + }); + bid.native = nativeAd; + }, +}; + +const ID_OUTSTREAM = { + RENDERER_URL: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + createRenderer(bidRequest) { + const renderer = Renderer.install({ + id: bidRequest.adUnitCode, + url: this.RENDERER_URL, + config: deepAccess(bidRequest, 'renderer.options'), + adUnitCode: bidRequest.adUnitCode + }); + try { + renderer.setRender(this.render); + } catch (err) { + logWarn('Prebid Error calling setRender on renderer', err); } + return renderer; + }, + + render(bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [bid.width, bid.height], + targetId: bid.adUnitCode, + adResponse: bid.adResponse, + rendererOptions: bid.renderer.getConfig() + }, ID_OUTSTREAM.handleRendererEvents.bind(null, bid)); + }); + }, + + handleRendererEvents(bid, id, eventName) { + bid.renderer.handleVideoEvent({ id, eventName }); + }, +}; - if (!impressionObject.pid && - !impressionObject.pubid && - !impressionObject.pkey && - !(impressionObject.banner && impressionObject.banner.w && impressionObject.banner.h)) { - outputObject.impressionObject = null; - outputObject.errorCode = this.CONSTANTS.ERROR_CODES.MISSING_PLACEMENT_PARAMS; +const ID_RAZR = { + RENDERER_URL: 'https://razr.improvedigital.com/renderer.js', + addBidData({bid, bidRequest}) { + if (this.isValidBid(bid)) { + bid.renderer = Renderer.install({ + url: this.RENDERER_URL, + config: {bidRequest} + }); + bid.renderer.setRender(this.render); } - return outputObject; - }; -} + }, + + isValidBid(bid) { + return bid && /razr:\/\//.test(bid.ad); + }, + + render(bid) { + const {bidRequest} = bid.renderer.getConfig(); + + const payload = { + type: 'prebid', + bidRequest, + bid, + config: mergeDeep( + {}, + config.getConfig('improvedigital.rendererConfig'), + deepAccess(bidRequest, 'params.rendererConfig') + ) + }; + + const razr = window.razr = window.razr || {}; + razr.queue = razr.queue || []; + razr.queue.push(payload); + } +}; + +const ID_UTIL = { + toBit(val) { + return val ? 1 : 0; + }, +}; diff --git a/modules/justpremiumBidAdapter.js b/modules/justpremiumBidAdapter.js index 9993421ad1a..e2ba92d51d9 100644 --- a/modules/justpremiumBidAdapter.js +++ b/modules/justpremiumBidAdapter.js @@ -4,8 +4,7 @@ import { deepAccess } from '../src/utils.js'; const BIDDER_CODE = 'justpremium' const GVLID = 62 const ENDPOINT_URL = 'https://pre.ads.justpremium.com/v/2.0/t/xhr' -const JP_ADAPTER_VERSION = '1.8.2' -const pixels = [] +const JP_ADAPTER_VERSION = '1.8.3' export const spec = { code: BIDDER_CODE, @@ -114,8 +113,10 @@ export const spec = { return bidResponses }, - getUserSyncs: function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { let url = 'https://pre.ads.justpremium.com/v/1.0/t/sync' + '?_c=' + 'a' + Math.random().toString(36).substring(7) + Date.now(); + let pixels = [] + if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean') && gdprConsent.gdprApplies && gdprConsent.consentString) { url = url + '&consentString=' + encodeURIComponent(gdprConsent.consentString) } @@ -128,6 +129,10 @@ export const spec = { url: url }) } + if (syncOptions.pixelEnabled && serverResponses.length !== 0) { + const pxsFromResponse = serverResponses.map(res => res?.body?.pxs).reduce((acc, cur) => acc.concat(cur), []).filter((obj) => obj !== undefined); + pixels = [...pixels, ...pxsFromResponse]; + } return pixels }, } diff --git a/modules/justpremiumBidAdapter.md b/modules/justpremiumBidAdapter.md index 45dcb7b7f99..e107cb80958 100644 --- a/modules/justpremiumBidAdapter.md +++ b/modules/justpremiumBidAdapter.md @@ -2,7 +2,6 @@ **Module Name**: Justpremium Bidder Adapter **Module Type**: Bidder Adapter -**Maintainer**: headerbidding-dev@justpremium.com # Description diff --git a/modules/loglyliftBidAdapter.js b/modules/loglyliftBidAdapter.js index e1319d08766..dd5f0af1cdf 100644 --- a/modules/loglyliftBidAdapter.js +++ b/modules/loglyliftBidAdapter.js @@ -1,13 +1,13 @@ import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { NATIVE } from '../src/mediaTypes.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; const BIDDER_CODE = 'loglylift'; const ENDPOINT_URL = 'https://bid.logly.co.jp/prebid/client/v1'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [NATIVE], + supportedMediaTypes: [BANNER, NATIVE], isBidRequestValid: function (bid) { return !!(bid.params && bid.params.adspotId); @@ -43,7 +43,8 @@ export const spec = { getUserSyncs: function (syncOptions, serverResponses) { const syncs = []; - if (syncOptions.iframeEnabled && serverResponses.length > 0) { + // sync if mediaType is native because not native ad itself has a function for sync + if (syncOptions.iframeEnabled && serverResponses.length > 0 && serverResponses[0].body.bids[0].native) { syncs.push({ type: 'iframe', url: 'https://sync.logly.co.jp/sync/sync.html' diff --git a/modules/loglyliftBidAdapter.md b/modules/loglyliftBidAdapter.md index 9bca238b03e..5505d66957d 100644 --- a/modules/loglyliftBidAdapter.md +++ b/modules/loglyliftBidAdapter.md @@ -12,6 +12,22 @@ Currently module supports only native mediaType. # Test Parameters ``` var adUnits = [ + // Banner adUnit + { + code: 'test-banner-code', + sizes: [[300, 250], [300, 600]], + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [{ + bidder: 'loglylift', + params: { + adspotId: 1302078 + } + }] + }, // Native adUnit { code: 'test-native-code', diff --git a/modules/mediasniperBidAdapter.js b/modules/mediasniperBidAdapter.js new file mode 100644 index 00000000000..3e57503f7fb --- /dev/null +++ b/modules/mediasniperBidAdapter.js @@ -0,0 +1,335 @@ +import { + deepAccess, + deepClone, + deepSetValue, + getWindowTop, + inIframe, + isArray, + isEmpty, + isFn, + isNumber, + isStr, + logWarn, + logError, + logMessage, + parseUrl, + getBidIdParameter, + triggerPixel, +} from '../src/utils.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'mediasniper'; +const DEFAULT_BID_TTL = 360; +const DEFAULT_CURRENCY = 'RUB'; +const DEFAULT_NET_REVENUE = true; +const ENDPOINT = 'https://sapi.bumlam.com/prebid/'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + logMessage('Hello!! bid: ', JSON.stringify(bid)); + + if (!bid || isEmpty(bid)) { + return false; + } + + if (!bid.params || isEmpty(bid.params)) { + return false; + } + + if (!isStr(bid.params.placementId) && !isNumber(bid.params.placementId)) { + return false; + } + + const banner = deepAccess(bid, 'mediaTypes.banner', {}); + if (!banner || isEmpty(banner)) { + return false; + } + + const sizes = deepAccess(bid, 'mediaTypes.banner.sizes', []); + if (!isArray(sizes) || isEmpty(sizes)) { + return false; + } + + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const payload = createOrtbTemplate(); + + deepSetValue(payload, 'id', bidderRequest.auctionId); + + validBidRequests.forEach((validBid) => { + let bid = deepClone(validBid); + + const imp = createImp(bid); + payload.imp.push(imp); + }); + + // params + const siteId = getBidIdParameter('siteid', validBidRequests[0].params) + ''; + deepSetValue(payload, 'site.id', siteId); + + // Assign payload.site from refererinfo + if (bidderRequest.refererInfo) { + if (bidderRequest.refererInfo.reachedTop) { + const sitePage = bidderRequest.refererInfo.referer; + deepSetValue(payload, 'site.page', sitePage); + deepSetValue( + payload, + 'site.domain', + parseUrl(sitePage, { + noDecodeWholeURL: true, + }).hostname + ); + + if (canAccessTopWindow()) { + deepSetValue(payload, 'site.ref', getWindowTop().document.referrer); + } + } + } + + const request = { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(payload), + }; + + return request; + }, + + interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; + + try { + if ( + serverResponse.body && + serverResponse.body.seatbid && + isArray(serverResponse.body.seatbid) + ) { + serverResponse.body.seatbid.forEach((bidderSeat) => { + if (!isArray(bidderSeat.bid) || !bidderSeat.bid.length) { + return; + } + + bidderSeat.bid.forEach((bid) => { + const newBid = { + requestId: bid.impid, + bidderCode: spec.code, + cpm: bid.price || 0, + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.adid || bid.id, + dealId: bid.dealid || null, + currency: serverResponse.body.cur || DEFAULT_CURRENCY, + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, // seconds. https://docs.prebid.org/dev-docs/faq.html#does-prebidjs-cache-bids + ad: bid.adm, + mediaType: BANNER, + burl: bid.nurl, + meta: { + advertiserDomains: + Array.isArray(bid.adomain) && bid.adomain.length + ? bid.adomain + : [], + mediaType: BANNER, + }, + }; + + logMessage('answer: ', JSON.stringify(newBid)); + + bidResponses.push(newBid); + }); + }); + } + } catch (e) { + logError(BIDDER_CODE, e); + } + + return bidResponses; + }, + + onBidWon: function (bid) { + if (!bid.burl) { + return; + } + + const url = bid.burl.replace(/\$\{AUCTION_PRICE\}/, bid.cpm); + + triggerPixel(url); + }, +}; +registerBidder(spec); + +/** + * Detects the capability to reach window.top. + * + * @returns {boolean} + */ +function canAccessTopWindow() { + try { + return !!getWindowTop().location.href; + } catch (error) { + return false; + } +} + +/** + * Returns an openRTB 2.5 object. + * This one will be populated at each step of the buildRequest process. + * + * @returns {object} + */ +function createOrtbTemplate() { + return { + id: '', + cur: [DEFAULT_CURRENCY], + imp: [], + site: {}, + device: { + ip: '', + js: 1, + ua: navigator.userAgent, + }, + user: {}, + }; +} + +/** + * Create the OpenRTB 2.5 imp object. + * + * @param {*} bid Prebid bid object from request + * @returns + */ +function createImp(bid) { + let placementId = ''; + if (isStr(bid.params.placementId)) { + placementId = bid.params.placementId; + } else if (isNumber(bid.params.placementId)) { + placementId = bid.params.placementId.toString(); + } + + const imp = { + id: bid.bidId, + tagid: placementId, + bidfloorcur: DEFAULT_CURRENCY, + secure: 1, + }; + + // There is no default floor. bidfloor is set only + // if the priceFloors module is activated and returns a valid floor. + const floor = getMinFloor(bid); + if (isNumber(floor)) { + imp.bidfloor = floor; + } + + // Only supports proper mediaTypes definition… + for (let mediaType in bid.mediaTypes) { + switch (mediaType) { + case BANNER: + imp.banner = createBannerImp(bid); + break; + } + } + + // dealid + const dealId = getBidIdParameter('dealid', bid.params); + if (dealId) { + imp.pmp = { + private_auction: 1, + deals: [ + { + id: dealId, + bidfloor: floor || 0, + bidfloorcur: DEFAULT_CURRENCY, + }, + ], + }; + } + + return imp; +} + +/** + * Returns floor from priceFloors module or MediaKey default value. + * + * @param {*} bid a Prebid.js bid (request) object + * @param {string} mediaType the mediaType or the wildcard '*' + * @param {string|array} size the size array or the wildcard '*' + * @returns {number|boolean} + */ +function getFloor(bid, mediaType, size = '*') { + if (!isFn(bid.getFloor)) { + return false; + } + + if (spec.supportedMediaTypes.indexOf(mediaType) === -1) { + logWarn( + `${BIDDER_CODE}: Unable to detect floor price for unsupported mediaType ${mediaType}. No floor will be used.` + ); + return false; + } + + const floor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType, + size, + }); + + return !isNaN(floor.floor) && floor.currency === DEFAULT_CURRENCY + ? floor.floor + : false; +} + +function getMinFloor(bid) { + const floors = []; + + for (let mediaType in bid.mediaTypes) { + const floor = getFloor(bid, mediaType); + + if (isNumber(floor)) { + floors.push(floor); + } + } + + if (!floors.length) { + return false; + } + + return floors.reduce((a, b) => { + return Math.min(a, b); + }); +} + +/** + * Returns an openRtb 2.5 banner object. + * + * @param {object} bid Prebid bid object from request + * @returns {object} + */ +function createBannerImp(bid) { + let sizes = bid.mediaTypes.banner.sizes; + const params = deepAccess(bid, 'params', {}); + + const banner = {}; + + banner.w = parseInt(sizes[0][0], 10); + banner.h = parseInt(sizes[0][1], 10); + + const format = []; + sizes.forEach(function (size) { + if (size.length && size.length > 1) { + format.push({ w: size[0], h: size[1] }); + } + }); + banner.format = format; + + banner.topframe = inIframe() ? 0 : 1; + banner.pos = params.pos || 0; + + return banner; +} diff --git a/modules/mediasniperBidAdapter.md b/modules/mediasniperBidAdapter.md new file mode 100644 index 00000000000..e47513c7fb2 --- /dev/null +++ b/modules/mediasniperBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +``` +Module Name: Mediasniper Bid Adapter +Module Type: Bidder Adapter +Maintainer: oleg@rtbtech.org +``` + +# Description + +Connects to Mediasniper demand source to fetch bids. + +# Test Parameters + +``` +var adUnits = [ +{ + code: 'test', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bids: [{ + bidder: 'mediasniper', + params: { + placementId: "123456" + } + }] +}, +``` diff --git a/modules/missenaBidAdapter.js b/modules/missenaBidAdapter.js index 30749e977a8..41bae4d6568 100644 --- a/modules/missenaBidAdapter.js +++ b/modules/missenaBidAdapter.js @@ -43,10 +43,10 @@ export const spec = { payload.consent_string = bidderRequest.gdprConsent.consentString; payload.consent_required = bidderRequest.gdprConsent.gdprApplies; } - + const baseUrl = bidRequest.params.baseUrl || ENDPOINT_URL; return { method: 'POST', - url: ENDPOINT_URL + '?' + formatQS({ t: bidRequest.params.apiKey }), + url: baseUrl + '?' + formatQS({ t: bidRequest.params.apiKey }), data: JSON.stringify(payload), }; }); diff --git a/modules/nativoBidAdapter.js b/modules/nativoBidAdapter.js index c9e6a1f659f..e07a124665f 100644 --- a/modules/nativoBidAdapter.js +++ b/modules/nativoBidAdapter.js @@ -12,8 +12,71 @@ const TIME_TO_LIVE = 360 const SUPPORTED_AD_TYPES = [BANNER] +/** + * Keep track of bid data by keys + * @returns {Object} - Map of bid data that can be referenced by multiple keys + */ +const BidDataMap = () => { + const referenceMap = {} + const bids = [] + + /** + * Add a refence to the index by key value + * @param {String} key - The key to store the index reference + * @param {Integer} index - The index value of the bidData + */ + function adKeyReference(key, index) { + if (!referenceMap.hasOwnProperty(key)) { + referenceMap[key] = index + } + } + + /** + * Adds a bid to the map + * @param {Object} bid - Bid data + * @param {Array/String} keys - Keys to reference the index value + */ + function addBidData(bid, keys) { + const index = bids.length + bids.push(bid) + + if (Array.isArray(keys)) { + keys.forEach((key) => { + adKeyReference(String(key), index) + }) + return + } + + adKeyReference(String(keys), index) + } + + /** + * Get's the bid data refrerenced by the key + * @param {String} key - The key value to find the bid data by + * @returns {Object} - The bid data + */ + function getBidData(key) { + const stringKey = String(key) + if (referenceMap.hasOwnProperty(stringKey)) { + return bids[referenceMap[stringKey]] + } + } + + // Return API + return { + addBidData, + getBidData, + } +} + const bidRequestMap = {} const adUnitsRequested = {} +const extData = {} + +// Filtering +const adsToFilter = new Set() +const advertisersToFilter = new Set() +const campaignsToFilter = new Set() // Prebid adapter referrence doc: https://docs.prebid.org/dev-docs/bidder-adaptor.html @@ -45,7 +108,7 @@ export const spec = { if (!bid.params) return bid.bidder === BIDDER_CODE // Check if any supplied parameters are invalid - const hasInvalidParameters = Object.keys(bid.params).some(key => { + const hasInvalidParameters = Object.keys(bid.params).some((key) => { const value = bid.params[key] const validityCheck = validParameter[key] @@ -69,8 +132,8 @@ export const spec = { */ buildRequests: function (validBidRequests, bidderRequest) { const placementIds = new Set() - const placmentBidIdMap = {} let placementId, pageUrl + const bidDataMap = BidDataMap() validBidRequests.forEach((request) => { pageUrl = deepAccess( request, @@ -83,13 +146,13 @@ export const spec = { placementIds.add(placementId) } - var key = placementId || request.adUnitCode - placmentBidIdMap[key] = { + const bidData = { bidId: request.bidId, size: getLargestSize(request.sizes), } + bidDataMap.addBidData(bidData, [placementId, request.adUnitCode]) }) - bidRequestMap[bidderRequest.bidderRequestId] = placmentBidIdMap + bidRequestMap[bidderRequest.bidderRequestId] = bidDataMap // Build adUnit data const adUnitData = { @@ -123,6 +186,20 @@ export const spec = { }, ] + // Add filtering + if (adsToFilter.size > 0) { + params.unshift({ key: 'ntv_atf', value: Array.from(adsToFilter).join(',') }) + } + + if (advertisersToFilter.size > 0) { + params.unshift({ key: 'ntv_avtf', value: Array.from(advertisersToFilter).join(',') }) + } + + if (campaignsToFilter.size > 0) { + params.unshift({ key: 'ntv_ctf', value: Array.from(campaignsToFilter).join(',') }) + } + + // Add placement IDs if (placementIds.size > 0) { // Convert Set to Array (IE 11 Safe) const placements = [] @@ -131,6 +208,7 @@ export const spec = { params.unshift({ key: 'ntv_ptd', value: placements.join(',') }) } + // Add GDPR params if (bidderRequest.gdprConsent) { // Put on the beginning of the qs param array params.unshift({ @@ -139,6 +217,7 @@ export const spec = { }) } + // Add USP params if (bidderRequest.uspConsent) { // Put on the beginning of the qs param array params.unshift({ key: 'us_privacy', value: bidderRequest.uspConsent }) @@ -195,6 +274,8 @@ export const spec = { }, } + if (bid.ext) extData[bid.id] = bid.ext + bidResponses.push(bidResponse) }) }) @@ -300,7 +381,15 @@ export const spec = { * Will be called when a bid from the adapter won the auction. * @param {Object} bid - The bid that won the auction */ - onBidWon: function (bid) {}, + onBidWon: function (bid) { + const ext = extData[bid.dealId] + + if (!ext) return + + appendFilterData(adsToFilter, ext.adsToFilter) + appendFilterData(advertisersToFilter, ext.advertisersToFilter) + appendFilterData(campaignsToFilter, ext.campaignsToFilter) + }, /** * Will be called when the adserver targeting has been set for a bid from the adapter. @@ -315,12 +404,14 @@ export const spec = { * @returns {String} - The bidId value associated with the corresponding placementId */ getAdUnitData: function (bidderRequestId, bid) { - var data = deepAccess(bidRequestMap, `${bidderRequestId}.${bid.impid}`) + const bidDataMap = bidRequestMap[bidderRequestId] - if (data) return data + const placementId = bid.impid + const adUnitCode = deepAccess(bid, 'ext.ad_unit_id') - var unitCode = deepAccess(bid, 'ext.ad_unit_id') - return deepAccess(bidRequestMap, `${bidderRequestId}.${unitCode}`) + return ( + bidDataMap.getBidData(adUnitCode) || bidDataMap.getBidData(placementId) + ) }, } registerBidder(spec) @@ -375,3 +466,14 @@ function getLargestSize(sizes, method = area) { * @returns The calculated area */ const area = (size) => size[0] * size[1] + +/** + * Save any filter data from winning bid requests for subsequent requests + * @param {Array} filter - The filter data bucket currently stored + * @param {Array} filterData - The filter data to add + */ +function appendFilterData(filter, filterData) { + if (filterData && Array.isArray(filterData) && filterData.length) { + filterData.forEach((ad) => filter.add(ad)) + } +} diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index 85537d382c2..365d692e614 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -13,7 +13,7 @@ export const spec = { isBidRequestValid: function(bid) { return !!( - bid.params.placement_id && isStr(bid.params.placement_id) + (bid.params.placement_id && isStr(bid.params.placement_id)) || (bid.params.group_id && isStr(bid.params.group_id)) ); }, @@ -28,9 +28,10 @@ export const spec = { 'ext': { 'prebid': { 'storedrequest': { - 'id': getBidIdParameter('placement_id', bid.params) + 'id': getPlacementId(bid) } }, + 'nextMillennium': { 'refresh_count': window.nmmRefreshCounts[bid.adUnitCode]++, } @@ -46,10 +47,12 @@ export const spec = { if (uspConsent) { postBody.regs.ext.us_privacy = uspConsent; } + if (gdprConsent) { if (typeof gdprConsent.gdprApplies !== 'undefined') { postBody.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; } + if (typeof gdprConsent.consentString !== 'undefined') { postBody.user = { ext: { consent: gdprConsent.consentString } @@ -91,6 +94,7 @@ export const spec = { meta: { advertiserDomains: bid.adomain || [] }, + ad: bid.adm }); }); @@ -125,4 +129,34 @@ export const spec = { }, }; +function getPlacementId(bid) { + const groupId = getBidIdParameter('group_id', bid.params) + const placementId = getBidIdParameter('placement_id', bid.params) + if (!groupId) return placementId + + let windowTop = getTopWindow(window) + let size = [] + if (bid.mediaTypes) { + if (bid.mediaTypes.banner) size = bid.mediaTypes.banner.sizes && bid.mediaTypes.banner.sizes[0] + if (bid.mediaTypes.video) size = bid.mediaTypes.video.playerSize + } + + const host = (windowTop && windowTop.location && windowTop.location.host) || '' + return `g${groupId};${size.join('x')};${host}` +} + +function getTopWindow(curWindow, nesting = 0) { + if (nesting > 10) { + return curWindow + } + + try { + if (curWindow.parent.document) { + return getTopWindow(curWindow.parent.window, ++nesting) + } + } catch (err) { + return curWindow + } +} + registerBidder(spec); diff --git a/modules/nextMillenniumBidAdapter.md b/modules/nextMillenniumBidAdapter.md index 048fe907ac7..136f97d94d5 100644 --- a/modules/nextMillenniumBidAdapter.md +++ b/modules/nextMillenniumBidAdapter.md @@ -21,8 +21,9 @@ Currently module supports only banner mediaType. bids: [{ bidder: 'nextMillennium', params: { - placement_id: '-1' + placement_id: '-1', + group_id: '6731' } }] }]; -``` \ No newline at end of file +``` diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js index a59bb635875..0689d3b04ce 100644 --- a/modules/nexx360BidAdapter.js +++ b/modules/nexx360BidAdapter.js @@ -1,17 +1,17 @@ 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 {BANNER, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = 'nexx360'; -const BIDDER_URL = 'https://fast.nexx360.io/prebid' -const CACHE_URL = 'https://fast.nexx360.io/cache' -const METRICS_TRACKER_URL = 'https://fast.nexx360.io/track-imp' +const BIDDER_URL = 'https://fast.nexx360.io/prebid'; +const CACHE_URL = 'https://fast.nexx360.io/cache'; +const METRICS_TRACKER_URL = 'https://fast.nexx360.io/track-imp'; export const spec = { code: BIDDER_CODE, aliases: ['revenuemaker'], // short code - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. * @@ -37,6 +37,7 @@ export const spec = { adUnits.push({ account: adunitValue.params.account, tagId: adunitValue.params.tagId, + videoExt: adunitValue.params.videoExt, label: adunitValue.adUnitCode, bidId: adunitValue.bidId, auctionId: adunitValue.auctionId, @@ -84,13 +85,13 @@ export const spec = { if (serverBody.hasOwnProperty('responses')) { Object.keys(serverBody['responses']).forEach(key => { value = serverBody['responses'][key]; + const url = `${CACHE_URL}?uuid=${value['uuid']}`; bidResponse = { requestId: value['bidId'], cpm: value['cpm'], currency: value['currency'], width: value['width'], height: value['height'], - adUrl: `${CACHE_URL}?uuid=${value['uuid']}`, ttl: value['ttl'], creativeId: value['creativeId'], netRevenue: true, @@ -105,6 +106,21 @@ export const spec = { } */ }; + if (value.type === 'banner') bidResponse.adUrl = url; + if (value.type === 'video') { + const params = { + type: 'prebid', + mediatype: 'video', + ssp: value.bidder, + tag_id: value.tagId, + consent: value.consent, + price: value.cpm, + }; + bidResponse.cpm = value.cpm; + bidResponse.mediaType = 'video'; + bidResponse.vastUrl = url; + bidResponse.vastImpUrl = `${METRICS_TRACKER_URL}?${new URLSearchParams(params).toString()}`; + } bidResponses.push(bidResponse); }); } @@ -133,7 +149,7 @@ export const spec = { */ onBidWon: function(bid) { // fires a pixel to confirm a winning bid - const params = { type: 'prebid' }; + const params = { type: 'prebid', mediatype: 'banner' }; if (bid.hasOwnProperty('nexx360')) { if (bid.nexx360.hasOwnProperty('ssp')) params.ssp = bid.nexx360.ssp; if (bid.nexx360.hasOwnProperty('tagId')) params.tag_id = bid.nexx360.tagId; diff --git a/modules/nexx360BidAdapter.md b/modules/nexx360BidAdapter.md index 882d83cb24e..532d48418b6 100644 --- a/modules/nexx360BidAdapter.md +++ b/modules/nexx360BidAdapter.md @@ -10,9 +10,13 @@ Maintainer: gabriel@nexx360.io Connects to Nexx360 network for bids. -Nexx360 bid adapter supports Banner only for the time being. +To use us as a bidder you must have an account and an active "tagId" on our Nexx360 platform. # Test Parameters + +## Web + +### Display ``` var adUnits = [ // Banner adUnit @@ -33,3 +37,23 @@ var adUnits = [ }, ]; ``` + +### Video Instream +``` + var videoAdUnit = { + code: 'video1', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [{ + bidder: 'nexx360', + params: { + account: '1067', + tagId: 'luvxjvgn' + } + }] + }; +``` diff --git a/modules/outbrainBidAdapter.js b/modules/outbrainBidAdapter.js index 439570e976e..e903f053c7e 100644 --- a/modules/outbrainBidAdapter.js +++ b/modules/outbrainBidAdapter.js @@ -27,9 +27,28 @@ export const spec = { gvlid: GVLID, supportedMediaTypes: [ NATIVE, BANNER ], isBidRequestValid: (bid) => { + if (typeof bid.params !== 'object') { + return false; + } + + if (typeof deepAccess(bid, 'params.publisher.id') !== 'string') { + return false; + } + + if (!!bid.params.tagid && typeof bid.params.tagid !== 'string') { + return false; + } + + if (!!bid.params.bcat && (typeof bid.params.bcat !== 'object' || !bid.params.bcat.every(item => typeof item === 'string'))) { + return false; + } + + if (!!bid.params.badv && (typeof bid.params.badv !== 'object' || !bid.params.badv.every(item => typeof item === 'string'))) { + return false; + } + return ( !!config.getConfig('outbrain.bidderUrl') && - !!deepAccess(bid, 'params.publisher.id') && !!(bid.nativeParams || bid.sizes) ); }, @@ -67,6 +86,13 @@ export const spec = { } } + if (typeof bid.getFloor === 'function') { + const floor = _getFloor(bid, bid.nativeParams ? NATIVE : BANNER); + if (floor) { + imp.bidfloor = floor; + } + } + return imp; }); @@ -190,7 +216,7 @@ export const spec = { registerBidder(spec); function parseNative(bid) { - const { assets, link, eventtrackers } = JSON.parse(bid.adm); + const { assets, link, privacy, eventtrackers } = JSON.parse(bid.adm); const result = { clickUrl: link.url, clickTrackers: link.clicktrackers || undefined @@ -202,6 +228,9 @@ function parseNative(bid) { result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; } }); + if (privacy) { + result.privacyLink = privacy; + } if (eventtrackers) { result.impressionTrackers = []; eventtrackers.forEach(tracker => { @@ -251,8 +280,8 @@ function getNativeAssets(bid) { if (bidParams.sizes) { const sizes = flatten(bidParams.sizes); - w = sizes[0]; - h = sizes[1]; + w = parseInt(sizes[0], 10); + h = parseInt(sizes[1], 10); } asset[props.name] = { @@ -291,3 +320,15 @@ function transformSizes(requestSizes) { return []; } + +function _getFloor(bid, type) { + const floorInfo = bid.getFloor({ + currency: CURRENCY, + mediaType: type, + size: '*' + }); + if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + return parseFloat(floorInfo.floor); + } + return null; +} diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index c9d22655a31..c4674132416 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -70,11 +70,12 @@ export function setBidderRtb (auctionDetails, customModuleConfig) { const moduleConfig = getModuleConfig(customModuleConfig) const acBidders = deepAccess(moduleConfig, 'params.acBidders') const maxSegs = deepAccess(moduleConfig, 'params.maxSegs') + const transformationConfigs = deepAccess(moduleConfig, 'params.transformations') || [] const segmentData = getSegments(maxSegs) acBidders.forEach(function (bidder) { const currConfig = bidderConfig[bidder] || {} - const nextConfig = mergeOrtbConfig(currConfig, segmentData) + const nextConfig = updateOrtbConfig(currConfig, segmentData.ac, transformationConfigs) // ORTB2 uses the `ac` segment IDs config.setBidderConfig({ bidders: [bidder], @@ -84,23 +85,33 @@ export function setBidderRtb (auctionDetails, customModuleConfig) { } /** - * Merges segments into existing bidder config + * Updates `user.data` object in existing bidder config with Permutive segments * @param {Object} currConfig - Current bidder config - * @param {Object} segmentData - Segment data + * @param {Object[]} transformationConfigs - array of objects with `id` and `config` properties, used to determine + * the transformations on user data to include the ORTB2 object + * @param {string[]} segmentIDs - Permutive segment IDs * @return {Object} Merged ortb2 object */ -function mergeOrtbConfig (currConfig, segmentData) { - const segment = segmentData.ac.map(seg => { - return { id: seg } - }) +function updateOrtbConfig (currConfig, segmentIDs, transformationConfigs) { const name = 'permutive.com' + + const permutiveUserData = { + name, + segment: segmentIDs.map(segmentId => ({ id: segmentId })), + } + + const transformedUserData = transformationConfigs + .filter(({ id }) => ortb2UserDataTransformations.hasOwnProperty(id)) + .map(({ id, config }) => ortb2UserDataTransformations[id](permutiveUserData, config)) + const ortbConfig = mergeDeep({}, currConfig) - const currSegments = deepAccess(ortbConfig, 'ortb2.user.data') || [] - const userSegment = currSegments + const currentUserData = deepAccess(ortbConfig, 'ortb2.user.data') || [] + + const updatedUserData = currentUserData .filter(el => el.name !== name) - .concat({ name, segment }) + .concat(permutiveUserData, transformedUserData) - deepSetValue(ortbConfig, 'ortb2.user.data', userSegment) + deepSetValue(ortbConfig, 'ortb2.user.data', updatedUserData) return ortbConfig } @@ -236,11 +247,11 @@ export function getSegments (maxSegs) { ac: [..._pcrprs, ..._ppam, ...legacySegs], rubicon: readSegments('_prubicons'), appnexus: readSegments('_papns'), - gam: readSegments('_pdfps') + gam: readSegments('_pdfps'), } - for (const type in segments) { - segments[type] = segments[type].slice(0, maxSegs) + for (const bidder in segments) { + segments[bidder] = segments[bidder].slice(0, maxSegs) } return segments @@ -260,6 +271,34 @@ function readSegments (key) { } } +const unknownIabSegmentId = '_unknown_' + +/** + * Functions to apply to ORT2B2 `user.data` objects. + * Each function should return an a new object containing a `name`, (optional) `ext` and `segment` + * properties. The result of the each transformation defined here will be appended to the array + * under `user.data` in the bid request. + */ +const ortb2UserDataTransformations = { + iab: (userData, config) => ({ + name: userData.name, + ext: { segtax: config.segtax }, + segment: (userData.segment || []) + .map(segment => ({ id: iabSegmentId(segment.id, config.iabIds) })) + .filter(segment => segment.id !== unknownIabSegmentId) + }) +} + +/** + * Transform a Permutive segment ID into an IAB audience taxonomy ID. + * @param {string} permutiveSegmentId + * @param {Object} iabIds object of mappings between Permutive and IAB segment IDs (key: permutive ID, value: IAB ID) + * @return {string} IAB audience taxonomy ID associated with the Permutive segment ID + */ +function iabSegmentId(permutiveSegmentId, iabIds) { + return iabIds[permutiveSegmentId] || unknownIabSegmentId +} + /** @type {RtdSubmodule} */ export const permutiveSubmodule = { name: MODULE_NAME, diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md index 0acd42405d1..5fa6e14a474 100644 --- a/modules/permutiveRtdProvider.md +++ b/modules/permutiveRtdProvider.md @@ -1,8 +1,11 @@ # Permutive Real-time Data Submodule + This submodule reads cohorts from Permutive and attaches them as targeting keys to bid requests. Using this module will deliver best targeting results, leveraging Permutive's real-time segmentation and modelling capabilities. ## Usage + Compile the Permutive RTD module into your Prebid build: + ``` gulp build --modules=rtdModule,permutiveRtdProvider ``` @@ -29,25 +32,38 @@ pbjs.setConfig({ ``` ## Supported Bidders + The Permutive RTD module sets Audience Connector cohorts as bidder-specific `ortb2.user.data` first-party data, following the Prebid `ortb2` convention, for any bidder included in `acBidders`. The module also supports bidder-specific data locations per ad unit (custom parameters) for the below bidders: -| Bidder | ID | Custom Cohorts | Audience Connector | -| ----------- | ---------- | -------------------- | ------------------ | -| Xandr | `appnexus` | Yes | Yes | -| Magnite | `rubicon` | Yes | No | -| Ozone | `ozone` | No | Yes | +| Bidder | ID | Custom Cohorts | Audience Connector | +| ------- | ---------- | -------------- | ------------------ | +| Xandr | `appnexus` | Yes | Yes | +| Magnite | `rubicon` | Yes | No | +| Ozone | `ozone` | No | Yes | Key-values details for custom parameters: -* **Custom Cohorts:** When enabling the respective Activation for a cohort in Permutive, this module will automatically attach that cohort ID to the bid request. There is no need to enable individual bidders in the module configuration, it will automatically reflect which SSP integrations you have enabled in your Permutive dashboard. Permutive cohorts will be sent in the `permutive` key-value. -* **Audience Connector:** You'll need to define which bidders should receive Audience Connector cohorts. You need to include the `ID` of any bidder in the `acBidders` array. Audience Connector cohorts will be sent in the `p_standard` key-value. +- **Custom Cohorts:** When enabling the respective Activation for a cohort in Permutive, this module will automatically attach that cohort ID to the bid request. There is no need to enable individual bidders in the module configuration, it will automatically reflect which SSP integrations you have enabled in your Permutive dashboard. Permutive cohorts will be sent in the `permutive` key-value. +- **Audience Connector:** You'll need to define which bidders should receive Audience Connector cohorts. You need to include the `ID` of any bidder in the `acBidders` array. Audience Connector cohorts will be sent in the `p_standard` key-value. ## Parameters -| Name | Type | Description | Default | -| ----------------- | -------------------- | ------------------ | ------------------ | -| name | String | This should always be `permutive` | - | -| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | -| params | Object | | - | -| params.acBidders | String[] | An array of bidders which should receive AC cohorts. | `[]` | -| params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` | + +| Name | Type | Description | Default | +| ---------------------- | -------- | --------------------------------------------------------------------------------------------- | ------- | +| name | String | This should always be `permutive` | - | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | +| params | Object | | - | +| params.acBidders | String[] | An array of bidders which should receive AC cohorts. | `[]` | +| params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` | +| params.transformations | Object[] | An array of configurations for ORTB2 user data transformations | | + +### The `transformations` parameter + +This array contains configurations for transformations we'll apply to the Permutive object in the ORTB2 `user.data` array. The results of these transformations will be appended to the `user.data` array that's attached to ORTB2 bid requests. + +#### Supported transformations + +| Name | ID | Config structure | Description | +| -------------- | --- | ------------------------------------------------- | ------------------------------------------------------------------------------------ | +| IAB taxonomies | iab | { segtax: number, iabIds: Object} | Transform segment IDs from Permutive to IAB (note: alpha version, subject to change) | diff --git a/modules/priceFloors.js b/modules/priceFloors.js index ca6e312dad4..ff4213f1330 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -138,7 +138,7 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let matchingData = { floorMin: floorData.floorMin || 0, - floorRuleValue: floorData.values[matchingRule] || floorData.default, + floorRuleValue: isNaN(floorData.values[matchingRule]) ? floorData.default : floorData.values[matchingRule], matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters matchingRule }; diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index 7aa3ad6088c..c0280e944ae 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -419,61 +419,14 @@ function user(bidRequest, bidderRequest) { } } if (bidRequest) { - if (bidRequest.userId) { - ext.eids = []; - addExternalUserId(ext.eids, bidRequest.userId.pubcid, 'pubcid.org'); - addExternalUserId(ext.eids, bidRequest.userId.britepoolid, 'britepool.com'); - addExternalUserId(ext.eids, bidRequest.userId.criteoId, 'criteo.com'); - addExternalUserId(ext.eids, bidRequest.userId.idl_env, 'liveramp.com'); - addExternalUserId(ext.eids, deepAccess(bidRequest, 'userId.id5id.uid'), 'id5-sync.com', deepAccess(bidRequest, 'userId.id5id.ext')); - addExternalUserId(ext.eids, deepAccess(bidRequest, 'userId.parrableId.eid'), 'parrable.com'); - addExternalUserId(ext.eids, bidRequest.userId.fabrickId, 'neustar.biz'); - addExternalUserId(ext.eids, deepAccess(bidRequest, 'userId.haloId.haloId'), 'audigent.com'); - addExternalUserId(ext.eids, bidRequest.userId.merkleId, 'merkleinc.com'); - addExternalUserId(ext.eids, bidRequest.userId.lotamePanoramaId, 'crwdcntrl.net'); - addExternalUserId(ext.eids, bidRequest.userId.connectid, 'verizonmedia.com'); - addExternalUserId(ext.eids, deepAccess(bidRequest, 'userId.uid2.id'), 'uidapi.com'); - // liveintent - if (bidRequest.userId.lipb && bidRequest.userId.lipb.lipbid) { - addExternalUserId(ext.eids, bidRequest.userId.lipb.lipbid, 'liveintent.com'); - } - // TTD - addExternalUserId(ext.eids, bidRequest.userId.tdid, 'adserver.org', { - rtiPartner: 'TDID' - }); - // digitrust - const digitrustResponse = bidRequest.userId.digitrustid; - if (digitrustResponse && digitrustResponse.data) { - var digitrust = {}; - if (digitrustResponse.data.id) { - digitrust.id = digitrustResponse.data.id; - } - if (digitrustResponse.data.keyv) { - digitrust.keyv = digitrustResponse.data.keyv; - } - ext.digitrust = digitrust; - } + let eids = bidRequest.userIdAsEids; + if (eids) { + ext.eids = eids; } } return { ext }; } -/** - * Produces external userid object in ortb 3.0 model. - */ -function addExternalUserId(eids, id, source, uidExt) { - if (id) { - var uid = { id }; - if (uidExt) { - uid.ext = uidExt; - } - eids.push({ - source, - uids: [ uid ] - }); - } -} - /** * Produces the regulations ortb object */ diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 34e4a04aac2..69335ff33a8 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -546,14 +546,20 @@ function subscribeToGamSlots() { window.googletag.pubads().addEventListener('slotRenderEnded', event => { const isMatchingAdSlot = isAdUnitCodeMatchingSlot(event.slot); // loop through auctions and adUnits and mark the info - Object.keys(cache.auctions).forEach(auctionId => { + // only mark first auction which finds a match + let hasMatch = false; + Object.keys(cache.auctions).find(auctionId => { (Object.keys(cache.auctions[auctionId].bids) || []).forEach(bidId => { let bid = cache.auctions[auctionId].bids[bidId]; // if this slot matches this bids adUnit, add the adUnit info - if (isMatchingAdSlot(bid.adUnit.adUnitCode)) { + // only mark it if it already has not been marked + if (!bid.adUnit.gamRendered && isMatchingAdSlot(bid.adUnit.adUnitCode)) { // mark this adUnit as having been rendered by gam cache.auctions[auctionId].gamHasRendered[bid.adUnit.adUnitCode] = true; + // this current auction has an adunit that matched the slot, so mark it as matched so next auciton is skipped + hasMatch = true; + bid.adUnit.gam = pick(event, [ // these come in as `null` from Gpt, which when stringified does not get removed // so set explicitly to undefined when not a number @@ -563,6 +569,9 @@ function subscribeToGamSlots() { 'adSlot', () => event.slot.getAdUnitPath(), 'isSlotEmpty', () => event.isEmpty || undefined ]); + + // this lets us know next iteration not to check this bids adunit + bid.adUnit.gamRendered = true; } }); // Now if all adUnits have gam rendered, send the payload @@ -575,6 +584,7 @@ function subscribeToGamSlots() { sendMessage.call(rubiconAdapter, auctionId, undefined, 'gam') } } + return hasMatch; }); }); } diff --git a/modules/seedingAllianceBidAdapter.js b/modules/seedingAllianceBidAdapter.js index b7aec0f8881..05dcf15909a 100755 --- a/modules/seedingAllianceBidAdapter.js +++ b/modules/seedingAllianceBidAdapter.js @@ -189,12 +189,12 @@ function parseNative(bid) { if (link.clicktrackers) { link.clicktrackers.forEach(function (clicktracker, index) { - link.clicktrackers[index] = clicktracker.replace(/\$\{AUCTION_PRICE\}/, bid.price); + link.clicktrackers[index] = clicktracker.replace(/\$\{AUCTION_PRICE\}/g, bid.price); }); } if (imptrackers) { imptrackers.forEach(function (imptracker, index) { - imptrackers[index] = imptracker.replace(/\$\{AUCTION_PRICE\}/, bid.price); + imptrackers[index] = imptracker.replace(/\$\{AUCTION_PRICE\}/g, bid.price); }); } diff --git a/modules/seedtagBidAdapter.js b/modules/seedtagBidAdapter.js index bae27d41028..2f61e0bc56a 100644 --- a/modules/seedtagBidAdapter.js +++ b/modules/seedtagBidAdapter.js @@ -154,9 +154,12 @@ export function getTimeoutUrl (data) { isArray(data[0].params) && data[0].params[0] ) { const params = data[0].params[0]; + const timeout = data[0].timeout + queryParams = '?publisherToken=' + params.publisherId + - '&adUnitId=' + params.adUnitId; + '&adUnitId=' + params.adUnitId + + '&timeout=' + timeout; } return SEEDTAG_SSP_ONTIMEOUT_ENDPOINT + queryParams; } diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index 215769e9812..830d26eda61 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -192,9 +192,9 @@ function _getGlobalFpd() { const fpd = {}; const context = {} const user = {}; - const ortbData = config.getLegacyFpd(config.getConfig('ortb2')) || {}; + const ortbData = config.getConfig('ortb2') || {}; - const fpdContext = Object.assign({}, ortbData.context); + const fpdContext = Object.assign({}, ortbData.site); const fpdUser = Object.assign({}, ortbData.user); _addEntries(context, fpdContext); diff --git a/modules/yandexBidAdapter.js b/modules/yandexBidAdapter.js new file mode 100644 index 00000000000..e20f71bc08d --- /dev/null +++ b/modules/yandexBidAdapter.js @@ -0,0 +1,107 @@ +import {parseUrl, formatQS, deepAccess} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'yandex'; +const BIDDER_URL = 'https://bs-metadsp.yandex.ru/metadsp'; +const DEFAULT_TTL = 180; +const SSP_ID = 10500; + +export const spec = { + code: BIDDER_CODE, + aliases: ['ya'], // short code + + isBidRequestValid: function(bid) { + return !!(bid.params && bid.params.pageId && bid.params.impId); + }, + + buildRequests: function(validBidRequests, bidderRequest) { + const gdprApplies = deepAccess(bidderRequest, 'gdprConsent.gdprApplies'); + const consentString = deepAccess(bidderRequest, 'gdprConsent.consentString'); + + let referrer = ''; + if (bidderRequest && bidderRequest.refererInfo) { + const url = parseUrl(bidderRequest.refererInfo.referer); + referrer = url.hostname; + } + + return validBidRequests.map((bidRequest) => { + const { params } = bidRequest; + const { pageId, impId, targetRef, withCredentials = true } = params; + + const queryParams = { + 'imp-id': impId, + 'target-ref': targetRef || referrer, + 'ssp-id': SSP_ID, + }; + if (gdprApplies !== undefined) { + queryParams['gdpr'] = 1; + queryParams['tcf-consent'] = consentString; + } + const imp = { + id: impId, + }; + + const bannerParams = deepAccess(bidRequest, 'mediaTypes.banner'); + if (bannerParams) { + const [ w, h ] = bannerParams.sizes[0]; + imp.banner = { + w, + h, + }; + } + + const queryParamsString = formatQS(queryParams); + return { + method: 'POST', + url: BIDDER_URL + `/${pageId}?${queryParamsString}`, + data: { + id: bidRequest.bidId, + imp: [imp], + site: { + page: referrer, + }, + }, + options: { + withCredentials, + }, + bidRequest, + } + }); + }, + + interpretResponse: function(serverResponse, {bidRequest}) { + let response = serverResponse.body; + if (!response.seatbid) { + return []; + } + + const { cur, seatbid } = serverResponse.body; + const rtbBids = seatbid + .map(seatbid => seatbid.bid) + .reduce((a, b) => a.concat(b), []); + + return rtbBids.map(rtbBid => { + let prBid = { + requestId: bidRequest.bidId, + cpm: rtbBid.price, + currency: cur || 'USD', + width: rtbBid.w, + height: rtbBid.h, + creativeId: rtbBid.adid, + + netRevenue: true, + ttl: DEFAULT_TTL, + + meta: { + advertiserDomains: rtbBid.adomain && rtbBid.adomain.length > 0 ? rtbBid.adomain : [], + } + }; + + prBid.ad = rtbBid.adm; + + return prBid; + }); + }, +} + +registerBidder(spec); diff --git a/modules/yandexBidAdapter.md b/modules/yandexBidAdapter.md new file mode 100644 index 00000000000..7a51d7bc5fb --- /dev/null +++ b/modules/yandexBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +``` +Module Name: Yandex Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@yandex-team.com +``` + +# Description + +Yandex Bidder Adapter for Prebid.js. + +# Parameters + +| Name | Scope | Description | Example | Type | +|---------------|----------|-------------------------|-----------|-----------| +| `pageId` | required | Page ID | `123` | `Integer` | +| `impId` | required | Block ID | `1` | `Integer` | + +# Test Parameters + +``` +var adUnits = [{ + code: 'banner-1', + mediaTypes: { + banner: { + sizes: [[240, 400]], + } + }, + bids: [{ + { + bidder: 'yandex', + params: { + pageId: 346580, + impId: 143, + }, + } + }] +}]; +``` diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index fd3540dce58..9535461f4c7 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -1,9 +1,9 @@ -import {_each, deepAccess, isArray, isPlainObject} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {find} from '../src/polyfill.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {Renderer} from '../src/Renderer.js'; -import {config} from '../src/config.js'; +import { _each, deepAccess, isArray, isPlainObject, timestamp } from '../src/utils.js' +import { registerBidder } from '../src/adapters/bidderFactory.js' +import { find } from '../src/polyfill.js' +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js' +import { Renderer } from '../src/Renderer.js' +import { config } from '../src/config.js' const ENDPOINT = 'https://ad.yieldlab.net' const BIDDER_CODE = 'yieldlab' @@ -174,6 +174,37 @@ export const spec = { } }) return bidResponses + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @param {Object} gdprConsent Is the GDPR Consent object wrapping gdprApplies {boolean} and consentString {string} attributes. + * @param {string} uspConsent Is the US Privacy Consent string. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + + if (syncOptions.iframeEnabled) { + let params = []; + params.push(`ts=${timestamp()}`); + params.push(`type=h`) + if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean')) { + params.push(`gdpr=${Number(gdprConsent.gdprApplies)}`); + } + if (gdprConsent && (typeof gdprConsent.consentString === 'string')) { + params.push(`gdpr_consent=${gdprConsent.consentString}`); + } + syncs.push({ + type: 'iframe', + url: `${ENDPOINT}/d/6846326/766/2x2?${params.join('&')}` + }); + } + + return syncs; } }; diff --git a/package-lock.json b/package-lock.json index 9fbd4aea6a8..e40addfc54d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "6.18.0-pre", + "version": "6.20.0-pre", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "6.15.0-pre", + "version": "6.20.0-pre", "license": "Apache-2.0", "dependencies": { "babel-plugin-transform-object-assign": "^6.22.0", @@ -19,7 +19,7 @@ "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "2.0.0" + "live-connect-js": "2.3.1" }, "devDependencies": { "@babel/core": "^7.16.7", @@ -33,7 +33,7 @@ "@wdio/mocha-framework": "^7.5.2", "@wdio/spec-reporter": "^7.5.2", "@wdio/sync": "^7.5.2", - "ajv": "5.5.2", + "ajv": "6.12.3", "assert": "^2.0.0", "babel-loader": "^8.0.5", "body-parser": "^1.19.0", @@ -1746,12 +1746,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@eslint/eslintrc/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "13.12.1", "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", @@ -1767,12 +1761,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3646,24 +3634,19 @@ } }, "node_modules/ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", "dev": true, "dependencies": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "node_modules/ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", - "dev": true, - "peerDependencies": { - "ajv": "^5.0.0" + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/amdefine": { @@ -8460,12 +8443,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "node_modules/eslint/node_modules/globals": { "version": "13.12.1", "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", @@ -8481,12 +8458,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/eslint/node_modules/semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -9198,9 +9169,9 @@ } }, "node_modules/fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "node_modules/fast-json-stable-stringify": { @@ -11503,6 +11474,27 @@ "node": ">=0.4.0" } }, + "node_modules/gulp-eslint/node_modules/ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "dependencies": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "node_modules/gulp-eslint/node_modules/ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true, + "peerDependencies": { + "ajv": "^5.0.0" + } + }, "node_modules/gulp-eslint/node_modules/ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -11678,6 +11670,12 @@ "node": ">=0.12" } }, + "node_modules/gulp-eslint/node_modules/fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, "node_modules/gulp-eslint/node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -11755,6 +11753,12 @@ "node": ">=4" } }, + "node_modules/gulp-eslint/node_modules/json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "node_modules/gulp-eslint/node_modules/levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -12544,18 +12548,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/har-validator/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/har-validator/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -14433,9 +14425,9 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { @@ -15148,9 +15140,9 @@ "dev": true }, "node_modules/live-connect-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-2.0.0.tgz", - "integrity": "sha512-Xhrj1JU5LoLjJuujjTlvDfc/n3Shzk2hPlYmLdCx/lsltFFVuCFa9uM8u5mcHlmOUKP5pu9I54bAITxZBMHoXg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-2.3.1.tgz", + "integrity": "sha512-4IT8NEOOTNmoYpw5CTxdugSF2w9xqfOujrEqx6zLPdTT3xq/lLdxxvRTREDi+qYHDsCDovdiNO3uOSoemdTCdA==", "dependencies": { "tiny-hashes": "1.0.1" }, @@ -19521,18 +19513,6 @@ "ajv": "^6.9.1" } }, - "node_modules/schema-utils/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -20988,12 +20968,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/table/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "node_modules/table/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -21174,18 +21148,6 @@ "ajv": "^6.9.1" } }, - "node_modules/terser-webpack-plugin/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -22809,18 +22771,6 @@ "ajv": "^6.9.1" } }, - "node_modules/webpack/node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -23325,6 +23275,7 @@ } }, "plugins/eslint": { + "name": "eslint-plugin-prebid", "version": "1.0.0", "dev": true, "license": "Apache-2.0" @@ -24475,12 +24426,6 @@ "uri-js": "^4.2.2" } }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "globals": { "version": "13.12.1", "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", @@ -24490,12 +24435,6 @@ "type-fest": "^0.20.2" } }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -26058,24 +25997,17 @@ "dev": true }, "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", "dev": true, "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, - "ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", - "dev": true, - "requires": {} - }, "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -29660,12 +29592,6 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "globals": { "version": "13.12.1", "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", @@ -29675,12 +29601,6 @@ "type-fest": "^0.20.2" } }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "semver": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", @@ -30426,9 +30346,9 @@ } }, "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "fast-json-stable-stringify": { @@ -32288,6 +32208,25 @@ } } }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "dev": true, + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "ajv-keywords": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "dev": true, + "requires": {} + }, "ansi-escapes": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", @@ -32433,6 +32372,12 @@ "tmp": "^0.0.33" } }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "dev": true + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -32498,6 +32443,12 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "dev": true + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -33158,18 +33109,6 @@ "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true } } }, @@ -34549,9 +34488,9 @@ "dev": true }, "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "json-stable-stringify-without-jsonify": { @@ -35129,9 +35068,9 @@ "dev": true }, "live-connect-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-2.0.0.tgz", - "integrity": "sha512-Xhrj1JU5LoLjJuujjTlvDfc/n3Shzk2hPlYmLdCx/lsltFFVuCFa9uM8u5mcHlmOUKP5pu9I54bAITxZBMHoXg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-2.3.1.tgz", + "integrity": "sha512-4IT8NEOOTNmoYpw5CTxdugSF2w9xqfOujrEqx6zLPdTT3xq/lLdxxvRTREDi+qYHDsCDovdiNO3uOSoemdTCdA==", "requires": { "tiny-hashes": "1.0.1" } @@ -38611,18 +38550,6 @@ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, "requires": {} - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true } } }, @@ -39803,12 +39730,6 @@ "uri-js": "^4.2.2" } }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -39969,18 +39890,6 @@ "dev": true, "requires": {} }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", @@ -41136,18 +41045,6 @@ "dev": true, "requires": {} }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", diff --git a/package.json b/package.json index c812bc123f0..47bcc94404f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "6.18.0-pre", + "version": "6.20.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { @@ -45,7 +45,7 @@ "@wdio/mocha-framework": "^7.5.2", "@wdio/spec-reporter": "^7.5.2", "@wdio/sync": "^7.5.2", - "ajv": "5.5.2", + "ajv": "6.12.3", "assert": "^2.0.0", "babel-loader": "^8.0.5", "body-parser": "^1.19.0", @@ -125,7 +125,7 @@ "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "2.0.0" + "live-connect-js": "2.3.1" }, "optionalDependencies": { "fsevents": "^2.3.2" diff --git a/src/Renderer.js b/src/Renderer.js index 830b979723c..f26a5a377c0 100644 --- a/src/Renderer.js +++ b/src/Renderer.js @@ -53,7 +53,7 @@ export function Renderer(options) { if (!isRendererPreferredFromAdUnit(adUnitCode)) { // we expect to load a renderer url once only so cache the request to load script this.cmd.unshift(runRender) // should render run first ? - loadExternalScript(url, moduleCode, this.callback); + loadExternalScript(url, moduleCode, this.callback, this.documentContext); } else { logWarn(`External Js not loaded by Renderer since renderer url and callback is already defined on adUnit ${adUnitCode}`); runRender() @@ -112,9 +112,18 @@ export function isRendererRequired(renderer) { * Render the bid returned by the adapter * @param {Object} renderer Renderer object installed by adapter * @param {Object} bid Bid response + * @param {Document} doc context document of bid */ -export function executeRenderer(renderer, bid) { - renderer.render(bid); +export function executeRenderer(renderer, bid, doc) { + let docContext = null; + if (renderer.config && renderer.config.documentResolver) { + docContext = renderer.config.documentResolver(bid, document, doc);// a user provided callback, which should return a Document, and expect the parameters; bid, sourceDocument, renderDocument + } + if (!docContext) { + docContext = document; + } + renderer.documentContext = docContext; + renderer.render(bid, renderer.documentContext); } function isRendererPreferredFromAdUnit(adUnitCode) { diff --git a/src/adloader.js b/src/adloader.js index 563fe039b2f..db128c6d7ba 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -1,7 +1,7 @@ import {includes} from './polyfill.js'; import { logError, logWarn, insertElement } from './utils.js'; -const _requestCache = {}; +const _requestCache = new WeakMap(); // The below list contains modules or vendors whom Prebid allows to load external JS. const _approvedLoadExternalJSList = [ 'adloox', @@ -18,9 +18,10 @@ const _approvedLoadExternalJSList = [ * Each unique URL will be loaded at most 1 time. * @param {string} url the url to load * @param {string} moduleCode bidderCode or module code of the module requesting this resource - * @param {function} [callback] callback function to be called after the script is loaded. + * @param {function} [callback] callback function to be called after the script is loaded + * @param {Document} [doc] the context document, in which the script will be loaded, defaults to loaded document */ -export function loadExternalScript(url, moduleCode, callback) { +export function loadExternalScript(url, moduleCode, callback, doc) { if (!moduleCode || !url) { logError('cannot load external script without url and moduleCode'); return; @@ -29,46 +30,60 @@ export function loadExternalScript(url, moduleCode, callback) { logError(`${moduleCode} not whitelisted for loading external JavaScript`); return; } + if (!doc) { + doc = document; // provide a "valid" key for the WeakMap + } // only load each asset once - if (_requestCache[url]) { + const storedCachedObject = getCacheObject(doc, url); + if (storedCachedObject) { if (callback && typeof callback === 'function') { - if (_requestCache[url].loaded) { + if (storedCachedObject.loaded) { // invokeCallbacks immediately callback(); } else { // queue the callback - _requestCache[url].callbacks.push(callback); + storedCachedObject.callbacks.push(callback); } } - return _requestCache[url].tag; + return storedCachedObject.tag; } - _requestCache[url] = { + const cachedDocObj = _requestCache.get(doc) || {}; + const cacheObject = { loaded: false, tag: null, callbacks: [] }; + cachedDocObj[url] = cacheObject; + _requestCache.set(doc, cachedDocObj); + if (callback && typeof callback === 'function') { - _requestCache[url].callbacks.push(callback); + cacheObject.callbacks.push(callback); } logWarn(`module ${moduleCode} is loading external JavaScript`); return requestResource(url, function () { - _requestCache[url].loaded = true; + cacheObject.loaded = true; try { - for (let i = 0; i < _requestCache[url].callbacks.length; i++) { - _requestCache[url].callbacks[i](); + for (let i = 0; i < cacheObject.callbacks.length; i++) { + cacheObject.callbacks[i](); } } catch (e) { logError('Error executing callback', 'adloader.js:loadExternalScript', e); } - }); + }, doc); - function requestResource(tagSrc, callback) { - var jptScript = document.createElement('script'); + function requestResource(tagSrc, callback, doc) { + if (!doc) { + doc = document; + } + var jptScript = doc.createElement('script'); jptScript.type = 'text/javascript'; jptScript.async = true; - _requestCache[url].tag = jptScript; + const cacheObject = getCacheObject(doc, url); + if (cacheObject) { + cacheObject.tag = jptScript; + } if (jptScript.readyState) { jptScript.onreadystatechange = function () { @@ -86,8 +101,15 @@ export function loadExternalScript(url, moduleCode, callback) { jptScript.src = tagSrc; // add the new script tag to the page - insertElement(jptScript); + insertElement(jptScript, doc); return jptScript; } + function getCacheObject(doc, url) { + const cachedDocObj = _requestCache.get(doc); + if (cachedDocObj && cachedDocObj[url]) { + return cachedDocObj[url]; + } + return null; // return new cache object? + } }; diff --git a/src/auction.js b/src/auction.js index 7a1b92b3c31..ae5c3c6156b 100644 --- a/src/auction.js +++ b/src/auction.js @@ -572,7 +572,8 @@ function getPreparedBidForAuction({adUnitCode, bid, auctionId}, {index = auction } if (renderer) { - bidObject.renderer = Renderer.install({ url: renderer.url }); + // be aware, an adapter could already have installed the bidder, in which case this overwrite's the existing adapter + bidObject.renderer = Renderer.install({ url: renderer.url, config: renderer.options });// rename options to config, to make it consistent? bidObject.renderer.setRender(renderer.render); } diff --git a/src/consentHandler.js b/src/consentHandler.js index 0df9e6fcb3b..a56d06c8c90 100644 --- a/src/consentHandler.js +++ b/src/consentHandler.js @@ -55,6 +55,9 @@ export class ConsentHandler { * @returns a promise than resolves to the consent data, or null if no consent data is available */ get promise() { + if (this.#ready) { + return Promise.resolve(this.#data); + } if (!this.#enabled) { this.#resolve(null); } diff --git a/src/prebid.js b/src/prebid.js index d3f5ec989f3..98655825e89 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -486,7 +486,7 @@ $$PREBID_GLOBAL$$.renderAd = hook('async', function (doc, id, options) { insertElement(creativeComment, doc, 'html'); if (isRendererRequired(renderer)) { - executeRenderer(renderer, bid); + executeRenderer(renderer, bid, doc); reinjectNodeIfRemoved(creativeComment, doc, 'html'); emitAdRenderSucceeded({ doc, bid, id }); } else if ((doc === document && !inIframe()) || mediaType === 'video') { diff --git a/src/utils.js b/src/utils.js index 1d91a4a35cf..33755a4fb82 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1355,3 +1355,12 @@ export function cyrb53Hash(str, seed = 0) { h2 = imul(h2 ^ (h2 >>> 16), 2246822507) ^ imul(h1 ^ (h1 >>> 13), 3266489909); return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(); } + +/** + * returns a window object, which holds the provided document or null + * @param {Document} doc + * @returns {Window} + */ +export function getWindowFromDocument(doc) { + return (doc) ? doc.defaultView : null; +} diff --git a/test/spec/adloader_spec.js b/test/spec/adloader_spec.js index 0c46cd2f171..0c50e66c63c 100644 --- a/test/spec/adloader_spec.js +++ b/test/spec/adloader_spec.js @@ -38,5 +38,35 @@ describe('adLoader', function () { adLoader.loadExternalScript('someURL1', 'criteo', callback); expect(utilsinsertElementStub.called).to.be.true; }); + + it('requires a url to be included once per document', function () { + function getDocSpec() { + return { + createElement: function() { + return { + + } + }, + getElementsByTagName: function() { + return { + firstChild: { + insertBefore: function() { + + } + } + } + } + + } + } + const doc1 = getDocSpec(); + const doc2 = getDocSpec(); + adLoader.loadExternalScript('someURL', 'criteo', () => {}, doc1); + adLoader.loadExternalScript('someURL', 'criteo', () => {}, doc1); + adLoader.loadExternalScript('someURL', 'criteo', () => {}, doc1); + adLoader.loadExternalScript('someURL', 'criteo', () => {}, doc2); + adLoader.loadExternalScript('someURL', 'criteo', () => {}, doc2); + expect(utilsinsertElementStub.callCount).to.equal(2); + }); }); }); diff --git a/test/spec/modules/admanBidAdapter_spec.js b/test/spec/modules/admanBidAdapter_spec.js index 33e6a8a89b9..89d140a7f25 100644 --- a/test/spec/modules/admanBidAdapter_spec.js +++ b/test/spec/modules/admanBidAdapter_spec.js @@ -181,7 +181,7 @@ describe('AdmanAdapter', function () { 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://pub.admanmedia.com/image?pbjs=1&coppa=0'); + expect(userSync[0].url).to.be.equal('https://sync.admanmedia.com/image?pbjs=1&coppa=0'); }); }); }); diff --git a/test/spec/modules/admaruBidAdapter_spec.js b/test/spec/modules/admaruBidAdapter_spec.js new file mode 100644 index 00000000000..a45ddae108f --- /dev/null +++ b/test/spec/modules/admaruBidAdapter_spec.js @@ -0,0 +1,124 @@ +import { expect } from 'chai'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec } from '../../../modules/admaruBidAdapter.js'; + +const ENDPOINT = 'https://p1.admaru.net/AdCall'; + +describe('Admaru Adapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('should exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValidForBanner', () => { + let bid = { + 'bidder': 'admaru', + 'params': { + 'pub_id': '1234', + 'adspace_id': '1234' + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250] + ] + } + }, + 'sizes': [[300, 250]], + 'bidId': '26e88c3c703e66', + 'bidderRequestId': '1a8ff729f6c1a3', + 'auctionId': 'cb65d954-ffe1-4f4a-8603-02b521c00333', + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + wrong: 'missing pub_id or adspace_id' + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequestsForBanner', () => { + let bidRequests = [ + { + 'bidder': 'admaru', + 'params': { + 'pub_id': '1234', + 'adspace_id': '1234' + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250] + ] + } + }, + 'sizes': [[300, 250]], + 'bidId': '26e88c3c703e66', + 'bidderRequestId': '1a8ff729f6c1a3', + 'auctionId': 'cb65d954-ffe1-4f4a-8603-02b521c00333' + } + ]; + + it('should add parameters to the tag', () => { + const request = spec.buildRequests(bidRequests); + const payload = request[0].data; + expect(payload.pub_id).to.equal('1234'); + expect(payload.adspace_id).to.equal('1234'); + // expect(payload.refererInfo).to.equal('{"referer": "https://www.admaru.com/test/admaru_prebid/icv_reminder/test.html","reachedTop": true,"numIframes": 1,"stack": ["https://www.admaru.com/test/admaru_prebid/icv_reminder/test.html","https://www.admaru.com/test/admaru_prebid/icv_reminder/test.html"]}'); + // expect(payload.os).to.equal('windows'); + // expect(payload.platform).to.equal('pc_web'); + expect(payload.bidderRequestId).to.equal('1a8ff729f6c1a3'); + expect(payload.bidId).to.equal('26e88c3c703e66'); + }); + + it('sends bid request to ENDPOINT via GET', () => { + const request = spec.buildRequests(bidRequests); + expect(request[0].url).to.contain(ENDPOINT); + expect(request[0].method).to.equal('GET'); + }); + }); + + describe('interpretResponseForBanner', () => { + let bidRequests = [ + { + 'bidder': 'admaru', + 'params': { + 'pub_id': '1234', + 'adspace_id': '1234' + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250] + ] + } + }, + 'sizes': [[300, 250]], + 'bidId': '26e88c3c703e66', + 'bidderRequestId': '1a8ff729f6c1a3', + 'auctionId': 'cb65d954-ffe1-4f4a-8603-02b521c00333' + } + ]; + + it('handles nobid responses', () => { + var request = spec.buildRequests(bidRequests); + let response = ''; + + let result = spec.interpretResponse(response, request[0]); + expect(result.length).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index 7055f2fb59b..117a6d5966a 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -17,6 +17,7 @@ const aliasEP = { navelix: 'https://ghb.hb.navelix.com/v2/auction/', bidsxchange: 'https://ghb.hbd.bidsxchange.com/v2/auction/', streamkey: 'https://ghb.hb.streamkey.net/v2/auction/', + janet: 'https://ghb.bidder.jmgads.com/v2/auction/', }; const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 11e92a1af2e..76d18c43e9d 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -100,6 +100,55 @@ describe('AppNexusAdapter', function () { expect(payload.tags[0].private_sizes).to.deep.equal([{width: 300, height: 250}]); }); + it('should add position in request', function() { + // set from bid.params + let bidRequest = deepClone(bidRequests[0]); + bidRequest.params.position = 'above'; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].position).to.exist; + expect(payload.tags[0].position).to.deep.equal(1); + + // set from mediaTypes.banner.pos = 1 + bidRequest = deepClone(bidRequests[0]); + bidRequest.mediaTypes = { + banner: { pos: 1 } + }; + + const request2 = spec.buildRequests([bidRequest]); + const payload2 = JSON.parse(request2.data); + + expect(payload2.tags[0].position).to.exist; + expect(payload2.tags[0].position).to.deep.equal(1); + + // set from mediaTypes.video.pos = 3 + bidRequest = deepClone(bidRequests[0]); + bidRequest.mediaTypes = { + video: { pos: 3 } + }; + + const request3 = spec.buildRequests([bidRequest]); + const payload3 = JSON.parse(request3.data); + + expect(payload3.tags[0].position).to.exist; + expect(payload3.tags[0].position).to.deep.equal(2); + + // bid.params trumps mediatypes + bidRequest = deepClone(bidRequests[0]); + bidRequest.params.position = 'above'; + bidRequest.mediaTypes = { + banner: { pos: 3 } + }; + + const request4 = spec.buildRequests([bidRequest]); + const payload4 = JSON.parse(request4.data); + + expect(payload4.tags[0].position).to.exist; + expect(payload4.tags[0].position).to.deep.equal(1); + }); + it('should add publisher_id in request', function() { let bidRequest = Object.assign({}, bidRequests[0], @@ -116,7 +165,7 @@ describe('AppNexusAdapter', function () { expect(payload.tags[0].publisher_id).to.deep.equal(1231234); expect(payload.publisher_id).to.exist; expect(payload.publisher_id).to.deep.equal(1231234); - }) + }); it('should add source and verison to the tag', function () { const request = spec.buildRequests(bidRequests); @@ -819,7 +868,7 @@ describe('AppNexusAdapter', function () { }); it('should add referer info to payload', function () { - const bidRequest = Object.assign({}, bidRequests[0]) + const bidRequest = Object.assign({}, bidRequests[0]); const bidderRequest = { refererInfo: { referer: 'https://example.com/page.html', @@ -844,6 +893,40 @@ describe('AppNexusAdapter', function () { }); }); + it('if defined, should include publisher pageUrl to normal referer info in payload', function () { + const bidRequest = Object.assign({}, bidRequests[0]); + sinon + .stub(config, 'getConfig') + .withArgs('pageUrl') + .returns('https://mypub.override.com/test/page.html'); + + const bidderRequest = { + refererInfo: { + referer: 'https://example.com/page.html', + reachedTop: true, + numIframes: 2, + stack: [ + 'https://example.com/page.html', + 'https://example.com/iframe1.html', + 'https://example.com/iframe2.html' + ] + } + } + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.referrer_detection).to.exist; + expect(payload.referrer_detection).to.deep.equal({ + rd_ref: 'https%3A%2F%2Fexample.com%2Fpage.html', + rd_top: true, + rd_ifs: 2, + rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(','), + rd_can: 'https://mypub.override.com/test/page.html' + }); + + config.getConfig.restore(); + }); + it('should populate schain if available', function () { const bidRequest = Object.assign({}, bidRequests[0], { schain: { diff --git a/test/spec/modules/betweenBidAdapter_spec.js b/test/spec/modules/betweenBidAdapter_spec.js index 65c200748e4..3baa92e35d5 100644 --- a/test/spec/modules/betweenBidAdapter_spec.js +++ b/test/spec/modules/betweenBidAdapter_spec.js @@ -283,12 +283,13 @@ describe('betweenBidAdapterTests', function () { let bids = spec.interpretResponse(serverResponse); expect(bids).to.have.lengthOf(1); let bid = bids[0]; - expect(bid.currency).to.equal('RUB'); + expect(bid.currency).to.equal('USD'); }); it('check getUserSyncs', function() { const syncs = spec.getUserSyncs({}, {}); - expect(syncs).to.be.an('array').that.to.have.lengthOf(1); + expect(syncs).to.be.an('array').that.to.have.lengthOf(2); expect(syncs[0]).to.deep.equal({type: 'iframe', url: 'https://ads.betweendigital.com/sspmatch-iframe'}); + expect(syncs[1]).to.deep.equal({type: 'image', url: 'https://ads.betweendigital.com/sspmatch'}); }); it('check sizes', function() { diff --git a/test/spec/modules/biddoBidAdapter_spec.js b/test/spec/modules/biddoBidAdapter_spec.js new file mode 100644 index 00000000000..25986b3407f --- /dev/null +++ b/test/spec/modules/biddoBidAdapter_spec.js @@ -0,0 +1,172 @@ +import {expect} from 'chai'; +import {spec} from 'modules/biddoBidAdapter.js'; + +describe('biddo bid adapter tests', function () { + describe('bid requests', function () { + it('should accept valid bid', function () { + const validBid = { + bidder: 'biddo', + params: {zoneId: 123}, + }; + + expect(spec.isBidRequestValid(validBid)).to.equal(true); + }); + + it('should reject invalid bid', function () { + const invalidBid = { + bidder: 'biddo', + params: {}, + }; + + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should correctly build payload string', function () { + const bidRequests = [{ + bidder: 'biddo', + params: {zoneId: 123}, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bidId: '23acc48ad47af5', + auctionId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + }]; + const payload = spec.buildRequests(bidRequests)[0].data; + + expect(payload).to.contain('ctype=div'); + expect(payload).to.contain('pzoneid=123'); + expect(payload).to.contain('width=300'); + expect(payload).to.contain('height=250'); + }); + + it('should support multiple bids', function () { + const bidRequests = [{ + bidder: 'biddo', + params: {zoneId: 123}, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bidId: '23acc48ad47af5', + auctionId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + }, { + bidder: 'biddo', + params: {zoneId: 321}, + mediaTypes: { + banner: { + sizes: [[728, 90]], + }, + }, + bidId: '23acc48ad47af52', + auctionId: '0fb4905b-9456-4152-86be-c6f6d259ba992', + bidderRequestId: '1c56ad30b9b8ca82', + transactionId: '92489f71-1bf2-49a0-adf9-000cea9347292', + }]; + const payload = spec.buildRequests(bidRequests); + + expect(payload).to.be.lengthOf(2); + }); + + it('should support multiple sizes', function () { + const bidRequests = [{ + bidder: 'biddo', + params: {zoneId: 123}, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: '23acc48ad47af5', + auctionId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + }]; + const payload = spec.buildRequests(bidRequests); + + expect(payload).to.be.lengthOf(2); + }); + }); + + describe('bid responses', function () { + it('should return complete bid response', function () { + const serverResponse = { + body: { + banner: { + hash: '1c56ad30b9b8ca8', + }, + hb: { + cpm: 0.5, + netRevenue: false, + adomains: ['securepubads.g.doubleclick.net'], + }, + template: { + html: '', + }, + }, + }; + const bidderRequest = { + bidId: '23acc48ad47af5', + params: { + requestedSizes: [300, 250], + }, + }; + + const bids = spec.interpretResponse(serverResponse, {bidderRequest}); + + expect(bids).to.be.lengthOf(1); + expect(bids[0].requestId).to.equal('23acc48ad47af5'); + expect(bids[0].creativeId).to.equal('1c56ad30b9b8ca8'); + expect(bids[0].width).to.equal(300); + expect(bids[0].height).to.equal(250); + expect(bids[0].ttl).to.equal(600); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].netRevenue).to.equal(false); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].meta.advertiserDomains).to.be.lengthOf(1); + expect(bids[0].meta.advertiserDomains[0]).to.equal('securepubads.g.doubleclick.net'); + }); + + it('should return empty bid response', function () { + const serverResponse = { + body: {}, + }; + const bidderRequest = { + bidId: '23acc48ad47af5', + params: { + requestedSizes: [300, 250], + }, + }; + + const bids = spec.interpretResponse(serverResponse, {bidderRequest}); + + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response 2', function () { + const serverResponse = { + body: { + template: { + html: '', + } + }, + }; + const bidderRequest = { + bidId: '23acc48ad47af5', + params: { + requestedSizes: [300, 250], + }, + }; + + const bids = spec.interpretResponse(serverResponse, {bidderRequest}); + + expect(bids).to.be.lengthOf(0); + }); + }); +}); diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js index 5a213589f4f..32f02def27e 100644 --- a/test/spec/modules/colossussspBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -109,7 +109,7 @@ describe('ColossussspAdapter', function () { } }); it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([]); + serverRequest = spec.buildRequests([], bidderRequest); let data = serverRequest.data; expect(data.placements).to.be.an('array').that.is.empty; }); diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index 6dc46192128..32fd2ddb2e2 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -8,7 +8,7 @@ import { } from 'modules/consentManagementUsp.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; -import {gdprDataHandler, uspDataHandler} from 'src/adapterManager.js'; +import {uspDataHandler} from 'src/adapterManager.js'; import 'src/prebid.js'; let expect = require('chai').expect; @@ -77,6 +77,11 @@ describe('consentManagement', function () { sinon.assert.calledOnce(utils.logWarn); sinon.assert.notCalled(utils.logInfo); }); + + it('should immediately start looking up consent data', () => { + setConsentConfig({usp: {cmpApi: 'invalid'}}); + expect(uspDataHandler.ready).to.be.true; + }); }); describe('valid setConsentConfig value', function () { diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index d95af454818..712e311e433 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -53,6 +53,11 @@ describe('consentManagement', function () { expect(consentMetadata).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }) + + it('should immediately look up consent data', () => { + setConsentConfig({gdpr: {cmpApi: 'invalid'}}); + expect(gdprDataHandler.ready).to.be.true; + }) }); describe('valid setConsentConfig value', function () { @@ -752,6 +757,11 @@ describe('consentManagement', function () { setConsentConfig(goodConfigWithAllowAuction); + sinon.assert.calledOnce(utils.logWarn); + sinon.assert.notCalled(utils.logError); + + [utils.logWarn, utils.logError].forEach((stub) => stub.reset()); + requestBidsHook(() => { didHookReturn = true; }, { bidsBackHandler: () => bidsBackHandlerReturn = true }); diff --git a/test/spec/modules/consumableBidAdapter_spec.js b/test/spec/modules/consumableBidAdapter_spec.js index f0b02913f96..b70cd6fe631 100644 --- a/test/spec/modules/consumableBidAdapter_spec.js +++ b/test/spec/modules/consumableBidAdapter_spec.js @@ -177,6 +177,69 @@ const AD_SERVER_RESPONSE = { } }; +const AD_SERVER_RESPONSE_2 = { + 'headers': null, + 'body': { + 'user': { 'key': 'ue1-2d33e91b71e74929b4aeecc23f4376f1' }, + 'pixels': [{ 'type': 'image', 'url': '//sync.serverbid.com/ss/' }], + 'bdr': 'notcx', + 'decisions': { + '2b0f82502298c9': { + 'adId': 2364764, + 'creativeId': 1950991, + 'flightId': 2788300, + 'campaignId': 542982, + 'clickUrl': 'https://e.serverbid.com/r', + 'impressionUrl': 'https://e.serverbid.com/i.gif', + 'contents': [{ + 'type': 'html', + 'body': '', + 'data': { + 'height': 90, + 'width': 728, + 'imageUrl': 'https://static.adzerk.net/Advertisers/b0ab77db8a7848c8b78931aed022a5ef.gif', + 'fileName': 'b0ab77db8a7848c8b78931aed022a5ef.gif' + }, + 'template': 'image' + }], + 'height': 90, + 'width': 728, + 'events': [], + 'pricing': {'price': 0.5, 'clearPrice': 0.5, 'revenue': 0.0005, 'rateType': 2, 'eCPM': 0.5}, + 'mediaType': 'banner', + 'cats': ['IAB1', 'IAB2', 'IAB3'], + 'networkId': 1234567, + }, + '123': { + 'adId': 2364764, + 'creativeId': 1950991, + 'flightId': 2788300, + 'campaignId': 542982, + 'clickUrl': 'https://e.serverbid.com/r', + 'impressionUrl': 'https://e.serverbid.com/i.gif', + 'contents': [{ + 'type': 'html', + 'body': '', + 'data': { + 'height': 90, + 'width': 728, + 'imageUrl': 'https://static.adzerk.net/Advertisers/b0ab77db8a7848c8b78931aed022a5ef.gif', + 'fileName': 'b0ab77db8a7848c8b78931aed022a5ef.gif' + }, + 'template': 'image' + }], + 'height': 90, + 'width': 728, + 'events': [], + 'pricing': {'price': 0.5, 'clearPrice': 0.5, 'revenue': 0.0005, 'rateType': 2, 'eCPM': 0.5}, + 'mediaType': 'banner', + 'cats': ['IAB1', 'IAB2'], + 'networkId': 2345678, + } + } + } +}; + const BUILD_REQUESTS_OUTPUT = { method: 'POST', url: 'https://e.serverbid.com/api/v2', @@ -285,7 +348,7 @@ describe('Consumable BidAdapter', function () { }); it('registers bids', function () { - let bids = spec.interpretResponse(AD_SERVER_RESPONSE, BUILD_REQUESTS_OUTPUT); + let bids = spec.interpretResponse(AD_SERVER_RESPONSE_2, BUILD_REQUESTS_OUTPUT); bids.forEach(b => { expect(b).to.have.property('cpm'); expect(b.cpm).to.be.above(0); @@ -299,9 +362,13 @@ describe('Consumable BidAdapter', function () { expect(b).to.have.property('currency', 'USD'); expect(b).to.have.property('creativeId'); expect(b).to.have.property('ttl', 30); - expect(b.meta).to.have.property('advertiserDomains'); expect(b).to.have.property('netRevenue', true); expect(b).to.have.property('referrer'); + expect(b.meta).to.have.property('advertiserDomains'); + expect(b.meta).to.have.property('primaryCatId'); + expect(b.meta).to.have.property('secondaryCatIds'); + expect(b.meta).to.have.property('networkId'); + expect(b.meta).to.have.property('mediaType'); }); }); @@ -333,6 +400,24 @@ describe('Consumable BidAdapter', function () { expect(opts.length).to.equal(1); }); + it('should return a sync url if iframe syncs are enabled and server response is empty', function () { + let opts = spec.getUserSyncs(syncOptions, []); + + expect(opts.length).to.equal(1); + }); + + it('should return a sync url if iframe syncs are enabled and server response does not contain a bdr attribute', function () { + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE]); + + expect(opts.length).to.equal(1); + }); + + it('should return a sync url if iframe syncs are enabled and server response contains a bdr attribute that is not cx', function () { + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE_2]); + + expect(opts.length).to.equal(1); + }); + it('should return a sync url if pixel syncs are enabled and some are returned from the server', function () { let syncOptions = {'pixelEnabled': true}; let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE]); diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index eaffca01e06..300e2104fae 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -410,6 +410,59 @@ describe('The DFP video support module', function () { expect(customParams).to.have.property('hb_cache_id', 'def'); }); + it('should keep the url protocol, host, and pathname when using url and params', function () { + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bid, + url: 'http://video.adserver.example/ads?sz=640x480&iu=/123/aduniturl&impl=s', + params: { + cust_params: { + hb_rand: 'random' + } + } + })); + + expect(url.protocol).to.equal('http:'); + expect(url.host).to.equal('video.adserver.example'); + expect(url.pathname).to.equal('/ads'); + }); + + it('should append to the url size param', () => { + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bid, + url: 'http://video.adserver.example/ads?sz=360x240&iu=/123/aduniturl&impl=s', + params: { + cust_params: { + hb_rand: 'random' + } + } + })); + + const queryObject = utils.parseQS(url.query); + expect(queryObject.sz).to.equal('360x240|640x480'); + }); + + it('should append to the existing url cust params', () => { + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bid, + url: 'http://video.adserver.example/ads?sz=360x240&iu=/123/aduniturl&impl=s&cust_params=existing_key%3Dexisting_value%26other_key%3Dother_value', + params: { + cust_params: { + hb_rand: 'random' + } + } + })); + + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); + + expect(customParams).to.have.property('existing_key', 'existing_value'); + expect(customParams).to.have.property('other_key', 'other_value'); + expect(customParams).to.have.property('hb_rand', 'random'); + }); + describe('adpod unit tests', function () { let amStub; let amGetAdUnitsStub; diff --git a/test/spec/modules/distroscaleBidAdapter_spec.js b/test/spec/modules/distroscaleBidAdapter_spec.js new file mode 100644 index 00000000000..e5a78bbad11 --- /dev/null +++ b/test/spec/modules/distroscaleBidAdapter_spec.js @@ -0,0 +1,213 @@ +import { expect } from 'chai'; +import { spec } from 'modules/distroscaleBidAdapter.js'; +import * as utils from 'src/utils.js'; + +describe('distroscaleBidAdapter', function() { + const DSNAME = 'distroscale'; + + describe('isBidRequestValid', function() { + it('with no param', function() { + expect(spec.isBidRequestValid({ + bidder: DSNAME, + params: {} + })).to.equal(false); + }); + + it('with pubid param', function() { + expect(spec.isBidRequestValid({ + bidder: DSNAME, + params: { + pubid: '12345' + } + })).to.equal(true); + }); + + it('with pubid and zoneid params', function() { + expect(spec.isBidRequestValid({ + bidder: DSNAME, + params: { + pubid: '12345', + zoneid: '67890' + } + })).to.equal(true); + }); + }); + + describe('buildRequests', function() { + const CONSENT_STRING = 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw'; + const BID_REQUESTS = [{ + 'bidder': DSNAME, + 'params': { + 'pubid': '12345', + 'zoneid': '67890' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[970, 250], [300, 250]] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'ca59932f-90f4-4dff-bed2-b90ffa2c2b6a', + 'sizes': [[970, 250], [300, 250]], + 'bidId': '20b96f0310083c', + 'bidderRequestId': '1dd684edba2006', + 'auctionId': '22ed3053-f76f-476c-a08e-dcda5862443d' + }]; + const BIDDER_REQUEST = { + 'bidderCode': DSNAME, + 'auctionId': '22ed3053-f76f-476c-a08e-dcda5862443d', + 'bidderRequestId': '1dd684edba2006', + 'refererInfo': { + 'referer': 'https://publisher.com/homepage.html', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'https://publisher.com/homepage.html' + ], + 'canonicalUrl': null + }, + 'gdprConsent': { + 'consentString': CONSENT_STRING, + 'gdprApplies': true + } + }; + + it('basic', function() { + const request = spec.buildRequests(BID_REQUESTS, BIDDER_REQUEST); + expect(request.method).to.equal('POST'); + expect(request.url).to.have.string('https://hb.jsrdn.com/hb?from=pbjs'); + expect(request.bidderRequest).to.deep.equal(BIDDER_REQUEST); + expect(request.data).to.exist; + expect(request.data.id).to.be.a('string').that.is.not.empty; + expect(request.data.at).to.equal(1); + expect(request.data.cur).to.deep.equal(['USD']); + expect(request.data.device).to.exist; + expect(request.data.site).to.exist; + expect(request.data.user).to.exist; + expect(request.data.imp).to.be.an('array').that.is.not.empty; + expect(request.data.imp[0]).to.exist; + expect(request.data.imp[0].id).to.equal(BID_REQUESTS[0].bidId); + expect(request.data.imp[0].tagid).to.equal(BID_REQUESTS[0].params.zoneid || ''); + expect(request.data.imp[0].secure).to.equal(1); + expect(request.data.imp[0].banner).to.exist; + expect(request.data.imp[0].banner.format).to.be.an('array').that.is.not.empty; + expect(request.data.imp[0].banner.format[0]).to.exist; + expect(request.data.imp[0].banner.format[0].w).to.equal(970); + expect(request.data.imp[0].banner.format[0].h).to.equal(250); + expect(request.data.imp[0].banner.w).to.equal(970); + expect(request.data.imp[0].banner.h).to.equal(250); + expect(request.data.imp[0].banner.pos).to.equal(0); + expect(request.data.imp[0].banner.topframe).to.be.oneOf([0, 1]); + expect(request.data.imp[0].ext).to.exist; + expect(request.data.imp[0].ext.pubid).to.equal(BID_REQUESTS[0].params.pubid); + expect(request.data.imp[0].ext.zoneid).to.equal(BID_REQUESTS[0].params.zoneid || ''); + }); + + it('gdpr', function() { + const request = spec.buildRequests(BID_REQUESTS, BIDDER_REQUEST); + expect(request.data).to.exist; + expect(request.data.regs).to.exist; + expect(request.data.regs.gdpr).to.equal(1); + expect(request.data.user).to.exist; + expect(request.data.user.consent).to.equal(CONSENT_STRING); + }); + }); + + describe('interpretResponse', function() { + const REQUEST = { + 'method': 'POST', + 'url': 'https://hb.jsrdn.com/hb?from=pbjs', + 'data': '{"id":"1648161050749","at":1,"cur":["USD"],"site":{"page":"https://publisher.com/homepage.html","domain":"publisher.com"},"device":{"ua":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36","js":1,"h":1200,"w":1920,"language":"en","dnt":0},"imp":[{"id":"20b96f0310083c","tagid":"67890","secure":1,"ext":{"pubid":"12345","zoneid":"67890"},"banner":{"pos":0,"w":970,"h":250,"topframe":1,"format":[{"w":970,"h":250}]}}],"user":{},"ext":{}}', + 'bidderRequest': { + 'bidderCode': DSNAME, + 'auctionId': '22ed3053-f76f-476c-a08e-dcda5862443d', + 'bidderRequestId': '1dd684edba2006', + 'bids': [{ + 'bidder': DSNAME, + 'params': { + 'pubid': '12345', + 'zoneid': '67890' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[970, 250], [300, 250]] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'ca59932f-90f4-4dff-bed2-b90ffa2c2b6a', + 'sizes': [[970, 250], [300, 250]], + 'bidId': '20b96f0310083c', + 'bidderRequestId': '1dd684edba2006', + 'auctionId': '22ed3053-f76f-476c-a08e-dcda5862443d' + }], + 'refererInfo': { + 'referer': 'https://publisher.com/homepage.html', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'https://publisher.com/homepage.html' + ], + 'canonicalUrl': null + } + } + }; + const RESPONSE = { + 'body': { + 'id': '1648161050749', + 'seatbid': [{ + 'bid': [{ + 'id': '212f1c7b-378b-47e4-8294-ac38658b33f6_0', + 'impid': '20b96f0310083c', + 'price': 0.1, + 'w': 970, + 'h': 250, + 'adm': "
" + }] + }], + 'cur': 'USD' + }, + 'headers': {} + }; + const SAMPLE_PARSED = [{ + 'requestId': '20b96f0310083c', + 'cpm': 0.1, + 'currency': 'USD', + 'width': 970, + 'height': 250, + 'creativeId': 'bbbbbbbb-648d-4e03-a5e2-7198bcd07cfe', + 'netRevenue': true, + 'ttl': 300, + 'ad': "
", + 'meta': { + 'advertiserDomains': [] + } + }]; + + it('valid bid response for banner ad', function() { + const result = spec.interpretResponse(RESPONSE, REQUEST); + const bid = RESPONSE.body.seatbid[0].bid[0]; + expect(result).to.have.lengthOf(1); + expect(result[0].requestId).to.equal(bid.impid); + expect(result[0].cpm).to.equal(Number(bid.price)); + expect(result[0].currency).to.equal(RESPONSE.body.cur); + expect(result[0].width).to.equal(Number(bid.w)); + expect(result[0].height).to.equal(Number(bid.h)); + expect(result[0].creativeId).to.be.a('string').that.is.not.empty; + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(300); + expect(result[0].ad).to.equal(bid.adm); + expect(result[0].meta).to.exist; + expect(result[0].meta.advertiserDomains).to.exist; + }); + + it('advertiserDomains is included when sent by server', function() { + const ADOMAIN = ['advertiser_adomain']; + let RESPONSE_CLONE = utils.deepClone(RESPONSE); + RESPONSE_CLONE.body.seatbid[0].bid[0].adomain = utils.deepClone(ADOMAIN); ; + let result = spec.interpretResponse(RESPONSE_CLONE, REQUEST); + expect(result[0].meta.advertiserDomains).to.deep.equal(ADOMAIN); + }); + }); +}); diff --git a/test/spec/modules/glimpseBidAdapter_spec.js b/test/spec/modules/glimpseBidAdapter_spec.js index 02f5b502a1b..7104493792f 100644 --- a/test/spec/modules/glimpseBidAdapter_spec.js +++ b/test/spec/modules/glimpseBidAdapter_spec.js @@ -1,9 +1,13 @@ -import { BANNER } from '../../../src/mediaTypes' -import { expect } from 'chai' -import { newBidder } from 'src/adapters/bidderFactory.js' -import { spec } from 'modules/glimpseBidAdapter.js' +import { expect } from 'chai'; +import { spec } from 'modules/glimpseBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config'; +import { BANNER } from '../../../src/mediaTypes'; -const ENDPOINT = 'https://api.glimpsevault.io/ads/serving/public/v1/prebid' +const ENDPOINT = 'https://market.glimpsevault.io/public/v1/prebid'; + +const nonStringValues = [null, undefined, 123, true, {}, [], () => {}]; +const nonArrayValues = [null, undefined, 123, true, {}, 'str', () => {}]; const mock = { bidRequest: { @@ -14,7 +18,7 @@ const mock = { adUnitCode: 'banner-div-a', sizes: [[300, 250]], params: { - placementId: 'glimpse-demo-300x250', + pid: 'glimpse-demo-300x250', }, }, bidderRequest: { @@ -23,10 +27,12 @@ const mock = { auctionId: '96692a73-307b-44b8-8e4f-ddfb40341570', timeout: 3000, gdprConsent: { - consentString: 'COzP517OzP517AcABBENAlCsAP_AAAAAAAwIF8NX-T5eL2vju2Zdt7JEaYwfZxyigOgThgQIsW8NwIeFbBoGP2EgHBG4JCQAGBAkkgCBAQMsHGBcCQAAgIgRiRKMYE2MjzNKBJJAigkbc0FACDVunsHS2ZCY70-8O__bPAviADAvUC-AAAAA.YAAAAAAAAAAA', + consentString: + 'COzP517OzP517AcABBENAlCsAP_AAAAAAAwIF8NX-T5eL2vju2Zdt7JEaYwfZxyigOgThgQIsW8NwIeFbBoGP2EgHBG4JCQAGBAkkgCBAQMsHGBcCQAAgIgRiRKMYE2MjzNKBJJAigkbc0FACDVunsHS2ZCY70-8O__bPAviADAvUC-AAAAA.YAAAAAAAAAAA', vendorData: {}, gdprApplies: true, }, + uspConsent: '1YYY', refererInfo: { numIframes: 0, reachedTop: true, @@ -39,7 +45,6 @@ const mock = { data: { bids: [ { - bidder: 'glimpse', requestId: '133baeded6ac94', creativeId: 'glimpse-demo-300x250', adUnitCode: 'banner-div-a', @@ -48,273 +53,379 @@ const mock = { width: 300, height: 250, cpm: 1.04, - pbAg: '1.04', - pbDg: '1.04', - pbHg: '1.04', - pbLg: '1.00', - pbMg: '1.05', netRevenue: true, mediaType: 'banner', ttl: 300, - } + }, ], }, }, -} +}; -const getBidRequest = () => getDeepCopy(mock.bidRequest) +const getBidRequest = () => getDeepCopy(mock.bidRequest); const getBidderRequest = () => ({ bids: [getBidRequest()], ...getDeepCopy(mock.bidderRequest), -}) +}); -const getBidResponseHelper = () => getDeepCopy(mock.bidResponse) const getBidResponse = () => ({ - body: getBidResponseHelper(), -}) + body: getDeepCopy(mock.bidResponse), +}); function getDeepCopy(object) { - return JSON.parse(JSON.stringify(object)) + return JSON.parse(JSON.stringify(object)); } describe('GlimpseProtocolAdapter', () => { - const glimpseAdapter = newBidder(spec) + const glimpseAdapter = newBidder(spec); describe('spec', () => { it('Has defined the glimpse gvlid', () => { - expect(spec.gvlid).to.equal(1012) - }) + expect(spec.gvlid).to.equal(1012); + }); it('Has defined glimpse as the bidder', () => { - expect(spec.code).to.equal('glimpse') - }) + expect(spec.code).to.equal('glimpse'); + }); it('Has defined valid mediaTypes', () => { - expect(spec.supportedMediaTypes).to.deep.equal([BANNER]) - }) - }) + expect(spec.supportedMediaTypes).to.deep.equal([BANNER]); + }); + }); describe('Inherited functions', () => { it('Functions exist and are valid types', () => { - expect(glimpseAdapter.callBids).to.exist.and.to.be.a('function') - expect(glimpseAdapter.getSpec).to.exist.and.to.be.a('function') - }) - }) + expect(glimpseAdapter.callBids).to.exist.and.to.be.a('function'); + expect(glimpseAdapter.getSpec).to.exist.and.to.be.a('function'); + }); + }); describe('isBidRequestValid', () => { - it('Returns true when a bid request has a valid placement id', () => { - const bidRequest = getBidRequest() - - const isValidBidRequest = spec.isBidRequestValid(bidRequest) - expect(isValidBidRequest).to.be.true - }) - - it('Returns false when params are empty', () => { - const bidRequest = getBidRequest() - bidRequest.params = {} - - const isValidBidRequest = spec.isBidRequestValid(bidRequest) - expect(isValidBidRequest).to.be.false - }) - - it('Returns false when params are null', () => { - const bidRequest = getBidRequest() - bidRequest.params = null - - const isValidBidRequest = spec.isBidRequestValid(bidRequest) - expect(isValidBidRequest).to.be.false - }) - - it('Returns false when params are undefined', () => { - const bidRequest = getBidRequest() - delete bidRequest.params - - const isValidBidRequest = spec.isBidRequestValid(bidRequest) - expect(isValidBidRequest).to.be.false - }) - - it('Returns false when params are invalid type', () => { - const bidRequest = getBidRequest() - bidRequest.params = 123 - - const isValidBidRequest = spec.isBidRequestValid(bidRequest) - expect(isValidBidRequest).to.be.false - }) + it('Returns true if placement id is non-empty string', () => { + const bidRequest = getBidRequest(); - it('Returns false when placement id is empty', () => { - const bidRequest = getBidRequest() - bidRequest.params.placementId = '' + const isValidBidRequest = spec.isBidRequestValid(bidRequest); + expect(isValidBidRequest).to.be.true; + }); - const isValidBidRequest = spec.isBidRequestValid(bidRequest) - expect(isValidBidRequest).to.be.false - }) + it('Returns false if no pid is provided', () => { + const bidRequest = getBidRequest(); + delete bidRequest.params.pid; - it('Returns false when placement id is null', () => { - const bidRequest = getBidRequest() - bidRequest.params.placementId = null + const isValidBidRequest = spec.isBidRequestValid(bidRequest); + expect(isValidBidRequest).to.be.false; + }); - const isValidBidRequest = spec.isBidRequestValid(bidRequest) - expect(isValidBidRequest).to.be.false - }) + it('Returns false if pid is empty string', () => { + const bidRequest = getBidRequest(); + bidRequest.params.pid = ''; - it('Returns false when placement id is undefined', () => { - const bidRequest = getBidRequest() - delete bidRequest.params.placementId + const isValidBidRequest = spec.isBidRequestValid(bidRequest); + expect(isValidBidRequest).to.be.false; + }); - const isValidBidRequest = spec.isBidRequestValid(bidRequest) - expect(isValidBidRequest).to.be.false - }) + it('Returns false if pid is not string', () => { + const bidRequest = getBidRequest(); + const invalidPids = nonStringValues; - it('Returns false when placement id has an invalid type', () => { - const bidRequest = getBidRequest() - bidRequest.params.placementId = 123 - - const isValidBidRequest = spec.isBidRequestValid(bidRequest) - expect(isValidBidRequest).to.be.false - }) - }) + invalidPids.forEach((invalidPid) => { + bidRequest.params.pid = invalidPid; + const isValidBidRequest = spec.isBidRequestValid(bidRequest); + expect(isValidBidRequest).to.be.false; + }); + }); + }); describe('buildRequests', () => { - const bidRequests = [getBidRequest()] - const bidderRequest = getBidderRequest() - - it('Adds GDPR consent', () => { - const request = spec.buildRequests(bidRequests, bidderRequest) - const payload = JSON.parse(request.data) - const expected = bidderRequest.gdprConsent.consentString - - expect(payload.data.gdprConsent).to.exist - expect(payload.data.gdprConsent.gdprApplies).to.be.true - expect(payload.data.gdprConsent.consentString).to.equal(expected) - }) - - it('Adds referer information', () => { - const request = spec.buildRequests(bidRequests, bidderRequest) - const payload = JSON.parse(request.data) - const expected = mock.bidderRequest.refererInfo.referer - - expect(payload.data.referer).to.equal(expected) - }) - - it('Sends a POST request to the Glimpse server', () => { - const request = spec.buildRequests(bidRequests) - - expect(request.url).to.equal(ENDPOINT) - expect(request.method).to.equal('POST') - }) - }) + const bidRequests = [getBidRequest()]; + const bidderRequest = getBidderRequest(); + + it('Adds additional info to api request query', () => { + const request = spec.buildRequests(bidRequests, bidderRequest); + const url = new URL(request.url); + const queries = new URLSearchParams(url.search); + + expect(queries.get('ver')).to.exist; + expect(queries.get('tmax')).to.exist; + expect(queries.get('gdpr')).to.equal( + bidderRequest.gdprConsent.consentString + ); + expect(queries.get('ccpa')).to.equal(bidderRequest.uspConsent); + }); + + it('Has correct payload shape', () => { + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.auth).to.be.a('string'); + expect(payload.data).to.be.an('object'); + expect(payload.data.referer).to.be.a('string'); + expect(payload.data.imp).to.be.an('array'); + expect(payload.data.fpd).to.be.an('object'); + }); + + it('Has referer information', () => { + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + const expected = mock.bidderRequest.refererInfo.referer; + + expect(payload.data.referer).to.equal(expected); + }); + + it('Has correct bids (imp) shape', () => { + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + const imp = payload.data.imp; + + imp.forEach((i) => { + expect(i.bid).to.be.a('string'); + expect(i.pid).to.be.a('string'); + expect(i.sizes).to.be.an('array').that.deep.include([300, 250]); + }); + }); + }); describe('interpretResponse', () => { - it('Handles valid bid responses', () => { - const bidResponse = getBidResponse() - const bids = spec.interpretResponse(bidResponse) - - expect(bids).to.have.lengthOf(1) - expect(bids[0].adUnitCode).to.equal(mock.bidRequest.adUnitCode) - }) - - it('Handles no bid responses', () => { - const bidResponse = getBidResponse() - bidResponse.body.data.bids = [] - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bids if body is empty', () => { - const bidResponse = getBidResponse() - bidResponse.body = {} - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bids if body is null', () => { - const bidResponse = getBidResponse() - bidResponse.body = null - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bids if body is undefined', () => { - const bidResponse = getBidResponse() - delete bidResponse.body - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bids if body is invalid type', () => { - const bidResponse = getBidResponse() - bidResponse.body = 123 - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bids if auth is empty', () => { - const bidResponse = getBidResponse() - bidResponse.body.auth = '' - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bids if auth is null', () => { - const bidResponse = getBidResponse() - bidResponse.body.auth = null - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bids if auth is undefined', () => { - const bidResponse = getBidResponse() - delete bidResponse.body.auth - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bids if auth is invalid type', () => { - const bidResponse = getBidResponse() - bidResponse.body.auth = 123 - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bid if data is empty', () => { - const bidResponse = getBidResponse() - bidResponse.body.data = {} - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bid if data is null', () => { - const bidResponse = getBidResponse() - bidResponse.body.data = null - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bid if data is undefined', () => { - const bidResponse = getBidResponse() - delete bidResponse.body.data - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - - it('Returns no bid if data is invalid type', () => { - const bidResponse = getBidResponse() - bidResponse.body.data = "This shouldn't be a string" - - const bids = spec.interpretResponse(bidResponse) - expect(bids).to.have.lengthOf(0) - }) - }) -}) + it('Returns valid bids', () => { + const bidResponse = getBidResponse(); + const bids = spec.interpretResponse(bidResponse); + + expect(bids).to.have.lengthOf(1); + expect(bids[0].adUnitCode).to.equal(mock.bidRequest.adUnitCode); + }); + + it('Returns no bids if auth is not string', () => { + const bidResponse = getBidResponse(); + const invalidAuths = nonStringValues; + + invalidAuths.forEach((invalidAuth) => { + bidResponse.body.auth = invalidAuth; + + const bids = spec.interpretResponse(bidResponse); + expect(bids).to.have.lengthOf(0); + }); + }); + + it('Returns no bids if bids is empty', () => { + const bidResponse = getBidResponse(); + bidResponse.body.data.bids = []; + + const bids = spec.interpretResponse(bidResponse); + expect(bids).to.have.lengthOf(0); + }); + + it('Returns no bids if bids is not array', () => { + const bidResponse = getBidResponse(); + const invalidBids = nonArrayValues; + + invalidBids.forEach((invalidBid) => { + bidResponse.body.data.bids = invalidBid; + + const bids = spec.interpretResponse(bidResponse); + expect(bids).to.have.lengthOf(0); + }); + }); + + it('Contains advertiserDomains', () => { + const bidResponse = getBidResponse(); + + const bids = spec.interpretResponse(bidResponse); + bids.forEach((bid) => { + expect(bid.meta.advertiserDomains).to.be.an('array'); + }); + }); + }); + + describe('optimize request fpd data', () => { + const bidRequests = [getBidRequest()]; + const bidderRequest = getBidderRequest(); + + const fpdMockBase = { + site: { + keywords: 'site,keywords', + ext: { + data: { + fpdProvider: { + dataArray: ['data1', 'data2'], + dataObject: { + data1: 'data1', + data2: 'data2', + }, + dataString: 'data1,data2', + }, + }, + }, + }, + user: { + keywords: 'user,keywords', + ext: { + data: { + fpdProvider: { + dataArray: ['data1', 'data2'], + dataObject: { + data1: 'data1', + data2: 'data2', + }, + dataString: 'data1,data2', + }, + }, + }, + }, + }; + + afterEach(() => { + config.getConfig.restore(); + }); + + it('should keep all non-empty fields', () => { + const fpdMock = fpdMockBase; + sinon.stub(config, 'getConfig').withArgs('ortb2').returns(fpdMock); + const expected = fpdMockBase; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + const fpd = payload.data.fpd; + + expect(fpd).to.deep.equal(expected); + }); + + it('should remove all empty objects', () => { + const fpdMock = getDeepCopy(fpdMockBase); + fpdMock.site.ext.data.fpdProvider.dataObject = {}; + fpdMock.user.ext.data.fpdProvider = {}; + sinon.stub(config, 'getConfig').withArgs('ortb2').returns(fpdMock); + + const expected = { + site: { + keywords: 'site,keywords', + ext: { + data: { + fpdProvider: { + dataArray: ['data1', 'data2'], + dataString: 'data1,data2', + }, + }, + }, + }, + user: { + keywords: 'user,keywords', + }, + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + const fpd = payload.data.fpd; + + expect(fpd).to.deep.equal(expected); + }); + + it('should remove all empty arrays', () => { + const fpdMock = getDeepCopy(fpdMockBase); + fpdMock.site.ext.data.fpdProvider.dataArray = []; + fpdMock.user.ext.data.fpdProvider.dataArray = []; + sinon.stub(config, 'getConfig').withArgs('ortb2').returns(fpdMock); + + const expected = { + site: { + keywords: 'site,keywords', + ext: { + data: { + fpdProvider: { + dataObject: { + data1: 'data1', + data2: 'data2', + }, + dataString: 'data1,data2', + }, + }, + }, + }, + user: { + keywords: 'user,keywords', + ext: { + data: { + fpdProvider: { + dataObject: { + data1: 'data1', + data2: 'data2', + }, + dataString: 'data1,data2', + }, + }, + }, + }, + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + const fpd = payload.data.fpd; + + expect(fpd).to.deep.equal(expected); + }); + + it('should remove all empty strings', () => { + const fpdMock = getDeepCopy(fpdMockBase); + fpdMock.site.keywords = ''; + fpdMock.site.ext.data.fpdProvider.dataString = ''; + fpdMock.user.keywords = ''; + fpdMock.user.ext.data.fpdProvider.dataString = ''; + sinon.stub(config, 'getConfig').withArgs('ortb2').returns(fpdMock); + + const expected = { + site: { + ext: { + data: { + fpdProvider: { + dataArray: ['data1', 'data2'], + dataObject: { + data1: 'data1', + data2: 'data2', + }, + }, + }, + }, + }, + user: { + ext: { + data: { + fpdProvider: { + dataArray: ['data1', 'data2'], + dataObject: { + data1: 'data1', + data2: 'data2', + }, + }, + }, + }, + }, + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + const fpd = payload.data.fpd; + + expect(fpd).to.deep.equal(expected); + }); + + it('should remove all empty fields', () => { + const fpdMock = getDeepCopy(fpdMockBase); + fpdMock.site.keywords = ''; + fpdMock.site.ext.data.fpdProvider.dataArray = []; + fpdMock.site.ext.data.fpdProvider.dataObject = {}; + fpdMock.site.ext.data.fpdProvider.dataString = ''; + fpdMock.user.keywords = ''; + fpdMock.user.ext.data.fpdProvider.dataArray = []; + fpdMock.user.ext.data.fpdProvider.dataObject = {}; + fpdMock.user.ext.data.fpdProvider.dataString = ''; + sinon.stub(config, 'getConfig').withArgs('ortb2').returns(fpdMock); + + const expected = {}; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + const fpd = payload.data.fpd; + + expect(fpd).to.deep.equal(expected); + }); + }); +}); diff --git a/test/spec/modules/iasRtdProvider_spec.js b/test/spec/modules/iasRtdProvider_spec.js index 192b2c6e3c3..0d52c594fb5 100644 --- a/test/spec/modules/iasRtdProvider_spec.js +++ b/test/spec/modules/iasRtdProvider_spec.js @@ -29,7 +29,7 @@ describe('iasRtdProvider is a RTD provider that', function () { const value = iasSubModule.init(config); expect(value).to.equal(false); }); - it('returns false missing pubId param', function () { + it('returns true with only the pubId param', function () { const config = { name: 'ias', waitForIt: true, @@ -40,6 +40,20 @@ describe('iasRtdProvider is a RTD provider that', function () { const value = iasSubModule.init(config); expect(value).to.equal(true); }); + it('returns true with the pubId and keyMappings params', function () { + const config = { + name: 'ias', + waitForIt: true, + params: { + pubId: '123456', + keyMappings: { + 'id': 'ias_id' + } + } + }; + const value = iasSubModule.init(config); + expect(value).to.equal(true); + }); }); describe('has a method `getBidRequestData` that', function () { it('exists', function () { @@ -75,34 +89,43 @@ describe('iasRtdProvider is a RTD provider that', function () { it('exists', function () { expect(iasSubModule.getTargetingData).to.be.a('function'); }); - it('invoke method', function () { - const targeting = iasSubModule.getTargetingData(adUnitsCode, config); - expect(adUnitsCode).to.length(2); - expect(targeting).to.be.not.null; - expect(targeting).to.be.not.empty; - expect(targeting['one-div-id']).to.be.not.null; - const targetingKeys = Object.keys(targeting['one-div-id']); - expect(targetingKeys.length).to.equal(10); - expect(targetingKeys['adt']).to.be.not.null; - expect(targetingKeys['alc']).to.be.not.null; - expect(targetingKeys['dlm']).to.be.not.null; - expect(targetingKeys['drg']).to.be.not.null; - expect(targetingKeys['hat']).to.be.not.null; - expect(targetingKeys['off']).to.be.not.null; - expect(targetingKeys['vio']).to.be.not.null; - expect(targetingKeys['fr']).to.be.not.null; - expect(targetingKeys['ias-kw']).to.be.not.null; - expect(targetingKeys['id']).to.be.not.null; - expect(targeting['one-div-id']['adt']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['alc']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['dlm']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['drg']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['hat']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['off']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['vio']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['fr']).to.be.eq('false'); - expect(targeting['one-div-id']['id']).to.be.eq('4813f7a2-1f22-11ec-9bfd-0a1107f94461'); - }); + describe('invoke method', function () { + it('returns a targeting object with the right shape', function () { + const targeting = iasSubModule.getTargetingData(adUnitsCode, config); + expect(adUnitsCode).to.length(2); + expect(targeting).to.be.not.null; + expect(targeting).to.be.not.empty; + expect(targeting['one-div-id']).to.be.not.null; + }); + it('returns the right keys', function () { + const targeting = iasSubModule.getTargetingData(adUnitsCode, config); + const targetingKeys = Object.keys(targeting['one-div-id']); + expect(targetingKeys.length).to.equal(10); + expect(targetingKeys).to.include('adt', 'adt key missing from the targeting object'); + expect(targetingKeys).to.include('alc', 'alc key missing from the targeting object'); + expect(targetingKeys).to.include('dlm', 'dlm key missing from the targeting object'); + expect(targetingKeys).to.include('drg', 'drg key missing from the targeting object'); + expect(targetingKeys).to.include('hat', 'hat key missing from the targeting object'); + expect(targetingKeys).to.include('off', 'off key missing from the targeting object'); + expect(targetingKeys).to.include('vio', 'vio key missing from the targeting object'); + expect(targetingKeys).to.include('fr', 'fr key missing from the targeting object'); + expect(targetingKeys).to.include('ias-kw', 'ias-kw key missing from the targeting object'); + expect(targetingKeys).to.not.include('id', 'id key present in the targeting object, should have been renamed to ias_id'); + expect(targetingKeys).to.include('ias_id', 'ias_id key missing from the targeting object'); + }); + it('returns the right values', function () { + const targeting = iasSubModule.getTargetingData(adUnitsCode, config); + expect(targeting['one-div-id']['adt']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['alc']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['dlm']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['drg']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['hat']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['off']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['vio']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['fr']).to.be.eq('false'); + expect(targeting['one-div-id']['ias_id']).to.be.eq('4813f7a2-1f22-11ec-9bfd-0a1107f94461'); + }); + }) }); }); @@ -110,7 +133,10 @@ const config = { name: 'ias', waitForIt: true, params: { - pubId: 1234 + pubId: 1234, + keyMappings: { + 'id': 'ias_id' + } } }; diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 64c276ce806..9dcc11f5aa1 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -1,14 +1,14 @@ import { expect } from 'chai'; -import { ImproveDigitalAdServerJSClient, spec } from 'modules/improvedigitalBidAdapter.js'; +import { spec } from 'modules/improvedigitalBidAdapter.js'; import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; describe('Improve Digital Adapter Tests', function () { - const idClient = new ImproveDigitalAdServerJSClient('hb'); - - const METHOD = 'GET'; - const URL = 'https://ice.360yield.com/hb'; - const PARAM_PREFIX = 'jsonp='; + const METHOD = 'POST'; + const URL = 'https://ad.360yield.com/pb'; + const INSTREAM_TYPE = 1; + const OUTSTREAM_TYPE = 3; const simpleBidRequest = { bidder: 'improvedigital', @@ -22,10 +22,10 @@ describe('Improve Digital Adapter Tests', function () { auctionId: '192721e36a0239', mediaTypes: { banner: { - sizes: [[300, 250], [160, 600], ['blah', 150], [-1, 300], [300, -5]] + sizes: [[300, 250], [160, 600]] } }, - sizes: [[300, 250], [160, 600], ['blah', 150], [-1, 300], [300, -5]] + sizes: [[300, 250], [160, 600]] }; const videoParams = { @@ -53,7 +53,7 @@ describe('Improve Digital Adapter Tests', function () { const multiFormatBidRequest = utils.deepClone(simpleBidRequest); multiFormatBidRequest.mediaTypes = { banner: { - sizes: [[300, 250], [160, 600], ['blah', 150], [-1, 300], [300, -5]] + sizes: [[300, 250], [160, 600]] }, video: { context: 'outstream', @@ -145,66 +145,85 @@ describe('Improve Digital Adapter Tests', function () { describe('buildRequests', function () { it('should make a well-formed request objects', function () { - const requests = spec.buildRequests([simpleBidRequest], bidderRequest); - expect(requests).to.be.an('array'); - expect(requests.length).to.equal(1); - - const request = requests[0]; + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); + const request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; + expect(request).to.be.an('object'); 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.schain).to.not.exist; - expect(params.bid_request.user).to.not.exist; - expect(params.bid_request.imp).to.deep.equal([ + + const payload = JSON.parse(request.data); + expect(payload).to.be.an('object'); + expect(payload.id).to.be.a('string'); + expect(payload.tmax).not.to.exist; + expect(payload.cur).to.be.an('array'); + expect(payload.regs).to.not.exist; + expect(payload.schain).to.not.exist; + expect(payload.source).to.be.an('object'); + expect(payload.device).to.be.an('object'); + expect(payload.user).to.not.exist; + expect(payload.imp).to.deep.equal([ { id: '33e9500b21129f', - pid: 1053688, - tid: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', - banner: {} + secure: 0, + ext: { + bidder: { + placementId: 1053688, + } + }, + banner: { + format: [ + {w: 300, h: 250}, + {w: 160, h: 600}, + ] + } } ]); + getConfigStub.restore(); }); 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]; + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); + const request = spec.buildRequests([multiFormatBidRequest], multiFormatBidderRequest)[0]; + expect(request).to.be.an('object'); 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))); - 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([ + + const payload = JSON.parse(request.data); + expect(payload).to.be.an('object'); + expect(payload.imp).to.deep.equal([ { id: '33e9500b21129f', - pid: 1053688, - tid: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', - banner: {} + secure: 0, + ext: { + bidder: { + placementId: 1053688, + } + }, + video: { + placement: OUTSTREAM_TYPE, + w: 640, + h: 480, + mimes: ['video/mp4'], + }, + banner: { + format: [ + {w: 300, h: 250}, + {w: 160, h: 600}, + ] + } } ]); + getConfigStub.restore(); }); it('should set placementKey and publisherId for smart tags', function () { - 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'); + const payload = JSON.parse(spec.buildRequests([simpleSmartTagBidRequest], bidderRequest)[0].data); + expect(payload.imp[0].ext.bidder.publisherId).to.equal(1032); + expect(payload.imp[0].ext.bidder.placementKey).to.equal('data_team_test_hb_smoke_test'); }); it('should add keyValues', function () { @@ -215,102 +234,92 @@ describe('Improve Digital Adapter Tests', function () { ] }; bidRequest.params.keyValues = keyValues; - 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); + const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].ext.bidder.keyValues).to.deep.equal(keyValues); }); - it('should add single size filter', function () { - const bidRequest = Object.assign({}, simpleBidRequest); - const size = { - w: 800, - h: 600 - }; - bidRequest.params.size = size; - 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 - // is to maintain backward compatibily - expect(params.bid_request.imp[0].banner.format).to.not.exist; - }); + // it('should add single size filter', function () { + // const bidRequest = Object.assign({}, simpleBidRequest); + // const size = { + // w: 800, + // h: 600 + // }; + // bidRequest.params.size = size; + // const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest).data); + // expect(payload.imp[0].banner).to.deep.equal(size); + // // When single size filter is set, format shouldn't be populated. This + // // is to maintain backward compatibily + // expect(payload.imp[0].banner.format).to.not.exist; + // }); it('should add currency', function () { const bidRequest = Object.assign({}, simpleBidRequest); const getConfigStub = sinon.stub(config, 'getConfig').returns('JPY'); - 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'); + const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.cur).to.deep.equal(['JPY']); getConfigStub.restore(); }); it('should add bid floor', function () { const bidRequest = Object.assign({}, simpleBidRequest); - let request = spec.buildRequests([bidRequest], bidderRequest)[0]; - let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + let payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); // Floor price currency shouldn't be populated without a floor price - expect(params.bid_request.imp[0].bidfloorcur).to.not.exist; + expect(payload.imp[0].bidfloorcur).to.not.exist; // Default floor price currency bidRequest.params.bidFloor = 0.05; - 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'); + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].bidfloor).to.equal(0.05); + expect(payload.imp[0].bidfloorcur).to.equal('USD'); // Floor price currency bidRequest.params.bidFloorCur = 'eUR'; - request = spec.buildRequests([bidRequest])[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('EUR'); + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].bidfloor).to.equal(0.05); + expect(payload.imp[0].bidfloorcur).to.equal('EUR'); // getFloor defined -> use it over bidFloor let getFloorResponse = { currency: 'USD', floor: 3 }; bidRequest.getFloor = () => getFloorResponse; - request = spec.buildRequests([bidRequest])[0]; - params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].bidfloor).to.equal(3); - expect(params.bid_request.imp[0].bidfloorcur).to.equal('USD'); + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].bidfloor).to.equal(3); + // expect(payload.imp[0].bidfloorcur).to.equal('USD'); }); it('should add GDPR consent string', function () { const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestGdpr)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.gdpr).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); - expect(params.bid_request.user.ext.consented_providers_settings.consented_providers).to.exist.and.to.deep.equal([1, 35, 41, 101]); + const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequestGdpr)[0].data); + expect(payload.regs.ext.gdpr).to.exist.and.to.equal(1); + expect(payload.user.ext.consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + expect(payload.user.ext.consented_providers_settings.consented_providers).to.exist.and.to.deep.equal([1, 35, 41, 101]); }); it('should add CCPA consent string', function () { const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], { uspConsent: '1YYY' })[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.us_privacy).to.equal('1YYY'); + const request = spec.buildRequests([bidRequest], {...bidderRequest, ...{ uspConsent: '1YYY' }}); + const payload = JSON.parse(request[0].data); + expect(payload.regs.ext.us_privacy).to.equal('1YYY'); }); it('should add referrer', function () { const bidRequest = Object.assign({}, simpleBidRequest); const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.referrer).to.equal('https://blah.com/test.html'); + const payload = JSON.parse(request.data); + expect(payload.site.page).to.equal('https://blah.com/test.html'); }); it('should not add video params for banner', function () { const bidRequest = JSON.parse(JSON.stringify(simpleBidRequest)); bidRequest.params.video = videoParams; const request = spec.buildRequests([bidRequest], bidderRequest)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.not.exist; + const payload = JSON.parse(request.data); + expect(payload.imp[0].video).to.not.exist; }); - it('should add ad type for instream video', function () { + it('should add correct placement value for instream and outstream video', function () { let bidRequest = JSON.parse(JSON.stringify(simpleBidRequest)); - bidRequest.mediaType = 'video'; - 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']); - expect(params.bid_request.imp[0].video).to.not.exist; + let payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].video).to.not.exist; bidRequest = JSON.parse(JSON.stringify(simpleBidRequest)); bidRequest.mediaTypes = { @@ -319,32 +328,45 @@ describe('Improve Digital Adapter Tests', function () { playerSize: [640, 480] } }; - 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']); - expect(params.bid_request.imp[0].video).to.not.exist; - }); - - 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; - expect(params.bid_request.imp[0].video).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; - expect(params.bid_request.imp[0].video).to.not.exist; + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].video.placement).to.exist.and.equal(1); + bidRequest.mediaTypes.video.context = 'outstream'; + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].video.placement).to.exist.and.equal(3); }); it('should set video params for instream', function() { const bidRequest = JSON.parse(JSON.stringify(instreamBidRequest)); + delete bidRequest.mediaTypes.video.playerSize; + const videoParams = { + mimes: ['video/mp4'], + skip: 1, + skipmin: 5, + skipafter: 30, + minduration: 15, + maxduration: 60, + startdelay: 5, + minbitrate: 500, + maxbitrate: 2000, + w: 1024, + h: 640, + placement: INSTREAM_TYPE, + }; bidRequest.params.video = videoParams; - const request = spec.buildRequests([bidRequest])[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal(videoParams); + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; + const payload = JSON.parse(request.data); + expect(payload.imp[0].video).to.deep.equal(videoParams); + }); + + it('should set video playerSize over video params', () => { + const bidRequest = JSON.parse(JSON.stringify(instreamBidRequest)); + bidRequest.params.video = { + w: 1024, h: 640 + } + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; + const payload = JSON.parse(request.data); + expect(payload.imp[0].video.h).equal(480); + expect(payload.imp[0].video.w).equal(640); }); it('should set skip params only if skip=1', function() { @@ -357,22 +379,28 @@ describe('Improve Digital Adapter Tests', function () { } bidRequest.params.video = videoTest; let request = spec.buildRequests([bidRequest])[0]; - let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal(videoTest); + let payload = JSON.parse(request.data); + expect(payload.imp[0].video.skip).to.equal(1); + expect(payload.imp[0].video.skipmin).to.equal(5); + expect(payload.imp[0].video.skipafter).to.equal(30); // 0 - leave out skipmin and skipafter videoTest.skip = 0; bidRequest.params.video = videoTest; request = spec.buildRequests([bidRequest])[0]; - params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal({ skip: 0 }); + payload = JSON.parse(request.data); + expect(payload.imp[0].video.skip).to.equal(0); + expect(payload.imp[0].video.skipmin).to.not.exist; + expect(payload.imp[0].video.skipafter).to.not.exist; // other videoTest.skip = 'blah'; bidRequest.params.video = videoTest; request = spec.buildRequests([bidRequest])[0]; - params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.not.exist; + payload = JSON.parse(request.data); + expect(payload.imp[0].video.skip).to.not.exist; + expect(payload.imp[0].video.skipmin).to.not.exist; + expect(payload.imp[0].video.skipafter).to.not.exist; }); it('should ignore invalid/unexpected video params', function() { @@ -387,51 +415,36 @@ describe('Improve Digital Adapter Tests', function () { videoTestInvParam.blah = 1; bidRequest.params.video = videoTestInvParam; let request = spec.buildRequests([bidRequest])[0]; - let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal(videoTest); + let payload = JSON.parse(request.data); + expect(payload.imp[0].video.blah).not.to.exist; }); it('should set video params for outstream', function() { const bidRequest = JSON.parse(JSON.stringify(outstreamBidRequest)); bidRequest.params.video = videoParams; const request = spec.buildRequests([bidRequest])[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal(videoParams); + const payload = JSON.parse(request.data); + expect(payload.imp[0].video).to.deep.equal({...{ + mimes: ['video/mp4'], + placement: OUTSTREAM_TYPE, + w: bidRequest.mediaTypes.video.playerSize[0], + h: bidRequest.mediaTypes.video.playerSize[1], + }, + ...videoParams}); }); - + // it('should set video params for multi-format', function() { const bidRequest = JSON.parse(JSON.stringify(multiFormatBidRequest)); bidRequest.params.video = videoParams; const request = spec.buildRequests([bidRequest])[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal(videoParams); - }); - - 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 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 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].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(); + const payload = JSON.parse(request.data); + const testVideoParams = Object.assign({ + placement: OUTSTREAM_TYPE, + w: 640, + h: 480, + mimes: ['video/mp4'], + }, videoParams); + expect(payload.imp[0].video).to.deep.equal(testVideoParams); }); it('should add schain', function () { @@ -439,8 +452,8 @@ describe('Improve Digital Adapter Tests', function () { const bidRequest = Object.assign({}, simpleBidRequest); bidRequest.schain = schain; const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.schain).to.equal(schain); + const payload = JSON.parse(request.data); + expect(payload.source.ext.schain).to.equal(schain); }); it('should add eids', function () { @@ -455,8 +468,8 @@ describe('Improve Digital Adapter Tests', function () { const bidRequest = Object.assign({}, simpleBidRequest); bidRequest.userId = userId; const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.user).to.deep.equal(expectedUserObject); + const payload = JSON.parse(request.data); + expect(payload.user).to.deep.equal(expectedUserObject); }); it('should return 2 requests', function () { @@ -486,8 +499,8 @@ describe('Improve Digital Adapter Tests', function () { const getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); 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({ + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner).to.deep.equal({ format: [ { w: 300, h: 250 }, { w: 160, h: 600 } @@ -506,8 +519,8 @@ describe('Improve Digital Adapter Tests', function () { }; bidRequest.params.size = size; 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({ + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner).to.deep.equal({ format: [ { w: 300, h: 250 }, { w: 160, h: 600 } @@ -516,434 +529,396 @@ describe('Improve Digital Adapter Tests', function () { getConfigStub.restore(); }); - it('should set pagecat and genre âžž fpd:ortb2.site', function() { - config.setConfig(JSON.parse('{"ortb2":{"site":{"cat":["IAB2"],"pagecat":["IAB2-2"],"content":{"genre":"Adventure"}}}}')); - const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.pagecat).to.be.an('array'); - expect(params.bid_request.pagecat).to.deep.equal(['IAB2-2']); - expect(params.bid_request.genre).to.be.a('string'); - expect(params.bid_request.genre).be.equal('Adventure'); - }); + it('should set GPID and Instl Signal', function () { + const bidRequest = Object.assign({ + ortb2Imp: { + instl: true, + ext: { + gpid: '/123/ID-FORMAT', + data: { + pbadslot: '/123/ID-FORMAT-PBADSLOT', + adserver: { + adslot: '/123/ID-FORMAT-ADSERVER-PB-ADSLOT', + } + } + }, + } + }, simpleBidRequest); + let request = spec.buildRequests([bidRequest], bidderRequest)[0]; + let payload = JSON.parse(request.data); + expect(payload.imp[0].ext.gpid).to.equal('/123/ID-FORMAT'); + expect(payload.imp[0].instl).to.equal(1); - it('should not set pagecat and genre when malformed data provided âžž fpd:ortb2.site', function() { - config.setConfig(JSON.parse('{"ortb2":{"site":{"pagecat":"IAB2-2","content":{"genre":["Adventure"]}}}}')); - const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.pagecat).does.not.exist; - expect(params.bid_request.genre).does.not.exist; + delete bidRequest.ortb2Imp.ext.gpid; + request = spec.buildRequests([bidRequest], bidderRequest)[0]; + payload = JSON.parse(request.data); + expect(payload.imp[0].ext.gpid).to.equal('/123/ID-FORMAT-PBADSLOT'); + + delete bidRequest.ortb2Imp.ext.data.pbadslot; + request = spec.buildRequests([bidRequest], bidderRequest)[0]; + payload = JSON.parse(request.data); + expect(payload.imp[0].ext.gpid).to.equal('/123/ID-FORMAT-ADSERVER-PB-ADSLOT'); + + delete bidRequest.ortb2Imp.ext.data.adserver; + delete bidRequest.ortb2Imp.instl; + request = spec.buildRequests([bidRequest], bidderRequest)[0]; + payload = JSON.parse(request.data); + expect(payload.imp[0].ext.gpid).to.not.exist; + expect(payload.imp[0].instl).to.not.exist; }); - it('should use cat when pagecat not available âžž fpd:ortb2.site', function() { - config.setConfig(JSON.parse('{"ortb2":{"site":{"cat":["IAB2"]}}}')); - const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.pagecat).to.be.an('array'); - expect(params.bid_request.pagecat).to.deep.equal(['IAB2']); + it('should not set site when app is defined in FPD', function () { + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('ortb2.app').returns({ content: 'XYZ' }); + let request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; + let payload = JSON.parse(request.data); + expect(payload.site).does.not.exist; + expect(payload.app).does.exist; + expect(payload.app.content).does.exist.and.equal('XYZ'); + getConfigStub.restore(); }); - it('should format pagecat correctly âžž fpd:ortb2.site', function() { - config.setConfig(JSON.parse('{"ortb2":{"site":{"cat":["IAB2", ["IAB-1"], "IAB3", 123, ""]}}}')); - const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.pagecat).to.be.an('array'); - expect(params.bid_request.pagecat).to.deep.equal([ - 'IAB2', - 'IAB3' - ] - ); + it('should not set site when app is defined in CONFIG', function () { + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('app').returns({ content: 'XYZ' }); + let request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; + let payload = JSON.parse(request.data); + expect(payload.site).does.not.exist; + expect(payload.app).does.exist; + expect(payload.app.content).does.exist.and.equal('XYZ'); + getConfigStub.restore(); }); - it('should set coppa', function() { - sinon.stub(config, 'getConfig') - .withArgs('coppa') - .returns(true); - const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.coppa).to.equal(1); + it('should set correct site params', function () { + let getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('site').returns({ + content: 'XYZ', + page: 'https://improveditigal.com/', + domain: 'improveditigal.com' + }); + let request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + let payload = JSON.parse(request.data); + expect(payload.site.content).does.exist.and.equal('XYZ'); + expect(payload.site.page).does.exist.and.equal('https://improveditigal.com/'); + expect(payload.site.domain).does.exist.and.equal('improveditigal.com'); + getConfigStub.reset(); + + request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + payload = JSON.parse(request.data); + expect(payload.site.content).does.not.exist; + expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); + expect(payload.site.domain).does.exist.and.equal('blah.com'); + + getConfigStub.withArgs('ortb2.site').returns({ + content: 'ZZZ', + }); + request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + payload = JSON.parse(request.data); + expect(payload.site.content).does.exist.and.equal('ZZZ'); + expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); + expect(payload.site.domain).does.exist.and.equal('blah.com'); + getConfigStub.restore(); + }); - config.getConfig.restore(); + it('should set pageUrl as site param', function () { + let getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('pageUrl').returns('https://improvidigital.com/test-page'); + let request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + let payload = JSON.parse(request.data); + expect(payload.site.page).does.exist.and.equal('https://improvidigital.com/test-page'); + expect(payload.site.domain).does.exist.and.equal('improvidigital.com'); + getConfigStub.reset(); + + getConfigStub.withArgs('pageUrl').returns(undefined); + request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + payload = JSON.parse(request.data); + expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); + expect(payload.site.domain).does.exist.and.equal('blah.com'); + getConfigStub.restore(); }); - it('should undefined coppa', function() { - const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.coppa).to.equal(undefined); + it('should set site when app not available', function () { + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('app').returns(undefined); + let request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; + let payload = JSON.parse(request.data); + expect(payload.site).does.exist; + expect(payload.app).does.not.exist; + getConfigStub.restore(); }); }); const serverResponse = { 'body': { - 'id': '687a06c541d8d1', - 'site_id': 191642, - 'bid': [ + 'id': '99f9a9e6-5126-425b-822c-8b4edad2a719', + 'cur': 'EUR', + 'seatbid': [ { - 'isNet': false, - 'id': '33e9500b21129f', - 'advid': '5279', - 'price': 1.45888594164456, - 'nurl': 'https://ice.360yield.com/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=', - 'h': 290, - 'pid': 1053688, - 'sync': [ - 'https://link1', - 'https://link2' - ], - 'crid': '422031', - 'w': 600, - 'cid': '99006', - 'adm': 'document.writeln(\"\\\"\\\"\\/<\\/a>\");document.writeln(\"<\\/improvedigital_ad_output_information>\");' - } - ], - 'debug': '' - } - }; - - const serverResponseRazr = { - 'body': { - 'id': '687a06c541d8d1', - 'site_id': 191642, - 'bid': [ - { - 'isNet': false, - 'id': '33e9500b21129f', - 'advid': '5279', - 'price': 1.45888594164456, - 'nurl': 'https://ice.360yield.com/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=', - 'h': 290, - 'pid': 1053688, - 'sync': [ - 'https://link1', - 'https://link2' + 'bid': [ + { + 'ext': { + 'improvedigital': { + 'line_item_id': 320896, + 'bidder_id': 0, + 'brand_name': '', + 'buying_type': 'classic', + 'agency_id': '0' + } + }, + 'exp': 120, + 'crid': '510265', + 'price': 1.9200543539802946, + 'id': '35adfe19-d6e9-46b9-9f7d-20da7026b965', + 'w': 728, + 'impid': '33e9500b21129f', + 'h': 90, + 'adm': '  ', + 'cid': '123159' + } ], - 'crid': '422031', - 'w': 600, - 'cid': '99006', - 'adm': 'document.writeln("', - privacy: 'https://www.myprivacyurl.com' - } + 'crid': '544456', + 'exp': 120, + 'id': '52098fad-20c1-476b-a4fa-41e275e5a4a5', + 'price': 1.8600000000000003, + 'adm': "{\"ver\":\"1.1\",\"imptrackers\":[\"https://secure.adnxs.com/imptr?id=52311&t=2\",\"https://euw-ice.360yield.com/imp_pixel?ic=hcUBlCANx1FabHBf6FR2gC7UO4xEyXahdZAn0-B5qL-bb3A74BJ1smyWIyW7IWcC0SOjSXzVpevTHXxTqJ.sf.Qhahyy6tSo.0j1QWfXlH8sM4-8vKWjMjw-x.IrJJNlwkQ0s1CdwcwTefcLXm5l2E-W19VhACuV7f3mgrZMNjiSw.SjJAfyPC3SIyAMRjYfj53UmjriQ46T7lhmkqxK8wHmksYCdbZc3PZESk8NWl28sxdjNvnYYCFMcJbeav.LOLabyTXfwy-1cEPbQs.IKMRZIKaqccTDPV3wOtzbNv0jQzatd3Nnv-PGFQcjQ-GW3i27W04Fws4kodpFSn-B6VwZAjzLzoyd5gBncyRnAyCplEbgHU5sZ1IyKHWjgCl3ZtRIK5vqrRD5D-xqgSnOi7-phG.CqZWDZ4bMDSfQg2ZnbvUTyGKcEl0WR59dW5izTMV4Fjizcrvr5T-t.zMbGwz.hGnmLIyhTqh.IcwW.GiDLVExlDlix5S1LXIWVsSyrQ==\"],\"assets\":[{\"id\":1,\"data\":{\"value\":\"ImproveDigital\",\"type\":1}},{\"id\":3,\"data\":{\"value\":\"Test content.\",\"type\":2}},{\"id\":0,\"title\":{\"text\":\"Sample Prebid Test Title\"}}],\"link\":{\"url\":\"https://euw-ice.360yield.com/click/hcUBlHOV7YhVse8RyBa0ajjyPa9Vt17e4g-1m3cRj3E67vq-RYux.SiUeAmBfNBcoOqkUc6A15AWmi4yFu5K-BdkaYjildyyk7fNLyR6hWr411kv4vrFwm5jrIBceuHS6K8oN69f.uCo8zGTdR2TbSlldwcpahQPlufZU.6VaMsu4IC53uEiUT5vb7kAw6TTlxuGBNq6zaGryiWEV2.N3YYJDTyYPh8tv-ZFyeFZFm0Gnjv.xWbC.70JcRUVU9UelQaPsTpTWYTXBhJt84YJUw1-GNtaLNVLSjjZbVoA2fsMti5p6OBmF.7u39on2OPgvseIkSmge7Pqg63pRqdP75hp.DAEk6OkcN1jGnwP2DSbvpaSbin5lVqjfO0B-wnQgfQTCUtM5v4JmkNweLhUf9Q-x.nPKLW5SccEk9ZFXzY2-1wpT3PWm8Tix3NRscLPZub9wHzL..pl6ip8cQ9hp16UjwT4H6RMAxL0R7bl-h2pAicGAzYmuO7ntRESKUoIWA==//http%3A%2F%2Fquantum-advertising.com%2Ffr%2F\"},\"jstracker\":\"\"}", + 'impid': '2d7a7db325c6f', + 'cid': '196108' + } + ], + 'seat': 'improvedigital' } - ], - debug: '' + ] } }; const serverResponseVideo = { 'body': { - 'id': '687a06c541d8d1', - 'site_id': 191642, - 'bid': [ + 'id': '8ed20675-8934-430c-b645-1ccd17b35839', + 'cur': 'EUR', + 'seatbid': [ { - 'isNet': false, - 'id': '33e9500b21129f', - 'advid': '5279', - 'price': 1.45888594164456, - 'nurl': 'http://ice.360yield.com/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=', - 'h': 290, - 'pid': 1053688, - 'sync': [ - 'http://link1', - 'http://link2' + 'bid': [ + { + 'ext': { + 'improvedigital': { + 'line_item_id': 321329, + 'bidder_id': 0, + 'brand_name': '', + 'buying_type': 'classic', + 'agency_id': '0' + } + }, + 'exp': 120, + 'crid': '484367', + 'price': 9.600271769901472, + 'id': 'b131fd7b-5759-4b72-800e-60e69291e7d9', + 'adomain': [ + 'improvedigital.com' + ], + 'impid': '33e9500b21129f', + 'adm': '', + 'w': 640, + 'h': 480, + 'cid': '123159' + } ], - 'crid': '422031', - 'w': 600, - 'cid': '99006', - 'adm': '', - 'ad_type': 'video' + 'seat': 'improvedigital' } ], - 'debug': '' } }; - const nativeEventtrackers = [ - { - event: 1, - method: 1, - url: 'https://www.mytracker.com/imptracker' - }, - { - event: 1, - method: 2, - url: 'https://www.mytracker.com/tracker.js' + const serverResponseRazr = { + body: { + 'id': '2adac6a5fe04df', + 'cur': 'EUR', + 'ext': { + 'improvedigital': { + 'sync': [ + 'https://d5p.de17a.com/getuid/improve_digital?publisher_user_id=ce26f11e-567a-4eb7-bf94-51752e293ca5&publisher_dsp_id=61&publisher_call_type=redirect&gdpr=1&gdpr_consent=CPU22FrPU22FrAcABBENCDCsAP_AAH_AAChQIltf_X__b3_j-_5_f_t0eY1P9_7_v-0zjhfdt-8N3f_X_L8X42M7vF36pq4KuR4Eu3LBIQdlHOHcTUmw6okVrzPsbk2cr7NKJ7PEmnMbO2dYGH9_n93TuZKY7______z_v-v_v____f_7-3_3__5_3---_e_V_99zLv9____39nP___9v-_9_____4IhgEmGpeQBdmWODJtGlUKIEYVhIdAKACigGFoisIHVwU7K4CfUELABCagJwIgQYgowYBAAIJAEhEQEgB4IBEARAIAAQAqwEIACNgEFgBYGAQACgGhYgRQBCBIQZHBUcpgQFSLRQT2ViCUHexphCGWeBFAo_oqEBGs0QLAyEhYOY4AkBLxZIHmKF8gAAAAA.f_gAD_gAAAAA&publisher_redirecturl=https://euw-ice.360yield.com/match' + ] + } + }, + 'seatbid': [ + { + 'bid': [ + { + 'ext': { + 'improvedigital': { + 'line_item_id': 410573, + 'bidder_id': 0, + 'brand_name': '', + 'buying_type': 'classic', + 'agency_id': '0' + } + }, + 'exp': 120, + 'crid': '544063', + 'price': 1.9199364935359489, + 'id': '1fcf4dd8-a783-48ed-b59c-8fc8eeccb024', + 'adomain': [ + 'improvedigital.com' + ], + 'w': 970, + 'impid': '33e9500b21129f', + 'h': 250, + 'adm': '\n\n\n\n\n\n\n\n ', + 'cid': '187354' + } + ], + 'seat': 'improvedigital' + } + ] } - ]; + }; describe('interpretResponse', function () { const expectedBid = [ { - 'ad': '', - 'creativeId': '422031', - 'cpm': 1.45888594164456, - 'currency': 'USD', - 'height': 290, - 'mediaType': 'banner', - 'netRevenue': false, - 'requestId': '33e9500b21129f', - 'ttl': 300, - 'width': 600 + requestId: '33e9500b21129f', + cpm: 1.9200543539802946, + currency: 'EUR', + width: 728, + height: 90, + ttl: 300, + ad: '  ', + creativeId: '510265', + dealId: 320896, + netRevenue: false, + mediaType: BANNER, + meta: { + advertiserDomains: [] + } } ]; const expectedTwoBids = [ expectedBid[0], { - 'ad': '', - 'creativeId': '422033', - 'cpm': 1.23, - 'currency': 'USD', - 'height': 400, - 'mediaType': 'banner', - 'netRevenue': true, - 'requestId': '1234', - 'ttl': 300, - 'width': 700 - } - ]; - - const expectedBidNative = [ - { - mediaType: 'native', - creativeId: '422031', - cpm: 1.45888594164456, - currency: 'USD', - height: 290, - netRevenue: false, requestId: '33e9500b21129f', + cpm: 1.9200543539802946, + currency: 'EUR', + width: 300, + height: 250, ttl: 300, - width: 600, - native: { - title: 'Native title', - body: 'Native body', - body2: 'body2', - cta: 'Do it', - sponsoredBy: 'Improve Digital', - rating: '4', - likes: '10105', - downloads: '150000', - price: '3.99', - salePrice: '4.49', - phone: '(123) 456-7890', - address: '123 Main Street, Anywhere USA', - displayUrl: 'https://myurl.com', - icon: { - url: 'https://blah.com/icon.jpg', - height: 30, - width: 40 - }, - image: { - url: 'https://blah.com/image.jpg', - height: 200, - width: 800 - }, - clickUrl: 'https://advertiser.com', - clickTrackers: ['https://click.tracker.com/click?impid=123'], - impressionTrackers: [ - 'https://ice.360yield.com/imp_pixel?ic=wVm', - 'https://imptrack1.com', - 'https://imptrack2.com' - ], - javascriptTrackers: '', - privacyLink: 'https://www.myprivacyurl.com' + ad: '  ', + creativeId: '479163', + dealId: 320896, + netRevenue: false, + mediaType: BANNER, + meta: { + advertiserDomains: [] } } ]; const expectedBidInstreamVideo = [ { - 'vastXml': '', - 'creativeId': '422031', - 'cpm': 1.45888594164456, - 'currency': 'USD', - 'height': 290, - 'mediaType': 'video', - 'netRevenue': false, - 'requestId': '33e9500b21129f', - 'ttl': 300, - 'width': 600 + requestId: '33e9500b21129f', + cpm: 9.600271769901472, + currency: 'EUR', + ttl: 300, + vastXml: '', + creativeId: '484367', + dealId: 321329, + netRevenue: false, + mediaType: VIDEO, + meta: { + advertiserDomains: ['improvedigital.com'], + } } ]; const expectedBidOutstreamVideo = utils.deepClone(expectedBidInstreamVideo); expectedBidOutstreamVideo[0].adResponse = { - content: expectedBidOutstreamVideo[0].vastXml, - height: expectedBidOutstreamVideo[0].height, - width: expectedBidOutstreamVideo[0].width + content: expectedBidOutstreamVideo[0].vastXml }; it('should return a well-formed display bid', function () { @@ -965,50 +940,35 @@ describe('Improve Digital Adapter Tests', function () { const response = JSON.parse(JSON.stringify(serverResponse)); let bids; - delete response.body.bid[0].lid; - response.body.bid[0].buying_type = 'deal_id'; + delete response.body.seatbid[0].bid[0].ext.improvedigital.line_item_id; + response.body.seatbid[0].bid[0].ext.improvedigital.buying_type = 'deal_id'; 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; + response.body.seatbid[0].bid[0].ext.improvedigital.line_item_id = 268515; + delete response.body.seatbid[0].bid[0].ext.improvedigital.buying_type; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.not.exist; - response.body.bid[0].lid = 268515; - response.body.bid[0].buying_type = 'rtb'; + response.body.seatbid[0].bid[0].ext.improvedigital.line_item_id = 268515; + response.body.seatbid[0].bid[0].ext.improvedigital.buying_type = 'rtb'; 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'; + response.body.seatbid[0].bid[0].ext.improvedigital.line_item_id = 268515; + response.body.seatbid[0].bid[0].ext.improvedigital.buying_type = 'classic'; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.equal(268515); - response.body.bid[0].lid = 268515; - response.body.bid[0].buying_type = 'deal_id'; + response.body.seatbid[0].bid[0].ext.improvedigital.line_item_id = 268515; + response.body.seatbid[0].bid[0].ext.improvedigital.buying_type = 'deal_id'; 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, {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, {bidderRequest}); - expect(bids[0].dealId).to.not.exist; - - response.body.bid[0].lid = [ 268515, 12456, 34567 ]; - response.body.bid[0].buying_type = [ 'rtb', 'deal_id', 'deal_id' ]; - bids = spec.interpretResponse(response, {bidderRequest}); - expect(bids[0].dealId).to.equal(12456); }); it('should set currency', function () { const response = JSON.parse(JSON.stringify(serverResponse)); - response.body.bid[0].currency = 'eur'; + response.body.cur = 'eur'; const bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].currency).to.equal('EUR'); }); @@ -1018,35 +978,35 @@ describe('Improve Digital Adapter Tests', function () { let bids; // Price missing or 0 - response.body.bid[0].price = 0; + response.body.seatbid[0].bid[0].price = 0; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); - delete response.body.bid[0].price; + delete response.body.seatbid[0].bid[0]; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); - response.body.bid[0].price = null; + response.body.seatbid[0].bid[0] = []; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); // errorCode present response = JSON.parse(JSON.stringify(serverResponse)); - response.body.bid[0].errorCode = undefined; + response.body.seatbid[0].bid[0].errorCode = undefined; 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; + delete response.body.seatbid[0].bid[0].adm; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); - response.body.bid[0].adm = null; + response.body.seatbid[0].bid[0].adm = null; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); }); it('should set netRevenue', function () { const response = JSON.parse(JSON.stringify(serverResponse)); - response.body.bid[0].isNet = true; + response.body.seatbid[0].bid[0].ext.improvedigital.is_net = true; const bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].netRevenue).to.equal(true); }); @@ -1054,30 +1014,31 @@ describe('Improve Digital Adapter Tests', function () { it('should set advertiserDomains', function () { const adomain = ['domain.com']; const response = JSON.parse(JSON.stringify(serverResponse)); - response.body.bid[0].adomain = adomain; + response.body.seatbid[0].bid[0].adomain = adomain; const bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].meta.advertiserDomains).to.equal(adomain); }); - + // // Native ads it('should return a well-formed native ad bid', function () { - 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); - - // eventtrackers - const response = JSON.parse(JSON.stringify(serverResponseNative)); - const expectedBids = JSON.parse(JSON.stringify(expectedBidNative)); - response.body.bid[0].native.eventtrackers = nativeEventtrackers; - expectedBids[0].native.impressionTrackers = [ - 'https://ice.360yield.com/imp_pixel?ic=wVm', - 'https://www.mytracker.com/imptracker' - ]; - expectedBids[0].native.javascriptTrackers = ''; - bids = spec.interpretResponse(response, {bidderRequest}); - delete bids[0].ortbNative; - expect(bids).to.deep.equal(expectedBids); + const nativeBidderRequest = JSON.parse(JSON.stringify(bidderRequest)); + nativeBidderRequest.bids[0].bidId = '2d7a7db325c6f'; + delete nativeBidderRequest.bids[0].mediaTypes.banner; + nativeBidderRequest.bids[0].mediaTypes.native = {}; + const bids = spec.interpretResponse(serverResponseNative, {bidderRequest: nativeBidderRequest}); + // Verify Native Response + expect(bids[0].native).to.exist; + const nativeBid = bids[0].native; + const nativeResp = JSON.parse(serverResponseNative.body.seatbid[0].bid[0].adm); + // Verify Native Response + expect(nativeBid.clickUrl).to.exist.and.equal(nativeResp.link.url); + expect(nativeBid.impressionTrackers).to.exist.and.deep.equal(nativeResp.imptrackers); + expect(nativeBid.javascriptTrackers).to.exist.and.deep.equal(nativeResp.jstracker); + + // Verify Assets + expect(nativeBid.title).to.exist.and.equal('Sample Prebid Test Title'); + expect(nativeBid.sponsoredBy).to.exist.and.equal('ImproveDigital'); + expect(nativeBid.body).to.exist.and.equal('Test content.'); }); // Video diff --git a/test/spec/modules/justpremiumBidAdapter_spec.js b/test/spec/modules/justpremiumBidAdapter_spec.js index 3686418a991..b08be01461b 100644 --- a/test/spec/modules/justpremiumBidAdapter_spec.js +++ b/test/spec/modules/justpremiumBidAdapter_spec.js @@ -65,6 +65,24 @@ describe('justpremium adapter', function () { } } + const serverResponses = [ + { + 'body': { + 'bid': {}, + 'pass': { + '141952': true + }, + 'deals': {}, + 'pxs': [ + { + 'url': 'https://url.com', + 'type': 'image' + } + ] + } + } + ] + describe('isBidRequestValid', function () { it('Verifies bidder code', function () { expect(spec.code).to.equal('justpremium') @@ -97,7 +115,7 @@ describe('justpremium adapter', function () { expect(jpxRequest.id).to.equal(adUnits[0].params.zone) expect(jpxRequest.mediaTypes && jpxRequest.mediaTypes.banner && jpxRequest.mediaTypes.banner.sizes).to.not.equal('undefined') expect(jpxRequest.version.prebid).to.equal('$prebid.version$') - expect(jpxRequest.version.jp_adapter).to.equal('1.8.2') + expect(jpxRequest.version.jp_adapter).to.equal('1.8.3') expect(jpxRequest.pubcid).to.equal('0000000') expect(jpxRequest.uids.tdid).to.equal('1111111') expect(jpxRequest.uids.id5id.uid).to.equal('2222222') @@ -185,7 +203,7 @@ describe('justpremium adapter', function () { }) describe('getUserSyncs', function () { - it('Verifies sync options', function () { + it('Verifies sync options for iframe', function () { const options = spec.getUserSyncs({iframeEnabled: true}, {}, {gdprApplies: true, consentString: 'BOOgjO9OOgjO9APABAENAi-AAAAWd'}, '1YYN') expect(options).to.not.be.undefined expect(options[0].type).to.equal('iframe') @@ -193,5 +211,11 @@ describe('justpremium adapter', function () { expect(options[0].url).to.match(/&consentString=BOOgjO9OOgjO9APABAENAi-AAAAWd/) expect(options[0].url).to.match(/&usPrivacy=1YYN/) }) + it('Returns array of user sync pixels', function () { + const options = spec.getUserSyncs({pixelEnabled: true}, serverResponses) + expect(options).to.not.be.undefined + expect(Array.isArray(options)).to.be.true + expect(options[0].type).to.equal('image') + }) }) }) diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index f1de2f3bf93..4893f7cbb30 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -66,7 +66,7 @@ describe('LiveIntentId', function() { consentString: 'consentDataString' }) liveIntentIdSubmodule.getId(defaultConfigParams); - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?wpn=prebid.*us_privacy=1YNY.*&gdpr=1&gdpr_consent=consentDataString.*/); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&gdpr_consent=consentDataString.*/); }); it('should fire an event when getId and a hash is provided', function() { @@ -88,7 +88,7 @@ describe('LiveIntentId', function() { } } }}); - expect(server.requests[0].url).to.match(/https:\/\/collector.liveintent.com\/j\?aid=a-0001&wpn=prebid.*/); + expect(server.requests[0].url).to.match(/https:\/\/collector.liveintent.com\/j\?.*aid=a-0001.*&wpn=prebid.*/); }); it('should initialize LiveConnect and emit an event with a privacy string when decode', function() { @@ -195,7 +195,7 @@ describe('LiveIntentId', function() { it('should include the LiveConnect identifier when calling the LiveIntent Identity Exchange endpoint', function() { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' - getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); + getCookieStub.withArgs('_lc2_fpi').returns(oldCookie) let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); @@ -211,7 +211,7 @@ describe('LiveIntentId', function() { it('should include the LiveConnect identifier and additional Identifiers to resolve', function() { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' - getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); + getCookieStub.withArgs('_lc2_fpi').returns(oldCookie); getDataFromLocalStorageStub.withArgs('_thirdPC').returns('third-pc'); const configParams = { params: { ...defaultConfigParams.params, diff --git a/test/spec/modules/loglyliftBidAdapter_spec.js b/test/spec/modules/loglyliftBidAdapter_spec.js index 6a68ef856ff..baa6ff05f39 100644 --- a/test/spec/modules/loglyliftBidAdapter_spec.js +++ b/test/spec/modules/loglyliftBidAdapter_spec.js @@ -3,6 +3,24 @@ import { spec } from '../../../modules/loglyliftBidAdapter'; import * as utils from 'src/utils.js'; describe('loglyliftBidAdapter', function () { + const bannerBidRequests = [{ + bidder: 'loglylift', + bidId: '51ef8751f9aead', + params: { + adspotId: 16 + }, + adUnitCode: '/19968336/prebid_native_example_1', + transactionId: '10aee457-617c-4572-ab5b-99df1d73ccb4', + sizes: [[300, 250], [300, 600]], + bidderRequestId: '15da3afd9632d7', + auctionId: 'f890b7d9-e787-4237-ac21-6d8554abac9f', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + } + }]; + const nativeBidRequests = [{ bidder: 'loglylift', bidId: '254304ac29e265', @@ -51,6 +69,25 @@ describe('loglyliftBidAdapter', function () { timeout: 3000 }; + const bannerServerResponse = { + body: { + bids: [{ + requestId: '51ef8751f9aead', + cpm: 101.0234, + width: 300, + height: 250, + creativeId: '16', + currency: 'JPY', + netRevenue: true, + ttl: 60, + meta: { + advertiserDomains: ['advertiserexample.com'] + }, + ad: '
TEST
', + }] + } + }; + const nativeServerResponse = { body: { bids: [{ @@ -83,39 +120,43 @@ describe('loglyliftBidAdapter', function () { }; describe('isBidRequestValid', function () { - it('should return true if the adspotId parameter is present', function () { - expect(spec.isBidRequestValid(nativeBidRequests[0])).to.be.true; - }); + [nativeBidRequests, bannerBidRequests].forEach(bidRequests => { + it('should return true if the adspotId parameter is present', function () { + expect(spec.isBidRequestValid(bidRequests[0])).to.be.true; + }); - it('should return false if the adspotId parameter is not present', function () { - let bidRequest = utils.deepClone(nativeBidRequests[0]); - delete bidRequest.params.adspotId; - expect(spec.isBidRequestValid(bidRequest)).to.be.false; + it('should return false if the adspotId parameter is not present', function () { + let bidRequest = utils.deepClone(bidRequests[0]); + delete bidRequest.params.adspotId; + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }); }); }); describe('buildRequests', function () { - it('should generate a valid single POST request for multiple bid requests', function () { - const request = spec.buildRequests(nativeBidRequests, bidderRequest)[0]; - expect(request.method).to.equal('POST'); - expect(request.url).to.equal('https://bid.logly.co.jp/prebid/client/v1?adspot_id=16'); - expect(request.data).to.exist; - - const data = JSON.parse(request.data); - expect(data.auctionId).to.equal(nativeBidRequests[0].auctionId); - expect(data.bidderRequestId).to.equal(nativeBidRequests[0].bidderRequestId); - expect(data.transactionId).to.equal(nativeBidRequests[0].transactionId); - expect(data.adUnitCode).to.equal(nativeBidRequests[0].adUnitCode); - expect(data.bidId).to.equal(nativeBidRequests[0].bidId); - expect(data.mediaTypes).to.deep.equal(nativeBidRequests[0].mediaTypes); - expect(data.params).to.deep.equal(nativeBidRequests[0].params); - expect(data.prebidJsVersion).to.equal('$prebid.version$'); - expect(data.url).to.exist; - expect(data.domain).to.exist; - expect(data.referer).to.equal(bidderRequest.refererInfo.referer); - expect(data.auctionStartTime).to.equal(bidderRequest.auctionStart); - expect(data.currency).to.exist; - expect(data.timeout).to.equal(bidderRequest.timeout); + [nativeBidRequests, bannerBidRequests].forEach(bidRequests => { + it('should generate a valid single POST request for multiple bid requests', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://bid.logly.co.jp/prebid/client/v1?adspot_id=16'); + expect(request.data).to.exist; + + const data = JSON.parse(request.data); + expect(data.auctionId).to.equal(bidRequests[0].auctionId); + expect(data.bidderRequestId).to.equal(bidRequests[0].bidderRequestId); + expect(data.transactionId).to.equal(bidRequests[0].transactionId); + expect(data.adUnitCode).to.equal(bidRequests[0].adUnitCode); + expect(data.bidId).to.equal(bidRequests[0].bidId); + expect(data.mediaTypes).to.deep.equal(bidRequests[0].mediaTypes); + expect(data.params).to.deep.equal(bidRequests[0].params); + expect(data.prebidJsVersion).to.equal('$prebid.version$'); + expect(data.url).to.exist; + expect(data.domain).to.exist; + expect(data.referer).to.equal(bidderRequest.refererInfo.referer); + expect(data.auctionStartTime).to.equal(bidderRequest.auctionStart); + expect(data.currency).to.exist; + expect(data.timeout).to.equal(bidderRequest.timeout); + }); }); }); @@ -125,20 +166,40 @@ describe('loglyliftBidAdapter', function () { expect(interpretedResponse).to.be.an('array').that.is.empty; }); - it('should return valid response when passed valid server response', function () { - const request = spec.buildRequests(nativeBidRequests, bidderRequest)[0]; - const interpretedResponse = spec.interpretResponse(nativeServerResponse, request); - - expect(interpretedResponse).to.have.lengthOf(1); - expect(interpretedResponse[0].cpm).to.equal(nativeServerResponse.body.bids[0].cpm); - expect(interpretedResponse[0].width).to.equal(nativeServerResponse.body.bids[0].width); - expect(interpretedResponse[0].height).to.equal(nativeServerResponse.body.bids[0].height); - expect(interpretedResponse[0].creativeId).to.equal(nativeServerResponse.body.bids[0].creativeId); - expect(interpretedResponse[0].currency).to.equal(nativeServerResponse.body.bids[0].currency); - expect(interpretedResponse[0].netRevenue).to.equal(nativeServerResponse.body.bids[0].netRevenue); - expect(interpretedResponse[0].ttl).to.equal(nativeServerResponse.body.bids[0].ttl); - expect(interpretedResponse[0].native).to.deep.equal(nativeServerResponse.body.bids[0].native); - expect(interpretedResponse[0].meta.advertiserDomains[0]).to.equal(nativeServerResponse.body.bids[0].meta.advertiserDomains[0]); + describe('nativeServerResponse', function () { + it('should return valid response when passed valid server response', function () { + const request = spec.buildRequests(nativeBidRequests, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(nativeServerResponse, request); + + expect(interpretedResponse).to.have.lengthOf(1); + expect(interpretedResponse[0].cpm).to.equal(nativeServerResponse.body.bids[0].cpm); + expect(interpretedResponse[0].width).to.equal(nativeServerResponse.body.bids[0].width); + expect(interpretedResponse[0].height).to.equal(nativeServerResponse.body.bids[0].height); + expect(interpretedResponse[0].creativeId).to.equal(nativeServerResponse.body.bids[0].creativeId); + expect(interpretedResponse[0].currency).to.equal(nativeServerResponse.body.bids[0].currency); + expect(interpretedResponse[0].netRevenue).to.equal(nativeServerResponse.body.bids[0].netRevenue); + expect(interpretedResponse[0].ttl).to.equal(nativeServerResponse.body.bids[0].ttl); + expect(interpretedResponse[0].native).to.deep.equal(nativeServerResponse.body.bids[0].native); + expect(interpretedResponse[0].meta.advertiserDomains[0]).to.equal(nativeServerResponse.body.bids[0].meta.advertiserDomains[0]); + }); + }); + + describe('bannerServerResponse', function () { + it('should return valid response when passed valid server response', function () { + const request = spec.buildRequests(bannerBidRequests, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(bannerServerResponse, request); + + expect(interpretedResponse).to.have.lengthOf(1); + expect(interpretedResponse[0].cpm).to.equal(bannerServerResponse.body.bids[0].cpm); + expect(interpretedResponse[0].width).to.equal(bannerServerResponse.body.bids[0].width); + expect(interpretedResponse[0].height).to.equal(bannerServerResponse.body.bids[0].height); + expect(interpretedResponse[0].creativeId).to.equal(bannerServerResponse.body.bids[0].creativeId); + expect(interpretedResponse[0].currency).to.equal(bannerServerResponse.body.bids[0].currency); + expect(interpretedResponse[0].netRevenue).to.equal(bannerServerResponse.body.bids[0].netRevenue); + expect(interpretedResponse[0].ttl).to.equal(bannerServerResponse.body.bids[0].ttl); + expect(interpretedResponse[0].ad).to.equal(bannerServerResponse.body.bids[0].ad); + expect(interpretedResponse[0].meta.advertiserDomains[0]).to.equal(bannerServerResponse.body.bids[0].meta.advertiserDomains[0]); + }); }); }); @@ -161,12 +222,20 @@ describe('loglyliftBidAdapter', function () { expect(userSync).to.be.an('array').that.is.empty; }); - it('When nativeServerResponses empty, no userSync should be returned', function () { + it('When serverResponses empty, no userSync should be returned', function () { const syncOptions = { - 'iframeEnabled': false + 'iframeEnabled': true } let userSync = spec.getUserSyncs(syncOptions, []); expect(userSync).to.be.an('array').that.is.empty; }); + + it('When mediaType is banner, no userSync should be returned', function () { + const syncOptions = { + 'iframeEnabled': true + } + let userSync = spec.getUserSyncs(syncOptions, [bannerServerResponse]); + expect(userSync).to.be.an('array').that.is.empty; + }); }); }); diff --git a/test/spec/modules/mediasniperBidAdapter_spec.js b/test/spec/modules/mediasniperBidAdapter_spec.js new file mode 100644 index 00000000000..21ce5297c8f --- /dev/null +++ b/test/spec/modules/mediasniperBidAdapter_spec.js @@ -0,0 +1,506 @@ +import { expect } from 'chai'; +import { spec } from 'modules/mediasniperBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as utils from 'src/utils.js'; +import { BANNER } from '../../../src/mediaTypes.js'; + +const DEFAULT_CURRENCY = 'RUB'; +const DEFAULT_BID_TTL = 360; + +describe('mediasniperBidAdapter', function () { + const adapter = newBidder(spec); + let utilsMock; + let sandbox; + + const bid = { + bidder: 'mediasniper', + params: { siteid: 'testSiteID', placementId: '12345' }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: 'div-gpt-ad-1460505748561-0', + transactionId: '47789656-9e5c-4250-b7e0-2ce4cbe71a55', + sizes: [ + [300, 250], + [300, 600], + ], + bidId: '299320f4de980d', + bidderRequestId: '1c1b642f803242', + auctionId: '84212956-c377-40e8-b000-9885a06dc692', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + }; + + const bidderRequest = { + bidderCode: 'mediasniper', + auctionId: '84212956-c377-40e8-b000-9885a06dc692', + bidderRequestId: '1c1b642f803242', + bids: [bid], + auctionStart: 1620973766319, + timeout: 1000, + refererInfo: { + referer: + 'https://local.url/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + reachedTop: true, + isAmp: false, + numIframes: 0, + stack: [ + 'https://local.url/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + ], + canonicalUrl: null, + }, + start: 1620973766325, + }; + + beforeEach(function () { + utilsMock = sinon.mock(utils); + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + utilsMock.restore(); + sandbox.restore(); + }); + + describe('isBidRequestValid', function () { + it('should returns true when bid is provided with params', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should returns false when bid is provided with empty params', function () { + const noParamsBid = { + bidder: 'mediasniper', + params: {}, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: 'div-gpt-ad-1460505748561-0', + transactionId: '47789656-9e5c-4250-b7e0-2ce4cbe71a55', + sizes: [ + [300, 250], + [300, 600], + ], + bidId: '299320f4de980d', + }; + + expect(spec.isBidRequestValid(noParamsBid)).to.equal(false); + }); + + it('should returns false when bid is falsy or empty', function () { + const emptyBid = {}; + expect(spec.isBidRequestValid()).to.equal(false); + expect(spec.isBidRequestValid(false)).to.equal(false); + expect(spec.isBidRequestValid(emptyBid)).to.equal(false); + }); + + it('should return false when no sizes', function () { + const bannerNoSizeBid = { + bidder: 'mediasniper', + params: { placementId: '123' }, + mediaTypes: { + banner: {}, + }, + }; + + expect(spec.isBidRequestValid(bannerNoSizeBid)).to.equal(false); + }); + + it('should return false when empty sizes', function () { + const bannerEmptySizeBid = { + bidder: 'mediasniper', + params: { placementId: '123' }, + mediaTypes: { + banner: { sizes: [] }, + }, + }; + + expect(spec.isBidRequestValid(bannerEmptySizeBid)).to.equal(false); + }); + + it('should return false when mediaType is not supported', function () { + const bannerVideoBid = { + bidder: 'mediasniper', + params: { placementId: '123' }, + mediaTypes: { + video: {}, + }, + }; + + expect(spec.isBidRequestValid(bannerVideoBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('should create imp for supported mediaType only', function () { + const bidRequests = [utils.deepClone(bid)]; + const bidderRequestCopy = utils.deepClone(bidderRequest); + + bidderRequestCopy.bids = bidRequests[0]; + + const request = spec.buildRequests(bidRequests, bidderRequestCopy); + const data = JSON.parse(request.data); + + expect(data.imp.length).to.equal(1); + expect(data.imp[0].banner).to.exist; + }); + + it('should fill pmp only if dealid exists', function () { + const bidRequests = [utils.deepClone(bid)]; + const bidderRequestCopy = utils.deepClone(bidderRequest); + + bidRequests[0].params.dealid = '123'; + + const request = spec.buildRequests(bidRequests, bidderRequestCopy); + const data = JSON.parse(request.data); + + expect(data.imp.length).to.equal(1); + expect(data.imp[0].pmp).to.exist; + expect(data.imp[0].pmp.deals).to.exist; + expect(data.imp[0].pmp.deals.length).to.equal(1); + expect(data.imp[0].pmp.deals[0].id).to.equal( + bidRequests[0].params.dealid + ); + }); + + it('should fill site only if referer exists', function () { + const bidRequests = [utils.deepClone(bid)]; + const bidderRequestCopy = utils.deepClone(bidderRequest); + + bidderRequestCopy.refererInfo = {}; + + const request = spec.buildRequests(bidRequests, bidderRequestCopy); + const data = JSON.parse(request.data); + + expect(data.site.domain).to.not.exist; + expect(data.site.page).to.not.exist; + expect(data.site.ref).to.not.exist; + }); + + it('should fill site only if referer exists', function () { + const bidRequests = [utils.deepClone(bid)]; + const bidderRequestCopy = utils.deepClone(bidderRequest); + + bidderRequestCopy.refererInfo = null; + + const request = spec.buildRequests(bidRequests, bidderRequestCopy); + const data = JSON.parse(request.data); + + expect(data.site.domain).to.not.exist; + expect(data.site.page).to.not.exist; + expect(data.site.ref).to.not.exist; + }); + + it('should get expected properties with default values (no params set)', function () { + const bidRequests = [utils.deepClone(bid)]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const data = JSON.parse(request.data); + + // openRTB 2.5 + expect(data.cur[0]).to.equal(DEFAULT_CURRENCY); + expect(data.id).to.equal(bidderRequest.auctionId); + + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal(bidRequests[0].bidId); + expect(data.imp[0].banner.w).to.equal(300); + expect(data.imp[0].banner.h).to.equal(250); + expect(data.imp[0].banner.format[0].w).to.equal(300); + expect(data.imp[0].banner.format[0].h).to.equal(250); + expect(data.imp[0].banner.format[1].w).to.equal(300); + expect(data.imp[0].banner.format[1].h).to.equal(600); + expect(data.imp[0].banner.topframe).to.equal(0); + expect(data.imp[0].banner.pos).to.equal(0); + }); + + it('should get expected properties with values from params', function () { + const bidRequests = [utils.deepClone(bid)]; + bidRequests[0].params = { + pos: 2, + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].banner.pos).to.equal(2); + }); + + describe('PriceFloors module support', function () { + it('should not set `imp[]bidfloor` property when priceFloors module is not available', function () { + const bidRequests = [bid]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].banner).to.exist; + expect(data.imp[0].bidfloor).to.not.exist; + }); + + it('should not set `imp[]bidfloor` property when priceFloors module returns false', function () { + const bidWithPriceFloors = utils.deepClone(bid); + + bidWithPriceFloors.getFloor = () => { + return false; + }; + + const bidRequests = [bidWithPriceFloors]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const data = JSON.parse(request.data); + + expect(data.imp[0].banner).to.exist; + expect(data.imp[0].bidfloor).to.not.exist; + }); + + it('should get the highest floorPrice found when bid have several mediaTypes', function () { + const getFloorTest = (options) => { + switch (options.mediaType) { + case BANNER: + return { floor: 1, currency: DEFAULT_CURRENCY }; + default: + return false; + } + }; + + const bidWithPriceFloors = utils.deepClone(bid); + + bidWithPriceFloors.mediaTypes.video = { + playerSize: [600, 480], + }; + + bidWithPriceFloors.getFloor = getFloorTest; + + const bidRequests = [bidWithPriceFloors]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const data = JSON.parse(request.data); + + expect(data.imp[0].banner).to.exist; + expect(data.imp[0].bidfloor).to.equal(1); + }); + }); + }); + + describe('intrepretResponse', function () { + const rawServerResponse = { + body: { + id: '60839f99-d5f2-3ab3-b6ac-736b4fe9d0ae', + seatbid: [ + { + bid: [ + { + id: '60839f99-d5f2-3ab3-b6ac-736b4fe9d0ae_0_0', + impid: '1', + price: 58.01, + adid: 'string-id', + cid: 'string-id', + crid: 'string-id', + nurl: 'https://local.url/notif?index=ab-cd-ef&price=${AUCTION_PRICE}', + w: 300, + h: 250, + adomain: ['domain.io'], + adm: '', + }, + ], + seat: '', + }, + ], + cur: DEFAULT_CURRENCY, + ext: { protocol: '5.3' }, + }, + }; + + it('Returns empty array if no bid', function () { + const request = ''; + const response01 = spec.interpretResponse( + { body: { seatbid: [{ bid: [] }] } }, + request + ); + const response02 = spec.interpretResponse( + { body: { seatbid: [] } }, + request + ); + const response03 = spec.interpretResponse( + { body: { seatbid: null } }, + request + ); + const response04 = spec.interpretResponse( + { body: { seatbid: null } }, + request + ); + const response05 = spec.interpretResponse({ body: {} }, request); + const response06 = spec.interpretResponse({}, request); + + expect(response01.length).to.equal(0); + expect(response02.length).to.equal(0); + expect(response03.length).to.equal(0); + expect(response04.length).to.equal(0); + expect(response05.length).to.equal(0); + expect(response06.length).to.equal(0); + }); + + it('Log an error', function () { + const request = ''; + sinon.stub(utils, 'isArray').throws(); + utilsMock.expects('logError').once(); + spec.interpretResponse(rawServerResponse, request); + utils.isArray.restore(); + }); + + describe('Build banner response', function () { + it('Retrurn successful response', function () { + const request = ''; + const response = spec.interpretResponse(rawServerResponse, request); + + expect(response.length).to.equal(1); + expect(response[0].requestId).to.equal( + rawServerResponse.body.seatbid[0].bid[0].impid + ); + expect(response[0].cpm).to.equal( + rawServerResponse.body.seatbid[0].bid[0].price + ); + expect(response[0].width).to.equal( + rawServerResponse.body.seatbid[0].bid[0].w + ); + expect(response[0].height).to.equal( + rawServerResponse.body.seatbid[0].bid[0].h + ); + expect(response[0].creativeId).to.equal( + rawServerResponse.body.seatbid[0].bid[0].crid + ); + expect(response[0].dealId).to.equal(null); + expect(response[0].currency).to.equal(rawServerResponse.body.cur); + expect(response[0].netRevenue).to.equal(true); + expect(response[0].ttl).to.equal(DEFAULT_BID_TTL); + expect(response[0].ad).to.equal( + rawServerResponse.body.seatbid[0].bid[0].adm + ); + expect(response[0].mediaType).to.equal(BANNER); + expect(response[0].burl).to.equal( + rawServerResponse.body.seatbid[0].bid[0].nurl + ); + expect(response[0].meta).to.deep.equal({ + advertiserDomains: rawServerResponse.body.seatbid[0].bid[0].adomain, + mediaType: BANNER, + }); + }); + + it('shoud use adid if no crid', function () { + const raw = { + body: { + seatbid: [ + { + bid: [ + { + adid: 'string-id', + }, + ], + }, + ], + }, + }; + const response = spec.interpretResponse(raw, ''); + + expect(response[0].creativeId).to.equal( + raw.body.seatbid[0].bid[0].adid + ); + }); + + it('shoud use id if no crid or adid', function () { + const raw = { + body: { + seatbid: [ + { + bid: [ + { + id: '60839f99-d5f2-3ab3-b6ac-736b4fe9d0ae_0_0', + }, + ], + }, + ], + }, + }; + const response = spec.interpretResponse(raw, ''); + + expect(response[0].creativeId).to.equal(raw.body.seatbid[0].bid[0].id); + }); + + it('shoud use 0 if no cpm', function () { + const raw = { + body: { + seatbid: [ + { + bid: [{}], + }, + ], + }, + }; + const response = spec.interpretResponse(raw, ''); + + expect(response[0].cpm).to.equal(0); + }); + + it('shoud use dealid if exists', function () { + const raw = { + body: { + seatbid: [ + { + bid: [{ dealid: '123' }], + }, + ], + }, + }; + const response = spec.interpretResponse(raw, ''); + + expect(response[0].dealId).to.equal(raw.body.seatbid[0].bid[0].dealid); + }); + + it('shoud use DEFAUL_CURRENCY if no cur', function () { + const raw = { + body: { + seatbid: [ + { + bid: [{}], + }, + ], + }, + }; + const response = spec.interpretResponse(raw, ''); + + expect(response[0].currency).to.equal(DEFAULT_CURRENCY); + }); + }); + }); + + describe('onBidWon', function () { + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); + + afterEach(function () { + utils.triggerPixel.restore(); + }); + + it('Should not trigger pixel if bid does not contain burl', function () { + const result = spec.onBidWon({}); + expect(result).to.be.undefined; + expect(utils.triggerPixel.callCount).to.equal(0); + }); + + it('Should trigger pixel if bid.burl exists', function () { + const result = spec.onBidWon({ + cpm: 4.2, + burl: 'https://example.com/p=${AUCTION_PRICE}&foo=bar', + }); + + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.firstCall.args[0]).to.be.equal( + 'https://example.com/p=4.2&foo=bar' + ); + }); + }); +}); diff --git a/test/spec/modules/nativoBidAdapter_spec.js b/test/spec/modules/nativoBidAdapter_spec.js index 23f48f3661a..c552090cf6e 100644 --- a/test/spec/modules/nativoBidAdapter_spec.js +++ b/test/spec/modules/nativoBidAdapter_spec.js @@ -1,14 +1,10 @@ import { expect } from 'chai' import { spec } from 'modules/nativoBidAdapter.js' -// import { newBidder } from 'src/adapters/bidderFactory.js' -// import * as bidderFactory from 'src/adapters/bidderFactory.js' -// import { deepClone } from 'src/utils.js' -// import { config } from 'src/config.js' describe('nativoBidAdapterTests', function () { describe('isBidRequestValid', function () { let bid = { - bidder: 'nativo' + bidder: 'nativo', } it('should return true if no params found', function () { @@ -273,9 +269,7 @@ describe('getAdUnitData', () => { } const data = spec.getAdUnitData(9876543, { impid: 12345 }) - expect(Object.keys(data)).to.have.deep.members( - Object.keys(adUnitData) - ) + expect(Object.keys(data)).to.have.deep.members(Object.keys(adUnitData)) }) it('Falls back to ad unit code value', () => { @@ -290,9 +284,158 @@ describe('getAdUnitData', () => { }, } - const data = spec.getAdUnitData(9876543, { impid: 12345, ext: { ad_unit_code: '#test-code' } }) - expect(Object.keys(data)).to.have.deep.members( - Object.keys(adUnitData) - ) + const data = spec.getAdUnitData(9876543, { + impid: 12345, + ext: { ad_unit_code: '#test-code' }, + }) + expect(Object.keys(data)).to.have.deep.members(Object.keys(adUnitData)) + }) +}) + +describe('Response to Request Filter Flow', () => { + let bidRequests = [ + { + bidder: 'nativo', + params: { + placementId: '10433394', + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [300, 600], + ], + bidId: '27b02036ccfa6e', + bidderRequestId: '1372cd8bd8d6a8', + auctionId: 'cfc467e4-2707-48da-becb-bcaab0b2c114', + transactionId: '3b36e7e0-0c3e-4006-a279-a741239154ff', + }, + ] + + let response + + beforeEach(() => { + response = { + id: '126456', + seatbid: [ + { + seat: 'seat_0', + bid: [ + { + id: 'f70362ac-f3cf-4225-82a5-948b690927a6', + impid: '1', + price: 3.569, + adm: '', + h: 300, + w: 250, + cat: [], + adomain: ['test.com'], + crid: '1060_72_6760217', + }, + ], + }, + ], + cur: 'USD', + } + }) + + let bidderRequest = { + id: 123456, + bids: [ + { + params: { + placementId: 1, + }, + }, + ], + } + + // mock + spec.getAdUnitData = () => { + return { + bidId: 123456, + size: [300, 250], + } + } + + it('Appends NO filter based on previous response', () => { + // Getting the mock response + let result = spec.interpretResponse({ body: response }, { bidderRequest }) + + // Winning the bid + spec.onBidWon(result[0]) + + // Making another request + const request = spec.buildRequests(bidRequests, { + bidderRequestId: 123456, + refererInfo: { + referer: 'https://www.test.com', + }, + }) + expect(request.url).to.not.include('ntv_aft') + expect(request.url).to.not.include('ntv_avtf') + expect(request.url).to.not.include('ntv_ctf') + }) + + it('Appends Ads filter based on previous response', () => { + response.seatbid[0].bid[0].ext = { adsToFilter: ['12345'] } + + // Getting the mock response + let result = spec.interpretResponse({ body: response }, { bidderRequest }) + + // Winning the bid + spec.onBidWon(result[0]) + + // Making another request + const request = spec.buildRequests(bidRequests, { + bidderRequestId: 123456, + refererInfo: { + referer: 'https://www.test.com', + }, + }) + expect(request.url).to.include(`ntv_atf=12345`) + expect(request.url).to.not.include('ntv_avtf') + expect(request.url).to.not.include('ntv_ctf') + }) + + it('Appends Advertiser filter based on previous response', () => { + response.seatbid[0].bid[0].ext = { advertisersToFilter: ['1'] } + + // Getting the mock response + let result = spec.interpretResponse({ body: response }, { bidderRequest }) + + // Winning the bid + spec.onBidWon(result[0]) + + // Making another request + const request = spec.buildRequests(bidRequests, { + bidderRequestId: 123456, + refererInfo: { + referer: 'https://www.test.com', + }, + }) + expect(request.url).to.include(`ntv_atf=12345`) + expect(request.url).to.include('ntv_avtf=1') + expect(request.url).to.not.include('ntv_ctf') + }) + + it('Appends Campaign filter based on previous response', () => { + response.seatbid[0].bid[0].ext = { campaignsToFilter: ['234'] } + + // Getting the mock response + let result = spec.interpretResponse({ body: response }, { bidderRequest }) + + // Winning the bid + spec.onBidWon(result[0]) + + // Making another request + const request = spec.buildRequests(bidRequests, { + bidderRequestId: 123456, + refererInfo: { + referer: 'https://www.test.com', + }, + }) + expect(request.url).to.include(`ntv_atf=12345`) + expect(request.url).to.include('ntv_avtf=1') + expect(request.url).to.include('ntv_ctf=234') }) }) diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index 0dfee96d0ea..a8aa62f24d1 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -18,6 +18,47 @@ describe('nextMillenniumBidAdapterTests', function() { } ]; + const bidRequestDataGI = [ + { + adUnitCode: 'test-banner-gi', + bidId: 'bid1234', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { group_id: '1234' }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + + sizes: [[300, 250]], + uspConsent: '1---', + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + } + }, + + { + adUnitCode: 'test-video-gi', + bidId: 'bid1234', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { group_id: '1234' }, + mediaTypes: { + video: { + playerSize: [640, 480], + } + }, + + uspConsent: '1---', + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + } + }, + ]; + it('Request params check with GDPR and USP Consent', function () { const request = spec.buildRequests(bidRequestData, bidRequestData[0]); expect(JSON.parse(request[0].data).user.ext.consent).to.equal('kjfdniwjnifwenrif3'); @@ -38,6 +79,16 @@ describe('nextMillenniumBidAdapterTests', function() { expect(JSON.parse(request[0].data).id).to.equal('b06c5141-fe8f-4cdf-9d7d-54415490a917'); }); + it('use parameters group_id', function() { + for (let test of bidRequestDataGI) { + const request = spec.buildRequests([test]); + const requestData = JSON.parse(request[0].data); + const storeRequestId = requestData.ext.prebid.storedrequest.id; + const templateRE = /^g\d+;\d+x\d+;/; + expect(templateRE.test(storeRequestId)).to.be.true; + }; + }); + it('Check if refresh_count param is incremented', function() { const request = spec.buildRequests(bidRequestData); expect(JSON.parse(request[0].data).ext.nextMillennium.refresh_count).to.equal(3); diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js index 7501391cbfc..3d5b2554cda 100644 --- a/test/spec/modules/nexx360BidAdapter_spec.js +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -6,35 +6,62 @@ import * as utils from 'src/utils.js'; import { requestBidsHook } from 'modules/consentManagement.js'; describe('Nexx360 bid adapter tests', function () { - var DEFAULT_PARAMS = [{ - adUnitCode: 'banner-div', - bidId: 'aaaa1234', - auctionId: 'bbbb1234', - transactionId: 'cccc1234', - mediaTypes: { - banner: { - sizes: [ - [300, 250] - ] + const DISPLAY_BID_REQUEST = [{ + 'bidder': 'nexx360', + 'params': { + 'account': '1067', + 'tagId': 'luvxjvgn' + }, + 'userId': { + 'id5id': { + 'uid': 'ID5*hQ5WobYI9Od4u52qpaXVKHhxUa4DsOWRAlvaFajm8gINfI1oVAe3UK59416dT4TqDX1pj4MBJ5TYwir6x3JgBw1-avYHSnmvQDdRMbxmC2sNf3ggIRTbyQBdI1RjvHyeDYCsistnTXF_iKF1nutYeQ2BZ4P5d5muZTG7C2PXVFgNg-18io9dCiSjzJXx93KPDYRiuIwtsGGsp51rojlpFw2Fp_dUkjXl4CAblk58DvwNhobwQ27bnBP8F2-Pcs88DYcvKn4r6dm3Vi7ILttxDQ2IgZ2X44ClgjoWh-vRf6ANis8Z7uL16vO8q0P5C21eDYuc4v_KaZqN-p9YWEeEZQ2OpkbRL7n5NieVJExHM6ANkAlLZhVf2T-1906TAIHKDZFm_xMCa1jJfpBqZB2agw2TjfbK6wMtJeHiZaipSuUNlM_CSH0HVXtfMj9yfzjzDZZnltZQ9lvc4JhXye5AwA2X1f9Dhk8VURTvVdfEUlU', + 'ext': { + 'linkType': 2 + } } }, - bidder: 'nexx360', - params: { - account: '1067', - tagId: 'luvxjvgn' + 'userIdAsEids': [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5*hQ5WobYI9Od4u52qpaXVKHhxUa4DsOWRAlvaFajm8gINfI1oVAe3UK59416dT4TqDX1pj4MBJ5TYwir6x3JgBw1-avYHSnmvQDdRMbxmC2sNf3ggIRTbyQBdI1RjvHyeDYCsistnTXF_iKF1nutYeQ2BZ4P5d5muZTG7C2PXVFgNg-18io9dCiSjzJXx93KPDYRiuIwtsGGsp51rojlpFw2Fp_dUkjXl4CAblk58DvwNhobwQ27bnBP8F2-Pcs88DYcvKn4r6dm3Vi7ILttxDQ2IgZ2X44ClgjoWh-vRf6ANis8Z7uL16vO8q0P5C21eDYuc4v_KaZqN-p9YWEeEZQ2OpkbRL7n5NieVJExHM6ANkAlLZhVf2T-1906TAIHKDZFm_xMCa1jJfpBqZB2agw2TjfbK6wMtJeHiZaipSuUNlM_CSH0HVXtfMj9yfzjzDZZnltZQ9lvc4JhXye5AwA2X1f9Dhk8VURTvVdfEUlU', + 'atype': 1, + 'ext': { + 'linkType': 2 + } + } + ] + } + ], + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } }, + 'adUnitCode': 'banner-div', + 'transactionId': '9ad89d90-eb73-41b9-bf5f-7a8e2eecff27', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '4d9e29504f8af6', + 'bidderRequestId': '3423b6bd1a922c', + 'auctionId': '05e0a3a1-9f57-41f6-bbcb-2ba9c9e3d2d5', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 }]; - var BID_RESPONSE = {'body': { + const DISPLAY_BID_RESPONSE = {'body': { 'responses': [ { - 'bidId': '49a705a42610a', + 'bidId': '4d9e29504f8af6', 'cpm': 0.437245, 'width': 300, 'height': 250, 'creativeId': '98493581', 'currency': 'EUR', 'netRevenue': true, + 'type': 'banner', 'ttl': 360, 'uuid': 'ce6d1ee3-2a05-4d7c-b97a-9e62097798ec', 'bidder': 'appnexus', @@ -44,6 +71,56 @@ describe('Nexx360 bid adapter tests', function () { ], }}; + const VIDEO_BID_REQUEST = [ + { + 'bidder': 'nexx360', + 'params': { + 'account': '1067', + 'tagId': 'yqsc1tfj' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6], + 'playbackmethod': [2], + 'skip': 1 + } + }, + 'adUnitCode': 'video1', + 'transactionId': '5434c81c-7210-44ae-9014-67c75dee48d0', + 'sizes': [[640, 480]], + 'bidId': '22f90541e576a3', + 'bidderRequestId': '1d4549243f3bfd', + 'auctionId': 'ed21b528-bcab-47e2-8605-ec9b71000c89', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ] + + const VIDEO_BID_RESPONSE = {'body': { + 'responses': [ + { + 'bidId': '2c129e8e01859a', + 'type': 'video', + 'uuid': 'b8e7b2f0-c378-479f-aa4f-4f55d5d7d1d5', + 'cpm': 4.5421, + 'width': 1, + 'height': 1, + 'creativeId': '97517771', + 'currency': 'EUR', + 'netRevenue': true, + 'ttl': 360, + 'bidder': 'appnexus', + 'consent': 1, + 'tagId': 'yqsc1tfj' + } + ] + }}; + const DEFAULT_OPTIONS = { gdprConsent: { gdprApplies: true, @@ -69,22 +146,27 @@ describe('Nexx360 bid adapter tests', function () { }] }, }; - it('Verify build request', function () { - const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); + it('Verify banner build request', function () { + const request = spec.buildRequests(DISPLAY_BID_REQUEST, DEFAULT_OPTIONS); expect(request).to.have.property('url').and.to.equal('https://fast.nexx360.io/prebid'); expect(request).to.have.property('method').and.to.equal('POST'); const requestContent = JSON.parse(request.data); + expect(requestContent.userEids.length).to.be.eql(1); + expect(requestContent.userEids[0]).to.have.property('source').and.to.equal('id5-sync.com'); + expect(requestContent.userEids[0]).to.have.property('uids'); + expect(requestContent.userEids[0].uids[0]).to.have.property('id').and.to.equal('ID5*hQ5WobYI9Od4u52qpaXVKHhxUa4DsOWRAlvaFajm8gINfI1oVAe3UK59416dT4TqDX1pj4MBJ5TYwir6x3JgBw1-avYHSnmvQDdRMbxmC2sNf3ggIRTbyQBdI1RjvHyeDYCsistnTXF_iKF1nutYeQ2BZ4P5d5muZTG7C2PXVFgNg-18io9dCiSjzJXx93KPDYRiuIwtsGGsp51rojlpFw2Fp_dUkjXl4CAblk58DvwNhobwQ27bnBP8F2-Pcs88DYcvKn4r6dm3Vi7ILttxDQ2IgZ2X44ClgjoWh-vRf6ANis8Z7uL16vO8q0P5C21eDYuc4v_KaZqN-p9YWEeEZQ2OpkbRL7n5NieVJExHM6ANkAlLZhVf2T-1906TAIHKDZFm_xMCa1jJfpBqZB2agw2TjfbK6wMtJeHiZaipSuUNlM_CSH0HVXtfMj9yfzjzDZZnltZQ9lvc4JhXye5AwA2X1f9Dhk8VURTvVdfEUlU'); expect(requestContent.adUnits[0]).to.have.property('account').and.to.equal('1067'); expect(requestContent.adUnits[0]).to.have.property('tagId').and.to.equal('luvxjvgn'); expect(requestContent.adUnits[0]).to.have.property('label').and.to.equal('banner-div'); - expect(requestContent.adUnits[0]).to.have.property('bidId').and.to.equal('aaaa1234'); - expect(requestContent.adUnits[0]).to.have.property('auctionId').and.to.equal('bbbb1234'); + expect(requestContent.adUnits[0]).to.have.property('bidId').and.to.equal('4d9e29504f8af6'); + expect(requestContent.adUnits[0]).to.have.property('auctionId').and.to.equal('05e0a3a1-9f57-41f6-bbcb-2ba9c9e3d2d5'); expect(requestContent.adUnits[0]).to.have.property('mediatypes').exist; + expect(requestContent.adUnits[0].mediatypes).to.have.property('banner').exist; }); - it('Verify parse response', function () { - const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); - const response = spec.interpretResponse(BID_RESPONSE, request); + it('Verify banner parse response', function () { + const request = spec.buildRequests(DISPLAY_BID_REQUEST, DEFAULT_OPTIONS); + const response = spec.interpretResponse(DISPLAY_BID_RESPONSE, request); expect(response).to.have.lengthOf(1); const bid = response[0]; expect(bid.cpm).to.equal(0.437245); @@ -95,10 +177,44 @@ describe('Nexx360 bid adapter tests', function () { expect(bid.currency).to.equal('EUR'); expect(bid.netRevenue).to.equal(true); expect(bid.ttl).to.equal(360); - expect(bid.requestId).to.equal('49a705a42610a'); + expect(bid.requestId).to.equal('4d9e29504f8af6'); expect(bid.nexx360).to.exist; expect(bid.nexx360.ssp).to.equal('appnexus'); }); + + it('Verify video build request', function () { + const request = spec.buildRequests(VIDEO_BID_REQUEST, DEFAULT_OPTIONS); + expect(request).to.have.property('url').and.to.equal('https://fast.nexx360.io/prebid'); + expect(request).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request.data); + expect(requestContent.adUnits[0]).to.have.property('account').and.to.equal('1067'); + expect(requestContent.adUnits[0]).to.have.property('tagId').and.to.equal('yqsc1tfj'); + expect(requestContent.adUnits[0]).to.have.property('label').and.to.equal('video1'); + expect(requestContent.adUnits[0]).to.have.property('bidId').and.to.equal('22f90541e576a3'); + expect(requestContent.adUnits[0]).to.have.property('auctionId').and.to.equal('ed21b528-bcab-47e2-8605-ec9b71000c89'); + expect(requestContent.adUnits[0]).to.have.property('mediatypes').exist; + expect(requestContent.adUnits[0].mediatypes).to.have.property('video').exist; + }); + + it('Verify video parse response', function () { + const request = spec.buildRequests(VIDEO_BID_REQUEST, DEFAULT_OPTIONS); + const response = spec.interpretResponse(VIDEO_BID_RESPONSE, request); + expect(response).to.have.lengthOf(1); + const bid = response[0]; + expect(bid.cpm).to.equal(4.5421); + expect(bid.vastUrl).to.equal('https://fast.nexx360.io/cache?uuid=b8e7b2f0-c378-479f-aa4f-4f55d5d7d1d5'); + expect(bid.vastImpUrl).to.equal('https://fast.nexx360.io/track-imp?type=prebid&mediatype=video&ssp=appnexus&tag_id=yqsc1tfj&consent=1&price=4.5421'); + expect(bid.width).to.equal(1); + expect(bid.height).to.equal(1); + expect(bid.creativeId).to.equal('97517771'); + expect(bid.currency).to.equal('EUR'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(360); + expect(bid.requestId).to.equal('2c129e8e01859a'); + expect(bid.nexx360).to.exist; + expect(bid.nexx360.ssp).to.equal('appnexus'); + }); + it('Verifies bidder code', function () { expect(spec.code).to.equal('nexx360'); }); @@ -108,21 +224,21 @@ describe('Nexx360 bid adapter tests', function () { expect(spec.aliases[0]).to.equal('revenuemaker'); }); it('Verifies if bid request valid', function () { - expect(spec.isBidRequestValid(DEFAULT_PARAMS[0])).to.equal(true); + expect(spec.isBidRequestValid(DISPLAY_BID_REQUEST[0])).to.equal(true); }); it('Verifies bid won', function () { - const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); - const response = spec.interpretResponse(BID_RESPONSE, request); + const request = spec.buildRequests(DISPLAY_BID_REQUEST, DEFAULT_OPTIONS); + const response = spec.interpretResponse(DISPLAY_BID_RESPONSE, request); const won = spec.onBidWon(response[0]); expect(won).to.equal(true); }); it('Verifies user sync without cookie in bid response', function () { - var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + var syncs = spec.getUserSyncs({}, [DISPLAY_BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); expect(syncs).to.have.lengthOf(0); }); it('Verifies user sync with cookies in bid response', function () { - BID_RESPONSE.body.cookies = [{'type': 'image', 'url': 'http://www.cookie.sync.org/'}]; - var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent); + DISPLAY_BID_RESPONSE.body.cookies = [{'type': 'image', 'url': 'http://www.cookie.sync.org/'}]; + var syncs = spec.getUserSyncs({}, [DISPLAY_BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent); expect(syncs).to.have.lengthOf(1); expect(syncs[0]).to.have.property('type').and.to.equal('image'); expect(syncs[0]).to.have.property('url').and.to.equal('http://www.cookie.sync.org/'); diff --git a/test/spec/modules/outbrainBidAdapter_spec.js b/test/spec/modules/outbrainBidAdapter_spec.js index 4bc163aefe6..5dbdd049d82 100644 --- a/test/spec/modules/outbrainBidAdapter_spec.js +++ b/test/spec/modules/outbrainBidAdapter_spec.js @@ -100,6 +100,38 @@ describe('Outbrain Adapter', function () { } expect(spec.isBidRequestValid(bid)).to.equal(false) }) + it('should fail if tag id is not string', function () { + const bid = { + bidder: 'outbrain', + params: { + tagid: 123 + }, + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should fail if badv does not include strings', function () { + const bid = { + bidder: 'outbrain', + params: { + tagid: 123, + badv: ['a', 2, 'c'] + }, + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should fail if bcat does not include strings', function () { + const bid = { + bidder: 'outbrain', + params: { + tagid: 123, + bcat: ['a', 2, 'c'] + }, + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) it('should succeed with outbrain config', function () { const bid = { bidder: 'outbrain', @@ -362,6 +394,63 @@ describe('Outbrain Adapter', function () { {source: 'liveramp.com', uids: [{id: 'id-value', atype: 3}]} ]); }); + + it('should pass bidfloor', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + bidRequest.getFloor = function() { + return { + currency: 'USD', + floor: 1.23, + } + } + + const res = spec.buildRequests([bidRequest], commonBidderRequest) + const resData = JSON.parse(res.data) + expect(resData.imp[0].bidfloor).to.equal(1.23) + }); + + it('should transform string sizes to numbers', function () { + let bidRequest = { + bidId: 'bidId', + params: {}, + ...commonBidRequest, + ...nativeBidRequestParams, + }; + bidRequest.nativeParams.image.sizes = ['120', '100'] + + const expectedNativeAssets = { + assets: [ + { + required: 1, + id: 3, + img: { + type: 3, + w: 120, + h: 100 + } + }, + { + required: 1, + id: 0, + title: {} + }, + { + required: 0, + id: 5, + data: { + type: 1 + } + } + ] + } + + let res = spec.buildRequests([bidRequest], commonBidderRequest); + const resData = JSON.parse(res.data) + expect(resData.imp[0].native.request).to.equal(JSON.stringify(expectedNativeAssets)); + }); }) describe('interpretResponse', function () { @@ -382,7 +471,7 @@ describe('Outbrain Adapter', function () { impid: '1', price: 1.1, nurl: 'http://example.com/win/${AUCTION_PRICE}', - adm: '{"ver":"1.2","assets":[{"id":3,"required":1,"img":{"url":"http://example.com/img/url","w":120,"h":100}},{"id":0,"required":1,"title":{"text":"Test title"}},{"id":5,"data":{"value":"Test sponsor"}}],"link":{"url":"http://example.com/click/url"},"eventtrackers":[{"event":1,"method":1,"url":"http://example.com/impression"}]}', + adm: '{"ver":"1.2","assets":[{"id":3,"required":1,"img":{"url":"http://example.com/img/url","w":120,"h":100}},{"id":0,"required":1,"title":{"text":"Test title"}},{"id":5,"data":{"value":"Test sponsor"}}],"privacy":"http://example.com/privacy","link":{"url":"http://example.com/click/url"},"eventtrackers":[{"event":1,"method":1,"url":"http://example.com/impression"}]}', adomain: [ 'example.com' ], @@ -435,7 +524,8 @@ describe('Outbrain Adapter', function () { sponsoredBy: 'Test sponsor', impressionTrackers: [ 'http://example.com/impression', - ] + ], + privacyLink: 'http://example.com/privacy' } } ] diff --git a/test/spec/modules/permutiveRtdProvider_spec.js b/test/spec/modules/permutiveRtdProvider_spec.js index 7cf6b66f839..6b44ec2b065 100644 --- a/test/spec/modules/permutiveRtdProvider_spec.js +++ b/test/spec/modules/permutiveRtdProvider_spec.js @@ -51,6 +51,46 @@ describe('permutiveRtdProvider', function () { }]) }) }) + it('should include ortb2 user data transformation for IAB audience taxonomy', function() { + const moduleConfig = getConfig() + const bidderConfig = config.getBidderConfig() + const acBidders = moduleConfig.params.acBidders + const expectedTargetingData = transformedTargeting().ac.map(seg => { + return { id: seg } + }) + + Object.assign( + moduleConfig.params, + { + transformations: [{ + id: 'iab', + config: { + segtax: 4, + iabIds: { + 1000001: '9000009', + 1000002: '9000008' + } + } + }] + } + ) + + setBidderRtb({}, moduleConfig) + + acBidders.forEach(bidder => { + expect(bidderConfig[bidder].ortb2.user.data).to.deep.include.members([ + { + name: 'permutive.com', + segment: expectedTargetingData + }, + { + name: 'permutive.com', + ext: { segtax: 4 }, + segment: [{ id: '9000009' }, { id: '9000008' }] + } + ]) + }) + }) it('should not overwrite ortb2 config', function () { const moduleConfig = getConfig() const bidderConfig = config.getBidderConfig() @@ -78,7 +118,15 @@ describe('permutiveRtdProvider', function () { config: sampleOrtbConfig }) - setBidderRtb({}, moduleConfig) + const transformedUserData = { + name: 'transformation', + ext: { test: true }, + segment: [1, 2, 3] + } + + setBidderRtb({}, moduleConfig, { + testTransformation: userData => transformedUserData + }) acBidders.forEach(bidder => { expect(bidderConfig[bidder].ortb2.site.name).to.equal(sampleOrtbConfig.ortb2.site.name) @@ -293,6 +341,10 @@ describe('permutiveRtdProvider', function () { expect(isAcEnabled({ params: { acBidders: ['ozone'] } }, 'ozone')).to.equal(true) expect(isAcEnabled({ params: { acBidders: ['kjdvb'] } }, 'ozone')).to.equal(false) }) + it('checks if AC is enabled for Index', function () { + expect(isAcEnabled({ params: { acBidders: ['ix'] } }, 'ix')).to.equal(true) + expect(isAcEnabled({ params: { acBidders: ['kjdvb'] } }, 'ix')).to.equal(false) + }) }) }) @@ -313,7 +365,7 @@ function getConfig () { name: 'permutive', waitForIt: true, params: { - acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx'], + acBidders: ['appnexus', 'rubicon', 'ozone', 'trustx', 'ix'], maxSegs: 500 } } @@ -326,7 +378,7 @@ function transformedTargeting () { ac: [...data._pcrprs, ...data._ppam, ...data._psegs.filter(seg => seg >= 1000000)], appnexus: data._papns, rubicon: data._prubicons, - gam: data._pdfps + gam: data._pdfps, } } diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 7b297aa4c5a..a0c7903091c 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -8,15 +8,16 @@ import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import { server } from 'test/mocks/xhr.js'; import { createEidsArray } from 'modules/userId/eids.js'; -import {deepAccess, deepClone} from 'src/utils.js'; +import { deepAccess, deepClone } from 'src/utils.js'; import 'modules/appnexusBidAdapter.js' // appnexus alias test import 'modules/rubiconBidAdapter.js' // rubicon alias test import 'src/prebid.js' // $$PREBID_GLOBAL$$.aliasBidder test import 'modules/currency.js' // adServerCurrency test -import {hook} from '../../../src/hook.js'; -import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; -import {auctionManager} from '../../../src/auctionManager.js'; -import {stubAuctionIndex} from '../../helpers/indexStub.js'; +import { hook } from '../../../src/hook.js'; +import { decorateAdUnitsWithNativeParams } from '../../../src/native.js'; +import { auctionManager } from '../../../src/auctionManager.js'; +import { stubAuctionIndex } from '../../helpers/indexStub.js'; +import { registerBidder } from 'src/adapters/bidderFactory.js'; let CONFIG = { accountId: '1', @@ -283,7 +284,7 @@ const RESPONSE_OPENRTB = { 'win': 'http://wurl.org?id=333' }, 'meta': { - 'dchain': { 'ver': '1.0', 'complete': 0, 'nodes': [ { 'asi': 'magnite.com', 'bsid': '123456789', } ] } + 'dchain': { 'ver': '1.0', 'complete': 0, 'nodes': [{ 'asi': 'magnite.com', 'bsid': '123456789', }] } } }, 'bidder': { @@ -457,7 +458,9 @@ describe('S2S Adapter', function () { done = sinon.spy(); function prepRequest(req) { - req.ad_units.forEach((adUnit) => { delete adUnit.nativeParams }); + req.ad_units.forEach((adUnit) => { + delete adUnit.nativeParams + }); decorateAdUnitsWithNativeParams(req.ad_units); } @@ -537,7 +540,7 @@ describe('S2S Adapter', function () { expect(requestBid.source.tid).to.equal('437fbbf5-33f5-487a-8e16-a7112903cfe5'); }); - it('should block request if config did not define p1Consent URL in endpoint object config', function() { + it('should block request if config did not define p1Consent URL in endpoint object config', function () { let badConfig = utils.deepClone(CONFIG); badConfig.endpoint = { noP1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' }; config.setConfig({ s2sConfig: badConfig }); @@ -550,7 +553,7 @@ describe('S2S Adapter', function () { expect(server.requests.length).to.equal(0); }); - it('should block request if config did not define noP1Consent URL in endpoint object config', function() { + it('should block request if config did not define noP1Consent URL in endpoint object config', function () { let badConfig = utils.deepClone(CONFIG); badConfig.endpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' }; config.setConfig({ s2sConfig: badConfig }); @@ -578,7 +581,7 @@ describe('S2S Adapter', function () { expect(server.requests.length).to.equal(0); }); - it('should block request if config did not define any URLs in endpoint object config', function() { + it('should block request if config did not define any URLs in endpoint object config', function () { let badConfig = utils.deepClone(CONFIG); badConfig.endpoint = {}; config.setConfig({ s2sConfig: badConfig }); @@ -980,11 +983,11 @@ describe('S2S Adapter', function () { ).to.be.true; // if getFloor does not return number - getFloorResponse = {currency: 'EUR', floor: 'not a number'}; + getFloorResponse = { currency: 'EUR', floor: 'not a number' }; runTest(undefined, undefined); // if getFloor does not return currency - getFloorResponse = {floor: 1.1}; + getFloorResponse = { floor: 1.1 }; runTest(undefined, undefined); }); @@ -999,7 +1002,7 @@ describe('S2S Adapter', function () { sinon.spy(BID_REQUESTS[0].bids[0], 'getFloor'); // returns USD and string floor - getFloorResponse = {currency: 'USD', floor: '1.23'}; + getFloorResponse = { currency: 'USD', floor: '1.23' }; runTest(1.23, 'USD'); // make sure getFloor was called expect( @@ -1009,7 +1012,7 @@ describe('S2S Adapter', function () { ).to.be.true; // returns non USD and number floor - getFloorResponse = {currency: 'EUR', floor: 0.85}; + getFloorResponse = { currency: 'EUR', floor: 0.85 }; runTest(0.85, 'EUR'); }); @@ -1026,7 +1029,7 @@ describe('S2S Adapter', function () { sinon.spy(BID_REQUESTS[0].bids[0], 'getFloor'); // returns USD and string floor - getFloorResponse = {currency: 'JPY', floor: 97.2}; + getFloorResponse = { currency: 'JPY', floor: 97.2 }; runTest(97.2, 'JPY'); // make sure getFloor was called with JPY expect( @@ -1113,7 +1116,7 @@ describe('S2S Adapter', function () { it('should not include ext.aspectratios if adunit\'s aspect_ratios do not define radio_width and ratio_height', () => { const req = deepClone(REQUEST); - req.ad_units[0].mediaTypes.native.icon.aspect_ratios[0] = {'min_width': 1, 'min_height': 2}; + req.ad_units[0].mediaTypes.native.icon.aspect_ratios[0] = { 'min_width': 1, 'min_height': 2 }; prepRequest(req); adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); const nativeReq = JSON.parse(JSON.parse(server.requests[0].requestBody).imp[0].native.request); @@ -1217,16 +1220,22 @@ describe('S2S Adapter', function () { }); }); - it('skips pbs alias when skipPbsAliasing is enabled in adapter', function() { + it('skips pbs alias when skipPbsAliasing is enabled in adapter', function () { const s2sConfig = Object.assign({}, CONFIG, { endpoint: { p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' } }); config.setConfig({ s2sConfig: s2sConfig }); - + registerBidder({ + code: 'bidderCodeForTestSkipBPSAlias', + aliases: [{ + code: 'bidderCodeForTestSkipBPSAlias_Alias', + skipPbsAliasing: true + }] + }) const aliasBidder = { - bidder: 'mediafuse', + bidder: 'bidderCodeForTestSkipBPSAlias_Alias', params: { aid: 123 } }; @@ -1263,7 +1272,7 @@ describe('S2S Adapter', function () { const alias = 'foobar_1'; const aliasBidder = { bidder: alias, - params: { aid: 123456 } + params: { aid: 1234567 } }; const request = utils.deepClone(REQUEST); @@ -1731,7 +1740,7 @@ describe('S2S Adapter', function () { maxbids: 2 }]; - config.setConfig({multibid: multibid}); + config.setConfig({ multibid: multibid }); adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); const parsedRequestBody = JSON.parse(server.requests[0].requestBody); @@ -1745,14 +1754,14 @@ describe('S2S Adapter', function () { adapter.callBids(s2sBidRequest, bidRequests, addBidResponse, done, ajax); const parsedRequestBody = JSON.parse(server.requests[0].requestBody); - expect(parsedRequestBody.ext.prebid.channel).to.deep.equal({name: 'pbjs', version: 'v$prebid.version$'}); + expect(parsedRequestBody.ext.prebid.channel).to.deep.equal({ name: 'pbjs', version: 'v$prebid.version$' }); }); it('extPrebid is now mergedDeep -> should include default channel as well', () => { const s2sBidRequest = utils.deepClone(REQUEST); const bidRequests = utils.deepClone(BID_REQUESTS); - utils.deepSetValue(s2sBidRequest, 's2sConfig.extPrebid.channel', {test: 1}); + utils.deepSetValue(s2sBidRequest, 's2sConfig.extPrebid.channel', { test: 1 }); adapter.callBids(s2sBidRequest, bidRequests, addBidResponse, done, ajax); @@ -1796,10 +1805,10 @@ describe('S2S Adapter', function () { }; const bcat = ['IAB25', 'IAB7-39']; const badv = ['blockedAdv-1.com', 'blockedAdv-2.com']; - const allowedBidders = [ 'rubicon', 'appnexus' ]; + const allowedBidders = ['rubicon', 'appnexus']; const expected = allowedBidders.map(bidder => ({ - bidders: [ bidder ], + bidders: [bidder], config: { ortb2: { site: { @@ -1826,7 +1835,10 @@ describe('S2S Adapter', function () { } } })); - const commonContextExpected = utils.mergeDeep({'page': 'http://mytestpage.com', 'publisher': {'id': '1'}}, commonContext); + const commonContextExpected = utils.mergeDeep({ + 'page': 'http://mytestpage.com', + 'publisher': { 'id': '1' } + }, commonContext); config.setConfig({ fpd: { context: commonContext, user: commonUser, badv, bcat } }); config.setBidderConfig({ bidders: allowedBidders, config: { fpd: { context, user, bcat, badv } } }); @@ -2081,7 +2093,7 @@ describe('S2S Adapter', function () { }); it('should pass through default adserverTargeting if present in bidObject for video request', function () { - config.setConfig({s2sConfig: CONFIG}); + config.setConfig({ s2sConfig: CONFIG }); const cacheResponse = utils.deepClone(RESPONSE_OPENRTB); const targetingTestData = { hb_cache_path: '/cache', @@ -2103,7 +2115,7 @@ describe('S2S Adapter', function () { }); }); - it('should set the bidResponse currency to whats in the PBS response', function() { + it('should set the bidResponse currency to whats in the PBS response', function () { adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB)); sinon.assert.calledOnce(addBidResponse); @@ -2111,7 +2123,7 @@ describe('S2S Adapter', function () { expect(pbjsResponse).to.have.property('currency', 'EUR'); }); - it('should set the default bidResponse currency when not specified in OpenRTB', function() { + it('should set the default bidResponse currency when not specified in OpenRTB', function () { let modifiedResponse = utils.deepClone(RESPONSE_OPENRTB); modifiedResponse.cur = ''; adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); @@ -2424,7 +2436,7 @@ describe('S2S Adapter', function () { it('handles OpenRTB native responses', function () { const stub = sinon.stub(auctionManager, 'index'); - stub.get(() => stubAuctionIndex({adUnits: REQUEST.ad_units})); + stub.get(() => stubAuctionIndex({ adUnits: REQUEST.ad_units })); const s2sConfig = Object.assign({}, CONFIG, { endpoint: { p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' @@ -2461,14 +2473,14 @@ describe('S2S Adapter', function () { }); it('allows unrequested bids if config.allowUnknownBidderCodes', function () { - const cfg = {...CONFIG, allowUnknownBidderCodes: true}; - config.setConfig({s2sConfig: cfg}); - adapter.callBids({...REQUEST, s2sConfig: cfg}, BID_REQUESTS, addBidResponse, done, ajax); + const cfg = { ...CONFIG, allowUnknownBidderCodes: true }; + config.setConfig({ s2sConfig: cfg }); + adapter.callBids({ ...REQUEST, s2sConfig: cfg }, BID_REQUESTS, addBidResponse, done, ajax); const response = deepClone(RESPONSE_OPENRTB); response.seatbid[0].seat = 'unknown'; server.requests[0].respond(200, {}, JSON.stringify(response)); - expect(addBidResponse.calledWith(sinon.match.any, sinon.match({bidderCode: 'unknown'}))).to.be.true; + expect(addBidResponse.calledWith(sinon.match.any, sinon.match({ bidderCode: 'unknown' }))).to.be.true; }); it('uses "null" request\'s ID for all responses, when a null request is present', function () { @@ -2686,8 +2698,14 @@ describe('S2S Adapter', function () { expect(vendorConfig).to.have.property('adapter', 'prebidServer'); expect(vendorConfig.bidders).to.deep.equal(['appnexus']); expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig.endpoint).to.deep.equal({p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction'}); - expect(vendorConfig.syncEndpoint).to.deep.equal({p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync'}); + expect(vendorConfig.endpoint).to.deep.equal({ + p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', + noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction' + }); + expect(vendorConfig.syncEndpoint).to.deep.equal({ + p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', + noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' + }); expect(vendorConfig).to.have.property('timeout', 750); }); @@ -2707,7 +2725,10 @@ describe('S2S Adapter', function () { expect(vendorConfig).to.have.property('adapter', 'prebidServer'); expect(vendorConfig.bidders).to.deep.equal(['appnexus']); expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig.endpoint).to.deep.equal({p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid'}); + expect(vendorConfig.endpoint).to.deep.equal({ + p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', + noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' + }); expect(vendorConfig.syncEndpoint).to.be.undefined; expect(vendorConfig).to.have.property('timeout', 750); }); @@ -2728,8 +2749,14 @@ describe('S2S Adapter', function () { expect(vendorConfig).to.have.property('adapter', 'prebidServer'); expect(vendorConfig.bidders).to.deep.equal(['rubicon']); expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig.endpoint).to.deep.equal({p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction'}); - expect(vendorConfig.syncEndpoint).to.deep.equal({p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync'}); + expect(vendorConfig.endpoint).to.deep.equal({ + p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', + noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction' + }); + expect(vendorConfig.syncEndpoint).to.deep.equal({ + p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', + noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync' + }); expect(vendorConfig).to.have.property('timeout', 750); }); @@ -2748,8 +2775,14 @@ describe('S2S Adapter', function () { 'bidders': ['rubicon'], 'defaultVendor': 'rubicon', 'enabled': true, - 'endpoint': {p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction'}, - 'syncEndpoint': {p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync'}, + 'endpoint': { + p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', + noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction' + }, + 'syncEndpoint': { + p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', + noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync' + }, 'timeout': 750 }) }); @@ -2770,8 +2803,14 @@ describe('S2S Adapter', function () { accountId: 'abc', bidders: ['rubicon'], defaultVendor: 'rubicon', - endpoint: {p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction'}, - syncEndpoint: {p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync'}, + endpoint: { + p1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction', + noP1Consent: 'https://prebid-server.rubiconproject.com/openrtb2/auction' + }, + syncEndpoint: { + p1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync', + noP1Consent: 'https://prebid-server.rubiconproject.com/cookie_sync' + }, }) }); @@ -2807,7 +2846,8 @@ describe('S2S Adapter', function () { config.setConfig({ s2sConfig: { syncUrlModifier: { - appnexus: () => { } + appnexus: () => { + } } } }); @@ -2819,7 +2859,7 @@ describe('S2S Adapter', function () { // Add syncEndpoint so that the request goes to the User Sync endpoint // Modify the bidders property to include an alias for Rubicon adapter - s2sConfig.syncEndpoint = {p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync'}; + s2sConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; s2sConfig.bidders = ['appnexus', 'rubicon-alias']; const s2sBidRequest = utils.deepClone(REQUEST); @@ -2890,7 +2930,7 @@ describe('S2S Adapter', function () { it('should add cooperative sync flag to cookie_sync request if property is present', function () { let s2sConfig = utils.deepClone(CONFIG); s2sConfig.coopSync = false; - s2sConfig.syncEndpoint = {p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync'}; + s2sConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; const s2sBidRequest = utils.deepClone(REQUEST); s2sBidRequest.s2sConfig = s2sConfig; @@ -2905,7 +2945,7 @@ describe('S2S Adapter', function () { it('should not add cooperative sync flag to cookie_sync request if property is not present', function () { let s2sConfig = utils.deepClone(CONFIG); - s2sConfig.syncEndpoint = {p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync'}; + s2sConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; const s2sBidRequest = utils.deepClone(REQUEST); s2sBidRequest.s2sConfig = s2sConfig; @@ -2918,7 +2958,7 @@ describe('S2S Adapter', function () { expect(requestBid.coopSync).to.be.undefined; }); - it('should set imp banner if ortb2Imp.banner is present', function() { + it('should set imp banner if ortb2Imp.banner is present', function () { const consentConfig = { s2sConfig: CONFIG }; config.setConfig(consentConfig); const bidRequest = utils.deepClone(REQUEST); @@ -2937,7 +2977,7 @@ describe('S2S Adapter', function () { }); it('adds debug flag', function () { - config.setConfig({debug: true}); + config.setConfig({ debug: true }); let bidRequest = utils.deepClone(BID_REQUESTS); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index 40e2f796db2..6ea58e8c47a 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -230,6 +230,44 @@ describe('the price floors module', function () { }); describe('getFirstMatchingFloor', function () { + it('uses a 0 floor as overrite', function () { + let inputFloorData = { + currency: 'USD', + schema: { + delimiter: '|', + fields: ['adUnitCode'] + }, + values: { + 'test_div_1': 0, + 'test_div_2': 2 + }, + default: 0.5 + }; + + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0, + matchingFloor: 0, + matchingData: 'test_div_1', + matchingRule: 'test_div_1' + }); + + expect(getFirstMatchingFloor(inputFloorData, {...basicBidRequest, adUnitCode: 'test_div_2'}, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 2, + matchingFloor: 2, + matchingData: 'test_div_2', + matchingRule: 'test_div_2' + }); + + expect(getFirstMatchingFloor(inputFloorData, {...basicBidRequest, adUnitCode: 'test_div_3'}, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0.5, + matchingFloor: 0.5, + matchingData: 'test_div_3', + matchingRule: undefined + }); + }); 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({ diff --git a/test/spec/modules/pulsepointBidAdapter_spec.js b/test/spec/modules/pulsepointBidAdapter_spec.js index 92f7aa0b70d..c8ec0493d54 100644 --- a/test/spec/modules/pulsepointBidAdapter_spec.js +++ b/test/spec/modules/pulsepointBidAdapter_spec.js @@ -2,6 +2,7 @@ import {expect} from 'chai'; import {spec} from 'modules/pulsepointBidAdapter.js'; import {deepClone} from 'src/utils.js'; +import { config } from 'src/config.js'; describe('PulsePoint Adapter Tests', function () { const slotConfigs = [{ @@ -615,70 +616,21 @@ describe('PulsePoint Adapter Tests', function () { }); it('Verify common id parameters', function () { const bidRequests = deepClone(slotConfigs); - bidRequests[0].userId = { - pubcid: 'userid_pubcid', - tdid: 'userid_ttd', - digitrustid: { - data: { - id: 'userid_digitrust', - keyv: 4, - privacy: {optout: false}, - producer: 'ABC', - version: 2 + bidRequests[0].userIdAsEids = [{ + source: 'pubcid.org', + uids: [{ + id: 'userid_pubcid' + }] + }, { + source: 'adserver.org', + uids: [{ + id: 'userid_ttd', + ext: { + rtiPartner: 'TDID' } - } - }; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request).to.be.not.null; - const ortbRequest = request.data; - expect(request.data).to.be.not.null; - // user object - expect(ortbRequest.user).to.not.be.undefined; - expect(ortbRequest.user.ext).to.not.be.undefined; - expect(ortbRequest.user.ext.eids).to.not.be.undefined; - expect(ortbRequest.user.ext.eids).to.have.lengthOf(2); - expect(ortbRequest.user.ext.eids[0].source).to.equal('pubcid.org'); - expect(ortbRequest.user.ext.eids[0].uids).to.have.lengthOf(1); - expect(ortbRequest.user.ext.eids[0].uids[0].id).to.equal('userid_pubcid'); - expect(ortbRequest.user.ext.eids[1].source).to.equal('adserver.org'); - expect(ortbRequest.user.ext.eids[1].uids).to.have.lengthOf(1); - expect(ortbRequest.user.ext.eids[1].uids[0].id).to.equal('userid_ttd'); - expect(ortbRequest.user.ext.eids[1].uids[0].ext).to.not.be.null; - expect(ortbRequest.user.ext.eids[1].uids[0].ext.rtiPartner).to.equal('TDID'); - expect(ortbRequest.user.ext.digitrust).to.not.be.null; - expect(ortbRequest.user.ext.digitrust.id).to.equal('userid_digitrust'); - expect(ortbRequest.user.ext.digitrust.keyv).to.equal(4); - }); - it('Verify new external user id partners', function () { - const bidRequests = deepClone(slotConfigs); - bidRequests[0].userId = { - britepoolid: 'britepool_id123', - criteoId: 'criteo_id234', - idl_env: 'idl_id123', - id5id: { uid: 'id5id_234' }, - parrableId: { eid: 'parrable_id234' }, - lipb: { - lipbid: 'liveintent_id123' - }, - haloId: { - haloId: 'halo_user1' - }, - lotamePanoramaId: 'lotame_user2', - merkleId: 'merkle_user3', - fabrickId: 'fabrick_user4', - connectid: 'connect_user5', - uid2: { - id: 'uid2_user6' - } - }; - const userVerify = function(obj, source, id) { - expect(obj).to.deep.equal({ - source, - uids: [{ - id - }] - }); - }; + }] + } + ]; const request = spec.buildRequests(bidRequests, bidderRequest); expect(request).to.be.not.null; const ortbRequest = request.data; @@ -687,19 +639,7 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.user).to.not.be.undefined; expect(ortbRequest.user.ext).to.not.be.undefined; expect(ortbRequest.user.ext.eids).to.not.be.undefined; - expect(ortbRequest.user.ext.eids).to.have.lengthOf(12); - userVerify(ortbRequest.user.ext.eids[0], 'britepool.com', 'britepool_id123'); - userVerify(ortbRequest.user.ext.eids[1], 'criteo.com', 'criteo_id234'); - userVerify(ortbRequest.user.ext.eids[2], 'liveramp.com', 'idl_id123'); - userVerify(ortbRequest.user.ext.eids[3], 'id5-sync.com', 'id5id_234'); - userVerify(ortbRequest.user.ext.eids[4], 'parrable.com', 'parrable_id234'); - userVerify(ortbRequest.user.ext.eids[5], 'neustar.biz', 'fabrick_user4'); - userVerify(ortbRequest.user.ext.eids[6], 'audigent.com', 'halo_user1'); - userVerify(ortbRequest.user.ext.eids[7], 'merkleinc.com', 'merkle_user3'); - userVerify(ortbRequest.user.ext.eids[8], 'crwdcntrl.net', 'lotame_user2'); - userVerify(ortbRequest.user.ext.eids[9], 'verizonmedia.com', 'connect_user5'); - userVerify(ortbRequest.user.ext.eids[10], 'uidapi.com', 'uid2_user6'); - userVerify(ortbRequest.user.ext.eids[11], 'liveintent.com', 'liveintent_id123'); + expect(ortbRequest.user.ext.eids).to.deep.equal(bidRequests[0].userIdAsEids); }); it('Verify multiple adsizes', function () { const bidRequests = deepClone(slotConfigs); diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 354fbb53027..1f52e83dab9 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -13,7 +13,7 @@ import { setConfig, addBidResponseHook, } from 'modules/currency.js'; -import truncate from 'lodash.truncate'; + let Ajv = require('ajv'); let schema = require('./rubiconAnalyticsSchema.json'); let ajv = new Ajv({ @@ -578,21 +578,21 @@ const ANALYTICS_MESSAGE = { } }; -function performStandardAuction(gptEvents) { - 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); +function performStandardAuction(gptEvents, auctionId = MOCK.AUCTION_INIT.auctionId) { + events.emit(AUCTION_INIT, { ...MOCK.AUCTION_INIT, auctionId }); + events.emit(BID_REQUESTED, { ...MOCK.BID_REQUESTED, auctionId }); + events.emit(BID_RESPONSE, { ...MOCK.BID_RESPONSE[0], auctionId }); + events.emit(BID_RESPONSE, { ...MOCK.BID_RESPONSE[1], auctionId }); + events.emit(BIDDER_DONE, { ...MOCK.BIDDER_DONE, auctionId }); + events.emit(AUCTION_END, { ...MOCK.AUCTION_END, auctionId }); if (gptEvents && gptEvents.length) { gptEvents.forEach(gptEvent => mockGpt.emitEvent(gptEvent.eventName, gptEvent.params)); } - events.emit(SET_TARGETING, MOCK.SET_TARGETING); - events.emit(BID_WON, MOCK.BID_WON[0]); - events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(SET_TARGETING, { ...MOCK.SET_TARGETING, auctionId }); + events.emit(BID_WON, { ...MOCK.BID_WON[0], auctionId }); + events.emit(BID_WON, { ...MOCK.BID_WON[1], auctionId }); } describe('rubicon analytics adapter', function () { @@ -1779,6 +1779,50 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); + it('should only mark the first gam data not all matches', function () { + config.setConfig({ + rubicon: { + waitForGamSlots: true + } + }); + performStandardAuction(); + performStandardAuction([gptSlotRenderEnded0, gptSlotRenderEnded1], '32d332de-123a-32dg-2345-cefef3423324'); + + // tick the clock and both should fire + clock.tick(3000); + + expect(server.requests.length).to.equal(2); + + // first one should have GAM data + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + // trigger should be gam since all adunits had associated gam render + expect(message.trigger).to.be.equal('gam'); + expect(message.auctions[0].adUnits[0].gam).to.deep.equal({ + advertiserId: 1111, + creativeId: 2222, + lineItemId: 3333, + adSlot: '/19968336/header-bid-tag-0' + }); + expect(message.auctions[0].adUnits[1].gam).to.deep.equal({ + advertiserId: 4444, + creativeId: 5555, + lineItemId: 6666, + adSlot: '/19968336/header-bid-tag1' + }); + + // second one should NOT have gam data + request = server.requests[1]; + message = JSON.parse(request.requestBody); + validate(message); + + // trigger should be auctionEnd + expect(message.trigger).to.be.equal('auctionEnd'); + expect(message.auctions[0].adUnits[0].gam).to.be.undefined; + expect(message.auctions[0].adUnits[1].gam).to.be.undefined; + }); + it('should send request when waitForGamSlots is present but no bidWons are sent', function () { config.setConfig({ rubicon: { diff --git a/test/spec/modules/rubiconAnalyticsSchema.json b/test/spec/modules/rubiconAnalyticsSchema.json index 07239ca06ed..2d0dca42d23 100644 --- a/test/spec/modules/rubiconAnalyticsSchema.json +++ b/test/spec/modules/rubiconAnalyticsSchema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "title": "Prebid Auctions", "description": "A batched data object describing the lifecycle of an auction or multiple auction across a single page view.", "type": "object", diff --git a/test/spec/modules/seedtagBidAdapter_spec.js b/test/spec/modules/seedtagBidAdapter_spec.js index 159645ff6d6..1e0dca68d00 100644 --- a/test/spec/modules/seedtagBidAdapter_spec.js +++ b/test/spec/modules/seedtagBidAdapter_spec.js @@ -1,14 +1,17 @@ -import { expect } from 'chai' -import { spec, getTimeoutUrl } from 'modules/seedtagBidAdapter.js' -import * as utils from 'src/utils.js' +import { expect } from 'chai'; +import { spec, getTimeoutUrl } from 'modules/seedtagBidAdapter.js'; +import * as utils from 'src/utils.js'; -const PUBLISHER_ID = '0000-0000-01' -const ADUNIT_ID = '000000' +const PUBLISHER_ID = '0000-0000-01'; +const ADUNIT_ID = '000000'; function getSlotConfigs(mediaTypes, params) { return { params: params, - sizes: [[300, 250], [300, 600]], + sizes: [ + [300, 250], + [300, 600], + ], bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', @@ -17,328 +20,336 @@ function getSlotConfigs(mediaTypes, params) { mediaTypes: mediaTypes, src: 'client', transactionId: 'd704d006-0d6e-4a09-ad6c-179e7e758096', - adUnitCode: 'adunit-code' - } + adUnitCode: 'adunit-code', + }; } function createVideoSlotConfig(mediaType) { return getSlotConfigs(mediaType, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' - }) + placement: 'video', + }); } -describe('Seedtag Adapter', function() { - describe('isBidRequestValid method', function() { - describe('returns true', function() { +describe('Seedtag Adapter', function () { + 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 createBannerSlotConfig = placement => { + describe('and placement has the correct value', function () { + const createBannerSlotConfig = (placement) => { return getSlotConfigs( { banner: {} }, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement + placement, } - ) - } - const placements = ['banner', 'video', 'inImage', 'inScreen', 'inArticle'] - placements.forEach(placement => { - it('should be ' + placement, function() { + ); + }; + const placements = [ + 'banner', + 'video', + 'inImage', + 'inScreen', + 'inArticle', + ]; + placements.forEach((placement) => { + it('should be ' + placement, function () { const isBidRequestValid = spec.isBidRequestValid( createBannerSlotConfig(placement) - ) - expect(isBidRequestValid).to.equal(true) - }) - }) - }) - }) - describe('when video slot has all mandatory params', function() { + ); + expect(isBidRequestValid).to.equal(true); + }); + }); + }); + }); + describe('when video slot has all mandatory params', function () { it('should return true, when video context is instream', function () { const slotConfig = getSlotConfigs( { video: { context: 'instream', - playerSize: [[600, 200]] - } + playerSize: [[600, 200]], + }, }, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' + placement: 'video', } - ) - const isBidRequestValid = spec.isBidRequestValid(slotConfig) - expect(isBidRequestValid).to.equal(true) - }) + ); + const isBidRequestValid = spec.isBidRequestValid(slotConfig); + expect(isBidRequestValid).to.equal(true); + }); it('should return true, when video context is outstream', function () { const slotConfig = getSlotConfigs( { video: { context: 'outstream', - playerSize: [[600, 200]] - } + playerSize: [[600, 200]], + }, }, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' + placement: 'video', } - ) - const isBidRequestValid = spec.isBidRequestValid(slotConfig) - expect(isBidRequestValid).to.equal(true) - }) - }) - }) - describe('returns false', function() { - describe('when params are not correct', function() { + ); + 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) + return getSlotConfigs({ banner: {} }, params); } - it('does not have the PublisherToken.', function() { + it('does not have the PublisherToken.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ adUnitId: ADUNIT_ID, - placement: 'banner' + placement: 'banner', }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have the AdUnitId.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have the AdUnitId.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ publisherId: PUBLISHER_ID, - placement: 'banner' + placement: 'banner', }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have the placement.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have the placement.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ publisherId: PUBLISHER_ID, - adUnitId: ADUNIT_ID + adUnitId: ADUNIT_ID, }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have a the correct placement.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have a the correct placement.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'another_thing' + placement: 'another_thing', }) - ) - expect(isBidRequestValid).to.equal(false) - }) - }) - describe('when video mediaType object is not correct', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + }); + describe('when video mediaType object is not correct', function () { function createVideoSlotconfig(mediaType) { return getSlotConfigs(mediaType, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' - }) + placement: 'video', + }); } - it('is a void object', function() { + it('is a void object', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: {} }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have playerSize.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have playerSize.', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: { context: 'instream' } }) - ) - expect(isBidRequestValid).to.equal(false) - }) + ); + expect(isBidRequestValid).to.equal(false); + }); it('is outstream ', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: { context: 'outstream', - playerSize: [[600, 200]] - } + playerSize: [[600, 200]], + }, }) - ) - expect(isBidRequestValid).to.equal(true) - }) - describe('order does not matter', function() { - it('when video is not the first slot', function() { + ); + expect(isBidRequestValid).to.equal(true); + }); + describe('order does not matter', function () { + it('when video is not the first slot', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ banner: {}, video: {} }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('when video is the first slot', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('when video is the first slot', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: {}, banner: {} }) - ) - expect(isBidRequestValid).to.equal(false) - }) - }) - }) - }) - }) + ); + expect(isBidRequestValid).to.equal(false); + }); + }); + }); + }); + }); - describe('buildRequests method', function() { + describe('buildRequests method', function () { const bidderRequest = { refererInfo: { referer: 'referer' }, - timeout: 1000 - } + timeout: 1000, + }; const mandatoryParams = { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'banner' - } + placement: 'banner', + }; const inStreamParams = Object.assign({}, mandatoryParams, { video: { - mimes: 'mp4' - } - }) + mimes: 'mp4', + }, + }); const validBidRequests = [ getSlotConfigs({ banner: {} }, mandatoryParams), getSlotConfigs( { video: { context: 'instream', playerSize: [[300, 200]] } }, inStreamParams - ) - ] - 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://s.seedtag.com/c/hb/bid') - }) + ), + ]; + 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://s.seedtag.com/c/hb/bid'); + }); - it('Common data request should be correct', function() { - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.url).to.equal('referer') - expect(data.publisherToken).to.equal('0000-0000-01') - expect(typeof data.version).to.equal('string') - expect(['fixed', 'mobile', 'unknown'].indexOf(data.connectionType)).to.be.above(-1) - expect(data.bidRequests[0].adUnitCode).to.equal('adunit-code') - }) + it('Common data request should be correct', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.url).to.equal('referer'); + expect(data.publisherToken).to.equal('0000-0000-01'); + expect(typeof data.version).to.equal('string'); + expect( + ['fixed', 'mobile', 'unknown'].indexOf(data.connectionType) + ).to.be.above(-1); + expect(data.bidRequests[0].adUnitCode).to.equal('adunit-code'); + }); - describe('adPosition param', function() { - it('should sended when publisher set adPosition param', function() { + describe('adPosition param', function () { + it('should sended when publisher set adPosition param', function () { const params = Object.assign({}, mandatoryParams, { - adPosition: 1 - }) - const validBidRequests = [getSlotConfigs({ banner: {} }, params)] - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.bidRequests[0].adPosition).to.equal(1) - }) - it('should not sended when publisher has not set adPosition param', function() { + adPosition: 1, + }); + const validBidRequests = [getSlotConfigs({ banner: {} }, params)]; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.bidRequests[0].adPosition).to.equal(1); + }); + it('should not sended when publisher has not set adPosition param', function () { const validBidRequests = [ - getSlotConfigs({ banner: {} }, mandatoryParams) - ] - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.bidRequests[0].adPosition).to.equal(undefined) - }) - }) + getSlotConfigs({ banner: {} }, mandatoryParams), + ]; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.bidRequests[0].adPosition).to.equal(undefined); + }); + }); - describe('GDPR params', function() { - describe('when there arent 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() { + describe('GDPR params', function () { + describe('when there arent 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.cd).to.equal('consentString') - }) - it('cmps should be true and all gdpr parameters should be sended, when there are gdprApplies', function() { + 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.cd).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.cd).to.equal('consentString') - }) - it('should expose gvlid', function() { - expect(spec.gvlid).to.equal(157) - }) - }) - }) + 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.cd).to.equal('consentString'); + }); + it('should expose gvlid', function () { + expect(spec.gvlid).to.equal(157); + }); + }); + }); - 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.id).to.equal('30b31c1838de1e') + 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.id).to.equal('30b31c1838de1e'); expect(bannerBid.transactionId).to.equal( 'd704d006-0d6e-4a09-ad6c-179e7e758096' - ) - expect(bannerBid.supplyTypes[0]).to.equal('display') - expect(bannerBid.adUnitId).to.equal('000000') - expect(bannerBid.sizes[0][0]).to.equal(300) - expect(bannerBid.sizes[0][1]).to.equal(250) - expect(bannerBid.sizes[1][0]).to.equal(300) - expect(bannerBid.sizes[1][1]).to.equal(600) - expect(bannerBid.requestCount).to.equal(1) - }) - it('should request an InStream Video', function() { - const videoBid = bidRequests[1] - expect(videoBid.id).to.equal('30b31c1838de1e') + ); + expect(bannerBid.supplyTypes[0]).to.equal('display'); + expect(bannerBid.adUnitId).to.equal('000000'); + expect(bannerBid.sizes[0][0]).to.equal(300); + expect(bannerBid.sizes[0][1]).to.equal(250); + expect(bannerBid.sizes[1][0]).to.equal(300); + expect(bannerBid.sizes[1][1]).to.equal(600); + expect(bannerBid.requestCount).to.equal(1); + }); + it('should request an InStream Video', function () { + const videoBid = bidRequests[1]; + expect(videoBid.id).to.equal('30b31c1838de1e'); expect(videoBid.transactionId).to.equal( 'd704d006-0d6e-4a09-ad6c-179e7e758096' - ) - expect(videoBid.supplyTypes[0]).to.equal('video') - expect(videoBid.adUnitId).to.equal('000000') - expect(videoBid.videoParams.mimes).to.equal('mp4') - expect(videoBid.videoParams.w).to.equal(300) - expect(videoBid.videoParams.h).to.equal(200) - expect(videoBid.sizes[0][0]).to.equal(300) - expect(videoBid.sizes[0][1]).to.equal(250) - expect(videoBid.sizes[1][0]).to.equal(300) - expect(videoBid.sizes[1][1]).to.equal(600) - expect(videoBid.requestCount).to.equal(1) - }) - }) - }) + ); + expect(videoBid.supplyTypes[0]).to.equal('video'); + expect(videoBid.adUnitId).to.equal('000000'); + expect(videoBid.videoParams.mimes).to.equal('mp4'); + expect(videoBid.videoParams.w).to.equal(300); + expect(videoBid.videoParams.h).to.equal(200); + expect(videoBid.sizes[0][0]).to.equal(300); + expect(videoBid.sizes[0][1]).to.equal(250); + expect(videoBid.sizes[1][0]).to.equal(300); + expect(videoBid.sizes[1][1]).to.equal(600); + expect(videoBid.requestCount).to.equal(1); + }); + }); + }); - describe('interpret response method', function() { - it('should return a void array, when the server response are not correct.', function() { - const request = { data: JSON.stringify({}) } + 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 no 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 = { data: JSON.stringify({}) } + 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 no 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 = { data: JSON.stringify({}) }; const serverResponse = { body: { bids: [ @@ -352,28 +363,30 @@ describe('Seedtag Adapter', function() { mediaType: 'display', ttl: 360, nurl: 'testurl.com/nurl', - adomain: ['advertiserdomain.com'] - } + adomain: ['advertiserdomain.com'], + }, ], - cookieSync: { url: '' } - } - } - const bids = spec.interpretResponse(serverResponse, request) - expect(bids.length).to.equal(1) - expect(bids[0].requestId).to.equal('2159a54dc2566f') - expect(bids[0].cpm).to.equal(0.5) - expect(bids[0].width).to.equal(728) - expect(bids[0].height).to.equal(90) - expect(bids[0].currency).to.equal('USD') - expect(bids[0].netRevenue).to.equal(true) - expect(bids[0].ad).to.equal('content') - expect(bids[0].nurl).to.equal('testurl.com/nurl') - expect(bids[0].meta.advertiserDomains).to.deep.equal(['advertiserdomain.com']) - }) - }) - describe('the bid is a video', function() { - it('should return a instream bid', function() { - const request = { data: JSON.stringify({}) } + cookieSync: { url: '' }, + }, + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids.length).to.equal(1); + expect(bids[0].requestId).to.equal('2159a54dc2566f'); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].ad).to.equal('content'); + expect(bids[0].nurl).to.equal('testurl.com/nurl'); + expect(bids[0].meta.advertiserDomains).to.deep.equal([ + 'advertiserdomain.com', + ]); + }); + }); + describe('the bid is a video', function () { + it('should return a instream bid', function () { + const request = { data: JSON.stringify({}) }; const serverResponse = { body: { bids: [ @@ -386,114 +399,124 @@ describe('Seedtag Adapter', function() { height: 90, mediaType: 'video', ttl: 360, - nurl: undefined - } + nurl: undefined, + }, ], - cookieSync: { url: '' } - } - } - const bids = spec.interpretResponse(serverResponse, request) - expect(bids.length).to.equal(1) - expect(bids[0].requestId).to.equal('2159a54dc2566f') - expect(bids[0].cpm).to.equal(0.5) - expect(bids[0].width).to.equal(728) - expect(bids[0].height).to.equal(90) - expect(bids[0].currency).to.equal('USD') - expect(bids[0].netRevenue).to.equal(true) - expect(bids[0].vastXml).to.equal('content') - expect(bids[0].meta.advertiserDomains).to.deep.equal([]) - }) - }) - }) - }) + cookieSync: { url: '' }, + }, + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids.length).to.equal(1); + expect(bids[0].requestId).to.equal('2159a54dc2566f'); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].vastXml).to.equal('content'); + expect(bids[0].meta.advertiserDomains).to.deep.equal([]); + }); + }); + }); + }); - describe('user syncs method', function() { - it('should return empty array, when iframe sync option are disabled.', function() { - const syncOption = { iframeEnabled: false } - const serverResponses = [{ body: { cookieSync: 'someUrl' } }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(0) - }) - it('should return empty array, when the server response are wrong.', function() { - const syncOption = { iframeEnabled: true } - const serverResponses = [{ body: {} }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(0) - }) - it('should return empty array, when the server response are void.', function() { - const syncOption = { iframeEnabled: true } - const serverResponses = [{ body: { cookieSync: '' } }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(0) - }) - it('should return a array with the cookie sync, when the server response with a cookie sync.', function() { - const syncOption = { iframeEnabled: true } - const serverResponses = [{ body: { cookieSync: 'someUrl' } }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(1) - expect(cookieSyncArray[0].type).to.equal('iframe') - expect(cookieSyncArray[0].url).to.equal('someUrl') - }) - }) + describe('user syncs method', function () { + it('should return empty array, when iframe sync option are disabled.', function () { + const syncOption = { iframeEnabled: false }; + const serverResponses = [{ body: { cookieSync: 'someUrl' } }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(0); + }); + it('should return empty array, when the server response are wrong.', function () { + const syncOption = { iframeEnabled: true }; + const serverResponses = [{ body: {} }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(0); + }); + it('should return empty array, when the server response are void.', function () { + const syncOption = { iframeEnabled: true }; + const serverResponses = [{ body: { cookieSync: '' } }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(0); + }); + it('should return a array with the cookie sync, when the server response with a cookie sync.', function () { + const syncOption = { iframeEnabled: true }; + const serverResponses = [{ body: { cookieSync: 'someUrl' } }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(1); + expect(cookieSyncArray[0].type).to.equal('iframe'); + expect(cookieSyncArray[0].url).to.equal('someUrl'); + }); + }); describe('onTimeout', function () { - beforeEach(function() { - sinon.stub(utils, 'triggerPixel') - }) + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); - afterEach(function() { - utils.triggerPixel.restore() - }) + afterEach(function () { + utils.triggerPixel.restore(); + }); it('should return the correct endpoint', function () { - const params = { publisherId: '0000', adUnitId: '11111' } - const timeoutData = [{ params: [ params ] }]; + const params = { publisherId: '0000', adUnitId: '11111' }; + const timeout = 3000; + const timeoutData = [{ params: [params], timeout }]; const timeoutUrl = getTimeoutUrl(timeoutData); expect(timeoutUrl).to.equal( 'https://s.seedtag.com/se/hb/timeout?publisherToken=' + - params.publisherId + - '&adUnitId=' + - params.adUnitId - ) - }) + params.publisherId + + '&adUnitId=' + + params.adUnitId + + '&timeout=' + + timeout + ); + }); - it('should set the timeout pixel', function() { - const params = { publisherId: '0000', adUnitId: '11111' } - const timeoutData = [{ params: [ params ] }]; - spec.onTimeout(timeoutData) - expect(utils.triggerPixel.calledWith('https://s.seedtag.com/se/hb/timeout?publisherToken=' + - params.publisherId + - '&adUnitId=' + - params.adUnitId)).to.equal(true); - }) - }) + it('should set the timeout pixel', function () { + const params = { publisherId: '0000', adUnitId: '11111' }; + const timeout = 3000; + const timeoutData = [{ params: [params], timeout }]; + spec.onTimeout(timeoutData); + expect( + utils.triggerPixel.calledWith( + 'https://s.seedtag.com/se/hb/timeout?publisherToken=' + + params.publisherId + + '&adUnitId=' + + params.adUnitId + + '&timeout=' + + timeout + ) + ).to.equal(true); + }); + }); describe('onBidWon', function () { - beforeEach(function() { - sinon.stub(utils, 'triggerPixel') - }) + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); - afterEach(function() { - utils.triggerPixel.restore() - }) + afterEach(function () { + utils.triggerPixel.restore(); + }); - describe('without nurl', function() { - const bid = {} + describe('without nurl', function () { + const bid = {}; - it('does not create pixel ', function() { - spec.onBidWon(bid) + it('does not create pixel ', function () { + spec.onBidWon(bid); expect(utils.triggerPixel.called).to.equal(false); - }) - }) + }); + }); describe('with nurl', function () { - const nurl = 'http://seedtag_domain/won' - const bid = { nurl } + const nurl = 'http://seedtag_domain/won'; + const bid = { nurl }; - it('creates nurl pixel if bid nurl', function() { - spec.onBidWon({ nurl }) + it('creates nurl pixel if bid nurl', function () { + spec.onBidWon({ nurl }); expect(utils.triggerPixel.calledWith(nurl)).to.equal(true); - }) - }) - }) -}) + }); + }); + }); +}); diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index 6f2674dadc5..164188804a3 100644 --- a/test/spec/modules/tripleliftBidAdapter_spec.js +++ b/test/spec/modules/tripleliftBidAdapter_spec.js @@ -813,7 +813,7 @@ describe('triplelift adapter', function () { const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); const { data: payload } = request; expect(payload.ext.fpd.user).to.not.exist; - expect(payload.ext.fpd.context.data).to.haveOwnProperty('category'); + expect(payload.ext.fpd.context.ext.data).to.haveOwnProperty('category'); expect(payload.ext.fpd.context).to.haveOwnProperty('pmp_elig'); }); it('should send ad unit fpd if kvps are available', function() { diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 63107aa5107..ceb83b87ebb 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -51,6 +51,8 @@ import {akamaiDAPIdSubmodule} from 'modules/akamaiDAPIdSystem.js' import {kinessoIdSubmodule} from 'modules/kinessoIdSystem.js' import {adqueryIdSubmodule} from 'modules/adqueryIdSystem.js'; import * as mockGpt from '../integration/faker/googletag.js'; +import 'src/prebid.js'; +import {hook} from '../../../src/hook.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -107,10 +109,16 @@ describe('User ID', function () { } before(function () { + hook.ready(); localStorage.removeItem(PBJS_USER_ID_OPTOUT_NAME); }); beforeEach(function () { + // TODO: this whole suite needs to be redesigned; it is passing by accident + // some tests do not pass if consent data is available + // (there are functions here with signature `getId(config, storedId)`, but storedId is actually consentData) + // also, this file is ginormous; do we really need to test *all* id systems as one? + resetConsentData(); coreStorage.setCookie(CONSENT_LOCAL_STORAGE_NAME, '', EXPIRED_COOKIE_DATE); }); diff --git a/test/spec/modules/yandexBidAdapter_spec.js b/test/spec/modules/yandexBidAdapter_spec.js new file mode 100644 index 00000000000..833f883fb7c --- /dev/null +++ b/test/spec/modules/yandexBidAdapter_spec.js @@ -0,0 +1,166 @@ +import {assert, expect} from 'chai'; +import {spec} from 'modules/yandexBidAdapter.js'; +import {parseUrl} from 'src/utils.js'; +import {BANNER} from '../../../src/mediaTypes'; + +describe('Yandex adapter', function () { + function getBidConfig() { + return { + bidder: 'yandex', + params: { + pageId: 123, + impId: 1, + }, + }; + } + + function getBidRequest() { + return { + ...getBidConfig(), + bidId: 'bidid-1', + adUnitCode: 'adUnit-123', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ], + }, + }, + }; + } + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + const bid = getBidConfig(); + assert(spec.isBidRequestValid(bid)); + }); + + it('should return false when required params not found', function () { + expect(spec.isBidRequestValid({})).to.be.false; + }); + + it('should return false when required params.pageId are not passed', function () { + const bid = getBidConfig(); + delete bid.params.pageId; + + expect(spec.isBidRequestValid(bid)).to.be.false + }); + + it('should return false when required params.impId are not passed', function () { + const bid = getBidConfig(); + delete bid.params.impId; + + expect(spec.isBidRequestValid(bid)).to.be.false + }); + }); + + describe('buildRequests', function () { + const refererUrl = 'https://yandex.ru/secure-ads'; + + const gdprConsent = { + gdprApplies: 1, + consentString: 'concent-string', + apiVersion: 1, + }; + + const bidderRequest = { + refererInfo: { + referer: refererUrl + }, + gdprConsent + }; + + it('creates a valid banner request', function () { + const bannerRequest = getBidRequest(); + bannerRequest.getFloor = () => ({ + currency: 'USD', + // floor: 0.5 + }); + + const requests = spec.buildRequests([bannerRequest], bidderRequest); + + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + + expect(request).to.exist; + const { method, url, data } = request; + + expect(method).to.equal('POST'); + + const parsedRequestUrl = parseUrl(url); + const { search: query } = parsedRequestUrl + + expect(parsedRequestUrl.hostname).to.equal('bs-metadsp.yandex.ru'); + expect(parsedRequestUrl.pathname).to.equal('/metadsp/123'); + + expect(query['imp-id']).to.equal('1'); + expect(query['target-ref']).to.equal('yandex.ru'); + expect(query['ssp-id']).to.equal('10500'); + + expect(query['gdpr']).to.equal('1'); + expect(query['tcf-consent']).to.equal('concent-string'); + + expect(request.data).to.exist; + expect(data.site).to.not.equal(null); + expect(data.site.page).to.equal('yandex.ru'); + + // expect(data.device).to.not.equal(null); + // expect(data.device.w).to.equal(window.innerWidth); + // expect(data.device.h).to.equal(window.innerHeight); + + expect(data.imp).to.have.lengthOf(1); + expect(data.imp[0].banner).to.not.equal(null); + expect(data.imp[0].banner.w).to.equal(300); + expect(data.imp[0].banner.h).to.equal(250); + }); + }); + + describe('response handler', function () { + const bannerRequest = getBidRequest(); + + const bannerResponse = { + body: { + seatbid: [{ + bid: [ + { + impid: '1', + price: 0.3, + crid: 321, + adm: '', + w: 300, + h: 250, + adomain: [ + 'example.com' + ], + adid: 'yabs.123=', + } + ] + }], + cur: 'USD', + }, + }; + + it('handles banner responses', function () { + bannerRequest.bidRequest = { + mediaType: BANNER, + bidId: 'bidid-1', + }; + const result = spec.interpretResponse(bannerResponse, bannerRequest); + + expect(result).to.have.lengthOf(1); + expect(result[0]).to.exist; + + const rtbBid = result[0]; + expect(rtbBid.width).to.equal(300); + expect(rtbBid.height).to.equal(250); + expect(rtbBid.cpm).to.be.within(0.1, 0.5); + expect(rtbBid.ad).to.equal(''); + expect(rtbBid.currency).to.equal('USD'); + expect(rtbBid.netRevenue).to.equal(true); + expect(rtbBid.ttl).to.equal(180); + + expect(rtbBid.meta.advertiserDomains).to.deep.equal(['example.com']); + }); + }); +}); diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index f80cad46d50..e4d258ecdea 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -414,4 +414,40 @@ describe('yieldlabBidAdapter', function () { expect(result[0].vastUrl).to.include('&iab_content=id%3Afoo_id%2Cepisode%3A99%2Ctitle%3Afoo_title%252Cbar_title%2Cseries%3Afoo_series%2Cseason%3As1%2Cartist%3Afoo%2520bar%2Cgenre%3Abaz%2Cisrc%3ACC-XXX-YY-NNNNN%2Curl%3Ahttp%253A%252F%252Ffoo_url.de%2Ccat%3Acat1%7Ccat2%252Cppp%7Ccat3%257C%257C%257C%252F%252F%2Ccontext%3A7%2Ckeywords%3Ak1%252C%7Ck2..%2Clive%3A0') }) }) + + describe('getUserSyncs', function () { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: false + }; + const expectedUrlSnippets = ['https://ad.yieldlab.net/d/6846326/766/2x2?', 'ts=', 'type=h']; + + it('should return user sync as expected', function () { + const bidRequest = { + gdprConsent: { + consentString: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA', + gdprApplies: true + }, + uspConsent: '1YYY' + }; + const sync = spec.getUserSyncs(syncOptions, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(expectedUrlSnippets.every(urlSnippet => sync[0].url.includes(urlSnippet))); + expect(sync[0].url).to.have.string('gdpr=' + Number(bidRequest.gdprConsent.gdprApplies)); + expect(sync[0].url).to.have.string('gdpr_consent=' + bidRequest.gdprConsent.consentString); + // USP consent should be ignored + expect(sync[0].url).not.have.string('usp_consent='); + expect(sync[0].type).to.have.string('iframe'); + }); + + it('should return user sync even without gdprApplies in gdprConsent', function () { + const gdprConsent = { + consentString: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA' + } + const sync = spec.getUserSyncs(syncOptions, [], gdprConsent, undefined); + expect(expectedUrlSnippets.every(urlSnippet => sync[0].url.includes(urlSnippet))); + expect(sync[0].url).to.have.string('gdpr_consent=' + gdprConsent.consentString); + expect(sync[0].url).not.have.string('gdpr='); + expect(sync[0].type).to.have.string('iframe'); + }); + }); }) diff --git a/test/spec/renderer_spec.js b/test/spec/renderer_spec.js index 50e21d2cb36..6de06606136 100644 --- a/test/spec/renderer_spec.js +++ b/test/spec/renderer_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { Renderer } from 'src/Renderer.js'; +import { Renderer, executeRenderer } from 'src/Renderer.js'; import * as utils from 'src/utils.js'; import { loadExternalScript } from 'src/adloader.js'; require('test/mocks/adloaderStub.js'); @@ -212,5 +212,20 @@ describe('Renderer', function () { testRenderer.render() expect(loadExternalScript.called).to.be.true; }); + + it('call\'s documentResolver when configured', function () { + const documentResolver = sinon.spy(function(bid, sDoc, tDoc) { + return document; + }); + + let testRenderer = Renderer.install({ + url: 'https://httpbin.org/post', + config: { documentResolver: documentResolver } + }); + + executeRenderer(testRenderer, {}, {}); + + expect(documentResolver.called).to.be.true; + }); }); }); diff --git a/test/spec/unit/core/consentHandler_spec.js b/test/spec/unit/core/consentHandler_spec.js index bee5a2d9522..082ff34f90c 100644 --- a/test/spec/unit/core/consentHandler_spec.js +++ b/test/spec/unit/core/consentHandler_spec.js @@ -38,4 +38,22 @@ describe('Consent data handler', () => { }) }) }); + + it('should resolve .promise to new data if setConsentData is called a second time', (done) => { + let actual = null; + const d1 = {data: '1'}; + const d2 = {data: '2'}; + handler.enable(); + handler.promise.then((d) => actual = d); + handler.setConsentData(d1); + setTimeout(() => { + expect(actual).to.equal(d1); + handler.setConsentData(d2); + handler.promise.then((d) => actual = d); + setTimeout(() => { + expect(actual).to.equal(d2); + done(); + }) + }) + }); })