diff --git a/.babelrc.js b/.babelrc.js index 74d26caf0fd..43303f59a8b 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -14,16 +14,7 @@ module.exports = { [ useLocal('@babel/preset-env'), { - "targets": { - "browsers": [ - "chrome >= 75", - "safari >=10", - "edge >= 70", - "ff >= 70", - "ie >= 11", - "ios >= 11" - ] - } + "useBuiltIns": "entry" } ] ], diff --git a/README.md b/README.md index be16e9e3547..11fce459e56 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ module.exports = { loader: 'babel-loader', // presets and plugins for Prebid.js must be manually specified separate from your other babel rule. // this can be accomplished by requiring prebid's .babelrc.js file (requires Babel 7 and Node v8.9.0+) + // as of Prebid 6, babelrc.js only targets modern browsers. One can change the targets and build for + // older browsers if they prefer, but integration tests on ie11 were removed in Prebid.js 6.0 options: require('prebid.js/.babelrc.js') } } @@ -314,7 +316,7 @@ For instructions on writing tests for Prebid.js, see [Testing Prebid.js](http:// ### Supported Browsers -Prebid.js is supported on IE11 and modern browsers. +Prebid.js is supported on IE11 and modern browsers until 5.x. 6.x+ transpiles to target >0.25%; not Opera Mini; not IE11. ### Governance Review our governance model [here](https://github.com/prebid/Prebid.js/tree/master/governance.md). diff --git a/browsers.json b/browsers.json index aad51ca6383..dd3955c47ea 100644 --- a/browsers.json +++ b/browsers.json @@ -15,14 +15,6 @@ "device": null, "os": "Windows" }, - "bs_ie_11_windows_10": { - "base": "BrowserStack", - "os_version": "10", - "browser": "ie", - "browser_version": "11.0", - "device": null, - "os": "Windows" - }, "bs_chrome_90_windows_10": { "base": "BrowserStack", "os_version": "10", diff --git a/gulpfile.js b/gulpfile.js index 8609177a8b9..98caf1d7312 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -76,6 +76,7 @@ function lint(done) { 'modules/**/*.js', 'test/**/*.js', 'plugins/**/*.js', + '!plugins/**/node_modules/**', './*.js' ], { base: './' }) .pipe(gulpif(argv.nolintfix, eslint(), eslint({ fix: true }))) diff --git a/modules/adheseBidAdapter.js b/modules/adheseBidAdapter.js index 88f3e0e0e4f..145b5605bc2 100644 --- a/modules/adheseBidAdapter.js +++ b/modules/adheseBidAdapter.js @@ -2,6 +2,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'adhese'; const GVLID = 553; @@ -20,11 +21,15 @@ export const spec = { if (validBidRequests.length === 0) { return null; } + const { gdprConsent, refererInfo } = bidderRequest; + const adheseConfig = config.getConfig('adhese'); const gdprParams = (gdprConsent && gdprConsent.consentString) ? { xt: [gdprConsent.consentString] } : {}; const refererParams = (refererInfo && refererInfo.referer) ? { xf: [base64urlEncode(refererInfo.referer)] } : {}; - const commonParams = { ...gdprParams, ...refererParams }; + const globalCustomParams = (adheseConfig && adheseConfig.globalTargets) ? cleanTargets(adheseConfig.globalTargets) : {}; + const commonParams = { ...globalCustomParams, ...gdprParams, ...refererParams }; + const vastContentAsUrl = !(adheseConfig && adheseConfig.vastContentAsUrl == false); const slots = validBidRequests.map(bid => ({ slotname: bidToSlotName(bid), @@ -34,7 +39,7 @@ export const spec = { const payload = { slots: slots, parameters: commonParams, - vastContentAsUrl: true, + vastContentAsUrl: vastContentAsUrl, user: { ext: { eids: getEids(validBidRequests), diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 11c8b464a7e..2a54c45aa40 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -75,6 +75,7 @@ export const spec = { {code: 'denakop'}, {code: 'rtbanalytica'}, {code: 'unibots'}, + {code: 'catapultx'}, {code: 'ergadx'}, {code: 'turktelekom'} ], diff --git a/modules/adlivetechBidAdapter.md b/modules/adlivetechBidAdapter.md new file mode 100644 index 00000000000..612e669ea1a --- /dev/null +++ b/modules/adlivetechBidAdapter.md @@ -0,0 +1,61 @@ +# Overview + +Module Name: Adlivetech Bidder Adapter +Module Type: Bidder Adapter +Maintainer: grid-tech@themediagrid.com + +# Description + +Module that connects to Grid demand source to fetch bids. +The adapter is GDPR compliant and supports banner and video (instream and outstream). + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "adlivetech", + params: { + uid: '1', + bidFloor: 0.5 + } + } + ] + },{ + code: 'test-div', + sizes: [[728, 90]], + bids: [ + { + bidder: "adlivetech", + params: { + uid: 2, + keywords: { + brandsafety: ['disaster'], + topic: ['stress', 'fear'] + } + } + } + ] + }, + { + code: 'test-div', + sizes: [[728, 90]], + mediaTypes: { video: { + context: 'instream', + playerSize: [728, 90], + mimes: ['video/mp4'] + }, + bids: [ + { + bidder: "adlivetech", + params: { + uid: 11 + } + } + ] + } + ]; +``` diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index a1dff3d258d..e013ed553ef 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -37,7 +37,8 @@ const handleMeta = function () { } const getUsi = function (meta, ortb2, bidderRequest) { - const usi = (meta !== null) ? meta.usi : false; + let usi = (meta !== null && meta.usi) ? meta.usi : false; + if (ortb2 && ortb2.user && ortb2.user.id) { usi = ortb2.user.id } return usi } diff --git a/modules/adomikAnalyticsAdapter.js b/modules/adomikAnalyticsAdapter.js index 5bbee86df54..99f079b2574 100644 --- a/modules/adomikAnalyticsAdapter.js +++ b/modules/adomikAnalyticsAdapter.js @@ -12,6 +12,7 @@ const bidRequested = CONSTANTS.EVENTS.BID_REQUESTED; const bidResponse = CONSTANTS.EVENTS.BID_RESPONSE; const bidWon = CONSTANTS.EVENTS.BID_WON; const bidTimeout = CONSTANTS.EVENTS.BID_TIMEOUT; +const ua = navigator.userAgent; let adomikAdapter = Object.assign(adapter({}), { @@ -47,7 +48,7 @@ let adomikAdapter = Object.assign(adapter({}), type: 'request', event: { bidder: bid.bidder.toUpperCase(), - placementCode: bid.placementCode + placementCode: bid.adUnitCode } }); }); @@ -67,6 +68,10 @@ adomikAdapter.initializeBucketEvents = function() { adomikAdapter.bucketEvents = []; } +adomikAdapter.maxPartLength = function () { + return (ua.includes(' MSIE ')) ? 1600 : 60000; +}; + adomikAdapter.sendTypedEvent = function() { const groupedTypedEvents = adomikAdapter.buildTypedEvents(); @@ -108,9 +113,10 @@ adomikAdapter.sendTypedEvent = function() { // Encode object in base64 const encodedBuf = window.btoa(stringBulkEvents); - // Create final url and split it in 1600 characters max (+endpoint length) + // Create final url and split it (+endpoint length) const encodedUri = encodeURIComponent(encodedBuf); - const splittedUrl = encodedUri.match(/.{1,1600}/g); + const maxLength = adomikAdapter.maxPartLength(); + const splittedUrl = encodedUri.match(new RegExp(`.{1,${maxLength}}`, 'g')); splittedUrl.forEach((split, i) => { const partUrl = `${split}&id=${adomikAdapter.currentContext.id}&part=${i}&on=${splittedUrl.length - 1}`; @@ -121,7 +127,7 @@ adomikAdapter.sendTypedEvent = function() { adomikAdapter.sendWonEvent = function (wonEvent) { const stringWonEvent = JSON.stringify(wonEvent) - logInfo('Won event sent to adomik prebid analytic ' + wonEvent); + logInfo('Won event sent to adomik prebid analytic ' + stringWonEvent); // Encode object in base64 const encodedBuf = window.btoa(stringWonEvent); diff --git a/modules/airgridRtdProvider.js b/modules/airgridRtdProvider.js index 8d212204da8..f5403cca3eb 100644 --- a/modules/airgridRtdProvider.js +++ b/modules/airgridRtdProvider.js @@ -33,7 +33,7 @@ export function attachScriptTagToDOM(rtdConfig) { edktInitializor.load = function(e) { var p = e || 'sdk'; var n = document.createElement('script'); - n.type = 'text/javascript'; + n.type = 'module'; n.async = true; n.src = 'https://cdn.edkt.io/' + p + '/edgekit.min.js'; document.getElementsByTagName('head')[0].appendChild(n); diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 7dcbd74d779..c02eeccaea6 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -598,6 +598,10 @@ function newBid(serverBid, rtbBid, bidderRequest) { bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id }); } + if (rtbBid.brand_id) { + bid.meta = Object.assign({}, bid.meta, { brandId: rtbBid.brand_id }); + } + if (rtbBid.rtb.video) { // shared video properties used for all 3 contexts Object.assign(bid, { @@ -696,9 +700,11 @@ function newBid(serverBid, rtbBid, bidderRequest) { }); try { if (rtbBid.rtb.trackers) { - const url = rtbBid.rtb.trackers[0].impression_urls[0]; - const tracker = createTrackPixelHtml(url); - bid.ad += tracker; + for (let i = 0; i < rtbBid.rtb.trackers[0].impression_urls.length; i++) { + const url = rtbBid.rtb.trackers[0].impression_urls[i]; + const tracker = createTrackPixelHtml(url); + bid.ad += tracker; + } } } catch (error) { logError('Error appending tracking pixel', error); diff --git a/modules/atsAnalyticsAdapter.js b/modules/atsAnalyticsAdapter.js index d1e520b4b8f..f45d2e80055 100644 --- a/modules/atsAnalyticsAdapter.js +++ b/modules/atsAnalyticsAdapter.js @@ -20,7 +20,7 @@ export const analyticsUrl = 'https://analytics.rlcdn.com'; let handlerRequest = []; let handlerResponse = []; -let atsAnalyticsAdapterVersion = 2; +let atsAnalyticsAdapterVersion = 3; let browsersList = [ /* Googlebot */ @@ -222,7 +222,8 @@ function bidRequestedHandler(args) { auction_start: new Date(args.auctionStart).toJSON(), domain: window.location.hostname, pid: atsAnalyticsAdapter.context.pid, - adapter_version: atsAnalyticsAdapterVersion + adapter_version: atsAnalyticsAdapterVersion, + bid_won: false }; }); return requests; @@ -251,13 +252,14 @@ export function parseBrowser() { } } -function sendDataToAnalytic () { +function sendDataToAnalytic (events) { // send data to ats analytic endpoint try { - let dataToSend = {'Data': atsAnalyticsAdapter.context.events}; + let dataToSend = {'Data': events}; let strJSON = JSON.stringify(dataToSend); logInfo('ATS Analytics - tried to send analytics data!'); ajax(analyticsUrl, function () { + logInfo('ATS Analytics - events sent successfully!'); }, strJSON, {method: 'POST', contentType: 'application/json'}); } catch (err) { logError('ATS Analytics - request encounter an error: ', err); @@ -265,7 +267,7 @@ function sendDataToAnalytic () { } // preflight request, to check did publisher have permission to send data to analytics endpoint -function preflightRequest (envelopeSourceCookieValue) { +function preflightRequest (envelopeSourceCookieValue, events) { logInfo('ATS Analytics - preflight request!'); ajax(preflightUrl + atsAnalyticsAdapter.context.pid, { @@ -276,7 +278,8 @@ function preflightRequest (envelopeSourceCookieValue) { atsAnalyticsAdapter.setSamplingCookie(samplingRate); let samplingRateNumber = Number(samplingRate); if (data && samplingRate && atsAnalyticsAdapter.shouldFireRequest(samplingRateNumber) && envelopeSourceCookieValue != null) { - sendDataToAnalytic(); + logInfo('ATS Analytics - events to send: ', events); + sendDataToAnalytic(events); } }, error: function () { @@ -286,29 +289,6 @@ function preflightRequest (envelopeSourceCookieValue) { }, undefined, {method: 'GET', crossOrigin: true}); } -function callHandler(evtype, args) { - if (evtype === CONSTANTS.EVENTS.BID_REQUESTED) { - handlerRequest = handlerRequest.concat(bidRequestedHandler(args)); - } else if (evtype === CONSTANTS.EVENTS.BID_RESPONSE) { - handlerResponse.push(bidResponseHandler(args)); - } - if (evtype === CONSTANTS.EVENTS.AUCTION_END) { - if (handlerRequest.length) { - let events = []; - if (handlerResponse.length) { - events = handlerRequest.filter(request => handlerResponse.filter(function(response) { - if (request.bid_id === response.bid_id) { - Object.assign(request, response); - } - })); - } else { - events = handlerRequest; - } - atsAnalyticsAdapter.context.events = events; - } - } -} - let atsAnalyticsAdapter = Object.assign(adapter( { analyticsType @@ -316,22 +296,7 @@ let atsAnalyticsAdapter = Object.assign(adapter( { track({eventType, args}) { if (typeof args !== 'undefined') { - callHandler(eventType, args); - } - if (eventType === CONSTANTS.EVENTS.AUCTION_END) { - let envelopeSourceCookieValue = storage.getCookie('_lr_env_src_ats'); - try { - let samplingRateCookie = storage.getCookie('_lr_sampling_rate'); - if (!samplingRateCookie) { - preflightRequest(envelopeSourceCookieValue); - } else { - if (atsAnalyticsAdapter.shouldFireRequest(parseInt(samplingRateCookie)) && envelopeSourceCookieValue != null) { - sendDataToAnalytic(); - } - } - } catch (err) { - logError('ATS Analytics - preflight request encounter an error: ', err); - } + atsAnalyticsAdapter.callHandler(eventType, args); } } }); @@ -369,13 +334,69 @@ atsAnalyticsAdapter.enableAnalytics = function (config) { } atsAnalyticsAdapter.context = { events: [], - pid: config.options.pid + pid: config.options.pid, + bidWonTimeout: config.options.bidWonTimeout }; let initOptions = config.options; logInfo('ATS Analytics - adapter enabled! '); atsAnalyticsAdapter.originEnableAnalytics(initOptions); // call the base class function }; +atsAnalyticsAdapter.callHandler = function (evtype, args) { + if (evtype === CONSTANTS.EVENTS.BID_REQUESTED) { + handlerRequest = handlerRequest.concat(bidRequestedHandler(args)); + } else if (evtype === CONSTANTS.EVENTS.BID_RESPONSE) { + handlerResponse.push(bidResponseHandler(args)); + } + if (evtype === CONSTANTS.EVENTS.AUCTION_END) { + let bidWonTimeout = atsAnalyticsAdapter.context.bidWonTimeout ? atsAnalyticsAdapter.context.bidWonTimeout : 2000; + let events = []; + setTimeout(() => { + let winningBids = $$PREBID_GLOBAL$$.getAllWinningBids(); + logInfo('ATS Analytics - winning bids: ', winningBids) + // prepare format data for sending to analytics endpoint + if (handlerRequest.length) { + let wonEvent = {}; + if (handlerResponse.length) { + events = handlerRequest.filter(request => handlerResponse.filter(function (response) { + if (request.bid_id === response.bid_id) { + Object.assign(request, response); + } + })); + if (winningBids.length) { + events = events.filter(event => winningBids.filter(function (won) { + wonEvent.bid_id = won.requestId; + wonEvent.bid_won = true; + if (event.bid_id === wonEvent.bid_id) { + Object.assign(event, wonEvent); + } + })) + } + } else { + events = handlerRequest; + } + // check should we send data to analytics or not, check first cookie value _lr_sampling_rate + try { + let envelopeSourceCookieValue = storage.getCookie('_lr_env_src_ats'); + let samplingRateCookie = storage.getCookie('_lr_sampling_rate'); + if (!samplingRateCookie) { + preflightRequest(envelopeSourceCookieValue, events); + } else { + if (atsAnalyticsAdapter.shouldFireRequest(parseInt(samplingRateCookie)) && envelopeSourceCookieValue != null) { + logInfo('ATS Analytics - events to send: ', events); + sendDataToAnalytic(events); + } + } + // empty events array to not send duplicate events + events = []; + } catch (err) { + logError('ATS Analytics - preflight request encounter an error: ', err); + } + } + }, bidWonTimeout); + } +} + adaptermanager.registerAnalyticsAdapter({ adapter: atsAnalyticsAdapter, code: 'atsAnalytics', diff --git a/modules/atsAnalyticsAdapter.md b/modules/atsAnalyticsAdapter.md index 7c634f39ae2..17819ac61b3 100644 --- a/modules/atsAnalyticsAdapter.md +++ b/modules/atsAnalyticsAdapter.md @@ -17,6 +17,7 @@ Analytics adapter for Authenticated Traffic Solution(ATS), provided by LiveRamp. provider: 'atsAnalytics', options: { pid: '999', // publisher ID + bidWonTimeout: 2000 // on auction end for how long to wait for bid_won events, by default it's 2000 miliseconds, if it's not set it will be 2000 miliseconds. } } ``` diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index 09c8678d1ff..b2f63488e12 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -6,6 +6,7 @@ const BIDDER_CODE = 'between'; let ENDPOINT = 'https://ads.betweendigital.com/adjson?t=prebid'; const CODE_TYPES = ['inpage', 'preroll', 'midroll', 'postroll']; +const includes = require('core-js-pure/features/array/includes.js'); export const spec = { code: BIDDER_CODE, aliases: ['btw'], @@ -53,7 +54,7 @@ export const spec = { params.mind = video.mind; params.pos = 'atf'; ENDPOINT += '&jst=pvc'; - params.codeType = CODE_TYPES.includes(video.codeType) ? video.codeType : 'inpage'; + params.codeType = includes(CODE_TYPES, video.codeType) ? video.codeType : 'inpage'; } if (i.params.itu !== undefined) { diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index d527223964e..a1943afda8d 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -15,21 +15,27 @@ * @property {?string} keyName */ -import { deepClone, logError, isGptPubadsDefined } from '../src/utils.js'; +import { deepClone, logError, isGptPubadsDefined, isNumber, isFn, deepSetValue } from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajaxBuilder} from '../src/ajax.js'; import {loadExternalScript} from '../src/adloader.js'; import {getStorageManager} from '../src/storageManager.js'; import find from 'core-js-pure/features/array/find.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import includes from 'core-js-pure/features/array/includes.js'; const storage = getStorageManager(); /** @type {ModuleParams} */ let _moduleParams = {}; /** @type {null|Object} */ -let _predictionsData = null; +let _browsiData = null; /** @type {string} */ const DEF_KEYNAME = 'browsiViewability'; +/** @type {null | function} */ +let _dataReadyCallback = null; +/** @type {null|Object} */ +let _ic = {}; /** * add browsi script to page @@ -78,29 +84,49 @@ export function collectData() { getPredictionsFromServer(`//${_moduleParams.url}/prebid?${toUrlParams(predictorData)}`); } +/** + * wait for data from server + * call callback when data is ready + * @param {function} callback + */ +function waitForData(callback) { + if (_browsiData) { + _dataReadyCallback = null; + callback(_browsiData); + } else { + _dataReadyCallback = callback; + } +} + export function setData(data) { - _predictionsData = data; + _browsiData = data; + if (isFn(_dataReadyCallback)) { + _dataReadyCallback(_browsiData); + _dataReadyCallback = null; + } } -function sendDataToModule(adUnitsCodes) { +function getRTD(auc) { try { - const _predictions = (_predictionsData && _predictionsData.p) || {}; - return adUnitsCodes.reduce((rp, adUnitCode) => { - if (!adUnitCode) { + const _bp = (_browsiData && _browsiData.p) || {}; + return auc.reduce((rp, uc) => { + _ic[uc] = _ic[uc] || 0; + const _c = _ic[uc]; + if (!uc) { return rp } - const adSlot = getSlotByCode(adUnitCode); - const identifier = adSlot ? getMacroId(_predictionsData['pmd'], adSlot) : adUnitCode; - const predictionData = _predictions[identifier]; - rp[adUnitCode] = getKVObject(-1, _predictionsData['kn']); - if (!predictionData) { + const adSlot = getSlotByCode(uc); + const identifier = adSlot ? getMacroId(_browsiData['pmd'], adSlot) : uc; + const _pd = _bp[identifier]; + rp[uc] = getKVObject(-1); + if (!_pd) { return rp } - if (predictionData.p) { - if (!isIdMatchingAdUnit(adSlot, predictionData.w)) { + if (_pd.ps) { + if (!isIdMatchingAdUnit(adSlot, _pd.w)) { return rp; } - rp[adUnitCode] = getKVObject(predictionData.p, _predictionsData.kn); + rp[uc] = getKVObject(getCurrentData(_pd.ps, _c)); } return rp; }, {}); @@ -109,6 +135,31 @@ function sendDataToModule(adUnitsCodes) { } } +/** + * get prediction + * return -1 if prediction not found + * @param {object} predictionObject + * @param {number} _c + * @return {number} + */ +export function getCurrentData(predictionObject, _c) { + if (!predictionObject || !isNumber(_c)) { + return -1; + } + if (isNumber(predictionObject[_c])) { + return predictionObject[_c]; + } + if (Object.keys(predictionObject).length > 1) { + while (_c > 0) { + _c--; + if (isNumber(predictionObject[_c])) { + return predictionObject[_c]; + } + } + } + return -1; +} + /** * get all slots on page * @return {Object[]} slot GoogleTag slots @@ -122,12 +173,16 @@ function getAllSlots() { * @param {string?} keyName * @return {Object} key:value */ -function getKVObject(p, keyName) { +function getKVObject(p) { const prValue = p < 0 ? 'NA' : (Math.floor(p * 10) / 10).toFixed(2); let prObject = {}; - prObject[((_moduleParams['keyName'] || keyName || DEF_KEYNAME).toString())] = prValue.toString(); + prObject[getKey()] = prValue.toString(); return prObject; } + +function getKey() { + return ((_moduleParams['keyName'] || (_browsiData && _browsiData['kn']) || DEF_KEYNAME).toString()) +} /** * check if placement id matches one of given ad units * @param {Object} slot google slot @@ -238,6 +293,28 @@ function toUrlParams(data) { .join('&'); } +function setBidRequestsData(bidObj, callback) { + let adUnitCodes = bidObj.adUnitCodes; + let adUnits = bidObj.adUnits || getGlobal().adUnits || []; + if (adUnitCodes) { + adUnits = adUnits.filter(au => includes(adUnitCodes, au.code)); + } else { + adUnitCodes = adUnits.map(au => au.code); + } + waitForData(() => { + const data = getRTD(adUnitCodes); + if (data) { + adUnits.forEach(adUnit => { + const adUnitCode = adUnit.code; + if (data[adUnitCode]) { + deepSetValue(adUnit, 'ortb2Imp.ext.data.browsi', {[getKey()]: data[adUnitCode][getKey()]}); + } + }); + } + callback(); + }) +} + /** @type {RtdSubmodule} */ export const browsiSubmodule = { /** @@ -250,10 +327,21 @@ export const browsiSubmodule = { * @function * @param {string[]} adUnitsCodes */ - getTargetingData: sendDataToModule, + getTargetingData: getTargetingData, init: init, + getBidRequestData: setBidRequestsData }; +function getTargetingData(uc) { + const targetingData = getRTD(uc); + uc.forEach(auc => { + if (isNumber(_ic[auc])) { + _ic[auc] = _ic[auc] + 1; + } + }); + return targetingData; +} + function init(moduleConfig) { _moduleParams = moduleConfig.params; if (_moduleParams && _moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { diff --git a/modules/cleanioRtdProvider.js b/modules/cleanioRtdProvider.js new file mode 100644 index 00000000000..b9fdcef768e --- /dev/null +++ b/modules/cleanioRtdProvider.js @@ -0,0 +1,192 @@ +/** + * This module adds clean.io provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will wrap bid responses markup in clean.io agent script for protection + * @module modules/cleanioRtdProvider + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js'; +import { logError, generateUUID, insertElement } from '../src/utils.js'; + +// ============================ MODULE STATE =============================== + +/** + * @type {function(): void} + * Page-wide initialization step / strategy + */ +let onModuleInit = () => {}; + +/** + * @type {function(Object): void} + * Bid response mutation step / strategy. + */ +let onBidResponse = () => {}; + +/** + * @type {number} + * 0 for unknown, 1 for preloaded, -1 for error. + */ +let preloadStatus = 0; + +// ============================ MODULE LOGIC =============================== + +/** + * Page initialization step which just preloads the script, to be available whenever we start processing the bids. + * @param {string} scriptURL The script URL to preload + */ +function pageInitStepPreloadScript(scriptURL) { + const linkElement = document.createElement('link'); + linkElement.rel = 'preload'; + linkElement.as = 'script'; + linkElement.href = scriptURL; + linkElement.onload = () => { preloadStatus = 1; }; + linkElement.onerror = () => { preloadStatus = -1; }; + insertElement(linkElement); +} + +/** + * Page initialization step which adds the protector script to the whole page. With that, there is no need wrapping bids, and the coverage is better. + * @param {string} scriptURL The script URL to add to the page for protection + */ +function pageInitStepProtectPage(scriptURL) { + const scriptElement = document.createElement('script'); + scriptElement.type = 'text/javascript'; + scriptElement.src = scriptURL; + insertElement(scriptElement); +} + +/** + * Bid processing step which alters the ad HTML to contain bid-specific information, which can be used to identify the creative later. + * @param {Object} bidResponse Bid response data + */ +function bidWrapStepAugmentHtml(bidResponse) { + bidResponse.ad = `\n${bidResponse.ad}`; +} + +/** + * Bid processing step which applies creative protection by wrapping the ad HTML. + * @param {string} scriptURL + * @param {number} requiredPreload + * @param {Object} bidResponse + */ +function bidWrapStepProtectByWrapping(scriptURL, requiredPreload, bidResponse) { + // Still prepend bid info, it's always helpful to have creative data in its payload + bidWrapStepAugmentHtml(bidResponse); + + // If preloading failed, or if configuration requires us to finish preloading - + // we should not process this bid any further + if (preloadStatus < requiredPreload) { + return; + } + + const sid = generateUUID(); + bidResponse.ad = ` + + + `; +} + +/** + * Custom error class to differentiate validation errors + */ +class ConfigError extends Error { } + +/** + * The function to be called upon module init. Depending on the passed config, initializes properly init/bid steps or throws ConfigError. + * @param {Object} config + */ +function readConfig(config) { + if (!config.params) { + throw new ConfigError('Missing config parameters for clean.io RTD module provider.'); + } + + if (typeof config.params.cdnUrl !== 'string' || !/^https?:\/\//.test(config.params.cdnUrl)) { + throw new ConfigError('Parameter "cdnUrl" is a required string parameter, which should start with "http(s)://".'); + } + + if (typeof config.params.protectionMode !== 'string') { + throw new ConfigError('Parameter "protectionMode" is a required string parameter.'); + } + + const scriptURL = config.params.cdnUrl; + + switch (config.params.protectionMode) { + case 'full': + onModuleInit = () => pageInitStepProtectPage(scriptURL); + onBidResponse = (bidResponse) => bidWrapStepAugmentHtml(bidResponse); + break; + + case 'bids': + onModuleInit = () => pageInitStepPreloadScript(scriptURL); + onBidResponse = (bidResponse) => bidWrapStepProtectByWrapping(scriptURL, 0, bidResponse); + break; + + case 'bids-nowait': + onModuleInit = () => pageInitStepPreloadScript(scriptURL); + onBidResponse = (bidResponse) => bidWrapStepProtectByWrapping(scriptURL, 1, bidResponse); + break; + + default: + throw new ConfigError('Parameter "protectionMode" must be one of "full" | "bids" | "bids-nowait".'); + } +} + +// ============================ MODULE REGISTRATION =============================== + +/** + * The function which performs submodule registration. + */ +function beforeInit() { + submodule('realTimeData', /** @type {RtdSubmodule} */ ({ + name: 'clean.io', + + init: (config, userConsent) => { + try { + readConfig(config); + onModuleInit(); + return true; + } catch (err) { + if (err instanceof ConfigError) { + logError(err.message); + } + return false; + } + }, + + onBidResponseEvent: (bidResponse, config, userConsent) => { + onBidResponse(bidResponse); + } + })); +} + +/** + * Exporting local (and otherwise encapsulated to this module) functions + * for testing purposes + */ +export const __TEST__ = { + pageInitStepPreloadScript, + pageInitStepProtectPage, + bidWrapStepAugmentHtml, + bidWrapStepProtectByWrapping, + ConfigError, + readConfig, + beforeInit, +} + +beforeInit(); diff --git a/modules/cleanioRtdProvider.md b/modules/cleanioRtdProvider.md new file mode 100644 index 00000000000..7870a2719b6 --- /dev/null +++ b/modules/cleanioRtdProvider.md @@ -0,0 +1,59 @@ +# Overview + +``` +Module Name: clean.io Rtd provider +Module Type: Rtd Provider +Maintainer: nick@clean.io +``` + +The clean.io Realtime module provides effective anti-malvertising solution for publishers, including, but not limited to, +blocking unwanted 0- and 1-click redirects, deceptive ads or those with malicious landing pages, and various types of affiliate fraud. + +Using this module requires prior agreement with [clean.io](https://clean.io) to obtain the necessary distribution key. + + +# Integration + +clean.io Realtime module can be built just like any other prebid module: + +``` +gulp build --modules=cleanioRtdProvider,... +``` + + +# Configuration + +When built into prebid.js, this module can be configured through the following `pbjs.setConfig` call: + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'clean.io', + params: { + cdnUrl: 'https://abc1234567890.cloudfront.net/script.js', ///< Contact clean.io to get your own CDN URL + protectionMode: 'full', ///< Supported modes are 'full', 'bids' and 'bids-nowait', see below. + } + }] + } +}); +``` + + +## Configuration parameters + +{: .table .table-bordered .table-striped } +| Name | Type | Scope | Description | +| :------------ | :------------ | :------------ |:------------ | +| ``cdnUrl`` | ``string`` | Required | CDN URL of the script, which is to be used for protection. | +| ``protectionMode`` | ``'full' \| 'bids' \| 'bids-nowait'`` | Required | Integration mode. Please refer to the "Integration modes" section for details. | + + +## Integration modes + +{: .table .table-bordered .table-striped } +| Integration Mode | Parameter Value | Description | +| :------------ | :------------ | :------------ | +| Full page protection | ``'full'`` | Preferred mode. The module will add the protector agent script directly to the page, and it will protect all placements. This mode will make the most out of various behavioral detection mechanisms, and will also prevent typical malicious behaviors. Please note that in this mode, depending on Prebid library naming, Chrome may mistakenly tag non-ad-related content as ads: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/ad_tagging.md. | +| Bids-only protection | ``'bids'`` | The module will protect specific bid responses, more specifically, the HTML representing ad payload, by wrapping it into the agent script. Please note that in this mode, ads delivered directly, outside of Prebid integration, will not be protected, since the module can only access the ads coming through Prebid. | +| Bids-only protection with no delay on bid rendering | ``'bids-nowait'`` | Same as above, but in this mode, the script will also *not* wrap those bid responses, which arrived prior to successful preloading of agent script. | diff --git a/modules/codefuelBidAdapter.js b/modules/codefuelBidAdapter.js index ecb56c00d29..b9da86ac24e 100644 --- a/modules/codefuelBidAdapter.js +++ b/modules/codefuelBidAdapter.js @@ -1,5 +1,4 @@ import { deepAccess, isArray } from '../src/utils.js'; -import { config } from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; const BIDDER_CODE = 'codefuel'; @@ -34,8 +33,7 @@ export const spec = { const devicetype = getDeviceType() const publisher = setOnAny(validBidRequests, 'params.publisher'); const cur = CURRENCY; - // const endpointUrl = 'http://localhost:5000/prebid' - const endpointUrl = config.getConfig('codefuel.bidderUrl'); + const endpointUrl = 'https://ai-p-codefuel-ds-rtb-us-east-1-k8s.seccint.com/prebid' const timeout = bidderRequest.timeout; validBidRequests.forEach(bid => bid.netRevenue = 'net'); diff --git a/modules/codefuelBidAdapter.md b/modules/codefuelBidAdapter.md index 321ae3b6644..4e1a0dd6409 100644 --- a/modules/codefuelBidAdapter.md +++ b/modules/codefuelBidAdapter.md @@ -21,7 +21,7 @@ You will receive the URLs when contacting us. ``` pbjs.setConfig({ codefuel: { - bidderUrl: 'https://ai-i-codefuel-ds-rtb-us-east-1-k8s-internal.seccint.com/prebid', + bidderUrl: 'https://ai-p-codefuel-ds-rtb-us-east-1-k8s.seccint.com/prebid', usersyncUrl: 'https://usersync-url.com' } }); @@ -74,7 +74,7 @@ pbjs.setConfig({ pbjs.setConfig({ codefuel: { - bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + bidderUrl: 'https://ai-p-codefuel-ds-rtb-us-east-1-k8s.seccint.com/prebid' } }); ``` @@ -105,7 +105,7 @@ pbjs.setConfig({ pbjs.setConfig({ codefuel: { - bidderUrl: 'https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/' + bidderUrl: 'https://ai-p-codefuel-ds-rtb-us-east-1-k8s.seccint.com/prebid' } }); ``` diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index b98b72b59ad..812ec53d686 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -5,6 +5,7 @@ import { auctionManager } from '../src/auctionManager.js'; import find from 'core-js-pure/features/array/find.js'; import includes from 'core-js-pure/features/array/includes.js'; import { getStorageManager } from '../src/storageManager.js'; +import {ajax} from '../src/ajax.js'; const BIDDER_CODE = 'craft'; const URL_BASE = 'https://gacraft.jp/prebid-v3'; @@ -110,9 +111,10 @@ export const spec = { }, onBidWon: function(bid) { - var xhr = new XMLHttpRequest(); - xhr.open('POST', bid._prebidWon); - xhr.send(); + ajax(bid._prebidWon, null, null, { + method: 'POST', + contentType: 'application/json' + }); } }; diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 197ba19b1d6..43039e070c3 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -94,7 +94,7 @@ export const spec = { code: 'datablocks', // DATABLOCKS SCOPED OBJECT - db_obj: {metrics_host: 'prebid.datablocks.net', metrics: [], metrics_timer: null, metrics_queue_time: 1000, vis_optout: false, source_id: 0}, + db_obj: {metrics_host: 'prebid.dblks.net', metrics: [], metrics_timer: null, metrics_queue_time: 1000, vis_optout: false, source_id: 0}, // STORE THE DATABLOCKS BUYERID IN STORAGE store_dbid: function(dbid) { @@ -388,12 +388,12 @@ export const spec = { }; let sourceId = validRequests[0].params.source_id || 0; - let host = validRequests[0].params.host || 'prebid.datablocks.net'; + let host = validRequests[0].params.host || 'prebid.dblks.net'; // RETURN WITH THE REQUEST AND PAYLOAD return { method: 'POST', - url: `https://${sourceId}.${host}/openrtb/?sid=${sourceId}`, + url: `https://${host}/openrtb/?sid=${sourceId}`, data: { id: bidderRequest.auctionId, imp: imps, diff --git a/modules/deepintentBidAdapter.js b/modules/deepintentBidAdapter.js index d0c8eb29993..94167b92bb0 100644 --- a/modules/deepintentBidAdapter.js +++ b/modules/deepintentBidAdapter.js @@ -162,7 +162,7 @@ function buildImpression(bid) { impression = { id: bid.bidId, tagid: bid.params.tagId || '', - secure: window.location.protocol === 'https' ? 1 : 0, + secure: window.location.protocol === 'https:' ? 1 : 0, displaymanager: 'di_prebid', displaymanagerver: DI_M_V, ext: buildCustomParams(bid) diff --git a/modules/deltaprojectsBidAdapter.js b/modules/deltaprojectsBidAdapter.js new file mode 100644 index 00000000000..33df5bd252e --- /dev/null +++ b/modules/deltaprojectsBidAdapter.js @@ -0,0 +1,252 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { + _each, _map, isFn, isNumber, createTrackPixelHtml, deepAccess, parseUrl, logWarn, logError +} from '../src/utils.js'; +import {config} from '../src/config.js'; + +export const BIDDER_CODE = 'deltaprojects'; +export const BIDDER_ENDPOINT_URL = 'https://d5p.de17a.com/dogfight/prebid'; +export const USERSYNC_URL = 'https://userservice.de17a.com/getuid/prebid'; + +/** -- isBidRequestValid --**/ +function isBidRequestValid(bid) { + if (!bid) return false; + + if (bid.bidder !== BIDDER_CODE) return false; + + // publisher id is required + const publisherId = deepAccess(bid, 'params.publisherId') + if (!publisherId) { + logError('Invalid bid request, missing publisher id in params'); + return false; + } + + return true; +} + +/** -- Build requests --**/ +function buildRequests(validBidRequests, bidderRequest) { + /** == shared ==**/ + // -- build id + const id = bidderRequest.auctionId; + + // -- build site + const loc = parseUrl(bidderRequest.refererInfo.referer); + const publisherId = setOnAny(validBidRequests, 'params.publisherId'); + const siteId = setOnAny(validBidRequests, 'params.siteId'); + const site = { + id: siteId, + domain: loc.hostname, + page: loc.href, + ref: loc.href, + publisher: { id: publisherId }, + }; + + // -- build device + const ua = navigator.userAgent; + const device = { + ua, + w: screen.width, + h: screen.height + } + + // -- build user, reg + let user = { ext: {} }; + const regs = { ext: {} }; + const gdprConsent = bidderRequest && bidderRequest.gdprConsent; + if (gdprConsent) { + user.ext = { consent: gdprConsent.consentString }; + if (typeof gdprConsent.gdprApplies == 'boolean') { + regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0 + } + } + + // -- build tmax + let tmax = (bidderRequest && bidderRequest.timeout > 0) ? bidderRequest.timeout : undefined; + + // build bid specific + return validBidRequests.map(validBidRequest => { + const openRTBRequest = buildOpenRTBRequest(validBidRequest, id, site, device, user, tmax, regs); + return { + method: 'POST', + url: BIDDER_ENDPOINT_URL, + data: openRTBRequest, + options: { contentType: 'application/json' }, + bids: [validBidRequest], + }; + }); +} + +function buildOpenRTBRequest(validBidRequest, id, site, device, user, tmax, regs) { + // build cur + const currency = config.getConfig('currency.adServerCurrency') || deepAccess(validBidRequest, 'params.currency'); + const cur = currency && [currency]; + + // build impression + const impression = buildImpression(validBidRequest, currency); + + // build test + const test = deepAccess(validBidRequest, 'params.test') ? 1 : 0 + + const at = 1 + + // build source + const source = { + tid: validBidRequest.transactionId, + fd: 1, + } + + return { + id, + at, + imp: [impression], + site, + device, + user, + test, + tmax, + cur, + source, + regs, + ext: {}, + }; +} + +function buildImpression(bid, currency) { + const impression = { + id: bid.bidId, + tagid: bid.params.tagId, + ext: {}, + }; + + const bannerMediaType = deepAccess(bid, `mediaTypes.${BANNER}`); + impression.banner = buildImpressionBanner(bid, bannerMediaType); + + // bid floor + const bidFloor = getBidFloor(bid, BANNER, '*', currency); + if (bidFloor) { + impression.bidfloor = bidFloor.floor; + impression.bidfloorcur = bidFloor.currency; + } + + return impression; +} + +function buildImpressionBanner(bid, bannerMediaType) { + const bannerSizes = (bannerMediaType && bannerMediaType.sizes) || bid.sizes; + return { + format: _map(bannerSizes, ([width, height]) => ({ w: width, h: height })), + }; +} + +/** -- Interpret response --**/ +function interpretResponse(serverResponse) { + if (!serverResponse.body) { + logWarn('Response body is invalid, return !!'); + return []; + } + + const { body: { id, seatbid, cur } } = serverResponse; + if (!id || !seatbid) { + logWarn('Id / seatbid of response is invalid, return !!'); + return []; + } + + const bidResponses = []; + + _each(seatbid, seatbid => { + _each(seatbid.bid, bid => { + const bidObj = { + requestId: bid.impid, + cpm: parseFloat(bid.price), + width: parseInt(bid.w), + height: parseInt(bid.h), + creativeId: bid.crid || bid.id, + dealId: bid.dealid || null, + currency: cur, + netRevenue: true, + ttl: 60, + }; + + bidObj.mediaType = BANNER; + bidObj.ad = bid.adm; + if (bid.nurl) { + bidObj.ad += createTrackPixelHtml(decodeURIComponent(bid.nurl)); + } + if (bid.ext) { + bidObj[BIDDER_CODE] = bid.ext; + } + bidResponses.push(bidObj); + }); + }); + return bidResponses; +} + +/** -- On Bid Won -- **/ +function onBidWon(bid) { + let cpm = bid.cpm; + if (bid.currency && bid.currency !== bid.originalCurrency && typeof bid.getCpmInNewCurrency === 'function') { + cpm = bid.getCpmInNewCurrency(bid.originalCurrency); + } + const wonPrice = Math.round(cpm * 1000000); + const wonPriceMacroPatten = /\$\{AUCTION_PRICE:B64\}/g; + bid.ad = bid.ad.replace(wonPriceMacroPatten, wonPrice); +} + +/** -- Get user syncs --**/ +function getUserSyncs(syncOptions, serverResponses, gdprConsent) { + const syncs = [] + + if (syncOptions.pixelEnabled) { + let gdprParams; + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `?gdpr_consent=${gdprConsent.consentString}`; + } + } else { + gdprParams = ''; + } + syncs.push({ + type: 'image', + url: USERSYNC_URL + gdprParams + }); + } + return syncs; +} + +/** -- Get bid floor --**/ +export function getBidFloor(bid, mediaType, size, currency) { + if (isFn(bid.getFloor)) { + const bidFloorCurrency = currency || 'USD'; + const bidFloor = bid.getFloor({currency: bidFloorCurrency, mediaType: mediaType, size: size}); + if (isNumber(bidFloor.floor)) { + return bidFloor; + } + } +} + +/** -- Helper methods --**/ +function setOnAny(collection, key) { + for (let i = 0, result; i < collection.length; i++) { + result = deepAccess(collection[i], key); + if (result) { + return result; + } + } +} + +/** -- Register -- */ +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + onBidWon, + getUserSyncs, +}; + +registerBidder(spec); diff --git a/modules/deltaprojectsBidAdapter.md b/modules/deltaprojectsBidAdapter.md new file mode 100644 index 00000000000..97cef4dd228 --- /dev/null +++ b/modules/deltaprojectsBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +``` +Module Name: Delta Projects Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@deltaprojects.com +``` + +# Description + +Connects to Delta Projects DSP for bids. + +# Test Parameters +``` +// define banner unit +var bannerUnit = { + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]], + } + }, + // Replace this object to test a new Adapter! + bids: [{ + bidder: 'deltaprojects', + params: { + publisherId: '4' //required + } + }] +}; +``` + diff --git a/modules/engageyaBidAdapter.js b/modules/engageyaBidAdapter.js index 27d1bb15af8..23b0189931f 100644 --- a/modules/engageyaBidAdapter.js +++ b/modules/engageyaBidAdapter.js @@ -1,7 +1,4 @@ -import { - BANNER, - NATIVE -} from '../src/mediaTypes.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { createTrackPixelHtml } from '../src/utils.js'; const { @@ -10,14 +7,21 @@ const { const BIDDER_CODE = 'engageya'; const ENDPOINT_URL = 'https://recs.engageya.com/rec-api/getrecs.json'; const ENDPOINT_METHOD = 'GET'; +const SUPPORTED_SIZES = [ + [100, 75], [236, 202], [100, 100], [130, 130], [200, 200], [250, 250], [300, 272], [300, 250], [300, 230], [300, 214], [300, 187], [300, 166], [300, 150], [300, 133], [300, 120], [400, 200], [300, 200], [250, 377], [620, 410], [207, 311], [310, 166], [310, 333], [190, 106], [228, 132], [300, 174], [80, 60], [600, 500], [600, 600], [1080, 610], [1080, 610], [624, 350], [650, 1168], [1080, 1920], [300, 374] +]; -function getPageUrl() { - var pUrl = window.location.href; - if (isInIframe()) { - pUrl = document.referrer ? document.referrer : pUrl; +function getPageUrl(bidRequest, bidderRequest) { + if (bidRequest.params.pageUrl && bidRequest.params.pageUrl != '[PAGE_URL]') { + return bidRequest.params.pageUrl; } - pUrl = encodeURIComponent(pUrl); - return pUrl; + if (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { + return bidderRequest.refererInfo.referer; + } + const pageUrl = (isInIframe() && document.referrer) + ? document.referrer + : window.location.href; + return encodeURIComponent(pageUrl); } function isInIframe() { @@ -33,13 +37,14 @@ function getImageSrc(rec) { return rec.thumbnail_path.indexOf('http') === -1 ? 'https:' + rec.thumbnail_path : rec.thumbnail_path; } -function getImpressionTrackers(rec) { +function getImpressionTrackers(rec, response) { + const responseTrackers = [response.viewPxl]; if (!rec.trackers) { - return []; + return responseTrackers; } const impressionTrackers = rec.trackers.impressionPixels || []; const viewTrackers = rec.trackers.viewPixels || []; - return [...impressionTrackers, ...viewTrackers]; + return [...impressionTrackers, ...viewTrackers, ...responseTrackers]; } function parseNativeResponse(rec, response) { @@ -56,7 +61,7 @@ function parseNativeResponse(rec, response) { displayUrl: rec.url, cta: '', sponsoredBy: rec.displayName, - impressionTrackers: getImpressionTrackers(rec), + impressionTrackers: getImpressionTrackers(rec, response), }; } @@ -74,56 +79,65 @@ function parseBannerResponse(rec, response) { } const title = rec.title && rec.title.trim() ? `` : ''; const displayName = rec.displayName && title ? `` : ''; - const trackers = getImpressionTrackers(rec) + const trackers = getImpressionTrackers(rec, response) .map(createTrackPixelHtml) .join(''); return `${style}
${rec.title}${displayName}${title}${trackers}
`; } +function getImageSize(bidRequest) { + if (bidRequest.sizes && bidRequest.sizes.length > 0) { + return bidRequest.sizes[0]; + } else if (bidRequest.nativeParams && bidRequest.nativeParams.image && bidRequest.nativeParams.image.sizes) { + return bidRequest.nativeParams.image.sizes; + } + return [-1, -1]; +} + +function isValidSize([width, height]) { + if (!width || !height) { + return false; + } + return SUPPORTED_SIZES.some(([supportedWidth, supportedHeight]) => supportedWidth === width && supportedHeight === height); +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, NATIVE], - isBidRequestValid: function (bid) { - return bid && bid.params && bid.params.hasOwnProperty('widgetId') && bid.params.hasOwnProperty('websiteId') && !isNaN(bid.params.widgetId) && !isNaN(bid.params.websiteId); + + isBidRequestValid: function (bidRequest) { + return bidRequest && + bidRequest.params && + bidRequest.params.hasOwnProperty('widgetId') && + bidRequest.params.hasOwnProperty('websiteId') && + !isNaN(bidRequest.params.widgetId) && + !isNaN(bidRequest.params.websiteId) && + isValidSize(getImageSize(bidRequest)); }, buildRequests: function (validBidRequests, bidderRequest) { - var bidRequests = []; - if (validBidRequests && validBidRequests.length > 0) { - validBidRequests.forEach(function (bidRequest) { - if (bidRequest.params) { - var mediaType = bidRequest.hasOwnProperty('nativeParams') ? 1 : 2; - var imageWidth = -1; - var imageHeight = -1; - if (bidRequest.sizes && bidRequest.sizes.length > 0) { - imageWidth = bidRequest.sizes[0][0]; - imageHeight = bidRequest.sizes[0][1]; - } else if (bidRequest.nativeParams && bidRequest.nativeParams.image && bidRequest.nativeParams.image.sizes) { - imageWidth = bidRequest.nativeParams.image.sizes[0]; - imageHeight = bidRequest.nativeParams.image.sizes[1]; - } - - var widgetId = bidRequest.params.widgetId; - var websiteId = bidRequest.params.websiteId; - var pageUrl = (bidRequest.params.pageUrl && bidRequest.params.pageUrl != '[PAGE_URL]') ? bidRequest.params.pageUrl : ''; - if (!pageUrl) { - pageUrl = (bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer) ? bidderRequest.refererInfo.referer : getPageUrl(); - } - var bidId = bidRequest.bidId; - var finalUrl = ENDPOINT_URL + '?pubid=0&webid=' + websiteId + '&wid=' + widgetId + '&url=' + pageUrl + '&ireqid=' + bidId + '&pbtpid=' + mediaType + '&imw=' + imageWidth + '&imh=' + imageHeight; - if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprApplies && bidderRequest.consentString) { - finalUrl += '&is_gdpr=1&gdpr_consent=' + bidderRequest.consentString; - } - bidRequests.push({ - url: finalUrl, - method: ENDPOINT_METHOD, - data: '' - }); - } - }); + if (!validBidRequests) { + return []; } - - return bidRequests; + return validBidRequests.map(bidRequest => { + if (bidRequest.params) { + const mediaType = bidRequest.hasOwnProperty('nativeParams') ? 1 : 2; + const [imageWidth, imageHeight] = getImageSize(bidRequest); + const widgetId = bidRequest.params.widgetId; + const websiteId = bidRequest.params.websiteId; + const pageUrl = getPageUrl(bidRequest, bidderRequest); + const bidId = bidRequest.bidId; + let finalUrl = ENDPOINT_URL + '?pubid=0&webid=' + websiteId + '&wid=' + widgetId + '&url=' + pageUrl + '&ireqid=' + bidId + '&pbtpid=' + mediaType + '&imw=' + imageWidth + '&imh=' + imageHeight; + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprApplies && bidderRequest.consentString) { + finalUrl += '&is_gdpr=1&gdpr_consent=' + bidderRequest.consentString; + } + return { + url: finalUrl, + method: ENDPOINT_METHOD, + data: '' + }; + } + }).filter(Boolean); }, interpretResponse: function (serverResponse, bidRequest) { @@ -135,12 +149,12 @@ export const spec = { return response.recs.map(rec => { let bid = { requestId: response.ireqId, - cpm: rec.ecpm, width: response.imageWidth, height: response.imageHeight, creativeId: rec.postId, + cpm: rec.pecpm || (rec.ecpm / 100), currency: 'USD', - netRevenue: false, + netRevenue: !!rec.pecpm, ttl: 360, meta: { advertiserDomains: rec.domain ? [rec.domain] : [] }, } diff --git a/modules/glimpseBidAdapter.js b/modules/glimpseBidAdapter.js index b3646755d80..ea846c1a7b6 100644 --- a/modules/glimpseBidAdapter.js +++ b/modules/glimpseBidAdapter.js @@ -1,19 +1,22 @@ 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 storageManager = getStorageManager() +const GVLID = 1012 const BIDDER_CODE = 'glimpse' const ENDPOINT = 'https://api.glimpsevault.io/ads/serving/public/v1/prebid' const LOCAL_STORAGE_KEY = { - glimpse: { + vault: { jwt: 'gp_vault_jwt', }, } export const spec = { + gvlid: GVLID, code: BIDDER_CODE, supportedMediaTypes: [BANNER], @@ -37,20 +40,28 @@ export const spec = { * @returns {ServerRequest} */ buildRequests: (validBidRequests, bidderRequest) => { - const networkId = window.networkId || -1 - const bids = validBidRequests.map(processBidRequest) + const demo = config.getConfig('glimpse.demo') || false + const account = config.getConfig('glimpse.account') || -1 + const demand = config.getConfig('glimpse.demand') || 'glimpse' + const keywords = config.getConfig('glimpse.keywords') || {} + + const auth = getVaultJwt() const referer = getReferer(bidderRequest) const gdprConsent = getGdprConsentChoice(bidderRequest) - const jwt = getVaultJwt() + const bids = validBidRequests.map((bidRequest) => { + return processBidRequest(bidRequest, keywords) + }) const data = { - auth: jwt, + auth, data: { bidderCode: spec.code, - networkId, - bids, + demo, + account, + demand, referer, gdprConsent, + bids, } } @@ -65,10 +76,9 @@ export const spec = { /** * Parse response from Glimpse server * @param bidResponse {ServerResponse} - * @param bidRequest {BidRequest} * @returns {Bid[]} */ - interpretResponse: (bidResponse, bidRequest) => { + interpretResponse: (bidResponse) => { const isValidResponse = isValidBidResponse(bidResponse) if (isValidResponse) { @@ -81,16 +91,20 @@ export const spec = { }, } -function processBidRequest(bidRequest) { +function processBidRequest(bidRequest, globalKeywords) { const sizes = normalizeSizes(bidRequest.sizes) - const keywords = bidRequest.params.keywords || [] + const bidKeywords = bidRequest.params.keywords || {} + const keywords = { + ...globalKeywords, + ...bidKeywords, + } return { + unitCode: bidRequest.adUnitCode, bidId: bidRequest.bidId, placementId: bidRequest.params.placementId, - unitCode: bidRequest.adUnitCode, - sizes, keywords, + sizes, } } @@ -124,7 +138,8 @@ function getReferer(bidderRequest) { function getGdprConsentChoice(bidderRequest) { const hasGdprConsent = hasValue(bidderRequest) && - hasValue(bidderRequest.gdprConsent) + hasValue(bidderRequest.gdprConsent) && + hasStringValue(bidderRequest.gdprConsent.consentString) if (hasGdprConsent) { return bidderRequest.gdprConsent @@ -138,11 +153,11 @@ function getGdprConsentChoice(bidderRequest) { } function setVaultJwt(auth) { - storageManager.setDataInLocalStorage(LOCAL_STORAGE_KEY.glimpse.jwt, auth) + storageManager.setDataInLocalStorage(LOCAL_STORAGE_KEY.vault.jwt, auth) } function getVaultJwt() { - return storageManager.getDataFromLocalStorage(LOCAL_STORAGE_KEY.glimpse.jwt) || '' + return storageManager.getDataFromLocalStorage(LOCAL_STORAGE_KEY.vault.jwt) || '' } function isValidBidResponse(bidResponse) { diff --git a/modules/gptPreAuction.js b/modules/gptPreAuction.js index 6519572b383..2dc5a6983b9 100644 --- a/modules/gptPreAuction.js +++ b/modules/gptPreAuction.js @@ -55,15 +55,16 @@ export const appendPbAdSlot = adUnit => { const context = adUnit.ortb2Imp.ext.data; const { customPbAdSlot } = _currentConfig; - if (customPbAdSlot) { - context.pbadslot = customPbAdSlot(adUnit.code, deepAccess(context, 'adserver.adslot')); + // use context.pbAdSlot if set (if someone set it already, it will take precedence over others) + if (context.pbadslot) { return; } - // use context.pbAdSlot if set - if (context.pbadslot) { + if (customPbAdSlot) { + context.pbadslot = customPbAdSlot(adUnit.code, deepAccess(context, 'adserver.adslot')); return; } + // use data attribute 'data-adslotid' if set try { const adUnitCodeDiv = document.getElementById(adUnit.code); @@ -78,12 +79,17 @@ export const appendPbAdSlot = adUnit => { return; } context.pbadslot = adUnit.code; + return true; }; export const makeBidRequestsHook = (fn, adUnits, ...args) => { appendGptSlots(adUnits); adUnits.forEach(adUnit => { - appendPbAdSlot(adUnit); + const usedAdUnitCode = appendPbAdSlot(adUnit); + // gpid should be set to itself if already set, or to what pbadslot was (as long as it was not adUnit code) + if (!adUnit.ortb2Imp.ext.gpid && !usedAdUnitCode) { + adUnit.ortb2Imp.ext.gpid = adUnit.ortb2Imp.ext.data.pbadslot; + } }); return fn.call(this, adUnits, ...args); }; diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index f62b62b7a97..6d3f8ad2792 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -29,7 +29,7 @@ let hasSynced = false; export const spec = { code: BIDDER_CODE, - aliases: ['playwire'], + aliases: ['playwire', 'adlivetech'], supportedMediaTypes: [ BANNER, VIDEO ], /** * Determines whether or not the given bid request is valid. diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md index 8ffe29e091f..11f8ffc5609 100644 --- a/modules/id5IdSystem.md +++ b/modules/id5IdSystem.md @@ -1,14 +1,14 @@ # ID5 Universal ID -The ID5 Universal ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 Universal ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 Universal ID and detailed integration docs, please visit [our documentation](https://support.id5.io/portal/en/kb/articles/prebid-js-user-id-module). We also recommend that you sign up for our [release notes](https://id5.io/universal-id/release-notes) to stay up-to-date with any changes to the implementation of the ID5 Universal ID in Prebid. +The ID5 ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 ID and detailed integration docs, please visit [our documentation](https://support.id5.io/portal/en/kb/articles/prebid-js-user-id-module). -## ID5 Universal ID Registration +## ID5 ID Registration -The ID5 Universal ID is free to use, but requires a simple registration with ID5. Please visit [id5.io/universal-id](https://id5.io/universal-id) to sign up and request your ID5 Partner Number to get started. +The ID5 ID is free to use, but requires a simple registration with ID5. Please visit [our website](https://id5.io/solutions/#publishers) to sign up and request your ID5 Partner Number to get started. -The ID5 privacy policy is at [https://www.id5.io/platform-privacy-policy](https://www.id5.io/platform-privacy-policy). +The ID5 privacy policy is at [https://id5.io/platform-privacy-policy](https://id5.io/platform-privacy-policy). -## ID5 Universal ID Configuration +## ID5 ID Configuration First, make sure to add the ID5 submodule to your Prebid.js package with: @@ -46,7 +46,7 @@ pbjs.setConfig({ | Param under userSync.userIds[] | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | | name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | -| params | Required | Object | Details for the ID5 Universal ID. | | +| params | Required | Object | Details for the ID5 ID. | | | params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | | params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://support.id5.io/portal/en/kb/articles/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | | params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | diff --git a/modules/imRtdProvider.js b/modules/imRtdProvider.js index db2c51ccf51..7ab19c0e2d6 100644 --- a/modules/imRtdProvider.js +++ b/modules/imRtdProvider.js @@ -37,6 +37,34 @@ function setImDataInCookie(value) { ); } +/** +* @param {string} bidderName +*/ +export function getBidderFunction(bidderName) { + const biddersFunction = { + ix: function (bid, data) { + if (data.im_segments && data.im_segments.length) { + config.setConfig({ + ix: {firstPartyData: {im_segments: data.im_segments}}, + }); + } + return bid + }, + pubmatic: function (bid, data) { + if (data.im_segments && data.im_segments.length) { + const dctr = deepAccess(bid, 'params.dctr'); + deepSetValue( + bid, + 'params.dctr', + `${dctr ? dctr + '|' : ''}im_segments=${data.im_segments.join(',')}` + ); + } + return bid + } + } + return biddersFunction[bidderName] || null; +} + export function getCustomBidderFunction(config, bidder) { const overwriteFn = deepAccess(config, `params.overwrites.${bidder}`) @@ -73,9 +101,12 @@ export function setRealTimeData(bidConfig, moduleConfig, data) { adUnits.forEach(adUnit => { adUnit.bids.forEach(bid => { + const bidderFunction = getBidderFunction(bid.bidder); const overwriteFunction = getCustomBidderFunction(moduleConfig, bid.bidder); if (overwriteFunction) { overwriteFunction(bid, data, utils, config); + } else if (bidderFunction) { + bidderFunction(bid, data); } }) }); diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index 75da1509f19..d715ecf6663 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -77,12 +77,14 @@ function isBidRequestValid(bid) { function buildRequest(bidRequests, bidderRequest) { bidderRequest = bidderRequest || {}; const _placementIds = []; + const _adUnitCodes = []; let _loginId, _customEndpoint, _userId; let _ivAuctionStart = bidderRequest.auctionStart || Date.now(); bidRequests.forEach(function (bidRequest) { bidRequest.startTime = new Date().getTime(); _placementIds.push(bidRequest.params.placementId); + _adUnitCodes.push(bidRequest.adUnitCode); _loginId = _loginId || bidRequest.params.loginId; _customEndpoint = _customEndpoint || bidRequest.params.customEndpoint; _customUserSync = _customUserSync || bidRequest.params.customUserSync; @@ -99,6 +101,7 @@ function buildRequest(bidRequests, bidderRequest) { let userIdModel = getUserIds(_userId); let bidParamsJson = { placementIds: _placementIds, + adUnitCodes: _adUnitCodes, loginId: _loginId, auctionStartTime: _ivAuctionStart, bidVersion: CONSTANTS.PREBID_VERSION @@ -181,9 +184,12 @@ function handleResponse(responseObj, bidRequests) { const bidResponses = []; for (let i = 0; i < bidRequests.length; i++) { let bidRequest = bidRequests[i]; + let usedPlacementId = responseObj.UseAdUnitCode === true + ? bidRequest.params.placementId + '_' + bidRequest.adUnitCode + : bidRequest.params.placementId; - if (invibes.placementBids.indexOf(bidRequest.params.placementId) > -1) { - logInfo('Invibes Adapter - Placement was previously bid on ' + bidRequest.params.placementId); + if (invibes.placementBids.indexOf(usedPlacementId) > -1) { + logInfo('Invibes Adapter - Placement was previously bid on ' + usedPlacementId); continue; } @@ -191,21 +197,21 @@ function handleResponse(responseObj, bidRequests) { if (responseObj.AdPlacements != null) { for (let j = 0; j < responseObj.AdPlacements.length; j++) { let bidModel = responseObj.AdPlacements[j].BidModel; - if (bidModel != null && bidModel.PlacementId == bidRequest.params.placementId) { + if (bidModel != null && bidModel.PlacementId == usedPlacementId) { requestPlacement = responseObj.AdPlacements[j]; break; } } } else { let bidModel = responseObj.BidModel; - if (bidModel != null && bidModel.PlacementId == bidRequest.params.placementId) { + if (bidModel != null && bidModel.PlacementId == usedPlacementId) { requestPlacement = responseObj; } } - let bid = createBid(bidRequest, requestPlacement, responseObj.MultipositionEnabled); + let bid = createBid(bidRequest, requestPlacement, responseObj.MultipositionEnabled, usedPlacementId); if (bid !== null) { - invibes.placementBids.push(bidRequest.params.placementId); + invibes.placementBids.push(usedPlacementId); bidResponses.push(bid); } } @@ -213,9 +219,9 @@ function handleResponse(responseObj, bidRequests) { return bidResponses; } -function createBid(bidRequest, requestPlacement, multipositionEnabled) { +function createBid(bidRequest, requestPlacement, multipositionEnabled, usedPlacementId) { if (requestPlacement === null || requestPlacement.BidModel === null) { - logInfo('Invibes Adapter - Placement not configured for bidding ' + bidRequest.params.placementId); + logInfo('Invibes Adapter - Placement not configured for bidding ' + usedPlacementId); return null; } @@ -684,7 +690,7 @@ let keywords = (function () { return kw; }()); -// ===================== +// ====================== export function resetInvibes() { invibes.optIn = undefined; diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 5a6cae6d0c6..af981a226c0 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -1,6 +1,9 @@ import { deepAccess, parseGPTSingleSizeArray, inIframe, deepClone, logError, logWarn, isFn, contains, isInteger, isArray, deepSetValue, parseQueryStringParameters, isEmpty, mergeDeep, convertTypes } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; +import { EVENTS } from '../src/constants.json'; +import { getStorageManager } from '../src/storageManager.js'; +import events from '../src/events.js'; import find from 'core-js-pure/features/array/find.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { INSTREAM, OUTSTREAM } from '../src/video.js'; @@ -20,15 +23,23 @@ const VIDEO_TIME_TO_LIVE = 3600; // 1hr const NET_REVENUE = true; const MAX_REQUEST_SIZE = 8000; const MAX_REQUEST_LIMIT = 4; - const PRICE_TO_DOLLAR_FACTOR = { JPY: 1 }; const USER_SYNC_URL = 'https://js-sec.indexww.com/um/ixmatch.html'; const RENDERER_URL = 'https://js-sec.indexww.com/htv/video-player.js'; const FLOOR_SOURCE = { PBJS: 'p', IX: 'x' }; -// determines which eids we send and the rtiPartner field in ext - +export const ERROR_CODES = { + BID_SIZE_INVALID_FORMAT: 1, + BID_SIZE_NOT_INCLUDED: 2, + PROPERTY_NOT_INCLUDED: 3, + SITE_ID_INVALID_VALUE: 4, + BID_FLOOR_INVALID_FORMAT: 5, + IX_FPD_EXCEEDS_MAX_SIZE: 6, + EXCEEDS_MAX_SIZE: 7, + PB_FPD_EXCEEDS_MAX_SIZE: 8, + VIDEO_DURATION_INVALID: 9 +}; const FIRST_PARTY_DATA = { SITE: [ 'id', 'name', 'domain', 'cat', 'sectioncat', 'pagecat', 'page', 'ref', 'search', 'mobile', @@ -36,7 +47,6 @@ const FIRST_PARTY_DATA = { ], USER: ['id', 'buyeruid', 'yob', 'gender', 'keywords', 'customdata', 'geo', 'data', 'ext'] }; - const SOURCE_RTI_MAPPING = { 'liveramp.com': 'idl', 'netid.de': 'NETID', @@ -45,7 +55,6 @@ const SOURCE_RTI_MAPPING = { 'uidapi.com': 'UID2', 'adserver.org': 'TDID' }; - const PROVIDERS = [ 'britepoolid', 'id5id', @@ -62,9 +71,7 @@ const PROVIDERS = [ 'TDID', 'flocId' ]; - const REQUIRED_VIDEO_PARAMS = ['mimes', 'minduration', 'maxduration']; // note: protocol/protocols is also reqd - const VIDEO_PARAMS_ALLOW_LIST = [ 'mimes', 'minduration', 'maxduration', 'protocols', 'protocol', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', @@ -73,6 +80,17 @@ const VIDEO_PARAMS_ALLOW_LIST = [ 'delivery', 'pos', 'companionad', 'api', 'companiontype', 'ext', 'playerSize', 'w', 'h' ]; +const LOCAL_STORAGE_KEY = 'ixdiag'; +let hasRegisteredHandler = false; +export const storage = getStorageManager(GLOBAL_VENDOR_ID, BIDDER_CODE); + +// Possible values for bidResponse.seatBid[].bid[].mtype which indicates the type of the creative markup so that it can properly be associated with the right sub-object of the BidRequest.Imp. +const MEDIA_TYPES = { + Banner: 1, + Video: 2, + Audio: 3, + Native: 4 +} /** * Transform valid bid request config object to banner impression object that will be sent to ad server. @@ -125,7 +143,10 @@ function bidToVideoImp(bid) { } if (imp.video.minduration > imp.video.maxduration) { - logError(`IX Bid Adapter: video minduration [${imp.video.minduration}] cannot be greater than video maxduration [${imp.video.maxduration}]`); + logError( + `IX Bid Adapter: video minduration [${imp.video.minduration}] cannot be greater than video maxduration [${imp.video.maxduration}]`, + { bidder: BIDDER_CODE, code: ERROR_CODES.VIDEO_DURATION_INVALID } + ); return {}; } @@ -262,9 +283,14 @@ function parseBid(rawBid, currency, bidRequest) { bid.currency = currency; bid.creativeId = rawBid.hasOwnProperty('crid') ? rawBid.crid : '-'; - // in the event of a video - if (deepAccess(rawBid, 'ext.vasturl')) { + if (rawBid.mtype == MEDIA_TYPES.Video) { + bid.vastXml = rawBid.adm + } else if (rawBid.ext && rawBid.ext.vasturl) { bid.vastUrl = rawBid.ext.vasturl + } + + // in the event of a video + if ((rawBid.ext && rawBid.ext.vasturl) || rawBid.mtype == MEDIA_TYPES.Video) { bid.width = bidRequest.video.w; bid.height = bidRequest.video.h; bid.mediaType = VIDEO; @@ -498,6 +524,13 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r.ext.ixdiag[key] = ixdiag[key]; } + // Get cached errors stored in LocalStorage + const cachedErrors = getCachedErrors(); + + if (!isEmpty(cachedErrors)) { + r.ext.ixdiag.err = cachedErrors; + } + // if an schain is provided, send it along if (validBidRequests[0].schain) { r.source = { @@ -576,7 +609,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { const baseRequestSize = `${baseUrl}${parseQueryStringParameters({ ...payload, r: JSON.stringify(r) })}`.length; if (baseRequestSize > MAX_REQUEST_SIZE) { - logError('ix bidder: Base request size has exceeded maximum request size.'); + logError('IX Bid Adapter: Base request size has exceeded maximum request size.', { bidder: BIDDER_CODE, code: ERROR_CODES.EXCEEDS_MAX_SIZE }); return requests; } @@ -606,7 +639,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { } currentRequestSize += fpdRequestSize; } else { - logError('ix bidder: IX config FPD request size has exceeded maximum request size.'); + logError('IX Bid Adapter: IX config FPD request size has exceeded maximum request size.', { bidder: BIDDER_CODE, code: ERROR_CODES.IX_FPD_EXCEEDS_MAX_SIZE }); } } @@ -646,7 +679,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { } if (impressionObjects.length && BANNER in impressionObjects[0]) { - const { id, banner: { topframe } } = impressionObjects[0]; + const { id, banner: { topframe }, ext } = impressionObjects[0]; const _bannerImpression = { id, banner: { @@ -655,6 +688,12 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { }, } + if (ext.dfp_ad_unit_code) { + _bannerImpression.ext = { + dfp_ad_unit_code: ext.dfp_ad_unit_code + } + } + if ('bidfloor' in impressionObjects[0]) { _bannerImpression.bidfloor = impressionObjects[0].bidfloor; } @@ -707,7 +746,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { const fpdRequestSize = encodeURIComponent(JSON.stringify({ ...site, ...user })).length; currentRequestSize += fpdRequestSize; } else { - logError('ix bidder: FPD request size has exceeded maximum request size.'); + logError('IX Bid Adapter: FPD request size has exceeded maximum request size.', { bidder: BIDDER_CODE, code: ERROR_CODES.PB_FPD_EXCEEDS_MAX_SIZE }); } } @@ -940,10 +979,99 @@ function createMissingBannerImp(bid, imp, newSize) { } /** + * @typedef {Array[message: string, err: Object]} ErrorData + * @property {string} message - The error message. + * @property {object} err - The error object. + * @property {string} err.bidder - The bidder of the error. + * @property {string} err.code - The error code. + */ + +/** + * Error Event handler that receives type and arguments in a data object. + * + * @param {ErrorData} data + */ +function errorEventHandler(data) { + if (!storage.localStorageIsEnabled()) { + return; + } + + let currentStorage; + + try { + currentStorage = JSON.parse(storage.getDataFromLocalStorage(LOCAL_STORAGE_KEY) || '{}'); + } catch (e) { + logWarn('ix can not read ixdiag from localStorage.'); + } + + const todayDate = new Date(); + + Object.keys(currentStorage).map((errorDate) => { + const date = new Date(errorDate); + + if (date.setDate(date.getDate() + 7) - todayDate < 0) { + delete currentStorage[errorDate]; + } + }); + + if (data.type === 'ERROR' && data.arguments && data.arguments[1] && data.arguments[1].bidder === BIDDER_CODE) { + const todayString = todayDate.toISOString().slice(0, 10); + + const errorCode = data.arguments[1].code; + + if (errorCode) { + currentStorage[todayString] = currentStorage[todayString] || {}; + + if (!Number(currentStorage[todayString][errorCode])) { + currentStorage[todayString][errorCode] = 0; + } + + currentStorage[todayString][errorCode]++; + }; + } + + storage.setDataInLocalStorage(LOCAL_STORAGE_KEY, JSON.stringify(currentStorage)); +} + +/** + * Get ixdiag stored in LocalStorage and format to be added to request payload + * + * @returns {Object} Object with error codes and counts + */ +function getCachedErrors() { + if (!storage.localStorageIsEnabled()) { + return; + } + + const errors = {}; + let currentStorage; + + try { + currentStorage = JSON.parse(storage.getDataFromLocalStorage(LOCAL_STORAGE_KEY) || '{}'); + } catch (e) { + logError('ix can not read ixdiag from localStorage.'); + return null; + } + + Object.keys(currentStorage).forEach((date) => { + Object.keys(currentStorage[date]).forEach((code) => { + if (typeof currentStorage[date][code] === 'number') { + errors[code] = errors[code] + ? errors[code] + currentStorage[date][code] + : currentStorage[date][code]; + } + }); + }); + + return errors; +} + +/** + * * Initialize Outstream Renderer * @param {Object} bid */ -function outstreamRenderer (bid) { +function outstreamRenderer(bid) { bid.renderer.push(() => { var config = { width: bid.width, @@ -951,7 +1079,13 @@ function outstreamRenderer (bid) { timeout: 3000 }; - window.IXOutstreamPlayer(bid.vastUrl, bid.adUnitCode, config); + // IXOutstreamPlayer supports both vastUrl and vastXml, so we can pass either. + // Since vastUrl is going to be deprecated from exchange response, vastXml takes priority. + if (bid.vastXml) { + window.IXOutstreamPlayer(bid.vastXml, bid.adUnitCode, config); + } else { + window.IXOutstreamPlayer(bid.vastUrl, bid.adUnitCode, config); + } }); } @@ -960,7 +1094,7 @@ function outstreamRenderer (bid) { * @param {string} id * @returns {Renderer} */ -function createRenderer (id) { +function createRenderer(id) { const renderer = Renderer.install({ id: id, url: RENDERER_URL, @@ -994,6 +1128,12 @@ export const spec = { * @return {boolean} True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { + if (!hasRegisteredHandler) { + events.on(EVENTS.AUCTION_DEBUG, errorEventHandler); + events.on(EVENTS.AD_RENDER_FAILED, errorEventHandler); + hasRegisteredHandler = true; + } + const paramsVideoRef = deepAccess(bid, 'params.video'); const paramsSize = deepAccess(bid, 'params.size'); const mediaTypeBannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); @@ -1003,6 +1143,7 @@ export const spec = { const hasBidFloorCur = bid.params.hasOwnProperty('bidFloorCur'); if (bid.hasOwnProperty('mediaType') && !(contains(SUPPORTED_AD_TYPES, bid.mediaType))) { + logWarn('IX Bid Adapter: media type is not supported.'); return false; } @@ -1014,26 +1155,26 @@ export const spec = { // since there is an ix bidder level size, make sure its valid const ixSize = getFirstSize(paramsSize); if (!ixSize) { - logError('ix bidder params: size has invalid format.'); + logError('IX Bid Adapter: size has invalid format.', { bidder: BIDDER_CODE, code: ERROR_CODES.BID_SIZE_INVALID_FORMAT }); return false; } // check if the ix bidder level size, is present in ad unit level if (!includesSize(bid.sizes, ixSize) && !(includesSize(mediaTypeVideoPlayerSize, ixSize)) && !(includesSize(mediaTypeBannerSizes, ixSize))) { - logError('ix bidder params: bid size is not included in ad unit sizes or player size.'); + logError('IX Bid Adapter: bid size is not included in ad unit sizes or player size.', { bidder: BIDDER_CODE, code: ERROR_CODES.BID_SIZE_NOT_INCLUDED }); return false; } } if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { - logError('ix bidder params: siteId must be string or number value.'); + logError('IX Bid Adapter: siteId must be string or number value.', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); return false; } if (hasBidFloor || hasBidFloorCur) { if (!(hasBidFloor && hasBidFloorCur && isValidBidFloorParams(bid.params.bidFloor, bid.params.bidFloorCur))) { - logError('ix bidder params: bidFloor / bidFloorCur parameter has invalid format.'); + logError('IX Bid Adapter: bidFloor / bidFloorCur parameter has invalid format.', { bidder: BIDDER_CODE, code: ERROR_CODES.BID_FLOOR_INVALID_FORMAT }); return false; } } @@ -1042,7 +1183,7 @@ export const spec = { const errorList = checkVideoParams(mediaTypeVideoRef, paramsVideoRef); if (errorList.length) { errorList.forEach((err) => { - logError(err); + logError(err, { bidder: BIDDER_CODE, code: ERROR_CODES.PROPERTY_NOT_INCLUDED }); }); return false; } @@ -1163,6 +1304,16 @@ export const spec = { bids.push(bid); } + + if (deepAccess(requestBid, 'ext.ixdiag.err')) { + if (storage.localStorageIsEnabled()) { + try { + storage.removeDataFromLocalStorage(LOCAL_STORAGE_KEY); + } catch (e) { + logError('ix can not clear ixdiag from localStorage.'); + } + } + } } return bids; diff --git a/modules/justpremiumBidAdapter.js b/modules/justpremiumBidAdapter.js index fa0f939affc..56f9935ea6e 100644 --- a/modules/justpremiumBidAdapter.js +++ b/modules/justpremiumBidAdapter.js @@ -4,7 +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' +const JP_ADAPTER_VERSION = '1.8.1' const pixels = [] export const spec = { @@ -101,6 +101,11 @@ export const spec = { advertiserDomains: bid.adomain && bid.adomain.length > 0 ? bid.adomain : [] } } + if (bid.ext && bid.ext.pg) { + bidResponse.adserverTargeting = { + 'hb_deal_justpremium': 'jp_pg' + } + } bidResponses.push(bidResponse) } }) diff --git a/modules/livewrappedAnalyticsAdapter.js b/modules/livewrappedAnalyticsAdapter.js index 64806b793c2..5ef109aef96 100644 --- a/modules/livewrappedAnalyticsAdapter.js +++ b/modules/livewrappedAnalyticsAdapter.js @@ -3,6 +3,7 @@ import {ajax} from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; +import { getGlobal } from '../src/prebidGlobal.js'; const ANALYTICSTYPE = 'endpoint'; const URL = 'https://lwadm.com/analytics/10'; @@ -14,6 +15,7 @@ const TIMEOUTSENT = 8; const ADRENDERFAILEDSENT = 16; let initOptions; +let prebidGlobal = getGlobal(); export const BID_WON_TIMEOUT = 500; const cache = { @@ -79,6 +81,7 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE bidResponse.width = args.width; bidResponse.height = args.height; bidResponse.cpm = args.cpm; + bidResponse.originalCpm = prebidGlobal.convertCurrency(args.originalCpm, args.originalCurrency, args.currency); bidResponse.ttr = args.timeToRespond; bidResponse.readyToSend = 1; bidResponse.mediaType = args.mediaType == 'native' ? 2 : (args.mediaType == 'video' ? 4 : 1); @@ -237,6 +240,7 @@ function getResponses(gdpr, auctionIds) { width: bid.width, height: bid.height, cpm: bid.cpm, + orgCpm: bid.originalCpm, ttr: bid.ttr, IsBid: bid.isBid, mediaType: bid.mediaType, @@ -276,6 +280,7 @@ function getWins(gdpr, auctionIds) { width: bid.width, height: bid.height, cpm: bid.cpm, + orgCpm: bid.originalCpm, mediaType: bid.mediaType, gdpr: gdprPos, floor: bid.lwFloor ? bid.lwFloor : (bid.floorData ? bid.floorData.floorValue : undefined), diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js index 5339b653596..352c2d074e8 100644 --- a/modules/merkleIdSystem.js +++ b/modules/merkleIdSystem.js @@ -149,6 +149,11 @@ export const merkleIdSubmodule = { logInfo('User ID - merkleId stored id ' + storedId); const configParams = (config && config.params) || {}; + if (typeof configParams.endpoint !== 'string') { + logWarn('User ID - merkleId submodule endpoint string is not defined'); + configParams.endpoint = ID_URL + } + if (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) { logError('User ID - merkleId submodule does not currently handle consent strings'); return; diff --git a/modules/nativoBidAdapter.js b/modules/nativoBidAdapter.js index 9b7ffef8957..c9e6a1f659f 100644 --- a/modules/nativoBidAdapter.js +++ b/modules/nativoBidAdapter.js @@ -17,6 +17,16 @@ const adUnitsRequested = {} // Prebid adapter referrence doc: https://docs.prebid.org/dev-docs/bidder-adaptor.html +// Validity checks for optionsl paramters +const validParameter = { + url: (value) => typeof value === 'string', + placementId: (value) => { + const isString = typeof value === 'string' + const isNumber = typeof value === 'number' + return isString || isNumber + }, +} + export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -30,7 +40,23 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - return true + // We don't need any specific parameters to make a bid request + // If not parameters are supplied just verify it's the correct bidder code + if (!bid.params) return bid.bidder === BIDDER_CODE + + // Check if any supplied parameters are invalid + const hasInvalidParameters = Object.keys(bid.params).some(key => { + const value = bid.params[key] + const validityCheck = validParameter[key] + + // We don't have a test for this so it's not a paramter we care about + if (!validityCheck) return false + + // Return if the check is not passed + return !validityCheck(value) + }) + + return !hasInvalidParameters }, /** @@ -98,7 +124,11 @@ export const spec = { ] if (placementIds.size > 0) { - params.unshift({ key: 'ntv_ptd', value: placementIds.toString() }) + // Convert Set to Array (IE 11 Safe) + const placements = [] + placementIds.forEach((value) => placements.push(value)) + // Append to query string paramters + params.unshift({ key: 'ntv_ptd', value: placements.join(',') }) } if (bidderRequest.gdprConsent) { diff --git a/modules/oguryBidAdapter.js b/modules/oguryBidAdapter.js index 40843d58d02..1654f11249f 100644 --- a/modules/oguryBidAdapter.js +++ b/modules/oguryBidAdapter.js @@ -83,7 +83,8 @@ function buildRequests(validBidRequests, bidderRequest) { bidfloor: getFloor(bidRequest), banner: { format: sizes - } + }, + ext: bidRequest.params }); } }); diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index eb165e886e8..60b441e2c10 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -42,7 +42,12 @@ export const USER_ID_CODE_TO_QUERY_ARG = { novatiq: 'novatiqid', // Novatiq ID mwOpenLinkId: 'mwopenlinkid', // MediaWallah OpenLink ID dapId: 'dapid', // Akamai DAP ID - amxId: 'amxid' // AMX RTB ID + amxId: 'amxid', // AMX RTB ID + kpuid: 'kpuid', // Kinesso ID + publinkId: 'publinkid', // Publisher Link + naveggId: 'naveggid', // Navegg ID + imuid: 'imuid', // IM-UID by Intimate Merger + adtelligentId: 'adtelligentid' // Adtelligent ID }; export const spec = { diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index 976b191d5de..61ea8cdcb76 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -8,6 +8,7 @@ import { OUTSTREAM } from '../src/video.js'; const BIDDER_CODE = 'operaads'; const ENDPOINT = 'https://s.adx.opera.com/ortb/v2/'; +const USER_SYNC_ENDPOINT = 'https://s.adx.opera.com/usersync/page'; const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; @@ -137,6 +138,25 @@ export const spec = { * @return {UserSync[]} The user syncs which should be dropped. */ getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + if ('iframeEnabled' in syncOptions && syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: USER_SYNC_ENDPOINT + }]; + } + if ('pixelEnabled' in syncOptions && syncOptions.pixelEnabled) { + const pixels = deepAccess(serverResponses, '0.body.pixels') + if (Array.isArray(pixels)) { + const userSyncPixels = [] + for (const pixel of pixels) { + userSyncPixels.push({ + type: 'image', + url: pixel + }) + } + return userSyncPixels; + } + } return []; }, @@ -212,7 +232,7 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { ext: {} }, user: { - id: getUserId(bidRequest) + buyeruid: getUserId(bidRequest) } } diff --git a/modules/otmBidAdapter.js b/modules/otmBidAdapter.js new file mode 100644 index 00000000000..a0e91a480a2 --- /dev/null +++ b/modules/otmBidAdapter.js @@ -0,0 +1,146 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import {logInfo, logError, getBidIdParameter, _each, getValue, isFn, isPlainObject} from '../src/utils.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'otm'; +const OTM_BID_URL = 'https://ssp.otm-r.com/adjson'; +const DEF_CUR = 'RUB' + +export const spec = { + + code: BIDDER_CODE, + url: OTM_BID_URL, + supportedMediaTypes: [ BANNER ], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!bid.params.tid; + }, + + /** + * Build bidder requests. + * + * @param validBidRequests + * @param bidderRequest + * @returns {[]} + */ + buildRequests: function (validBidRequests, bidderRequest) { + logInfo('validBidRequests', validBidRequests); + + const bidRequests = []; + let tz = new Date().getTimezoneOffset() + let referrer = ''; + if (bidderRequest && bidderRequest.refererInfo) { + referrer = bidderRequest.refererInfo.referer; + } + + _each(validBidRequests, (bid) => { + let domain = getValue(bid.params, 'domain') || '' + let tid = getValue(bid.params, 'tid') + let cur = getValue(bid.params, 'currency') || DEF_CUR + let bidid = getBidIdParameter('bidId', bid) + let transactionid = getBidIdParameter('transactionId', bid) + let auctionid = getBidIdParameter('auctionId', bid) + let bidfloor = _getBidFloor(bid) + + _each(bid.sizes, size => { + let width = 0; + let height = 0; + if (size.length && typeof size[0] === 'number' && typeof size[1] === 'number') { + width = size[0]; + height = size[1]; + } + bidRequests.push({ + method: 'GET', + url: OTM_BID_URL, + data: { + tz: tz, + w: width, + h: height, + domain: domain, + l: referrer, + s: tid, + cur: cur, + bidid: bidid, + transactionid: transactionid, + auctionid: auctionid, + bidfloor: bidfloor, + }, + }) + }) + }) + return bidRequests; + }, + + /** + * Generate response. + * + * @param serverResponse + * @param request + * @returns {[]|*[]} + */ + interpretResponse: function (serverResponse, request) { + logInfo('serverResponse', serverResponse.body); + + const responsesBody = serverResponse ? serverResponse.body : {}; + const bidResponses = []; + try { + if (responsesBody.length === 0) { + return []; + } + + _each(responsesBody, (bid) => { + if (bid.ad) { + bidResponses.push({ + requestId: bid.bidid, + cpm: bid.cpm, + width: bid.w, + height: bid.h, + creativeId: bid.creativeid, + currency: bid.currency || 'RUB', + netRevenue: true, + ad: bid.ad, + ttl: bid.ttl, + transactionId: bid.transactionid, + meta: { + advertiserDomains: bid.adDomain ? [bid.adDomain] : [] + } + }); + } + }); + } catch (error) { + logError(error); + } + + return bidResponses; + } +}; + +/** + * Get floor value + * @param bid + * @returns {null|*} + * @private + */ +function _getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return bid.params.bidfloor ? bid.params.bidfloor : 0; + } + + let floor = bid.getFloor({ + currency: DEF_CUR, + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === DEF_CUR) { + return floor.floor; + } + return 0; +} + +registerBidder(spec); diff --git a/modules/otmBidAdapter.md b/modules/otmBidAdapter.md index 4962d3a8052..e5834da5729 100644 --- a/modules/otmBidAdapter.md +++ b/modules/otmBidAdapter.md @@ -1,36 +1,37 @@ # Overview -Module Name: OTM Bidder Adapter -Module Type: Bidder Adapter -Maintainer: ? +**Module Name**: OTM Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: e.kretsu@otm-r.com # Description -You can use this adapter to get a bid from otm-r.com. +OTM Bidder Adapter for Prebid.js. About: https://otm-r.com -About us : http://otm-r.com +Use `otm` as bidder: +# Params +- `tid` required, specific id AdUnit slot. +- `domain` optional, specific custom domain. +- `bidfloor` optional. -# Test Parameters -```javascript - var adUnits = [ - { - code: 'div-otm-example', - sizes: [[320, 480]], - bids: [ - { - bidder: "otm", - params: { - tid: "99", - bidfloor: 20 - } - } - ] - } - ]; +## AdUnits configuration example ``` + var adUnits = [{ + code: 'your-slot', //use exactly the same code as your slot div id. + mediaTypes: { + banner: { + sizes: [[320, 480]] + } + }, + bids: [{ + bidder: 'otm', + params: { + tid: 'XXXXX', + domain: 'specific custom domain, if needed', + bidfloor: 20 + } + }] + }]; -Where: - -* tid - A tag id (should have low cardinality) -* bidfloor - Floor price +``` diff --git a/modules/outbrainBidAdapter.js b/modules/outbrainBidAdapter.js index 3dd9c67dc98..439570e976e 100644 --- a/modules/outbrainBidAdapter.js +++ b/modules/outbrainBidAdapter.js @@ -40,6 +40,7 @@ export const spec = { const publisher = setOnAny(validBidRequests, 'params.publisher'); const bcat = setOnAny(validBidRequests, 'params.bcat'); const badv = setOnAny(validBidRequests, 'params.badv'); + const eids = setOnAny(validBidRequests, 'userIdAsEids') const cur = CURRENCY; const endpointUrl = config.getConfig('outbrain.bidderUrl'); const timeout = bidderRequest.timeout; @@ -105,6 +106,10 @@ export const spec = { deepSetValue(request, 'regs.coppa', config.getConfig('coppa') & 1) } + if (eids) { + deepSetValue(request, 'user.ext.eids', eids); + } + return { method: 'POST', url: endpointUrl, diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index e82fdfd02a9..b5cd0232187 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -731,7 +731,7 @@ const OPEN_RTB_PROTOCOL = { return; } const request = { - id: s2sBidRequest.tid, + id: firstBidRequest.auctionId, source: {tid: s2sBidRequest.tid}, tmax: s2sConfig.timeout, imp: imps, @@ -751,7 +751,7 @@ const OPEN_RTB_PROTOCOL = { } }; - // Sets pbjs version, can be overwritten below if channel exists in s2sConfig.extPrebid + // This is no longer overwritten unless name and version explicitly overwritten by extPrebid (mergeDeep) request.ext.prebid = Object.assign(request.ext.prebid, {channel: {name: 'pbjs', version: $$PREBID_GLOBAL$$.version}}) // set debug flag if in debug mode @@ -761,7 +761,7 @@ const OPEN_RTB_PROTOCOL = { // s2sConfig video.ext.prebid is passed through openrtb to PBS if (s2sConfig.extPrebid && typeof s2sConfig.extPrebid === 'object') { - request.ext.prebid = Object.assign(request.ext.prebid, s2sConfig.extPrebid); + request.ext.prebid = mergeDeep(request.ext.prebid, s2sConfig.extPrebid); } /** diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 2d53bda4e78..366a0326054 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -11,6 +11,7 @@ const USER_SYNC_URL_IFRAME = 'https://ads.pubmatic.com/AdServer/js/user_sync.htm const USER_SYNC_URL_IMAGE = 'https://image8.pubmatic.com/AdServer/ImgSync?p='; const DEFAULT_CURRENCY = 'USD'; const AUCTION_TYPE = 1; +const GROUPM_ALIAS = {code: 'groupm', gvlid: 98}; const UNDEFINED = undefined; const DEFAULT_WIDTH = 0; const DEFAULT_HEIGHT = 0; @@ -866,10 +867,10 @@ function _handleEids(payload, validBidRequests) { function _checkMediaType(bid, newBid) { // Create a regex here to check the strings - if (bid.ext && bid.ext['BidType'] != undefined) { - newBid.mediaType = MEDIATYPE[bid.ext.BidType]; + if (bid.ext && bid.ext['bidtype'] != undefined) { + newBid.mediaType = MEDIATYPE[bid.ext.bidtype]; } else { - logInfo(LOG_WARN_PREFIX + 'bid.ext.BidType does not exist, checking alternatively for mediaType') + logInfo(LOG_WARN_PREFIX + 'bid.ext.bidtype does not exist, checking alternatively for mediaType') var adm = bid.adm; var admStr = ''; var videoRegex = new RegExp(/VAST\s+version/); @@ -1005,6 +1006,7 @@ export const spec = { code: BIDDER_CODE, gvlid: 76, supportedMediaTypes: [BANNER, VIDEO, NATIVE], + aliases: [GROUPM_ALIAS], /** * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid * diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 7dce09f0d1d..c5242c71946 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -42,10 +42,10 @@ * @function? * @summary modify bid request data * @name RtdSubmodule#getBidRequestData - * @param {SubmoduleConfig} config - * @param {UserConsentData} userConsent * @param {Object} reqBidsConfigObj * @param {function} callback + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent */ /** diff --git a/modules/saambaaBidAdapter.js b/modules/saambaaBidAdapter.js new file mode 100644 index 00000000000..2810853532d --- /dev/null +++ b/modules/saambaaBidAdapter.js @@ -0,0 +1,420 @@ +import { deepAccess, isFn, generateUUID, parseUrl, isEmpty, parseSizesInput } from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { VIDEO, BANNER } from '../src/mediaTypes.js'; +import find from 'core-js-pure/features/array/find.js'; +import includes from 'core-js-pure/features/array/includes.js'; + +const ADAPTER_VERSION = '1.0'; +const BIDDER_CODE = 'saambaa'; + +export const VIDEO_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; +export const BANNER_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; +export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; +export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'skip', 'playerSize', 'context']; +export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; + +let pubid = ''; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid(bidRequest) { + if (typeof bidRequest != 'undefined') { + if (bidRequest.bidder !== BIDDER_CODE && typeof bidRequest.params === 'undefined') { return false; } + if (bidRequest === '' || bidRequest.params.placement === '' || bidRequest.params.pubid === '') { return false; } + return true; + } else { return false; } + }, + + buildRequests(bids, bidderRequest) { + let requests = []; + let videoBids = bids.filter(bid => isVideoBidValid(bid)); + let bannerBids = bids.filter(bid => isBannerBidValid(bid)); + videoBids.forEach(bid => { + pubid = getVideoBidParam(bid, 'pubid'); + requests.push({ + method: 'POST', + url: VIDEO_ENDPOINT + pubid, + data: createVideoRequestData(bid, bidderRequest), + bidRequest: bid + }); + }); + + bannerBids.forEach(bid => { + pubid = getBannerBidParam(bid, 'pubid'); + + requests.push({ + method: 'POST', + url: BANNER_ENDPOINT + pubid, + data: createBannerRequestData(bid, bidderRequest), + bidRequest: bid + }); + }); + return requests; + }, + + interpretResponse(serverResponse, {bidRequest}) { + let response = serverResponse.body; + if (response !== null && isEmpty(response) == false) { + if (isVideoBid(bidRequest)) { + let bidResponse = { + requestId: response.id, + bidderCode: BIDDER_CODE, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ttl: response.seatbid[0].bid[0].ttl || 60, + creativeId: response.seatbid[0].bid[0].crid, + currency: response.cur, + meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, + mediaType: VIDEO, + netRevenue: true + } + + if (response.seatbid[0].bid[0].adm) { + bidResponse.vastXml = response.seatbid[0].bid[0].adm; + bidResponse.adResponse = { + content: response.seatbid[0].bid[0].adm + }; + } else { + bidResponse.vastUrl = response.seatbid[0].bid[0].nurl; + } + + return bidResponse; + } else { + return { + requestId: response.id, + bidderCode: BIDDER_CODE, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ad: response.seatbid[0].bid[0].adm, + ttl: response.seatbid[0].bid[0].ttl || 60, + creativeId: response.seatbid[0].bid[0].crid, + currency: response.cur, + meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, + mediaType: BANNER, + netRevenue: true + } + } + } + } +}; + +function isBannerBid(bid) { + return deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); +} + +function isVideoBid(bid) { + return deepAccess(bid, 'mediaTypes.video'); +} + +function getBannerBidFloor(bid) { + let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'banner', size: '*' }) : {}; + return floorInfo.floor || getBannerBidParam(bid, 'bidfloor'); +} + +function getVideoBidFloor(bid) { + let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'video', size: '*' }) : {}; + return floorInfo.floor || getVideoBidParam(bid, 'bidfloor'); +} + +function isVideoBidValid(bid) { + return isVideoBid(bid) && getVideoBidParam(bid, 'pubid') && getVideoBidParam(bid, 'placement'); +} + +function isBannerBidValid(bid) { + return isBannerBid(bid) && getBannerBidParam(bid, 'pubid') && getBannerBidParam(bid, 'placement'); +} + +function getVideoBidParam(bid, key) { + return deepAccess(bid, 'params.video.' + key) || deepAccess(bid, 'params.' + key); +} + +function getBannerBidParam(bid, key) { + return deepAccess(bid, 'params.banner.' + key) || deepAccess(bid, 'params.' + key); +} + +function isMobile() { + return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); +} + +function isConnectedTV() { + return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); +} + +function getDoNotTrack() { + return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; +} + +function findAndFillParam(o, key, value) { + try { + if (typeof value === 'function') { + o[key] = value(); + } else { + o[key] = value; + } + } catch (ex) {} +} + +function getOsVersion() { + let clientStrings = [ + { s: 'Android', r: /Android/ }, + { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, + { s: 'Mac OS X', r: /Mac OS X/ }, + { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, + { s: 'Linux', r: /(Linux|X11)/ }, + { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, + { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, + { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, + { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, + { s: 'Windows Vista', r: /Windows NT 6.0/ }, + { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, + { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, + { s: 'UNIX', r: /UNIX/ }, + { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ } + ]; + let cs = find(clientStrings, cs => cs.r.test(navigator.userAgent)); + return cs ? cs.s : 'unknown'; +} + +function getFirstSize(sizes) { + return (sizes && sizes.length) ? sizes[0] : { w: undefined, h: undefined }; +} + +function parseSizes(sizes) { + return parseSizesInput(sizes).map(size => { + let [ width, height ] = size.split('x'); + return { + w: parseInt(width, 10) || undefined, + h: parseInt(height, 10) || undefined + }; + }); +} + +function getVideoSizes(bid) { + return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); +} + +function getBannerSizes(bid) { + return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); +} + +function getTopWindowReferrer() { + try { + return window.top.document.referrer; + } catch (e) { + return ''; + } +} + +function getVideoTargetingParams(bid) { + const result = {}; + const excludeProps = ['playerSize', 'context', 'w', 'h']; + Object.keys(Object(bid.mediaTypes.video)) + .filter(key => !includes(excludeProps, 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; +} + +function createVideoRequestData(bid, bidderRequest) { + let topLocation = getTopWindowLocation(bidderRequest); + let topReferrer = getTopWindowReferrer(); + + // if size is explicitly given via adapter params + let paramSize = getVideoBidParam(bid, 'size'); + let sizes = []; + let coppa = config.getConfig('coppa'); + + if (typeof paramSize !== 'undefined' && paramSize != '') { + sizes = parseSizes(paramSize); + } else { + sizes = getVideoSizes(bid); + } + const firstSize = getFirstSize(sizes); + let floor = (getVideoBidFloor(bid) == null || typeof getVideoBidFloor(bid) == 'undefined') ? 0.5 : getVideoBidFloor(bid); + let video = getVideoTargetingParams(bid); + const o = { + 'device': { + 'langauge': (global.navigator.language).split('-')[0], + 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), + 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, + 'js': 1, + 'os': getOsVersion() + }, + 'at': 2, + 'site': {}, + 'tmax': 3000, + 'cur': ['USD'], + 'id': bid.bidId, + 'imp': [], + 'regs': { + 'ext': { + } + }, + 'user': { + 'ext': { + } + } + }; + + o.site['page'] = topLocation.href; + o.site['domain'] = topLocation.hostname; + o.site['search'] = topLocation.search; + o.site['domain'] = topLocation.hostname; + o.site['ref'] = topReferrer; + o.site['mobile'] = isMobile() ? 1 : 0; + const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; + + o.device['dnt'] = getDoNotTrack() ? 1 : 0; + + findAndFillParam(o.site, 'name', function() { + return global.top.document.title; + }); + + findAndFillParam(o.device, 'h', function() { + return global.screen.height; + }); + findAndFillParam(o.device, 'w', function() { + return global.screen.width; + }); + + let placement = getVideoBidParam(bid, 'placement'); + + for (let j = 0; j < sizes.length; j++) { + o.imp.push({ + 'id': '' + j, + 'displaymanager': '' + BIDDER_CODE, + 'displaymanagerver': '' + ADAPTER_VERSION, + 'tagId': placement, + 'bidfloor': floor, + 'bidfloorcur': 'USD', + 'secure': secure, + 'video': Object.assign({ + 'id': generateUUID(), + 'pos': 0, + 'w': firstSize.w, + 'h': firstSize.h, + 'mimes': DEFAULT_MIMES + }, video) + + }); + } + if (coppa) { + o.regs.ext = {'coppa': 1}; + } + if (bidderRequest && bidderRequest.gdprConsent) { + let { gdprApplies, consentString } = bidderRequest.gdprConsent; + o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; + o.user.ext = {'consent': consentString}; + } + + return o; +} + +function getTopWindowLocation(bidderRequest) { + let url = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer; + return parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); +} + +function createBannerRequestData(bid, bidderRequest) { + let topLocation = getTopWindowLocation(bidderRequest); + let topReferrer = getTopWindowReferrer(); + + // if size is explicitly given via adapter params + + let paramSize = getBannerBidParam(bid, 'size'); + let sizes = []; + let coppa = config.getConfig('coppa'); + if (typeof paramSize !== 'undefined' && paramSize != '') { + sizes = parseSizes(paramSize); + } else { + sizes = getBannerSizes(bid); + } + + let floor = (getBannerBidFloor(bid) == null || typeof getBannerBidFloor(bid) == 'undefined') ? 0.1 : getBannerBidFloor(bid); + const o = { + 'device': { + 'langauge': (global.navigator.language).split('-')[0], + 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), + 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, + 'js': 1 + }, + 'at': 2, + 'site': {}, + 'tmax': 3000, + 'cur': ['USD'], + 'id': bid.bidId, + 'imp': [], + 'regs': { + 'ext': { + } + }, + 'user': { + 'ext': { + } + } + }; + + o.site['page'] = topLocation.href; + o.site['domain'] = topLocation.hostname; + o.site['search'] = topLocation.search; + o.site['domain'] = topLocation.hostname; + o.site['ref'] = topReferrer; + o.site['mobile'] = isMobile() ? 1 : 0; + const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; + + o.device['dnt'] = getDoNotTrack() ? 1 : 0; + + findAndFillParam(o.site, 'name', function() { + return global.top.document.title; + }); + + findAndFillParam(o.device, 'h', function() { + return global.screen.height; + }); + findAndFillParam(o.device, 'w', function() { + return global.screen.width; + }); + + let placement = getBannerBidParam(bid, 'placement'); + for (let j = 0; j < sizes.length; j++) { + let size = sizes[j]; + + o.imp.push({ + 'id': '' + j, + 'displaymanager': '' + BIDDER_CODE, + 'displaymanagerver': '' + ADAPTER_VERSION, + 'tagId': placement, + 'bidfloor': floor, + 'bidfloorcur': 'USD', + 'secure': secure, + 'banner': { + 'id': generateUUID(), + 'pos': 0, + 'w': size['w'], + 'h': size['h'] + } + }); + } + if (coppa) { + o.regs.ext = {'coppa': 1}; + } + if (bidderRequest && bidderRequest.gdprConsent) { + let { gdprApplies, consentString } = bidderRequest.gdprConsent; + o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; + o.user.ext = {'consent': consentString}; + } + + return o; +} +registerBidder(spec); diff --git a/modules/saambaaBidAdapter.md b/modules/saambaaBidAdapter.md index 2d391da7628..d58e3f0abfa 100755 --- a/modules/saambaaBidAdapter.md +++ b/modules/saambaaBidAdapter.md @@ -1,69 +1,66 @@ -# Overview - -``` -Module Name: Saambaa Bidder Adapter -Module Type: Bidder Adapter -Maintainer: matt.voigt@saambaa.com -``` - -# Description - -Connects to Saambaa exchange for bids. - -Saambaa bid adapter supports Banner and Video ads currently. - -For more informatio - -# Sample Display Ad Unit: For Publishers -```javascript - -var displayAdUnit = [ -{ - code: 'display', - mediaTypes: { - banner: { - sizes: [[300, 250],[320, 50]] - } - } - bids: [{ - bidder: 'saambaa', - params: { - pubid: '121ab139faf7ac67428a23f1d0a9a71b', - placement: 1234, - size: '320x50' - } - }] -}]; -``` - -# Sample Video Ad Unit: For Publishers -```javascript - -var videoAdUnit = { - code: 'video', - sizes: [320,480], - mediaTypes: { - video: { - playerSize : [[320, 480]], - context: 'instream' - } - }, - bids: [ - { - bidder: 'saambaa', - params: { - pubid: '121ab139faf7ac67428a23f1d0a9a71b', - placement: 1234, - size: "320x480", - video: { - id: 123, - skip: 1, - mimes : ['video/mp4', 'application/javascript'], - playbackmethod : [2,6], - maxduration: 30 - } - } - } - ] - }; +# Overview + +``` +Module Name: Saambaa Bidder Adapter +Module Type: Bidder Adapter +Maintainer: matt.voigt@saambaa.com +``` + +# Description + +Connects to Saambaa exchange for bids. + +Saambaa bid adapter supports Banner and Video ads currently. + +For more informatio + +# Sample Display Ad Unit: For Publishers +```javascript + +var displayAdUnit = [ +{ + code: 'display', + mediaTypes: { + banner: { + sizes: [[300, 250],[320, 50]] + } + } + bids: [{ + bidder: 'saambaa', + params: { + pubid: '121ab139faf7ac67428a23f1d0a9a71b', + placement: 1234, + size: '320x50' + } + }] +}]; +``` + +# Sample Video Ad Unit: For Publishers +```javascript + +var videoAdUnit = { + code: 'video', + sizes: [320,480], + mediaTypes: { + video: { + playerSize : [[320, 480]], + context: 'instream', + skip: 1, + mimes : ['video/mp4', 'application/javascript'], + playbackmethod : [2,6], + maxduration: 30 + } + }, + bids: [ + { + bidder: 'saambaa', + params: { + pubid: '121ab139faf7ac67428a23f1d0a9a71b', + placement: 1234, + size: "320x480" + } + } + ] + }; ``` \ No newline at end of file diff --git a/modules/seedtagBidAdapter.js b/modules/seedtagBidAdapter.js index cb646fe10c3..bae27d41028 100644 --- a/modules/seedtagBidAdapter.js +++ b/modules/seedtagBidAdapter.js @@ -13,6 +13,11 @@ const ALLOWED_PLACEMENTS = { banner: true, video: true } + +// Global Vendor List Id +// https://iabeurope.eu/vendor-list-tcf-v2-0/ +const GVLID = 157; + const mediaTypesMap = { [BANNER]: 'display', [VIDEO]: 'video' @@ -158,6 +163,7 @@ export function getTimeoutUrl (data) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, aliases: [SEEDTAG_ALIAS], supportedMediaTypes: [BANNER, VIDEO], /** diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index dd389b42098..63fdeb2fcef 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -5,7 +5,7 @@ import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = 'smaato'; const SMAATO_ENDPOINT = 'https://prebid.ad.smaato.net/oapi/prebid'; -const SMAATO_CLIENT = 'prebid_js_$prebid.version$_1.4' +const SMAATO_CLIENT = 'prebid_js_$prebid.version$_1.5' const CURRENCY = 'USD'; const buildOpenRtbBidRequest = (bidRequest, bidderRequest) => { @@ -37,6 +37,11 @@ const buildOpenRtbBidRequest = (bidRequest, bidderRequest) => { user: { ext: {} }, + source: { + ext: { + schain: bidRequest.schain + } + }, ext: { client: SMAATO_CLIENT } diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js index da63331cd0f..00c962445d9 100644 --- a/modules/smartxBidAdapter.js +++ b/modules/smartxBidAdapter.js @@ -161,11 +161,20 @@ export const spec = { domain: domain, publisher: { id: publisherId + }, + content: { + ext: { + prebid: { + name: 'pbjs', + version: '$prebid.version$' + } + } } }, device: device, at: at, - cur: cur + cur: cur, + ext: {} }; const userExt = {}; @@ -194,6 +203,8 @@ export const spec = { }; } + // requestPayload.user.ext.ver = pbjs.version; + // Targeting if (getBidIdParameter('data', bid.params.user)) { var targetingarr = []; @@ -336,6 +347,7 @@ function createOutstreamConfig(bid) { let confTitle = getBidIdParameter('title', bid.renderer.config.outstream_options); let confSkipOffset = getBidIdParameter('skipOffset', bid.renderer.config.outstream_options); let confDesiredBitrate = getBidIdParameter('desiredBitrate', bid.renderer.config.outstream_options); + let confVisibilityThreshold = getBidIdParameter('visibilityThreshold', bid.renderer.config.outstream_options); let elementId = getBidIdParameter('slot', bid.renderer.config.outstream_options) || bid.adUnitCode; logMessage('[SMARTX][renderer] Handle SmartX outstream renderer'); @@ -384,6 +396,10 @@ function createOutstreamConfig(bid) { smartPlayObj.desiredBitrate = confDesiredBitrate; } + if (confVisibilityThreshold) { + smartPlayObj.visibilityThreshold = confVisibilityThreshold; + } + smartPlayObj.adResponse = bid.vastContent; const divID = '[id="' + elementId + '"]'; diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index 01966f3d6b1..c5fc07320d8 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -83,8 +83,15 @@ export const spec = { 'lib_name': 'prebid', 'lib_v': '$prebid.version$', 'us': 0, + }; + const fpd = config.getConfig('ortb2'); + + if (fpd) { + payload.fpd = JSON.stringify(fpd); + } + if (config.getConfig('userSync') && config.getConfig('userSync').syncsPerBidder) { payload.us = config.getConfig('userSync').syncsPerBidder; } diff --git a/modules/tappxBidAdapter.js b/modules/tappxBidAdapter.js index a026b2cd6a6..e2225156128 100644 --- a/modules/tappxBidAdapter.js +++ b/modules/tappxBidAdapter.js @@ -7,9 +7,10 @@ import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; const BIDDER_CODE = 'tappx'; +const GVLID_CODE = 628; const TTL = 360; const CUR = 'USD'; -const TAPPX_BIDDER_VERSION = '0.1.1004'; +const TAPPX_BIDDER_VERSION = '0.1.1005'; const TYPE_CNN = 'prebidjs'; const LOG_PREFIX = '[TAPPX]: '; const VIDEO_SUPPORT = ['instream', 'outstream']; @@ -42,6 +43,7 @@ var hostDomain; export const spec = { code: BIDDER_CODE, + gvlid: GVLID_CODE, supportedMediaTypes: [BANNER, VIDEO], /** diff --git a/modules/trustxBidAdapter.js b/modules/trustxBidAdapter.js index e74dbd6b4d8..b2f0936e3c7 100644 --- a/modules/trustxBidAdapter.js +++ b/modules/trustxBidAdapter.js @@ -76,7 +76,7 @@ export const spec = { if (!userIdAsEids) { userIdAsEids = bid.userIdAsEids; } - const {params: {uid, keywords}, mediaTypes, bidId, adUnitCode, rtd} = bid; + const {params: {uid, keywords}, mediaTypes, bidId, adUnitCode, rtd, ortb2Imp} = bid; bidsMap[bidId] = bid; const bidFloor = _getFloor(mediaTypes || {}, bid); if (rtd) { @@ -102,6 +102,15 @@ export const spec = { } }; + if (ortb2Imp && ortb2Imp.ext && ortb2Imp.ext.data) { + impObj.ext.data = ortb2Imp.ext.data; + if (impObj.ext.data.adserver && impObj.ext.data.adserver.adslot) { + impObj.ext.gpid = impObj.ext.data.adserver.adslot.toString(); + } else { + impObj.ext.gpid = ortb2Imp.ext.data.pbadslot && ortb2Imp.ext.data.pbadslot.toString(); + } + } + if (!isEmpty(keywords)) { if (!pageKeywords) { pageKeywords = keywords; @@ -429,9 +438,10 @@ function addSegments(name, segName, segments, data, bidConfigName) { if (segments && segments.length) { data.push({ name: name, - segment: segments.map((seg) => { - return {name: segName, value: seg}; - }) + segment: segments + .map((seg) => seg && (seg.id || seg)) + .filter((seg) => seg && (typeof seg === 'string' || typeof seg === 'number')) + .map((seg) => ({ name: segName, value: seg.toString() })) }); } else if (bidConfigName) { const configData = config.getConfig('ortb2.user.data'); @@ -445,9 +455,10 @@ function addSegments(name, segName, segments, data, bidConfigName) { if (segData && segData.length) { data.push({ name: name, - segment: segData.map((seg) => { - return {name: segName, value: seg}; - }) + segment: segData + .map((seg) => seg && (seg.id || seg)) + .filter((seg) => seg && (typeof seg === 'string' || typeof seg === 'number')) + .map((seg) => ({ name: segName, value: seg.toString() })) }); } } diff --git a/modules/undertoneBidAdapter.js b/modules/undertoneBidAdapter.js index d9c9f84e050..fda6f47b2af 100644 --- a/modules/undertoneBidAdapter.js +++ b/modules/undertoneBidAdapter.js @@ -98,9 +98,16 @@ export const spec = { 'commons': commons }; const referer = bidderRequest.refererInfo.referer; + const canonicalUrl = getCanonicalUrl(); + if (referer) { + commons.referrer = referer; + } + if (canonicalUrl) { + commons.canonicalUrl = canonicalUrl; + } const hostname = parseUrl(referer).hostname; let domain = extractDomainFromHost(hostname); - const pageUrl = getCanonicalUrl() || referer; + const pageUrl = canonicalUrl || referer; const pubid = validBidRequests[0].params.publisherId; let reqUrl = `${URL}?pid=${pubid}&domain=${domain}`; diff --git a/modules/vidoomyBidAdapter.js b/modules/vidoomyBidAdapter.js index e69386f264a..182284410e6 100644 --- a/modules/vidoomyBidAdapter.js +++ b/modules/vidoomyBidAdapter.js @@ -9,7 +9,17 @@ const ENDPOINT = `https://d.vidoomy.com/api/rtbserver/prebid/`; const BIDDER_CODE = 'vidoomy'; const GVLID = 380; -const COOKIE_SYNC_JSON = 'https://vpaid.vidoomy.com/sync/urls.json'; +const COOKIE_SYNC_FALLBACK_URLS = [ + 'https://x.bidswitch.net/sync?ssp=vidoomy', + 'https://ib.adnxs.com/getuid?https%3A%2F%2Fa-prebid.vidoomy.com%2Fsetuid%3Fbidder%3Dadnxs%26gdpr%3D{{GDPR}}%26gdpr_consent%3D{{GDPR_CONSENT}}%26uid%3D%24UID', + 'https://pixel-sync.sitescout.com/dmp/pixelSync?nid=120&redir=https%3A%2F%2Fa.vidoomy.com%2Fapi%2Frtbserver%2Fcookie%3Fi%3DCEN%26uid%3D%7BuserId%7D', + 'https://sync.1rx.io/usersync2/vidoomy?redir=https%3A%2F%2Fa.vidoomy.com%2Fapi%2Frtbserver%2Fcookie%3Fi%3DUN%26uid%3D%5BRX_UUID%5D', + 'https://rtb.openx.net/sync/prebid?gdpr={{GDPR}}&gdpr_consent={{GDPR_CONSENT}}&r=https%3A%2F%2Fa-prebid.vidoomy.com%2Fsetuid%3Fbidder%3Dopenx%26uid%3D$%7BUID%7D', + 'https://ads.pubmatic.com/AdServer/js/user_sync.html?gdpr={{GDPR}}&gdpr_consent={{GDPR_CONSENT}}&us_privacy=&predirect=https%3A%2F%2Fa-prebid.vidoomy.com%2Fsetuid%3Fbidder%3Dpubmatic%26gdpr%3D{{GDPR}}%26gdpr_consent%3D{{GDPR_CONSENT}}%26uid%3D', + 'https://cm.adform.net/cookie?redirect_url=https%3A%2F%2Fa-prebid.vidoomy.com%2Fsetuid%3Fbidder%3Dadf%26gdpr%3D{{GDPR}}%26gdpr_consent%3D{{GDPR_CONSENT}}%26uid%3D%24UID', + 'https://ups.analytics.yahoo.com/ups/58531/occ?gdpr={{GDPR}}&gdpr_consent={{GDPR_CONSENT}}', + 'https://ap.lijit.com/pixel?redir=https%3A%2F%2Fa-prebid.vidoomy.com%2Fsetuid%3Fbidder%3Dsovrn%26gdpr%3D{{GDPR}}%26gdpr_consent%3D{{GDPR_CONSENT}}%26uid%3D%24UID' +]; const isBidRequestValid = bid => { if (!bid.params) { @@ -36,7 +46,7 @@ const isBidRequestValid = bid => { }; const isBidResponseValid = bid => { - if (!bid.requestId || !bid.cpm || !bid.ttl || !bid.currency) { + if (!bid || !bid.requestId || !bid.cpm || !bid.ttl || !bid.currency) { return false; } switch (bid.mediaType) { @@ -67,35 +77,33 @@ const buildRequests = (validBidRequests, bidderRequest) => { const videoContext = deepAccess(bid, 'mediaTypes.video.context'); - const queryParams = []; - queryParams.push(['id', bid.params.id]); - queryParams.push(['adtype', adType]); - queryParams.push(['w', w]); - queryParams.push(['h', h]); - queryParams.push(['pos', parseInt(bid.params.position) || 1]); - queryParams.push(['ua', navigator.userAgent]); - queryParams.push(['l', navigator.language && navigator.language.indexOf('-') !== -1 ? navigator.language.split('-')[0] : '']); - queryParams.push(['dt', /Mobi/.test(navigator.userAgent) ? 2 : 1]); - queryParams.push(['pid', bid.params.pid]); - queryParams.push(['requestId', bid.bidId]); - queryParams.push(['d', getDomainWithoutSubdomain(hostname)]); - queryParams.push(['sp', encodeURIComponent(aElement.href)]); + const queryParams = { + id: bid.params.id, + adtype: adType, + w, + h, + pos: parseInt(bid.params.position) || 1, + ua: navigator.userAgent, + l: navigator.language && navigator.language.indexOf('-') !== -1 ? navigator.language.split('-')[0] : '', + dt: /Mobi/.test(navigator.userAgent) ? 2 : 1, + pid: bid.params.pid, + requestId: bid.bidId, + d: getDomainWithoutSubdomain(hostname), + sp: encodeURIComponent(aElement.href), + usp: bidderRequest.uspConsent || '', + coppa: !!config.getConfig('coppa'), + videoContext: videoContext || '' + }; + if (bidderRequest.gdprConsent) { - queryParams.push(['gdpr', bidderRequest.gdprConsent.gdprApplies]); - queryParams.push(['gdprcs', bidderRequest.gdprConsent.consentString]); + queryParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + queryParams.gdprcs = bidderRequest.gdprConsent.consentString; } - queryParams.push(['usp', bidderRequest.uspConsent || '']); - queryParams.push(['coppa', !!config.getConfig('coppa')]); - - const rawQueryParams = queryParams.map(qp => qp.join('=')).join('&'); - cookieSync(bidderRequest) - - const url = `${ENDPOINT}?${rawQueryParams}`; return { method: 'GET', - url: url, - data: {videoContext} + url: ENDPOINT, + data: queryParams } }); return serverRequests; @@ -117,6 +125,7 @@ const render = (bid) => { const interpretResponse = (serverResponse, bidRequest) => { try { let responseBody = serverResponse.body; + if (!responseBody) return; if (responseBody.mediaType === 'video') { responseBody.ad = responseBody.vastUrl; const videoContext = bidRequest.data.videoContext; @@ -185,6 +194,21 @@ const interpretResponse = (serverResponse, bidRequest) => { } }; +function getUserSyncs (syncOptions, responses, gdprConsent, uspConsent) { + if (syncOptions.iframeEnabled || syncOptions.pixelEnabled) { + const pixelType = syncOptions.pixelEnabled ? 'image' : 'iframe'; + const urls = deepAccess(responses, '0.body.pixels') || COOKIE_SYNC_FALLBACK_URLS; + + return [].concat(urls).map(url => ({ + type: pixelType, + url: url + .replace('{{GDPR}}', gdprConsent ? gdprConsent.gdprApplies : '0') + .replace('{{GDPR_CONSENT}}', gdprConsent ? encodeURIComponent(gdprConsent.consentString) : '') + .replace('{{USP_CONSENT}}', uspConsent ? encodeURIComponent(uspConsent) : '') + })); + } +}; + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], @@ -192,84 +216,11 @@ export const spec = { buildRequests, interpretResponse, gvlid: GVLID, + getUserSyncs, }; registerBidder(spec); -let cookieSynced = false; -function cookieSync(bidderRequest) { - if (cookieSynced) return; - const xhr = new XMLHttpRequest(); - xhr.open('GET', COOKIE_SYNC_JSON) - xhr.addEventListener('load', function () { - const macro = Macro({ - gpdr: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.gdprApplies : '0', - gpdr_consent: bidderRequest.gdprConsent ? bidderRequest.gdprConsent.consentString : '', - }); - JSON.parse(this.responseText).filter(Boolean).forEach(url => { - firePixel(macro.replace(url)) - }) - }) - xhr.send() - cookieSynced = true; -} - -function firePixel(url) { - const img = document.createElement('img'); - img.width = 1; - img.height = 1; - img.src = url; - document.body.appendChild(img); - setTimeout(() => { - img.remove(); - }, 10000) -} - -function normalizeKey (x) { - return x.replace(/_/g, '').toLowerCase(); -} - -function Macro (obj) { - const macros = {}; - for (const key in obj) { - macros[normalizeKey(key)] = obj[key]; - } - - const set = (key, value) => { - macros[normalizeKey(key)] = typeof value === 'function' ? value : String(value); - }; - - return { - set, - setAll (obj) { - for (const key in obj) { - macros[normalizeKey(key)] = set(obj[key]); - } - }, - replace (string, extraMacros = {}) { - const allMacros = { - ...macros, - ...extraMacros, - }; - const regexes = [ - /{{\s*([a-zA-Z0-9_]+)\s*}}/g, - /\$\$\s*([a-zA-Z0-9_]+)\s*\$\$/g, - /\[\s*([a-zA-Z0-9_]+)\s*\]/g, - /\{\s*([a-zA-Z0-9_]+)\s*\}/g, - ]; - regexes.forEach(regex => { - string = string.replace(regex, (str, x) => { - x = normalizeKey(x); - let value = allMacros[x]; - value = typeof value === 'function' ? value(allMacros) : value; - return !value && value !== 0 ? '' : value; - }); - }); - return string; - }, - }; -} - function getDomainWithoutSubdomain (hostname) { const parts = hostname.split('.'); const newParts = []; diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index 1d80ea79e99..3442cbc8dd8 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -203,7 +203,7 @@ export const spec = { }, onTimeout: function(timeoutData) { // Call '/track/bid_timeout' with timeout data - triggerPixel(buildUrl(TRACK_TIMEOUT_PATH) + '?data=' + JSON.stringify(timeoutData)); + triggerPixel(buildUrl(TRACK_TIMEOUT_PATH) + '//' + JSON.stringify(timeoutData)); } }; diff --git a/modules/yahoosspBidAdapter.js b/modules/yahoosspBidAdapter.js index ac91596f8d0..101cb0ca9e3 100644 --- a/modules/yahoosspBidAdapter.js +++ b/modules/yahoosspBidAdapter.js @@ -144,9 +144,9 @@ function getAdapterMode() { function getResponseFormat(bid) { const adm = bid.adm; - if (adm.includes('o2playerSettings') || adm.includes('YAHOO.VideoPlatform.VideoPlayer') || adm.includes('AdPlacement')) { + if (adm.indexOf('o2playerSettings') !== -1 || adm.indexOf('YAHOO.VideoPlatform.VideoPlayer') !== -1 || adm.indexOf('AdPlacement') !== -1) { return BANNER; - } else if (adm.includes('VAST')) { + } else if (adm.indexOf('VAST') !== -1) { return VIDEO; } }; @@ -188,23 +188,23 @@ function validateAppendObject(validationType, allowedKeys, inputObject, appendTo for (const objectKey in inputObject) { switch (validationType) { case 'string': - if (allowedKeys.includes(objectKey) && isStr(inputObject[objectKey])) { + if (allowedKeys.indexOf(objectKey) !== -1 && isStr(inputObject[objectKey])) { outputObject[objectKey] = inputObject[objectKey]; }; break; case 'number': - if (allowedKeys.includes(objectKey) && isNumber(inputObject[objectKey])) { + if (allowedKeys.indexOf(objectKey) !== -1 && isNumber(inputObject[objectKey])) { outputObject[objectKey] = inputObject[objectKey]; }; break; case 'array': - if (allowedKeys.includes(objectKey) && isArray(inputObject[objectKey])) { + if (allowedKeys.indexOf(objectKey) !== -1 && isArray(inputObject[objectKey])) { outputObject[objectKey] = inputObject[objectKey]; }; break; case 'object': - if (allowedKeys.includes(objectKey) && isPlainObject(inputObject[objectKey])) { + if (allowedKeys.indexOf(objectKey) !== -1 && isPlainObject(inputObject[objectKey])) { outputObject[objectKey] = inputObject[objectKey]; }; break; diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index 994098cf5c8..c2f2b79a3b7 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -1,7 +1,7 @@ import { _each, isPlainObject, isArray, deepAccess } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' import find from 'core-js-pure/features/array/find.js' -import { VIDEO, BANNER } from '../src/mediaTypes.js' +import { VIDEO, BANNER, NATIVE } from '../src/mediaTypes.js' import { Renderer } from '../src/Renderer.js' import { config } from '../src/config.js'; @@ -15,7 +15,7 @@ const GVLID = 70 export const spec = { code: BIDDER_CODE, gvlid: GVLID, - supportedMediaTypes: [VIDEO, BANNER], + supportedMediaTypes: [VIDEO, BANNER, NATIVE], isBidRequestValid: function (bid) { if (bid && bid.params && bid.params.adslotId && bid.params.supplyId) { @@ -149,6 +149,27 @@ export const spec = { } } + if (isNative(bidRequest, adType)) { + const url = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}${pvId}` + bidResponse.adUrl = url + bidResponse.mediaType = NATIVE + const nativeImageAssetObj = find(matchedBid.native.assets, e => e.id === 2) + const nativeImageAsset = nativeImageAssetObj ? nativeImageAssetObj.img : {url: '', w: 0, h: 0}; + const nativeTitleAsset = find(matchedBid.native.assets, e => e.id === 1) + const nativeBodyAsset = find(matchedBid.native.assets, e => e.id === 3) + bidResponse.native = { + title: nativeTitleAsset ? nativeTitleAsset.title.text : '', + body: nativeBodyAsset ? nativeBodyAsset.data.value : '', + image: { + url: nativeImageAsset.url, + width: nativeImageAsset.w, + height: nativeImageAsset.h, + }, + clickUrl: matchedBid.native.link.url, + impressionTrackers: matchedBid.native.imptrackers, + }; + } + bidResponses.push(bidResponse) } }) @@ -162,16 +183,26 @@ export const spec = { * @param {String} adtype * @returns {Boolean} */ -function isVideo (format, adtype) { +function isVideo(format, adtype) { return deepAccess(format, 'mediaTypes.video') && adtype.toLowerCase() === 'video' } +/** + * Is this a native format? + * @param {Object} format + * @param {String} adtype + * @returns {Boolean} + */ +function isNative(format, adtype) { + return deepAccess(format, 'mediaTypes.native') && adtype.toLowerCase() === 'native' +} + /** * Is this an outstream context? * @param {Object} format * @returns {Boolean} */ -function isOutstream (format) { +function isOutstream(format) { let context = deepAccess(format, 'mediaTypes.video.context') return (context === 'outstream') } @@ -181,7 +212,7 @@ function isOutstream (format) { * @param {Object} format * @returns {Array} */ -function getPlayerSize (format) { +function getPlayerSize(format) { let playerSize = deepAccess(format, 'mediaTypes.video.playerSize') return (playerSize && isArray(playerSize[0])) ? playerSize[0] : playerSize } @@ -191,7 +222,7 @@ function getPlayerSize (format) { * @param {String} size * @returns {Array} */ -function parseSize (size) { +function parseSize(size) { return size.split('x').map(Number) } @@ -200,7 +231,7 @@ function parseSize (size) { * @param {Array} eids * @returns {String} */ -function createUserIdString (eids) { +function createUserIdString(eids) { let str = [] for (let i = 0; i < eids.length; i++) { str.push(eids[i].source + ':' + eids[i].uids[0].id) @@ -213,7 +244,7 @@ function createUserIdString (eids) { * @param {Object} obj * @returns {String} */ -function createQueryString (obj) { +function createQueryString(obj) { let str = [] for (var p in obj) { if (obj.hasOwnProperty(p)) { @@ -233,7 +264,7 @@ function createQueryString (obj) { * @param {Object} obj * @returns {String} */ -function createTargetingString (obj) { +function createTargetingString(obj) { let str = [] for (var p in obj) { if (obj.hasOwnProperty(p)) { @@ -250,7 +281,7 @@ function createTargetingString (obj) { * @param {Object} schain * @returns {String} */ -function createSchainString (schain) { +function createSchainString(schain) { const ver = schain.ver || '' const complete = (schain.complete === 1 || schain.complete === 0) ? schain.complete : '' const keys = ['asi', 'sid', 'hp', 'rid', 'name', 'domain', 'ext'] diff --git a/modules/yieldlabBidAdapter.md b/modules/yieldlabBidAdapter.md index e3360ab10be..1f52e26f5c7 100644 --- a/modules/yieldlabBidAdapter.md +++ b/modules/yieldlabBidAdapter.md @@ -11,53 +11,96 @@ Maintainer: solutions@yieldlab.de Module that connects to Yieldlab's demand sources # Test Parameters + +```javascript +const adUnits = [ + { + code: 'banner', + sizes: [ [ 728, 90 ] ], + bids: [{ + bidder: 'yieldlab', + params: { + adslotId: '5220336', + supplyId: '1381604', + targeting: { + key1: 'value1', + key2: 'value2' + }, + extId: 'abc', + iabContent: { + id: 'some_id', + episode: '1', + title: 'some title', + series: 'some series', + season: 's1', + artist: 'John Doe', + genre: 'some genre', + isrc: 'CC-XXX-YY-NNNNN', + url: 'http://foo_url.de', + cat: [ 'IAB1-1', 'IAB1-2', 'IAB2-10' ], + context: '7', + keywords: ['k1', 'k2'], + live: '0' + } + } + }] + }, + { + code: 'video', + sizes: [ [ 640, 480 ] ], + mediaTypes: { + video: { + context: 'instream' // or 'outstream' + } + }, + bids: [{ + bidder: 'yieldlab', + params: { + adslotId: '5220339', + supplyId: '1381604' + } + }] + }, + { + code: 'native', + mediaTypes: { + native: { + // native config + } + }, + bids: [{ + bidder: 'yieldlab', + params: { + adslotId: '5220339', + supplyId: '1381604' + } + }] + } +]; ``` - var adUnits = [ - { - code: "banner", - sizes: [[728, 90]], - bids: [{ - bidder: "yieldlab", - params: { - adslotId: "5220336", - supplyId: "1381604", - targeting: { - key1: "value1", - key2: "value2" - }, - extId: "abc", - iabContent: { - id: "some_id", - episode: "1", - title: "some title", - series: "some series", - season: "s1", - artist: "John Doe", - genre: "some genre", - isrc: "CC-XXX-YY-NNNNN", - url: "http://foo_url.de", - cat: ["IAB1-1", "IAB1-2", "IAB2-10"], - context: "7", - keywords: ["k1", "k2"], - live: "0" - } - } - }] - }, { - code: "video", - sizes: [[640, 480]], - mediaTypes: { - video: { - context: "instream" // or "outstream" - } - }, - bids: [{ - bidder: "yieldlab", - params: { - adslotId: "5220339", - supplyId: "1381604" - } - }] - } - ]; + +# Multi-Format Setup + +A general overview of how to set up multi-format ads can be found in the offical Prebid.js docs. See: [show multi-format ads](https://docs.prebid.org/dev-docs/show-multi-format-ads.html) + +When setting up multi-format ads with Yieldlab make sure to always add at least one eligible Adslot per given media type in the ad unit configuration. + +```javascript +const adUnit = { + code: 'multi-format-adslot', + mediaTypes: { + banner: { + sizes: [ [ 728, 90 ] ] + }, + native: { + // native config + } + }, + bids: [ + // banner Adslot + { bidder: 'yieldlab', params: { adslotId: '1234', supplyId: '42' } }, + // native Adslot + { bidder: 'yieldlab', params: { adslotId: '2345', supplyId: '42' } } + ] +}; ``` diff --git a/modules/yieldoneBidAdapter.js b/modules/yieldoneBidAdapter.js index b12f314da2e..fe5a63cab51 100644 --- a/modules/yieldoneBidAdapter.js +++ b/modules/yieldoneBidAdapter.js @@ -1,4 +1,4 @@ -import { deepAccess, isEmpty, parseSizesInput, isStr, logWarn } from '../src/utils.js'; +import {deepAccess, isEmpty, isStr, logWarn, parseSizesInput} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; @@ -11,6 +11,8 @@ const VIDEO_PLAYER_URL = 'https://img.ak.impact-ad.jp/ic/pone/ivt/firstview/js/d const CMER_PLAYER_URL = 'https://an.cmertv.com/hb/renderer/cmertv-video-yone-prebid.min.js'; const VIEWABLE_PERCENTAGE_URL = 'https://img.ak.impact-ad.jp/ic/pone/ivt/firstview/js/prebid-adformat-config.js'; +const DEFAULT_VIDEO_SIZE = {w: 640, h: 360}; + export const spec = { code: BIDDER_CODE, aliases: ['y1'], @@ -40,16 +42,18 @@ export const spec = { t: 'i' }; - const videoMediaType = deepAccess(bidRequest, 'mediaTypes.video'); - if ((isEmpty(bidRequest.mediaType) && isEmpty(bidRequest.mediaTypes)) || - (bidRequest.mediaType === BANNER || (bidRequest.mediaTypes && bidRequest.mediaTypes[BANNER]))) { - const sizes = deepAccess(bidRequest, 'mediaTypes.banner.sizes') || bidRequest.sizes; - payload.sz = parseSizesInput(sizes).join(','); - } else if (bidRequest.mediaType === VIDEO || videoMediaType) { - const sizes = deepAccess(bidRequest, 'mediaTypes.video.playerSize') || bidRequest.sizes; - const size = parseSizesInput(sizes)[0]; - payload.w = size.split('x')[0]; - payload.h = size.split('x')[1]; + const mediaType = getMediaType(bidRequest); + switch (mediaType) { + case BANNER: + payload.sz = getBannerSizes(bidRequest); + break; + case VIDEO: + const videoSize = getVideoSize(bidRequest); + payload.w = videoSize.w; + payload.h = videoSize.h; + break; + default: + break; } // LiveRampID @@ -167,6 +171,106 @@ export const spec = { }, } +/** + * NOTE: server side does not yet support multiple formats. + * @param {Object} bidRequest - + * @param {boolean} [enabledOldFormat = true] - default: `true`. + * @return {string|null} - `"banner"` or `"video"` or `null`. + */ +function getMediaType(bidRequest, enabledOldFormat = true) { + let hasBannerType = Boolean(deepAccess(bidRequest, 'mediaTypes.banner')); + let hasVideoType = Boolean(deepAccess(bidRequest, 'mediaTypes.video')); + + if (enabledOldFormat) { + hasBannerType = hasBannerType || bidRequest.mediaType === BANNER || + (isEmpty(bidRequest.mediaTypes) && isEmpty(bidRequest.mediaType)); + hasVideoType = hasVideoType || bidRequest.mediaType === VIDEO; + } + + if (hasBannerType && hasVideoType) { + const playerParams = deepAccess(bidRequest, 'params.playerParams') + if (playerParams) { + return VIDEO; + } else { + return BANNER; + } + } else if (hasBannerType) { + return BANNER; + } else if (hasVideoType) { + return VIDEO; + } + + return null; +} + +/** + * NOTE: + * If `mediaTypes.banner` exists, then `mediaTypes.banner.sizes` must also exist. + * The reason for this is that Prebid.js will perform the verification and + * if `mediaTypes.banner.sizes` is inappropriate, it will delete the entire `mediaTypes.banner`. + * @param {Object} bidRequest - + * @param {Object} bidRequest.banner - + * @param {Array} bidRequest.banner.sizes - + * @param {boolean} [enabledOldFormat = true] - default: `true`. + * @return {string} - strings like `"300x250"` or `"300x250,728x90"`. + */ +function getBannerSizes(bidRequest, enabledOldFormat = true) { + let sizes = deepAccess(bidRequest, 'mediaTypes.banner.sizes'); + + if (enabledOldFormat) { + sizes = sizes || bidRequest.sizes; + } + + return parseSizesInput(sizes).join(','); +} + +/** + * @param {Object} bidRequest - + * @param {boolean} [enabledOldFormat = true] - default: `true`. + * @param {boolean} [enabledFlux = true] - default: `true`. + * @return {{w: number, h: number}} - + */ +function getVideoSize(bidRequest, enabledOldFormat = true, enabledFlux = true) { + /** + * @param {Array | Array>} sizes - + * @return {{w: number, h: number} | null} - + */ + const _getPlayerSize = (sizes) => { + let result = null; + + const size = parseSizesInput(sizes)[0]; + if (isEmpty(size)) { + return result; + } + + const splited = size.split('x'); + const sizeObj = {w: parseInt(splited[0], 10), h: parseInt(splited[1], 10)}; + const _isValidPlayerSize = !(isEmpty(sizeObj)) && (isFinite(sizeObj.w) && isFinite(sizeObj.h)); + if (!_isValidPlayerSize) { + return result; + } + + result = sizeObj; + return result; + } + + let playerSize = _getPlayerSize(deepAccess(bidRequest, 'mediaTypes.video.playerSize')); + + if (enabledOldFormat) { + playerSize = playerSize || _getPlayerSize(bidRequest.sizes); + } + + if (enabledFlux) { + // NOTE: `video.playerSize` in Flux is always [1,1]. + if (playerSize && (playerSize.w === 1 && playerSize.h === 1)) { + // NOTE: `params.playerSize` is a specific object to support `FLUX`. + playerSize = _getPlayerSize(deepAccess(bidRequest, 'params.playerSize')); + } + } + + return playerSize || DEFAULT_VIDEO_SIZE; +} + function newRenderer(response) { const renderer = Renderer.install({ id: response.uid, diff --git a/package.json b/package.json index 1795cbe2fe6..072a0d714b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "5.20.0-pre", + "version": "6.2.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { @@ -11,6 +11,11 @@ "type": "git", "url": "https://github.com/prebid/Prebid.js.git" }, + "browserslist": [ + "> 0.25%", + "not IE 11", + "not op_mini all" + ], "keywords": [ "advertising", "auction", diff --git a/src/adapterManager.js b/src/adapterManager.js index 3ee0dff81ad..9a041543cf8 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -262,14 +262,16 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a } let adUnitsS2SCopy = getAdUnitCopyForPrebidServer(adUnits, s2sConfig); - let tid = generateUUID(); + + // uniquePbsTid is so we know which server to send which bids to during the callBids function + let uniquePbsTid = generateUUID(); adaptersServerSide.forEach(bidderCode => { const bidderRequestId = getUniqueIdentifierStr(); const bidderRequest = { bidderCode, auctionId, bidderRequestId, - tid, + uniquePbsTid, bids: hookedGetBids({bidderCode, auctionId, bidderRequestId, 'adUnits': deepClone(adUnitsS2SCopy), labels, src: CONSTANTS.S2S.SRC}), auctionStart: auctionStart, timeout: s2sConfig.timeout, @@ -350,7 +352,7 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request serverBidRequests.forEach(serverBidRequest => { var index = -1; for (var i = 0; i < uniqueServerBidRequests.length; ++i) { - if (serverBidRequest.tid === uniqueServerBidRequests[i].tid) { + if (serverBidRequest.uniquePbsTid === uniqueServerBidRequests[i].uniquePbsTid) { index = i; break; } @@ -360,7 +362,10 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request } }); - let counter = 0 + let counter = 0; + + // $.source.tid MUST be a unique UUID and also THE SAME between all PBS Requests for a given Auction + const sourceTid = generateUUID(); _s2sConfigs.forEach((s2sConfig) => { if (s2sConfig && uniqueServerBidRequests[counter] && includes(s2sConfig.bidders, uniqueServerBidRequests[counter].bidderCode)) { // s2s should get the same client side timeout as other client side requests. @@ -370,13 +375,13 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request } : undefined); let adaptersServerSide = s2sConfig.bidders; const s2sAdapter = _bidderRegistry[s2sConfig.adapter]; - let tid = uniqueServerBidRequests[counter].tid; + let uniquePbsTid = uniqueServerBidRequests[counter].uniquePbsTid; let adUnitsS2SCopy = uniqueServerBidRequests[counter].adUnitsS2SCopy; - let uniqueServerRequests = serverBidRequests.filter(serverBidRequest => serverBidRequest.tid === tid) + let uniqueServerRequests = serverBidRequests.filter(serverBidRequest => serverBidRequest.uniquePbsTid === uniquePbsTid); if (s2sAdapter) { - let s2sBidRequest = {tid, 'ad_units': adUnitsS2SCopy, s2sConfig}; + let s2sBidRequest = {tid: sourceTid, 'ad_units': adUnitsS2SCopy, s2sConfig}; if (s2sBidRequest.ad_units.length) { let doneCbs = uniqueServerRequests.map(bidRequest => { bidRequest.start = timestamp(); @@ -391,7 +396,8 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request // fire BID_REQUESTED event for each s2s bidRequest uniqueServerRequests.forEach(bidRequest => { - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + // add the new sourceTid + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, {...bidRequest, tid: sourceTid}); }); // make bid requests diff --git a/src/auction.js b/src/auction.js index 498f08d6c73..059c09bc2ff 100644 --- a/src/auction.js +++ b/src/auction.js @@ -613,7 +613,8 @@ export const getPriceGranularity = (mediaType, bidReq) => { * @returns {function} */ export const getPriceByGranularity = (granularity) => { - return (bid) => { + return (bid, bidReq) => { + granularity = granularity || getPriceGranularity(bid.mediaType, bidReq); if (granularity === CONSTANTS.GRANULARITY_OPTIONS.AUTO) { return bid.pbAg; } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.DENSE) { @@ -646,14 +647,14 @@ export const getAdvertiserDomain = () => { * @param {BidRequest} bidReq * @returns {*} */ -export function getStandardBidderSettings(mediaType, bidderCode, bidReq) { +export function getStandardBidderSettings(mediaType, bidderCode) { // factory for key value objs function createKeyVal(key, value) { return { key, val: (typeof value === 'function') - ? function (bidResponse) { - return value(bidResponse); + ? function (bidResponse, bidReq) { + return value(bidResponse, bidReq); } : function (bidResponse) { return getValue(bidResponse, value); @@ -661,7 +662,6 @@ export function getStandardBidderSettings(mediaType, bidderCode, bidReq) { }; } const TARGETING_KEYS = CONSTANTS.TARGETING_KEYS; - const granularity = getPriceGranularity(mediaType, bidReq); let bidderSettings = $$PREBID_GLOBAL$$.bidderSettings; if (!bidderSettings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD]) { @@ -671,7 +671,7 @@ export function getStandardBidderSettings(mediaType, bidderCode, bidReq) { bidderSettings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD][CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] = [ createKeyVal(TARGETING_KEYS.BIDDER, 'bidderCode'), createKeyVal(TARGETING_KEYS.AD_ID, 'adId'), - createKeyVal(TARGETING_KEYS.PRICE_BUCKET, getPriceByGranularity(granularity)), + createKeyVal(TARGETING_KEYS.PRICE_BUCKET, getPriceByGranularity()), createKeyVal(TARGETING_KEYS.SIZE, 'size'), createKeyVal(TARGETING_KEYS.DEAL, 'dealId'), createKeyVal(TARGETING_KEYS.SOURCE, 'source'), @@ -716,12 +716,12 @@ export function getKeyValueTargetingPairs(bidderCode, custBidObj, bidReq) { // 1) set the keys from "standard" setting or from prebid defaults if (bidderSettings) { // initialize default if not set - const standardSettings = getStandardBidderSettings(custBidObj.mediaType, bidderCode, bidReq); - setKeys(keyValues, standardSettings, custBidObj); + const standardSettings = getStandardBidderSettings(custBidObj.mediaType, bidderCode); + setKeys(keyValues, standardSettings, custBidObj, bidReq); // 2) set keys from specific bidder setting override if they exist if (bidderCode && bidderSettings[bidderCode] && bidderSettings[bidderCode][CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING]) { - setKeys(keyValues, bidderSettings[bidderCode], custBidObj); + setKeys(keyValues, bidderSettings[bidderCode], custBidObj, bidReq); custBidObj.sendStandardTargeting = bidderSettings[bidderCode].sendStandardTargeting; } } @@ -734,7 +734,7 @@ export function getKeyValueTargetingPairs(bidderCode, custBidObj, bidReq) { return keyValues; } -function setKeys(keyValues, bidderSettings, custBidObj) { +function setKeys(keyValues, bidderSettings, custBidObj, bidReq) { var targeting = bidderSettings[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING]; custBidObj.size = custBidObj.getSize(); @@ -748,7 +748,7 @@ function setKeys(keyValues, bidderSettings, custBidObj) { if (isFn(value)) { try { - value = value(custBidObj); + value = value(custBidObj, bidReq); } catch (e) { logError('bidmanager', 'ERROR', e); } diff --git a/test/spec/modules/adheseBidAdapter_spec.js b/test/spec/modules/adheseBidAdapter_spec.js index 78ca25d6be2..3fe0a62b2a0 100644 --- a/test/spec/modules/adheseBidAdapter_spec.js +++ b/test/spec/modules/adheseBidAdapter_spec.js @@ -1,5 +1,6 @@ import {expect} from 'chai'; import {spec} from 'modules/adheseBidAdapter.js'; +import {config} from 'src/config.js'; const BID_ID = 456; const TTL = 360; @@ -131,12 +132,21 @@ describe('AdheseAdapter', function () { expect(JSON.parse(req.data)).to.not.have.key('eids'); }); - it('should request vast content as url', function () { + it('should request vast content as url by default', function () { let req = spec.buildRequests([ minimalBid() ], bidderRequest); expect(JSON.parse(req.data).vastContentAsUrl).to.equal(true); }); + it('should request vast content as markup when configured', function () { + sinon.stub(config, 'getConfig').withArgs('adhese').returns({ vastContentAsUrl: false }); + + let req = spec.buildRequests([ minimalBid() ], bidderRequest); + + expect(JSON.parse(req.data).vastContentAsUrl).to.equal(false); + config.getConfig.restore(); + }); + it('should include bids', function () { let bid = minimalBid(); let req = spec.buildRequests([ bid ], bidderRequest); @@ -155,6 +165,22 @@ describe('AdheseAdapter', function () { expect(req.url).to.equal('https://ads-demo.adhese.com/json'); }); + + it('should include params specified in the config', function () { + sinon.stub(config, 'getConfig').withArgs('adhese').returns({ globalTargets: { 'tl': [ 'all' ] } }); + let req = spec.buildRequests([ minimalBid() ], bidderRequest); + + expect(JSON.parse(req.data).parameters).to.deep.include({ 'tl': [ 'all' ] }); + config.getConfig.restore(); + }); + + it('should give priority to bid params over config params', function () { + sinon.stub(config, 'getConfig').withArgs('adhese').returns({ globalTargets: { 'xt': ['CONFIG_CONSENT_STRING'] } }); + let req = spec.buildRequests([ minimalBid() ], bidderRequest); + + expect(JSON.parse(req.data).parameters).to.deep.include({ 'xt': [ 'CONSENT_STRING' ] }); + config.getConfig.restore(); + }); }); describe('interpretResponse', () => { diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index 20dbaad1cc6..20b035011fe 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -251,6 +251,24 @@ describe('adnuntiusBidAdapter', function () { expect(request[0]).to.have.property('url') expect(request[0].url).to.equal(ENDPOINT_URL_SEGMENTS); }); + + it('should user user ID if present in ortb2.user.id field', function () { + config.setBidderConfig({ + bidders: ['adnuntius', 'other'], + config: { + ortb2: { + user: { + id: usi + } + } + } + }); + + const request = config.runWithBidder('adnuntius', () => spec.buildRequests(bidRequests)); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('url') + expect(request[0].url).to.equal(ENDPOINT_URL); + }); }); describe('user privacy', function () { diff --git a/test/spec/modules/adomikAnalyticsAdapter_spec.js b/test/spec/modules/adomikAnalyticsAdapter_spec.js index 1414b2402b9..8f87c73f1b4 100644 --- a/test/spec/modules/adomikAnalyticsAdapter_spec.js +++ b/test/spec/modules/adomikAnalyticsAdapter_spec.js @@ -1,5 +1,6 @@ import adomikAnalytics from 'modules/adomikAnalyticsAdapter.js'; import {expect} from 'chai'; + let events = require('src/events'); let adapterManager = require('src/adapterManager').default; let constants = require('src/constants.json'); @@ -8,6 +9,7 @@ describe('Adomik Prebid Analytic', function () { let sendEventStub; let sendWonEventStub; let clock; + before(function () { clock = sinon.useFakeTimers(); }); @@ -91,7 +93,7 @@ describe('Adomik Prebid Analytic', function () { type: 'request', event: { bidder: 'BIDDERTEST', - placementCode: 'placementtest', + placementCode: '0000', } }); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 9396c1e1928..fbcce5a1322 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -1047,7 +1047,8 @@ describe('AppNexusAdapter', function () { 'trackers': [ { 'impression_urls': [ - 'https://lax1-ib.adnxs.com/impression' + 'https://lax1-ib.adnxs.com/impression', + 'https://www.test.com/tracker' ], 'video_events': {} } @@ -1334,6 +1335,20 @@ describe('AppNexusAdapter', function () { expect(Object.keys(result[0].meta)).to.include.members(['advertiserId']); }); + it('should add brand id', function() { + let responseBrandId = deepClone(response); + responseBrandId.tags[0].ads[0].brand_id = 123; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseBrandId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['brandId']); + }); + it('should add advertiserDomains', function() { let responseAdvertiserId = deepClone(response); responseAdvertiserId.tags[0].ads[0].adomain = ['123']; diff --git a/test/spec/modules/atsAnalyticsAdapter_spec.js b/test/spec/modules/atsAnalyticsAdapter_spec.js index 7f662ffd06d..cae90a19223 100644 --- a/test/spec/modules/atsAnalyticsAdapter_spec.js +++ b/test/spec/modules/atsAnalyticsAdapter_spec.js @@ -12,11 +12,15 @@ let constants = require('src/constants.json'); export const storage = getStorageManager(); let sandbox; +let clock; +let now = new Date(); + describe('ats analytics adapter', function () { beforeEach(function () { sinon.stub(events, 'getEvents').returns([]); storage.setCookie('_lr_env_src_ats', 'true', 'Thu, 01 Jan 1970 00:00:01 GMT'); sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(now.getTime()); }); afterEach(function () { @@ -25,18 +29,20 @@ describe('ats analytics adapter', function () { atsAnalyticsAdapter.disableAnalytics(); Math.random.restore(); sandbox.restore(); + clock.restore(); }); describe('track', function () { it('builds and sends request and response data', function () { sinon.stub(Math, 'random').returns(0.99); - sinon.stub(atsAnalyticsAdapter, 'shouldFireRequest').returns(true); sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25'); - let now = new Date(); + now.setTime(now.getTime() + 3600000); storage.setCookie('_lr_env_src_ats', 'true', now.toUTCString()); storage.setCookie('_lr_sampling_rate', '10', now.toUTCString()); + this.timeout(2100); + let initOptions = { pid: '10433394' }; @@ -62,7 +68,7 @@ describe('ats analytics adapter', function () { 'refererInfo': { 'referer': 'https://example.com/dev' }, - 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7', + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7' }; // prepare general auction - response let bidResponse = { @@ -90,7 +96,7 @@ describe('ats analytics adapter', function () { let expectedAfterBid = { 'Data': [{ 'has_envelope': true, - 'adapter_version': 2, + 'adapter_version': 3, 'bidder': 'appnexus', 'bid_id': '30c77d079cdf17', 'auction_id': 'a5b849e5-87d7-4205-8300-d063084fcfb7', @@ -103,10 +109,30 @@ describe('ats analytics adapter', function () { 'response_time_stamp': '2020-02-03T14:23:11.978Z', 'currency': 'USD', 'cpm': 0.5, - 'net_revenue': true + 'net_revenue': true, + 'bid_won': true }] }; + let wonRequest = { + 'adId': '2eddfdc0c791dc', + 'mediaType': 'banner', + 'requestId': '30c77d079cdf17', + 'cpm': 0.5, + 'creativeId': 29681110, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7', + 'statusMessage': 'Bid available', + 'responseTimestamp': 1633525319061, + 'requestTimestamp': 1633525319258, + 'bidder': 'appnexus', + 'adUnitCode': 'div-gpt-ad-1438287399331-0', + 'size': '300x250', + 'status': 'rendered' + }; + // lets simulate that some bidders timeout let bidTimeoutArgsV1 = [ { @@ -148,6 +174,14 @@ describe('ats analytics adapter', function () { // Step 5: Send auction end event events.emit(constants.EVENTS.AUCTION_END, {}); + // Step 6: Send bid won event + events.emit(constants.EVENTS.BID_WON, wonRequest); + + sandbox.stub($$PREBID_GLOBAL$$, 'getAllWinningBids').callsFake((key) => { + return [wonRequest] + }); + + clock.tick(2000); let requests = server.requests.filter(req => { return req.url.indexOf(analyticsUrl) > -1; @@ -156,13 +190,12 @@ describe('ats analytics adapter', function () { expect(requests.length).to.equal(1); let realAfterBid = JSON.parse(requests[0].requestBody); - // Step 6: assert real data after bid and expected data + + // Step 7: assert real data after bid and expected data expect(realAfterBid['Data']).to.deep.equal(expectedAfterBid['Data']); // check that the publisher ID is configured via options expect(atsAnalyticsAdapter.context.pid).to.equal(initOptions.pid); - - atsAnalyticsAdapter.shouldFireRequest.restore(); }) it('check browser is safari', function () { sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25'); @@ -204,7 +237,7 @@ describe('ats analytics adapter', function () { sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25'); sinon.stub(Math, 'random').returns(0.99); // publisher can try to pass anything they want but we will set sampling rate to 100, which means we will have 1% of requests - let result = atsAnalyticsAdapter.shouldFireRequest(10); + let result = atsAnalyticsAdapter.shouldFireRequest(8); expect(result).to.equal(true); }) it('should not fire analytics request if math random is something other then 0.99', function () { diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js index ee37d16905b..32e1c7fe795 100644 --- a/test/spec/modules/browsiRtdProvider_spec.js +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -1,5 +1,6 @@ import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; import {makeSlot} from '../integration/faker/googletag.js'; +import * as utils from '../../../src/utils' describe('browsi Real time data sub module', function () { const conf = { @@ -29,11 +30,11 @@ describe('browsi Real time data sub module', function () { }); it('should match placement with ad unit', function () { - const slot = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); + const slot = makeSlot({code: '/123/abc', divId: 'browsiAd_1'}); - const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250']); // true - const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_300x250', '/57778053/Browsi']); // true - const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/57778053/Browsi_Demo_Low']); // false + const test1 = browsiRTD.isIdMatchingAdUnit(slot, ['/123/abc']); // true + const test2 = browsiRTD.isIdMatchingAdUnit(slot, ['/123/abc', '/456/def']); // true + const test3 = browsiRTD.isIdMatchingAdUnit(slot, ['/123/def']); // false const test4 = browsiRTD.isIdMatchingAdUnit(slot, []); // true expect(test1).to.equal(true); @@ -43,12 +44,12 @@ describe('browsi Real time data sub module', function () { }); it('should return correct macro values', function () { - const slot = makeSlot({code: '/57778053/Browsi_Demo_300x250', divId: 'browsiAd_1'}); + const slot = makeSlot({code: '/123/abc', divId: 'browsiAd_1'}); slot.setTargeting('test', ['test', 'value']); // slot getTargeting doesn't act like GPT so we can't expect real value const macroResult = browsiRTD.getMacroId({p: '/'}, slot); - expect(macroResult).to.equal('/57778053/Browsi_Demo_300x250/NA'); + expect(macroResult).to.equal('/123/abc/NA'); const macroResultB = browsiRTD.getMacroId({}, slot); expect(macroResultB).to.equal('browsiAd_1'); @@ -72,7 +73,7 @@ describe('browsi Real time data sub module', function () { it('should return prediction from server', function () { makeSlot({code: 'hasPrediction', divId: 'hasPrediction'}); const data = { - p: {'hasPrediction': {p: 0.234}}, + p: {'hasPrediction': {ps: {0: 0.234}}}, kn: 'bv', pmd: undefined }; @@ -80,4 +81,58 @@ describe('browsi Real time data sub module', function () { expect(browsiRTD.browsiSubmodule.getTargetingData(['hasPrediction'])).to.eql({hasPrediction: {bv: '0.20'}}); }) }) + + describe('should return matching prediction', function () { + const predictions = { + 0: 0.123, + 1: 0.254, + 3: 0, + 4: 0.8 + } + const singlePrediction = { + 0: 0.123 + } + it('should return raw value if valid', function () { + expect(browsiRTD.getCurrentData(predictions, 0)).to.equal(0.123); + expect(browsiRTD.getCurrentData(predictions, 1)).to.equal(0.254); + }) + it('should return 0 for prediction = 0', function () { + expect(browsiRTD.getCurrentData(predictions, 3)).to.equal(0); + }) + it('should return -1 for invalid params', function () { + expect(browsiRTD.getCurrentData(null, 3)).to.equal(-1); + expect(browsiRTD.getCurrentData(predictions, null)).to.equal(-1); + }) + it('should return prediction according to object keys length ', function () { + expect(browsiRTD.getCurrentData(singlePrediction, 0)).to.equal(0.123); + expect(browsiRTD.getCurrentData(singlePrediction, 1)).to.equal(-1); + expect(browsiRTD.getCurrentData(singlePrediction, 2)).to.equal(-1); + expect(browsiRTD.getCurrentData(predictions, 4)).to.equal(0.8); + expect(browsiRTD.getCurrentData(predictions, 5)).to.equal(0.8); + expect(browsiRTD.getCurrentData(predictions, 8)).to.equal(0.8); + }) + }) + describe('should set bid request data', function () { + const data = { + p: { + 'adUnit1': {ps: {0: 0.234}}, + 'adUnit2': {ps: {0: 0.134}}}, + kn: 'bv', + pmd: undefined + }; + browsiRTD.setData(data); + const fakeAdUnits = [ + { + code: 'adUnit1' + }, + { + code: 'adUnit2' + } + ] + browsiRTD.browsiSubmodule.getBidRequestData({adUnits: fakeAdUnits}, () => {}, {}, null); + it('should set ad unit params with prediction values', function () { + expect(utils.deepAccess(fakeAdUnits[0], 'ortb2Imp.ext.data.browsi')).to.eql({bv: '0.20'}); + expect(utils.deepAccess(fakeAdUnits[1], 'ortb2Imp.ext.data.browsi')).to.eql({bv: '0.10'}); + }) + }) }); diff --git a/test/spec/modules/cleanioRtdProvider_spec.js b/test/spec/modules/cleanioRtdProvider_spec.js new file mode 100644 index 00000000000..47c4b1b4961 --- /dev/null +++ b/test/spec/modules/cleanioRtdProvider_spec.js @@ -0,0 +1,188 @@ +import * as utils from '../../../src/utils.js'; +import * as hook from '../../../src/hook.js' + +import { __TEST__ } from '../../../modules/cleanioRtdProvider.js'; + +const { + readConfig, + ConfigError, + pageInitStepPreloadScript, + pageInitStepProtectPage, + bidWrapStepAugmentHtml, + bidWrapStepProtectByWrapping, + beforeInit, +} = __TEST__; + +sinon.assert.expose(chai.assert, { prefix: 'sinon' }); + +const fakeScriptURL = 'https://example.com/script.js'; + +function makeFakeBidResponse() { + return { + ad: 'hello ad', + bidderCode: 'BIDDER', + creativeId: 'CREATIVE', + cpm: 1.23, + }; +} + +describe('clean.io RTD module', function () { + describe('readConfig()', function() { + it('should throw ConfigError on invalid configurations', function() { + expect(() => readConfig({})).to.throw(ConfigError); + expect(() => readConfig({ params: {} })).to.throw(ConfigError); + expect(() => readConfig({ params: { protectionMode: 'bids' } })).to.throw(ConfigError); + expect(() => readConfig({ params: { cdnUrl: 'abc' } })).to.throw(ConfigError); + expect(() => readConfig({ params: { cdnUrl: 'abc', protectionMode: 'bids' } })).to.throw(ConfigError); + expect(() => readConfig({ params: { cdnUrl: 'https://abc1234567890.cloudfront.net/script.js', protectionMode: '123' } })).to.throw(ConfigError); + }); + + it('should accept valid configurations', function() { + expect(() => readConfig({ params: { cdnUrl: 'https://abc1234567890.cloudfront.net/script.js', protectionMode: 'full' } })).to.not.throw(); + expect(() => readConfig({ params: { cdnUrl: 'https://abc1234567890.cloudfront.net/script.js', protectionMode: 'bids' } })).to.not.throw(); + expect(() => readConfig({ params: { cdnUrl: 'https://abc1234567890.cloudfront.net/script.js', protectionMode: 'bids-nowait' } })).to.not.throw(); + }); + }); + + describe('Module initialization step', function() { + let insertElementStub; + beforeEach(function() { + insertElementStub = sinon.stub(utils, 'insertElement'); + }); + afterEach(function() { + utils.insertElement.restore(); + }); + + it('pageInitStepPreloadScript() should insert link/preload element', function() { + pageInitStepPreloadScript(fakeScriptURL); + + sinon.assert.calledOnce(insertElementStub); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.tagName === 'LINK')); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.rel === 'preload')); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.as === 'script')); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.href === fakeScriptURL)); + }); + + it('pageInitStepProtectPage() should insert script element', function() { + pageInitStepProtectPage(fakeScriptURL); + + sinon.assert.calledOnce(insertElementStub); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.tagName === 'SCRIPT')); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.type === 'text/javascript')); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.src === fakeScriptURL)); + }); + }); + + function ensurePrependToBidResponse(fakeBidResponse) { + expect(fakeBidResponse).to.have.own.property('ad').which.is.a('string'); + expect(fakeBidResponse.ad).to.contain(''); + } + + function ensureWrapBidResponse(fakeBidResponse, scriptUrl) { + expect(fakeBidResponse).to.have.own.property('ad').which.is.a('string'); + expect(fakeBidResponse.ad).to.contain(`src="${scriptUrl}"`); + expect(fakeBidResponse.ad).to.contain('agent.put(ad)'); + } + + describe('Bid processing step', function() { + it('bidWrapStepAugmentHtml() should prepend bid-specific information in a comment', function() { + const fakeBidResponse = makeFakeBidResponse(); + bidWrapStepAugmentHtml(fakeBidResponse); + ensurePrependToBidResponse(fakeBidResponse); + }); + + it('bidWrapStepProtectByWrapping() should wrap payload into a script tag', function() { + const fakeBidResponse = makeFakeBidResponse(); + bidWrapStepProtectByWrapping(fakeScriptURL, 0, fakeBidResponse); + ensureWrapBidResponse(fakeBidResponse, fakeScriptURL); + }); + }); + + describe('Sumbodule execution', function() { + let submoduleStub; + let insertElementStub; + beforeEach(function () { + submoduleStub = sinon.stub(hook, 'submodule'); + insertElementStub = sinon.stub(utils, 'insertElement'); + }); + afterEach(function () { + utils.insertElement.restore(); + submoduleStub.restore(); + }); + + function getModule() { + beforeInit(); + + expect(submoduleStub.calledOnceWith('realTimeData')).to.equal(true); + + const registeredSubmoduleDefinition = submoduleStub.getCall(0).args[1]; + expect(registeredSubmoduleDefinition).to.be.an('object'); + expect(registeredSubmoduleDefinition).to.have.own.property('name', 'clean.io'); + expect(registeredSubmoduleDefinition).to.have.own.property('init').that.is.a('function'); + expect(registeredSubmoduleDefinition).to.have.own.property('onBidResponseEvent').that.is.a('function'); + + return registeredSubmoduleDefinition; + } + + it('should register clean.io RTD submodule provider', function () { + getModule(); + }); + + it('should refuse initialization with incorrect parameters', function () { + const { init } = getModule(); + expect(init({ params: { cdnUrl: 'abc', protectionMode: 'full' } }, {})).to.equal(false); // too short distribution name + sinon.assert.notCalled(insertElementStub); + }); + + it('should iniitalize in full (page) protection mode', function () { + const { init, onBidResponseEvent } = getModule(); + expect(init({ params: { cdnUrl: 'https://abc1234567890.cloudfront.net/script.js', protectionMode: 'full' } }, {})).to.equal(true); + sinon.assert.calledOnce(insertElementStub); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.tagName === 'SCRIPT')); + + const fakeBidResponse = makeFakeBidResponse(); + onBidResponseEvent(fakeBidResponse, {}, {}); + ensurePrependToBidResponse(fakeBidResponse); + }); + + it('should iniitalize in bids (frame) protection mode', function () { + const { init, onBidResponseEvent } = getModule(); + expect(init({ params: { cdnUrl: 'https://abc1234567890.cloudfront.net/script.js', protectionMode: 'bids' } }, {})).to.equal(true); + sinon.assert.calledOnce(insertElementStub); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.tagName === 'LINK')); + + const fakeBidResponse = makeFakeBidResponse(); + onBidResponseEvent(fakeBidResponse, {}, {}); + ensureWrapBidResponse(fakeBidResponse, 'https://abc1234567890.cloudfront.net/script.js'); + }); + + it('should respect preload status in bids-nowait protection mode', function () { + const { init, onBidResponseEvent } = getModule(); + expect(init({ params: { cdnUrl: 'https://abc1234567890.cloudfront.net/script.js', protectionMode: 'bids-nowait' } }, {})).to.equal(true); + sinon.assert.calledOnce(insertElementStub); + sinon.assert.calledWith(insertElementStub, sinon.match(elem => elem.tagName === 'LINK')); + const preloadLink = insertElementStub.getCall(0).args[0]; + expect(preloadLink).to.have.property('onload').which.is.a('function'); + expect(preloadLink).to.have.property('onerror').which.is.a('function'); + + const fakeBidResponse1 = makeFakeBidResponse(); + onBidResponseEvent(fakeBidResponse1, {}, {}); + ensurePrependToBidResponse(fakeBidResponse1); + + // Simulate successful preloading + preloadLink.onload(); + + const fakeBidResponse2 = makeFakeBidResponse(); + onBidResponseEvent(fakeBidResponse2, {}, {}); + ensureWrapBidResponse(fakeBidResponse2, 'https://abc1234567890.cloudfront.net/script.js'); + + // Simulate error + preloadLink.onerror(); + + // Now we should fallback to just prepending + const fakeBidResponse3 = makeFakeBidResponse(); + onBidResponseEvent(fakeBidResponse3, {}, {}); + ensurePrependToBidResponse(fakeBidResponse3); + }); + }); +}); diff --git a/test/spec/modules/codefuelBidAdapter_spec.js b/test/spec/modules/codefuelBidAdapter_spec.js index 808c221af07..a2549012d84 100644 --- a/test/spec/modules/codefuelBidAdapter_spec.js +++ b/test/spec/modules/codefuelBidAdapter_spec.js @@ -183,7 +183,7 @@ describe('Codefuel Adapter', function () { tmax: 500 } const res = spec.buildRequests([bidRequest], commonBidderRequest) - expect(res.url).to.equal('https://bidder-url.com') + expect(res.url).to.equal('https://ai-p-codefuel-ds-rtb-us-east-1-k8s.seccint.com/prebid') expect(res.data).to.deep.equal(expectedData) }) diff --git a/test/spec/modules/datablocksBidAdapter_spec.js b/test/spec/modules/datablocksBidAdapter_spec.js index 0ec12905430..ff7b0aad48c 100644 --- a/test/spec/modules/datablocksBidAdapter_spec.js +++ b/test/spec/modules/datablocksBidAdapter_spec.js @@ -96,8 +96,8 @@ const bidderRequest = { refererInfo: { numIframes: 0, reachedTop: true, - referer: 'https://v5demo.datablocks.net/test', - stack: ['https://v5demo.datablocks.net/test'] + referer: 'https://7560.v5demo.datablocks.net/test', + stack: ['https://7560.v5demo.datablocks.net/test'] }, start: Date.now(), timeout: 10000 @@ -452,7 +452,7 @@ describe('DatablocksAdapter', function() { it('Returns valid URL', function() { expect(request.url).to.exist; - expect(request.url).to.equal('https://7560.v5demo.datablocks.net/openrtb/?sid=7560'); + expect(request.url).to.equal('https://v5demo.datablocks.net/openrtb/?sid=7560'); }); it('Creates an array of request objects', function() { diff --git a/test/spec/modules/deltaprojectsBidAdapter_spec.js b/test/spec/modules/deltaprojectsBidAdapter_spec.js new file mode 100644 index 00000000000..382415eab62 --- /dev/null +++ b/test/spec/modules/deltaprojectsBidAdapter_spec.js @@ -0,0 +1,399 @@ +import { expect } from 'chai'; +import { + BIDDER_CODE, + BIDDER_ENDPOINT_URL, + spec, USERSYNC_URL, + getBidFloor +} from 'modules/deltaprojectsBidAdapter.js'; + +const BID_REQ_REFER = 'http://example.com/page?param=val'; + +describe('deltaprojectsBidAdapter', function() { + describe('isBidRequestValid', function () { + function makeBid() { + return { + bidder: BIDDER_CODE, + params: { + publisherId: '12345' + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + } + + it('should return true when bidder set correctly', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when bid request is null', function () { + expect(spec.isBidRequestValid(undefined)).to.equal(false); + }); + + it('should return false when bidder not set correctly', function () { + let bid = makeBid(); + delete bid.bidder; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when publisher id is not set', function () { + let bid = makeBid(); + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const BIDREQ = { + bidder: BIDDER_CODE, + params: { + tagId: '403370', + siteId: 'example.com', + }, + sizes: [ + [300, 250], + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + } + const bidRequests = [BIDREQ]; + const bannerRequest = spec.buildRequests(bidRequests, {refererInfo: { referer: BID_REQ_REFER }})[0]; + const bannerRequestBody = bannerRequest.data; + + it('send bid request with test tag if it is set in the param', function () { + const TEST_TAG = 1; + const bidRequest = Object.assign({}, BIDREQ, { + params: { ...BIDREQ.params, test: TEST_TAG }, + }); + const bidderRequest = { refererInfo: { referer: BID_REQ_REFER } }; + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; + expect(request.data.test).to.equal(TEST_TAG); + }); + + it('send bid request with correct timeout', function () { + const TMAX = 10; + const bidderRequest = { refererInfo: { referer: BID_REQ_REFER }, timeout: TMAX }; + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.tmax).to.equal(TMAX); + }); + + it('send bid request to the correct endpoint URL', function () { + expect(bannerRequest.url).to.equal(BIDDER_ENDPOINT_URL); + }); + + it('sends bid request to our endpoint via POST', function () { + expect(bannerRequest.method).to.equal('POST'); + }); + + it('sends screen dimensions', function () { + expect(bannerRequestBody.device.w).to.equal(screen.width); + expect(bannerRequestBody.device.h).to.equal(screen.height); + }); + + it('includes the ad size in the bid request', function () { + expect(bannerRequestBody.imp[0].banner.format[0].w).to.equal(BIDREQ.sizes[0][0]); + expect(bannerRequestBody.imp[0].banner.format[0].h).to.equal(BIDREQ.sizes[0][1]); + }); + + it('sets domain and href correctly', function () { + expect(bannerRequestBody.site.domain).to.equal(BIDREQ.params.siteId); + expect(bannerRequestBody.site.page).to.equal(BID_REQ_REFER); + }); + + const gdprBidRequests = [{ + bidder: BIDDER_CODE, + params: { + tagId: '403370', + siteId: 'example.com' + }, + sizes: [ + [300, 250] + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475' + }]; + const consentString = 'BOJ/P2HOJ/P2HABABMAAAAAZ+A=='; + + const GDPR_REQ_REFERER = 'http://localhost:9876/' + function getGdprRequestBody(gdprApplies, consentString) { + const gdprRequest = spec.buildRequests(gdprBidRequests, { + gdprConsent: { + gdprApplies: gdprApplies, + consentString: consentString, + }, + refererInfo: { + referer: GDPR_REQ_REFERER, + }, + })[0]; + return gdprRequest.data; + } + + it('should handle gdpr applies being present and true', function() { + const gdprRequestBody = getGdprRequestBody(true, consentString); + expect(gdprRequestBody.regs.ext.gdpr).to.equal(1); + expect(gdprRequestBody.user.ext.consent).to.equal(consentString); + }) + + it('should handle gdpr applies being present and false', function() { + const gdprRequestBody = getGdprRequestBody(false, consentString); + expect(gdprRequestBody.regs.ext.gdpr).to.equal(0); + expect(gdprRequestBody.user.ext.consent).to.equal(consentString); + }) + + it('should handle gdpr applies being undefined', function() { + const gdprRequestBody = getGdprRequestBody(undefined, consentString); + expect(gdprRequestBody.regs).to.deep.equal({ext: {}}); + expect(gdprRequestBody.user.ext.consent).to.equal(consentString); + }) + + it('should handle gdpr consent being undefined', function() { + const gdprRequest = spec.buildRequests(gdprBidRequests, {refererInfo: { referer: GDPR_REQ_REFERER }})[0]; + const gdprRequestBody = gdprRequest.data; + expect(gdprRequestBody.regs).to.deep.equal({ ext: {} }); + expect(gdprRequestBody.user).to.deep.equal({ ext: {} }); + }) + }); + + describe('interpretResponse', function () { + const bidRequests = [ + { + bidder: BIDDER_CODE, + params: { + tagId: '403370', + siteId: 'example.com', + currency: 'USD', + }, + sizes: [ + [300, 250], + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }, + ]; + const request = spec.buildRequests(bidRequests, {refererInfo: { referer: BID_REQ_REFER }})[0]; + function makeResponse() { + return { + body: { + id: '5e5c23a5ba71e78', + seatbid: [ + { + bid: [ + { + id: '6vmb3isptf', + crid: 'deltaprojectscreative', + impid: '322add653672f68', + price: 1.22, + adm: '', + attr: [5], + h: 90, + nurl: 'http://nurl', + w: 728, + } + ], + seat: 'MOCK' + } + ], + bidid: '5e5c23a5ba71e78', + cur: 'USD' + } + }; + } + const expectedBid = { + requestId: '322add653672f68', + cpm: 1.22, + width: 728, + height: 90, + creativeId: 'deltaprojectscreative', + dealId: null, + currency: 'USD', + netRevenue: true, + mediaType: 'banner', + ttl: 60, + ad: '
' + }; + + it('should get incorrect bid response if response body is missing', function () { + let response = makeResponse(); + delete response.body; + let result = spec.interpretResponse(response, request); + expect(result.length).to.equal(0); + }); + + it('should get incorrect bid response if id or seat id of response body is missing', function () { + let response1 = makeResponse(); + delete response1.body.id; + let result1 = spec.interpretResponse(response1, request); + expect(result1.length).to.equal(0); + + let response2 = makeResponse(); + delete response2.body.seatbid; + let result2 = spec.interpretResponse(response2, request); + expect(result2.length).to.equal(0); + }); + + it('should get the correct bid response', function () { + let result = spec.interpretResponse(makeResponse(), request); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedBid); + }); + + it('should handle a missing crid', function () { + let noCridResponse = makeResponse(); + delete noCridResponse.body.seatbid[0].bid[0].crid; + const fallbackCrid = noCridResponse.body.seatbid[0].bid[0].id; + let noCridResult = Object.assign({}, expectedBid, {'creativeId': fallbackCrid}); + let result = spec.interpretResponse(noCridResponse, request); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(noCridResult); + }); + + it('should handle a missing nurl', function () { + let noNurlResponse = makeResponse(); + delete noNurlResponse.body.seatbid[0].bid[0].nurl; + let noNurlResult = Object.assign({}, expectedBid); + noNurlResult.ad = ''; + let result = spec.interpretResponse(noNurlResponse, request); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(noNurlResult); + }); + + it('handles empty bid response', function () { + let response = { + body: { + id: '5e5c23a5ba71e78', + seatbid: [] + } + }; + let result = spec.interpretResponse(response, request); + expect(result.length).to.equal(0); + }); + + it('should keep custom properties', () => { + const customProperties = {test: 'a test message', param: {testParam: 1}}; + const expectedResult = Object.assign({}, expectedBid, {[spec.code]: customProperties}); + const response = makeResponse(); + response.body.seatbid[0].bid[0].ext = customProperties; + const result = spec.interpretResponse(response, request); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedResult); + }); + }); + + describe('onBidWon', function () { + const OPEN_RTB_RESP = { + body: { + id: 'abc', + seatbid: [ + { + bid: [ + { + 'id': 'abc*123*456', + 'impid': 'xxxxxxx', + 'price': 46.657196, + 'adm': '