diff --git a/README.md b/README.md index 11fce459e56..bc0e64afa06 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,7 @@ As you make code changes, the bundles will be rebuilt and the page reloaded auto ## Contribute -Many SSPs, bidders, and publishers have contributed to this project. [Hundreds of bidders](https://github.com/prebid/Prebid.js/tree/master/src/adapters) are supported by Prebid.js. +Many SSPs, bidders, and publishers have contributed to this project. [Hundreds of bidders](https://github.com/prebid/Prebid.js/tree/master/modules) are supported by Prebid.js. For guidelines, see [Contributing](./CONTRIBUTING.md). diff --git a/browsers.json b/browsers.json index dd3955c47ea..bd6bd5772d6 100644 --- a/browsers.json +++ b/browsers.json @@ -1,65 +1,49 @@ { - "bs_edge_17_windows_10": { + "bs_edge_latest_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "edge", - "browser_version": "17.0", + "browser_version": "latest", "device": null, "os": "Windows" }, - "bs_edge_90_windows_10": { - "base": "BrowserStack", - "os_version": "10", - "browser": "edge", - "browser_version": "90.0", - "device": null, - "os": "Windows" - }, - "bs_chrome_90_windows_10": { + "bs_chrome_latest_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "chrome", - "browser_version": "90.0", + "browser_version": "latest", "device": null, "os": "Windows" }, - "bs_chrome_79_windows_10": { + "bs_chrome_87_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "chrome", - "browser_version": "79.0", - "device": null, - "os": "Windows" - }, - "bs_firefox_88_windows_10": { - "base": "BrowserStack", - "os_version": "10", - "browser": "firefox", - "browser_version": "88.0", + "browser_version": "87.0", "device": null, "os": "Windows" }, - "bs_firefox_72_windows_10": { + "bs_firefox_latest_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "firefox", - "browser_version": "72.0", + "browser_version": "latest", "device": null, "os": "Windows" }, - "bs_safari_14_mac_bigsur": { + "bs_safari_latest_mac_bigsur": { "base": "BrowserStack", "os_version": "Big Sur", "browser": "safari", - "browser_version": "14.0", + "browser_version": "latest", "device": null, "os": "OS X" }, - "bs_safari_12_mac_mojave": { + "bs_safari_15_catalina": { "base": "BrowserStack", - "os_version": "Mojave", + "os_version": "Catalina", "browser": "safari", - "browser_version": "12.0", + "browser_version": "13.1", "device": null, "os": "OS X" } diff --git a/gulpfile.js b/gulpfile.js index 8609177a8b9..c86357372f1 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 }))) @@ -140,12 +141,6 @@ function watch(done) { done(); }; -function makeModuleList(modules) { - return modules.map(module => { - return '"' + module + '"' - }); -} - function makeDevpackPkg() { var cloned = _.cloneDeep(webpackConfig); cloned.devtool = 'source-map'; @@ -157,7 +152,6 @@ function makeDevpackPkg() { return gulp.src([].concat(moduleSources, analyticsSources, 'src/prebid.js')) .pipe(helpers.nameModules(externalModules)) .pipe(webpackStream(cloned, webpack)) - .pipe(replace(/('|")v\$prebid\.modulesList\$('|")/g, makeModuleList(externalModules))) .pipe(gulp.dest('build/dev')) .pipe(connect.reload()); } @@ -175,7 +169,6 @@ function makeWebpackPkg() { .pipe(helpers.nameModules(externalModules)) .pipe(webpackStream(cloned, webpack)) .pipe(terser()) - .pipe(replace(/('|")v\$prebid\.modulesList\$('|")/g, makeModuleList(externalModules))) .pipe(gulpif(file => file.basename === 'prebid-core.js', header(banner, { prebid: prebid }))) .pipe(gulp.dest('build/dist')); } diff --git a/integrationExamples/gpt/weboramaRtdProvider_example.html b/integrationExamples/gpt/weboramaRtdProvider_example.html index 824d7a2f0c7..66e4a57d2a6 100644 --- a/integrationExamples/gpt/weboramaRtdProvider_example.html +++ b/integrationExamples/gpt/weboramaRtdProvider_example.html @@ -17,22 +17,31 @@ debug: true, realTimeData: { auctionDelay: 1000, - dataProviders: [ - { + dataProviders: [{ name: "weborama", waitForIt: true, params: { - weboCtxConf: { - setTargeting: true, - token: "to-be-defined", - targetURL: "https://prebid.org/", - defaultProfile: { - webo_ctx: ['moon'] - } - } - } - } - ] + weboCtxConf: { + token: "to-be-defined", // mandatory + targetURL: "https://prebid.org", // default is document.URL + setPrebidTargeting: true, // default + sendToBidders: true, // default + defaultProfile: { // optional + webo_ctx: ['moon'], + webo_ds: ['bar'] + } + }, + weboUserDataConf: { + setPrebidTargeting: true, // default + sendToBidders: true, // default + defaultProfile: { // optional + webo_cs: ['Red'], + webo_audiences: ['bam'] + }, + localStorageProfileKey: 'webo_wam2gam_entry' // default + } + } + }] } }); }); @@ -54,7 +63,7 @@ } }, bids: [{ - bidder: 'appnexus', + bidder: 'smartadserver', params: { placementId: 1 } diff --git a/karma.conf.maker.js b/karma.conf.maker.js index cf5999ba85e..abff3f043b9 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -166,7 +166,7 @@ module.exports = function(codeCoverage, browserstack, watchMode, file) { browserNoActivityTimeout: 3e5, // default 10000 captureTimeout: 3e5, // default 60000, browserDisconnectTolerance: 3, - concurrency: 5, + concurrency: 6, plugins: plugins } diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index 2bdbdd6414b..af67bb2bf48 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -1,11 +1,20 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { - deepAccess, uniques, isArray, getWindowTop, isGptPubadsDefined, isSlotMatchingAdUnitCode, logInfo, logWarn, - getWindowSelf + deepAccess, + uniques, + isArray, + getWindowTop, + isGptPubadsDefined, + isSlotMatchingAdUnitCode, + logInfo, + logWarn, + getWindowSelf, + mergeDeep, } from '../src/utils.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +// **************************** UTILS *************************** // const BIDDER_CODE = '33across'; const END_POINT = 'https://ssc.33across.com/api/v1/hb'; const SYNC_ENDPOINT = 'https://ssc-cms.33across.com/ps/?m=xch&rt=html&ru=deb'; @@ -42,6 +51,14 @@ const adapterState = { const NON_MEASURABLE = 'nm'; +function getTTXConfig() { + const ttxSettings = Object.assign({}, + config.getConfig('ttxSettings') + ); + + return ttxSettings; +} + // **************************** VALIDATION *************************** // function isBidRequestValid(bid) { return ( @@ -74,6 +91,7 @@ function _validateGUID(bid) { function _validateBanner(bid) { const banner = deepAccess(bid, 'mediaTypes.banner'); + // If there's no banner no need to validate against banner rules if (banner === undefined) { return true; @@ -140,91 +158,125 @@ function _validateVideo(bid) { // NOTE: With regards to gdrp consent data, the server will independently // infer the gdpr applicability therefore, setting the default value to false function buildRequests(bidRequests, bidderRequest) { + const { + ttxSettings, + gdprConsent, + uspConsent, + pageUrl + } = _buildRequestParams(bidRequests, bidderRequest); + + const groupedRequests = _buildRequestGroups(ttxSettings, bidRequests); + + const serverRequests = []; + + for (const key in groupedRequests) { + serverRequests.push( + _createServerRequest({ + bidRequests: groupedRequests[key], + gdprConsent, + uspConsent, + pageUrl, + ttxSettings + }) + ) + } + + return serverRequests; +} + +function _buildRequestParams(bidRequests, bidderRequest) { + const ttxSettings = getTTXConfig(); + const gdprConsent = Object.assign({ consentString: undefined, gdprApplies: false }, bidderRequest && bidderRequest.gdprConsent); const uspConsent = bidderRequest && bidderRequest.uspConsent; + const pageUrl = (bidderRequest && bidderRequest.refererInfo) ? (bidderRequest.refererInfo.referer) : (undefined); adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(uniques); - return bidRequests.map(bidRequest => _createServerRequest( - { - bidRequest, - gdprConsent, - uspConsent, - pageUrl - }) - ); + return { + ttxSettings, + gdprConsent, + uspConsent, + pageUrl + } +} + +function _buildRequestGroups(ttxSettings, bidRequests) { + const bidRequestsComplete = bidRequests.map(_inferProduct); + const enableSRAMode = ttxSettings && ttxSettings.enableSRAMode; + const keyFunc = (enableSRAMode === true) ? _getSRAKey : _getMRAKey; + + return _groupBidRequests(bidRequestsComplete, keyFunc); +} + +function _groupBidRequests(bidRequests, keyFunc) { + const groupedRequests = {}; + + bidRequests.forEach((req) => { + const key = keyFunc(req); + + groupedRequests[key] = groupedRequests[key] || []; + groupedRequests[key].push(req); + }); + + return groupedRequests; +} + +function _getSRAKey(bidRequest) { + return `${bidRequest.params.siteId}:${bidRequest.params.productId}`; +} + +function _getMRAKey(bidRequest) { + return `${bidRequest.bidId}`; } // Infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request -// NOTE: At this point, TTX only accepts request for a single impression -function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl}) { +function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, pageUrl, ttxSettings }) { const ttxRequest = {}; - const params = bidRequest.params; + const { siteId, test } = bidRequests[0].params; /* * Infer data for the request payload */ - ttxRequest.imp = [{}]; + ttxRequest.imp = []; - if (deepAccess(bidRequest, 'mediaTypes.banner')) { - ttxRequest.imp[0].banner = { - ..._buildBannerORTB(bidRequest) - } - } - - if (deepAccess(bidRequest, 'mediaTypes.video')) { - ttxRequest.imp[0].video = _buildVideoORTB(bidRequest); - } - - ttxRequest.imp[0].ext = { - ttx: { - prod: _getProduct(bidRequest) - } - }; + bidRequests.forEach((req) => { + ttxRequest.imp.push(_buildImpORTB(req)); + }); - ttxRequest.site = { id: params.siteId }; + ttxRequest.site = { id: siteId }; if (pageUrl) { ttxRequest.site.page = pageUrl; } - // Go ahead send the bidId in request to 33exchange so it's kept track of in the bid response and - // therefore in ad targetting process - ttxRequest.id = bidRequest.bidId; + ttxRequest.id = bidRequests[0].auctionId; if (gdprConsent.consentString) { - ttxRequest.user = setExtension( - ttxRequest.user, - 'consent', - gdprConsent.consentString - ) + ttxRequest.user = setExtensions(ttxRequest.user, { + 'consent': gdprConsent.consentString + }); } - if (Array.isArray(bidRequest.userIdAsEids) && bidRequest.userIdAsEids.length > 0) { - ttxRequest.user = setExtension( - ttxRequest.user, - 'eids', - bidRequest.userIdAsEids - ) + if (Array.isArray(bidRequests[0].userIdAsEids) && bidRequests[0].userIdAsEids.length > 0) { + ttxRequest.user = setExtensions(ttxRequest.user, { + 'eids': bidRequests[0].userIdAsEids + }); } - ttxRequest.regs = setExtension( - ttxRequest.regs, - 'gdpr', - Number(gdprConsent.gdprApplies) - ); + ttxRequest.regs = setExtensions(ttxRequest.regs, { + 'gdpr': Number(gdprConsent.gdprApplies) + }); if (uspConsent) { - ttxRequest.regs = setExtension( - ttxRequest.regs, - 'us_privacy', - uspConsent - ) + ttxRequest.regs = setExtensions(ttxRequest.regs, { + 'us_privacy': uspConsent + }); } ttxRequest.ext = { @@ -237,16 +289,14 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl } }; - if (bidRequest.schain) { - ttxRequest.source = setExtension( - ttxRequest.source, - 'schain', - bidRequest.schain - ) + if (bidRequests[0].schain) { + ttxRequest.source = setExtensions(ttxRequest.source, { + 'schain': bidRequests[0].schain + }); } // Finally, set the openRTB 'test' param if this is to be a test bid - if (params.test === 1) { + if (test === 1) { ttxRequest.test = 1; } @@ -259,8 +309,7 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl }; // Allow the ability to configure the HB endpoint for testing purposes. - const ttxSettings = config.getConfig('ttxSettings'); - const url = (ttxSettings && ttxSettings.url) || `${END_POINT}?guid=${params.siteId}`; + const url = (ttxSettings && ttxSettings.url) || `${END_POINT}?guid=${siteId}`; // Return the server request return { @@ -272,14 +321,36 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl } // BUILD REQUESTS: SET EXTENSIONS -function setExtension(obj = {}, key, value) { - return Object.assign({}, obj, { - ext: Object.assign({}, obj.ext, { - [key]: value - }) +function setExtensions(obj = {}, extFields) { + return mergeDeep({}, obj, { + 'ext': extFields }); } +// BUILD REQUESTS: IMP +function _buildImpORTB(bidRequest) { + const imp = { + id: bidRequest.bidId, + ext: { + ttx: { + prod: deepAccess(bidRequest, 'params.productId') + } + } + }; + + if (deepAccess(bidRequest, 'mediaTypes.banner')) { + imp.banner = { + ..._buildBannerORTB(bidRequest) + } + } + + if (deepAccess(bidRequest, 'mediaTypes.video')) { + imp.video = _buildVideoORTB(bidRequest); + } + + return imp; +} + // BUILD REQUESTS: SIZE INFERENCE function _transformSizes(sizes) { if (isArray(sizes) && sizes.length === 2 && !isArray(sizes[0])) { @@ -297,6 +368,14 @@ function _getSize(size) { } // BUILD REQUESTS: PRODUCT INFERENCE +function _inferProduct(bidRequest) { + return mergeDeep({}, bidRequest, { + params: { + productId: _getProduct(bidRequest) + } + }); +} + function _getProduct(bidRequest) { const { params, mediaTypes } = bidRequest; @@ -367,7 +446,7 @@ function _buildVideoORTB(bidRequest) { const video = {} - const {w, h} = _getSize(videoParams.playerSize[0]); + const { w, h } = _getSize(videoParams.playerSize[0]); video.w = w; video.h = h; @@ -388,11 +467,11 @@ function _buildVideoORTB(bidRequest) { if (product === PRODUCT.INSTREAM) { video.startdelay = video.startdelay || 0; video.placement = 1; - }; + } // bidfloors if (typeof bidRequest.getFloor === 'function') { - const bidfloors = _getBidFloors(bidRequest, {w: video.w, h: video.h}, VIDEO); + const bidfloors = _getBidFloors(bidRequest, { w: video.w, h: video.h }, VIDEO); if (bidfloors) { Object.assign(video, { @@ -404,6 +483,7 @@ function _buildVideoORTB(bidRequest) { }); } } + return video; } @@ -556,54 +636,61 @@ function _isIframe() { } // **************************** INTERPRET RESPONSE ******************************** // -// NOTE: At this point, the response from 33exchange will only ever contain one bid -// i.e. the highest bid function interpretResponse(serverResponse, bidRequest) { - const bidResponses = []; - - // If there are bids, look at the first bid of the first seatbid (see NOTE above for assumption about ttx) - if (serverResponse.body.seatbid.length > 0 && serverResponse.body.seatbid[0].bid.length > 0) { - bidResponses.push(_createBidResponse(serverResponse.body)); - } - - return bidResponses; + const { seatbid, cur = 'USD' } = serverResponse.body; + + if (!isArray(seatbid)) { + return []; + } + + // Pick seats with valid bids and convert them into an Array of responses + // in format expected by Prebid Core + return seatbid + .filter((seat) => ( + isArray(seat.bid) && + seat.bid.length > 0 + )) + .reduce((acc, seat) => { + return acc.concat( + seat.bid.map((bid) => _createBidResponse(bid, cur)) + ); + }, []); } -// All this assumes that only one bid is ever returned by ttx -function _createBidResponse(response) { +function _createBidResponse(bid, cur) { const isADomainPresent = - response.seatbid[0].bid[0].adomain && response.seatbid[0].bid[0].adomain.length; - const bid = { - requestId: response.id, + bid.adomain && bid.adomain.length; + const bidResponse = { + requestId: bid.impid, 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, - mediaType: deepAccess(response.seatbid[0].bid[0], 'ext.ttx.mediaType', BANNER), - currency: response.cur, + cpm: bid.price, + width: bid.w, + height: bid.h, + ad: bid.adm, + ttl: bid.ttl || 60, + creativeId: bid.crid, + mediaType: deepAccess(bid, 'ext.ttx.mediaType', BANNER), + currency: cur, netRevenue: true } if (isADomainPresent) { - bid.meta = { - advertiserDomains: response.seatbid[0].bid[0].adomain + bidResponse.meta = { + advertiserDomains: bid.adomain }; } - if (bid.mediaType === VIDEO) { - const vastType = deepAccess(response.seatbid[0].bid[0], 'ext.ttx.vastType', 'xml'); + if (bidResponse.mediaType === VIDEO) { + const vastType = deepAccess(bid, 'ext.ttx.vastType', 'xml'); if (vastType === 'xml') { - bid.vastXml = bid.ad; + bidResponse.vastXml = bidResponse.ad; } else { - bid.vastUrl = bid.ad; + bidResponse.vastUrl = bidResponse.ad; } } - return bid; + return bidResponse; } // **************************** USER SYNC *************************** // diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 264cf5f9fcb..b000772f214 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -308,7 +308,7 @@ function getElementFromTopWindow(element, currentWindow) { function autoDetectAdUnitElementIdFromGpt(adUnitCode) { const autoDetectedAdUnit = getGptSlotInfoForAdUnitCode(adUnitCode); - if (autoDetectedAdUnit && autoDetectedAdUnit.divId) { + if (autoDetectedAdUnit.divId) { return autoDetectedAdUnit.divId; } }; 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/admixerBidAdapter.js b/modules/admixerBidAdapter.js index bb91ddcdfc8..dfb76a03804 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -1,14 +1,15 @@ import { logError } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; +import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; const BIDDER_CODE = 'admixer'; -const ALIASES = ['go2net', 'adblender', 'adsyield']; +const ALIASES = ['go2net', 'adblender', 'adsyield', 'futureads']; const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.2.aspx'; export const spec = { code: BIDDER_CODE, aliases: ALIASES, - supportedMediaTypes: ['banner', 'video'], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. */ 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 99f079b2574..d17bcfd27aa 100644 --- a/modules/adomikAnalyticsAdapter.js +++ b/modules/adomikAnalyticsAdapter.js @@ -76,6 +76,8 @@ adomikAdapter.sendTypedEvent = function() { const groupedTypedEvents = adomikAdapter.buildTypedEvents(); const bulkEvents = { + testId: adomikAdapter.currentContext.testId, + testValue: adomikAdapter.currentContext.testValue, uid: adomikAdapter.currentContext.uid, ahbaid: adomikAdapter.currentContext.id, hostname: window.location.hostname, @@ -126,6 +128,8 @@ adomikAdapter.sendTypedEvent = function() { }; adomikAdapter.sendWonEvent = function (wonEvent) { + let keyValues = { testId: adomikAdapter.currentContext.testId, testValue: adomikAdapter.currentContext.testValue } + wonEvent = {...wonEvent, ...keyValues} const stringWonEvent = JSON.stringify(wonEvent) logInfo('Won event sent to adomik prebid analytic ' + stringWonEvent); @@ -205,6 +209,8 @@ adomikAdapter.enableAnalytics = function (config) { adomikAdapter.currentContext = { uid: initOptions.id, url: initOptions.url, + testId: initOptions.testId, + testValue: initOptions.testValue, id: '', timeouted: false, } diff --git a/modules/adplusBidAdapter.js b/modules/adplusBidAdapter.js new file mode 100644 index 00000000000..c001781a792 --- /dev/null +++ b/modules/adplusBidAdapter.js @@ -0,0 +1,203 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +// #region Constants +export const BIDDER_CODE = 'adplus'; +export const ADPLUS_ENDPOINT = 'https://ssp.ad-plus.com.tr/server/headerBidding'; +export const DGID_CODE = 'adplus_dg_id'; +export const SESSION_CODE = 'adplus_s_id'; +export const storage = getStorageManager(undefined, BIDDER_CODE); +const COOKIE_EXP = 1000 * 60 * 60 * 24; // 1 day +// #endregion + +// #region Helpers +export function isValidUuid (uuid) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + uuid + ); +} + +function getSessionId() { + let sid = storage.cookiesAreEnabled() && storage.getCookie(SESSION_CODE); + + if ( + !sid || !isValidUuid(sid) + ) { + sid = utils.generateUUID(); + setSessionId(sid); + } + + return sid; +} + +function setSessionId(sid) { + if (storage.cookiesAreEnabled()) { + const expires = new Date(Date.now() + COOKIE_EXP).toISOString(); + + storage.setCookie(SESSION_CODE, sid, expires); + } +} +// #endregion + +// #region Bid request validation +function isBidRequestValid(bid) { + if (!bid) { + utils.logError(BIDDER_CODE, 'bid, can not be empty', bid); + return false; + } + + if (!bid.params) { + utils.logError(BIDDER_CODE, 'bid.params is required.'); + return false; + } + + if (!bid.params.adUnitId || typeof bid.params.adUnitId !== 'string') { + utils.logError( + BIDDER_CODE, + 'bid.params.adUnitId is missing or has wrong type.' + ); + return false; + } + + if (!bid.params.inventoryId || typeof bid.params.inventoryId !== 'string') { + utils.logError( + BIDDER_CODE, + 'bid.params.inventoryId is missing or has wrong type.' + ); + return false; + } + + if ( + !bid.mediaTypes || + !bid.mediaTypes[BANNER] || + !utils.isArray(bid.mediaTypes[BANNER].sizes) || + bid.mediaTypes[BANNER].sizes.length <= 0 || + !utils.isArrayOfNums(bid.mediaTypes[BANNER].sizes[0]) + ) { + utils.logError(BIDDER_CODE, 'Wrong or missing size parameters.'); + return false; + } + + return true; +} +// #endregion + +// #region Building the bid requests +/** + * + * @param {object} bid + * @returns + */ +function createBidRequest(bid) { + // Developer Params + const { + inventoryId, + adUnitId, + extraData, + yearOfBirth, + gender, + categories, + latitude, + longitude, + sdkVersion, + } = bid.params; + + return { + method: 'GET', + url: ADPLUS_ENDPOINT, + data: utils.cleanObj({ + bidId: bid.bidId, + inventoryId, + adUnitId, + adUnitWidth: bid.mediaTypes[BANNER].sizes[0][0], + adUnitHeight: bid.mediaTypes[BANNER].sizes[0][1], + extraData, + yearOfBirth, + gender, + categories, + latitude, + longitude, + sdkVersion: sdkVersion || '1', + session: getSessionId(), + interstitial: 0, + token: typeof window.top === 'object' && window.top[DGID_CODE] ? window.top[DGID_CODE] : undefined, + secure: window.location.protocol === 'https:' ? 1 : 0, + screenWidth: screen.width, + screenHeight: screen.height, + language: window.navigator.language || 'en-US', + pageUrl: window.location.href, + domain: window.location.hostname, + referrer: window.location.referrer, + }), + }; +} + +function buildRequests(validBidRequests, bidderRequest) { + return validBidRequests.map((req) => createBidRequest(req)); +} +// #endregion + +// #region Interpreting Responses +/** + * + * @param {HeaderBiddingResponse} responseData + * @param { object } bidParams + * @returns + */ +function createAdResponse(responseData, bidParams) { + return { + requestId: responseData.requestID, + cpm: responseData.cpm, + currency: responseData.currency, + width: responseData.width, + height: responseData.height, + creativeId: responseData.creativeID, + dealId: responseData.dealID, + netRevenue: responseData.netRevenue, + ttl: responseData.ttl, + ad: responseData.ad, + mediaType: responseData.mediaType, + meta: { + advertiserDomains: responseData.advertiserDomains, + primaryCatId: utils.isArray(responseData.categoryIDs) && responseData.categoryIDs.length > 0 + ? responseData.categoryIDs[0] : undefined, + secondaryCatIds: responseData.categoryIDs, + }, + }; +} + +function interpretResponse(response, request) { + // In case of empty response + if ( + response.body == null || + !utils.isArray(response.body) || + response.body.length === 0 + ) { + return []; + } + const bids = response.body.map((bid) => createAdResponse(bid)); + return bids; +} +// #endregion + +// #region Bidder +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + onTimeout(timeoutData) { + utils.logError('Adplus adapter timed out for the auction.', timeoutData); + }, + onBidWon(bid) { + utils.logInfo( + `Adplus adapter won the auction. Bid id: ${bid.bidId}, Ad Unit Id: ${bid.adUnitId}, Inventory Id: ${bid.inventoryId}` + ); + }, +}; + +registerBidder(spec); +// #endregion diff --git a/modules/adplusBidAdapter.md b/modules/adplusBidAdapter.md new file mode 100644 index 00000000000..dce9e4a312f --- /dev/null +++ b/modules/adplusBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +Module Name: AdPlus Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: adplus.destek@yaani.com.tr + +# Description + +AdPlus Prebid.js Bidder Adapter. Only banner formats are supported. + +About us : https://ssp.ad-plus.com.tr/ + +# Test Parameters + +```javascript +var adUnits = [ + { + code: "div-adplus", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + ], + }, + }, + bids: [ + { + bidder: "adplus", + params: { + inventoryId: "-1", + adUnitId: "-3", + }, + }, + ], + }, +]; +``` diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 334309aec5c..65776bf79a7 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,6 +1,7 @@ import { deepAccess, buildUrl, parseSizesInput } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; +import { createEidsArray } from './userId/eids.js'; import find from 'core-js-pure/features/array/find.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; @@ -97,17 +98,21 @@ export const spec = { PageRefreshed: getPageRefreshed() }; - if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent) { payload.gdprConsent = { consentString: bidderRequest.gdprConsent.consentString, consentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : null }; } - if (bidderRequest && bidderRequest.uspConsent) { + if (bidderRequest.uspConsent) { payload.uspConsent = bidderRequest.uspConsent; } + if (deepAccess(bidderRequest, 'userId')) { + payload.userId = createEidsArray(bidderRequest.userId); + } + const data = JSON.stringify(payload); const options = { withCredentials: true diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index c02eeccaea6..5480d1eedca 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -1,4 +1,4 @@ -import { convertCamelToUnderscore, isArray, isNumber, isPlainObject, logError, logInfo, deepAccess, logMessage, convertTypes, isStr, getParameterByName, deepClone, chunk, logWarn, getBidRequest, createTrackPixelHtml, isEmpty, transformBidderParamKeywords, getMaxValueFromArray, fill, getMinValueFromArray, isArrayOfNums, isFn } from '../src/utils.js'; +import { convertCamelToUnderscore, isArray, isNumber, isPlainObject, logError, logInfo, deepAccess, logMessage, convertTypes, isStr, getParameterByName, deepClone, chunk, logWarn, getBidRequest, createTrackPixelHtml, isEmpty, transformBidderParamKeywords, getMaxValueFromArray, fill, getMinValueFromArray, isArrayOfNums, isFn, isAllowZeroCpmBidsEnabled } from '../src/utils.js'; import { Renderer } from '../src/Renderer.js'; import { config } from '../src/config.js'; import { registerBidder, getIabSubCategory } from '../src/adapters/bidderFactory.js'; @@ -78,7 +78,6 @@ export const spec = { { code: 'districtm', gvlid: 144 }, { code: 'adasta' }, { code: 'beintoo', gvlid: 618 }, - { code: 'targetVideo' }, ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -293,7 +292,8 @@ export const spec = { serverResponse.tags.forEach(serverBid => { const rtbBid = getRtbBid(serverBid); if (rtbBid) { - if (rtbBid.cpm !== 0 && includes(this.supportedMediaTypes, rtbBid.ad_type)) { + const cpmCheck = (isAllowZeroCpmBidsEnabled(bidderRequest.bidderCode)) ? rtbBid.cpm >= 0 : rtbBid.cpm > 0; + if (cpmCheck && includes(this.supportedMediaTypes, rtbBid.ad_type)) { const bid = newBid(serverBid, rtbBid, bidderRequest); bid.mediaType = parseMediaType(rtbBid); bids.push(bid); @@ -598,6 +598,22 @@ function newBid(serverBid, rtbBid, bidderRequest) { bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id }); } + // temporary function; may remove at later date if/when adserver fully supports dchain + function setupDChain(rtbBid) { + let dchain = { + ver: '1.0', + complete: 0, + nodes: [{ + bsid: rtbBid.buyer_member_id.toString() + }], + }; + + return dchain; + } + if (rtbBid.buyer_member_id) { + bid.meta = Object.assign({}, bid.meta, {dchain: setupDChain(rtbBid)}); + } + if (rtbBid.brand_id) { bid.meta = Object.assign({}, bid.meta, { brandId: rtbBid.brand_id }); } diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index a882a796851..e705156d4a2 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -1,4 +1,4 @@ -import { logWarn, deepAccess, isArray, parseSizesInput, isFn, parseUrl, getUniqueIdentifierStr } from '../src/utils.js'; +import { logWarn, deepAccess, deepSetValue, deepClone, isArray, parseSizesInput, isFn, parseUrl, getUniqueIdentifierStr } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; @@ -6,7 +6,7 @@ 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.18'; +const ADAPTER_VERSION = '1.19'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; const CURRENCY = 'USD'; @@ -360,6 +360,7 @@ function createVideoRequestData(bid, bidderRequest) { let tagid = getVideoBidParam(bid, 'tagid'); let topLocation = getTopWindowLocation(bidderRequest); let eids = getEids(bid); + let ortb2 = deepClone(config.getConfig('ortb2')); let payload = { isPrebid: true, appId: appId, @@ -378,6 +379,7 @@ function createVideoRequestData(bid, bidderRequest) { displaymanagerver: ADAPTER_VERSION }], site: { + ...deepAccess(ortb2, 'site', {}), page: topLocation.href, domain: topLocation.hostname }, @@ -389,39 +391,32 @@ function createVideoRequestData(bid, bidderRequest) { js: 1, geo: {} }, - regs: { - ext: {} - }, - source: { - ext: {} - }, - user: { - ext: {} - }, + app: deepAccess(ortb2, 'app'), + user: deepAccess(ortb2, 'user'), cur: [CURRENCY] }; if (bidderRequest && bidderRequest.uspConsent) { - payload.regs.ext.us_privacy = bidderRequest.uspConsent; + deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); } if (bidderRequest && bidderRequest.gdprConsent) { let { gdprApplies, consentString } = bidderRequest.gdprConsent; - payload.regs.ext.gdpr = gdprApplies ? 1 : 0; - payload.user.ext.consent = consentString; + deepSetValue(payload, 'regs.ext.gdpr', gdprApplies ? 1 : 0); + deepSetValue(payload, 'user.ext.consent', consentString); } if (bid.schain) { - payload.source.ext.schain = bid.schain; + deepSetValue(payload, 'source.ext.schain', bid.schain); } if (eids.length > 0) { - payload.user.ext.eids = eids; + deepSetValue(payload, 'user.ext.eids', eids); } let connection = navigator.connection || navigator.webkitConnection; if (connection && connection.effectiveType) { - payload.device.connectiontype = connection.effectiveType; + deepSetValue(payload, 'device.connectiontype', connection.effectiveType); } return payload; @@ -439,8 +434,10 @@ function createBannerRequestData(bids, bidderRequest) { sizes: getBannerSizes(bid) }; }); + let ortb2 = deepClone(config.getConfig('ortb2')); let payload = { slots: slots, + ortb2: ortb2, page: topLocation.href, domain: topLocation.hostname, search: topLocation.search, diff --git a/modules/beopBidAdapter.js b/modules/beopBidAdapter.js index a6bc8a5687d..8f875a32eb1 100644 --- a/modules/beopBidAdapter.js +++ b/modules/beopBidAdapter.js @@ -99,16 +99,18 @@ export const spec = { } function buildTrackingParams(data, info, value) { + const accountId = data.params.accountId; return { - pid: data.params.accountId, + pid: accountId === undefined ? data.ad.match(/account: \“([a-f\d]{24})\“/)[1] : accountId, nid: data.params.networkId, nptnid: data.params.networkPartnerId, - bid: data.bidId, + bid: data.bidId || data.requestId, sl_n: data.adUnitCode, aid: data.auctionId, se_ca: 'bid', se_ac: info, - se_va: value + se_va: value, + url: window.location.href }; } 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/criteoBidAdapter.js b/modules/criteoBidAdapter.js index a4ec99e4fa8..18f1aaf7678 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -24,7 +24,7 @@ const LOG_PREFIX = 'Criteo: '; Unminified source code can be found in the privately shared repo: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js */ const FAST_BID_VERSION_PLACEHOLDER = '%FAST_BID_VERSION%'; -export const FAST_BID_VERSION_CURRENT = 113; +export const FAST_BID_VERSION_CURRENT = 116; const FAST_BID_VERSION_LATEST = 'latest'; const FAST_BID_VERSION_NONE = 'none'; const PUBLISHER_TAG_URL_TEMPLATE = 'https://static.criteo.net/js/ld/publishertag.prebid' + FAST_BID_VERSION_PLACEHOLDER + '.js'; diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index 9e082dffbf4..c9caa78e5e7 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -26,6 +26,7 @@ export const RENDERER_URL = 'https://cdn.cwi.re/prebid/renderer/LATEST/renderer. export const CW_PAGE_VIEW_ID = generateUUID(); const LS_CWID_KEY = 'cw_cwid'; const CW_GROUPS_QUERY = 'cwgroups'; +const CW_CREATIVE_QUERY = 'cwcreative'; const storage = getStorageManager(); @@ -161,6 +162,7 @@ export const spec = { let refgroups = []; + const cwCreativeId = getQueryVariable(CW_CREATIVE_QUERY); const rgQuery = getQueryVariable(CW_GROUPS_QUERY); if (rgQuery !== null) { refgroups = rgQuery.split(','); @@ -171,6 +173,7 @@ export const spec = { const payload = { cwid: localStorageCWID, refgroups, + cwcreative: cwCreativeId, slots: slots, httpRef: referer || '', pageViewId: CW_PAGE_VIEW_ID, 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/dchain.js b/modules/dchain.js new file mode 100644 index 00000000000..6a1bd1ebf70 --- /dev/null +++ b/modules/dchain.js @@ -0,0 +1,149 @@ +import includes from 'core-js-pure/features/array/includes.js'; +import { config } from '../src/config.js'; +import { getHook } from '../src/hook.js'; +import { _each, isStr, isArray, isPlainObject, hasOwn, deepClone, deepAccess, logWarn, logError } from '../src/utils.js'; + +const shouldBeAString = ' should be a string'; +const shouldBeAnObject = ' should be an object'; +const shouldBeAnArray = ' should be an Array'; +const shouldBeValid = ' is not a valid dchain property'; +const MODE = { + STRICT: 'strict', + RELAXED: 'relaxed', + OFF: 'off' +}; +const MODES = []; // an array of modes +_each(MODE, mode => MODES.push(mode)); + +export function checkDchainSyntax(bid, mode) { + let dchainObj = deepClone(bid.meta.dchain); + let failPrefix = 'Detected something wrong in bid.meta.dchain object for bid:'; + let failMsg = ''; + const dchainPropList = ['ver', 'complete', 'nodes', 'ext']; + + function appendFailMsg(msg) { + failMsg += '\n' + msg; + } + + function printFailMsg() { + if (mode === MODE.STRICT) { + logError(failPrefix, bid, '\n', dchainObj, failMsg); + } else { + logWarn(failPrefix, bid, `\n`, dchainObj, failMsg); + } + } + + let dchainProps = Object.keys(dchainObj); + dchainProps.forEach(prop => { + if (!includes(dchainPropList, prop)) { + appendFailMsg(`dchain.${prop}` + shouldBeValid); + } + }); + + if (dchainObj.complete !== 0 && dchainObj.complete !== 1) { + appendFailMsg(`dchain.complete should be 0 or 1`); + } + + if (!isStr(dchainObj.ver)) { + appendFailMsg(`dchain.ver` + shouldBeAString); + } + + if (hasOwn(dchainObj, 'ext')) { + if (!isPlainObject(dchainObj.ext)) { + appendFailMsg(`dchain.ext` + shouldBeAnObject); + } + } + + if (!isArray(dchainObj.nodes)) { + appendFailMsg(`dchain.nodes` + shouldBeAnArray); + printFailMsg(); + if (mode === MODE.STRICT) return false; + } else { + const nodesPropList = ['asi', 'bsid', 'rid', 'name', 'domain', 'ext']; + dchainObj.nodes.forEach((node, index) => { + if (!isPlainObject(node)) { + appendFailMsg(`dchain.nodes[${index}]` + shouldBeAnObject); + } else { + let nodeProps = Object.keys(node); + nodeProps.forEach(prop => { + if (!includes(nodesPropList, prop)) { + appendFailMsg(`dchain.nodes[${index}].${prop}` + shouldBeValid); + } + + if (prop === 'ext') { + if (!isPlainObject(node.ext)) { + appendFailMsg(`dchain.nodes[${index}].ext` + shouldBeAnObject); + } + } else { + if (!isStr(node[prop])) { + appendFailMsg(`dchain.nodes[${index}].${prop}` + shouldBeAString); + } + } + }); + } + }); + } + + if (failMsg.length > 0) { + printFailMsg(); + if (mode === MODE.STRICT) { + return false; + } + } + return true; +} + +function isValidDchain(bid) { + let mode = MODE.STRICT; + const dchainConfig = config.getConfig('dchain'); + + if (dchainConfig && isStr(dchainConfig.validation) && MODES.indexOf(dchainConfig.validation) != -1) { + mode = dchainConfig.validation; + } + + if (mode === MODE.OFF) { + return true; + } else { + return checkDchainSyntax(bid, mode); + } +} + +export function addBidResponseHook(fn, adUnitCode, bid) { + const basicDchain = { + ver: '1.0', + complete: 0, + nodes: [] + }; + + if (deepAccess(bid, 'meta.networkId') && deepAccess(bid, 'meta.networkName')) { + basicDchain.nodes.push({ name: bid.meta.networkName, bsid: bid.meta.networkId.toString() }); + } + basicDchain.nodes.push({ name: bid.bidderCode }); + + let bidDchain = deepAccess(bid, 'meta.dchain'); + if (bidDchain && isPlainObject(bidDchain)) { + let result = isValidDchain(bid); + + if (result) { + // extra check in-case mode is OFF and there is a setup issue + if (isArray(bidDchain.nodes)) { + bid.meta.dchain.nodes.push({ asi: bid.bidderCode }); + } else { + logWarn('bid.meta.dchain.nodes did not exist or was not an array; did not append prebid dchain.', bid); + } + } else { + // remove invalid dchain + delete bid.meta.dchain; + } + } else { + bid.meta.dchain = basicDchain; + } + + fn(adUnitCode, bid); +} + +export function init() { + getHook('addBidResponse').before(addBidResponseHook, 35); +} + +init(); diff --git a/modules/dchain.md b/modules/dchain.md new file mode 100644 index 00000000000..f01b3483f3c --- /dev/null +++ b/modules/dchain.md @@ -0,0 +1,45 @@ +# dchain module + +Refer: +- https://iabtechlab.com/buyers-json-demand-chain/ + +## Sample code for dchain setConfig and dchain object +``` +pbjs.setConfig({ + "dchain": { + "validation": "strict" + } +}); +``` + +``` +bid.meta.dchain: { + "complete": 0, + "ver": "1.0", + "ext": {...}, + "nodes": [{ + "asi": "abc", + "bsid": "123", + "rid": "d4e5f6", + "name": "xyz", + "domain": "mno", + "ext": {...} + }, ...] +} +``` + +## Workflow +The dchain module is not enabled by default as it may not be necessary for all publishers. +If required, dchain module can be included as following +``` + $ gulp build --modules=dchain,pubmaticBidAdapter,openxBidAdapter,rubiconBidAdapter,sovrnBidAdapter +``` + +The dchain module will validate a bidder's dchain object (if it was defined). Bidders should assign their dchain object into `bid.meta` field. If the dchain object is valid, it will remain in the bid object for later use. + +If it was not defined, the dchain will create a default dchain object for prebid. + +## Validation modes +- ```strict```: It is the default validation mode. In this mode, dchain object will not be accpeted from adapters if it is invalid. Errors are thrown for invalid dchain object. +- ```relaxed```: In this mode, errors are thrown for an invalid dchain object but the invalid dchain object is still accepted from adapters. +- ```off```: In this mode, no validations are performed and dchain object is accepted as is from adapters. \ No newline at end of file diff --git a/modules/dspxBidAdapter.js b/modules/dspxBidAdapter.js index 16c06073c41..09de5254745 100644 --- a/modules/dspxBidAdapter.js +++ b/modules/dspxBidAdapter.js @@ -12,7 +12,7 @@ const GVLID = 602; export const spec = { code: BIDDER_CODE, gvlid: GVLID, - aliases: ['dspx'], + aliases: [], supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bid) { return !!(bid.params.placement); diff --git a/modules/emx_digitalBidAdapter.js b/modules/emx_digitalBidAdapter.js index 3ab9af8e523..0ed23f11631 100644 --- a/modules/emx_digitalBidAdapter.js +++ b/modules/emx_digitalBidAdapter.js @@ -257,10 +257,18 @@ export const spec = { tagid, secure }; + + // adding gpid support + let gpid = deepAccess(bid, 'ortb2Imp.ext.data.adserver.adslot'); + if (!gpid) { + gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + } + if (gpid) { + data.ext = {gpid: gpid.toString()}; + } let typeSpecifics = isVideo ? { video: emxAdapter.buildVideo(bid) } : { banner: emxAdapter.buildBanner(bid) }; let bidfloorObj = bidfloor > 0 ? { bidfloor, bidfloorcur: DEFAULT_CUR } : {}; let emxBid = Object.assign(data, typeSpecifics, bidfloorObj); - emxImps.push(emxBid); }); 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/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index 4798158bb3a..eca31dd5a95 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -312,6 +312,12 @@ export const spec = { requestParams._fw_us_privacy = bidderRequest.uspConsent; } + // Add schain object + var schain = currentBidRequest.schain; + if (schain) { + requestParams.schain = schain; + } + var vastParams = currentBidRequest.params.vastUrlParams; if (typeof vastParams === 'object') { for (var key in vastParams) { diff --git a/modules/futureads.md b/modules/futureads.md new file mode 100644 index 00000000000..7b1c1d55b7f --- /dev/null +++ b/modules/futureads.md @@ -0,0 +1,48 @@ +# Overview +Module Name: Future Ads Bidder Adapter +Module Type: Bidder Adapter +Maintainer: contact@futureads.io +# Description +Connects to Future Ads demand source to fetch bids. +Banner and Video formats are supported. +Please use ```futureads``` as the bidder code. +# Test Parameters +``` +var adUnits = [ + { + code: 'desktop-banner-ad-div', + sizes: [[300, 250]], // a display size + bids: [ + { + bidder: "futureads", + params: { + zone: '2eb6bd58-865c-47ce-af7f-a918108c3fd2' + } + } + ] + },{ + code: 'mobile-banner-ad-div', + sizes: [[300, 50]], // a mobile size + bids: [ + { + bidder: "futureads", + params: { + zone: '62211486-c50b-4356-9f0f-411778d31fcc' + } + } + ] + },{ + code: 'video-ad', + sizes: [[300, 50]], + mediaType: 'video', + bids: [ + { + bidder: "futureads", + params: { + zone: 'ebeb1e79-8cb4-4473-b2d0-2e24b7ff47fd' + } + } + ] + }, +]; +``` 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/goldbachBidAdapter.js b/modules/goldbachBidAdapter.js new file mode 100644 index 00000000000..8057925a62c --- /dev/null +++ b/modules/goldbachBidAdapter.js @@ -0,0 +1,1176 @@ +import { Renderer } from '../src/Renderer.js'; +import { + isEmpty, + convertCamelToUnderscore, + isFn, + createTrackPixelHtml, + convertTypes, + deepClone, + fill, + getParameterByName, + getMaxValueFromArray, + getMinValueFromArray, + chunk, + isArray, + isArrayOfNums, + isNumber, + isStr, + isPlainObject, + logError, + logInfo, + logMessage, + deepAccess, + getBidRequest, + transformBidderParamKeywords +} from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder, getIabSubCategory } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO, ADPOD } from '../src/mediaTypes.js'; +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 { OUTSTREAM, INSTREAM } from '../src/video.js'; + +const BIDDER_CODE = 'goldbach'; +const URL = 'https://ib.adnxs.com/ut/v3/prebid'; +const PRICING_URL = 'https://templates.da-services.ch/01_universal/burda_prebid/1.0/json/sizeCPMMapping.json'; +const URL_SIMPLE = 'https://ib.adnxs-simple.com/ut/v3/prebid'; +const VIDEO_TARGETING = ['id', 'minduration', 'maxduration', + 'skippable', 'playback_method', 'frameworks', 'context', 'skipoffset']; +const VIDEO_RTB_TARGETING = ['minduration', 'maxduration', 'skip', 'skipafter', 'playbackmethod', 'api']; +const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language']; +const APP_DEVICE_PARAMS = ['geo', 'device_id']; // appid is collected separately +const DEBUG_PARAMS = ['enabled', 'dongle', 'member_id', 'debug_timeout']; +const DEFAULT_PRICE_MAPPING = { + '0x0': 2.5, + '300x600': 5, + '800x250': 6, + '350x600': 6 +}; +let PRICE_MAPPING; +const VIDEO_MAPPING = { + playback_method: { + 'unknown': 0, + 'auto_play_sound_on': 1, + 'auto_play_sound_off': 2, + 'click_to_play': 3, + 'mouse_over': 4, + 'auto_play_sound_unknown': 5 + }, + context: { + 'unknown': 0, + 'pre_roll': 1, + 'mid_roll': 2, + 'post_roll': 3, + 'outstream': 4, + 'in-banner': 5 + } +}; +const NATIVE_MAPPING = { + body: 'description', + body2: 'desc2', + cta: 'ctatext', + image: { + serverName: 'main_image', + requiredParams: { required: true } + }, + icon: { + serverName: 'icon', + requiredParams: { required: true } + }, + sponsoredBy: 'sponsored_by', + privacyLink: 'privacy_link', + salePrice: 'saleprice', + displayUrl: 'displayurl' +}; +const SOURCE = 'pbjs'; +const MAX_IMPS_PER_REQUEST = 15; +const mappingFileUrl = 'https://acdn.adnxs-simple.com/prebid/appnexus-mapping/mappings.json'; +const SCRIPT_TAG_START = ' { + if (Array.isArray(bid.params.placementId)) { + const ids = bid.params.placementId; + for (let i = 0; i < ids.length; i++) { + const newBid = Object.assign({}, bid, {params: {placementId: ids[i]}}); + localBidRequests.push(newBid) + } + } else { + localBidRequests.push(bid); + } + }); + const tags = localBidRequests.map(bidToTag); + const userObjBid = find(bidRequests, hasUserInfo); + let userObj = {}; + if (config.getConfig('coppa') === true) { + userObj = { 'coppa': true }; + } + if (userObjBid) { + Object.keys(userObjBid.params.user) + .filter(param => includes(USER_PARAMS, param)) + .forEach((param) => { + let uparam = convertCamelToUnderscore(param); + if (param === 'segments' && isArray(userObjBid.params.user[param])) { + let segs = []; + userObjBid.params.user[param].forEach(val => { + if (isNumber(val)) { + segs.push({'id': val}); + } else if (isPlainObject(val)) { + segs.push(val); + } + }); + userObj[uparam] = segs; + } else if (param !== 'segments') { + userObj[uparam] = userObjBid.params.user[param]; + } + }); + } + + const appDeviceObjBid = find(bidRequests, hasAppDeviceInfo); + let appDeviceObj; + if (appDeviceObjBid && appDeviceObjBid.params && appDeviceObjBid.params.app) { + appDeviceObj = {}; + Object.keys(appDeviceObjBid.params.app) + .filter(param => includes(APP_DEVICE_PARAMS, param)) + .forEach(param => appDeviceObj[param] = appDeviceObjBid.params.app[param]); + } + + const appIdObjBid = find(bidRequests, hasAppId); + let appIdObj; + if (appIdObjBid && appIdObjBid.params && appDeviceObjBid.params.app && appDeviceObjBid.params.app.id) { + appIdObj = { + appid: appIdObjBid.params.app.id + }; + } + + let debugObj = {}; + let debugObjParams = {}; + const debugBidRequest = find(bidRequests, hasDebug); + if (debugBidRequest && debugBidRequest.debug) { + debugObj = debugBidRequest.debug; + } + + if (debugObj && debugObj.enabled) { + Object.keys(debugObj) + .filter(param => includes(DEBUG_PARAMS, param)) + .forEach(param => { + debugObjParams[param] = debugObj[param]; + }); + } + + const memberIdBid = find(bidRequests, hasMemberId); + const member = memberIdBid ? parseInt(memberIdBid.params.member, 10) : 0; + const schain = bidRequests[0].schain; + const omidSupport = find(bidRequests, hasOmidSupport); + + const payload = { + tags: [...tags], + user: userObj, + sdk: { + source: SOURCE, + version: '$prebid.version$' + }, + schain: schain + }; + + if (omidSupport) { + payload['iab_support'] = { + omidpn: 'Appnexus', + omidpv: '$prebid.version$' + } + } + + if (member > 0) { + payload.member_id = member; + } + + if (appDeviceObjBid) { + payload.device = appDeviceObj + } + if (appIdObjBid) { + payload.app = appIdObj; + } + + if (config.getConfig('adpod.brandCategoryExclusion')) { + payload.brand_category_uniqueness = true; + } + + if (debugObjParams.enabled) { + payload.debug = debugObjParams; + logInfo('Debug Auction Settings:\n\n' + JSON.stringify(debugObjParams, null, 4)); + } + + if (bidderRequest && bidderRequest.gdprConsent) { + // note - objects for impbus use underscore instead of camelCase + payload.gdpr_consent = { + consent_string: bidderRequest.gdprConsent.consentString, + consent_required: bidderRequest.gdprConsent.gdprApplies + }; + + if (bidderRequest.gdprConsent.addtlConsent && bidderRequest.gdprConsent.addtlConsent.indexOf('~') !== -1) { + let ac = bidderRequest.gdprConsent.addtlConsent; + // pull only the ids from the string (after the ~) and convert them to an array of ints + let acStr = ac.substring(ac.indexOf('~') + 1); + payload.gdpr_consent.addtl_consent = acStr.split('.').map(id => parseInt(id, 10)); + } + } + + if (bidderRequest && bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent + } + + if (bidderRequest && bidderRequest.refererInfo) { + let refererinfo = { + rd_ref: encodeURIComponent(bidderRequest.refererInfo.referer), + rd_top: bidderRequest.refererInfo.reachedTop, + rd_ifs: bidderRequest.refererInfo.numIframes, + rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',') + } + payload.referrer_detection = refererinfo; + } + + const hasAdPodBid = find(bidRequests, hasAdPod); + if (hasAdPodBid) { + bidRequests.filter(hasAdPod).forEach(adPodBid => { + const adPodTags = createAdPodRequest(tags, adPodBid); + // don't need the original adpod placement because it's in adPodTags + const nonPodTags = payload.tags.filter(tag => tag.uuid !== adPodBid.bidId); + payload.tags = [...nonPodTags, ...adPodTags]; + }); + } + + if (bidRequests[0].userId) { + let eids = []; + + addUserId(eids, deepAccess(bidRequests[0], `userId.flocId.id`), 'chrome.com', null); + addUserId(eids, deepAccess(bidRequests[0], `userId.criteoId`), 'criteo.com', null); + addUserId(eids, deepAccess(bidRequests[0], `userId.netId`), 'netid.de', null); + addUserId(eids, deepAccess(bidRequests[0], `userId.idl_env`), 'liveramp.com', null); + addUserId(eids, deepAccess(bidRequests[0], `userId.tdid`), 'adserver.org', 'TDID'); + addUserId(eids, deepAccess(bidRequests[0], `userId.uid2.id`), 'uidapi.com', 'UID2'); + + if (eids.length) { + payload.eids = eids; + } + } + + if (tags[0].publisher_id) { + payload.publisher_id = tags[0].publisher_id; + } + + const request = formatRequest(payload, bidderRequest); + // add pricing endpoint + return [{method: 'GET', url: PRICING_URL, options: {withCredentials: false}}, request]; + }, + + parseAndMapCpm: function(serverResponse) { + const responseBody = serverResponse.body; + if (Array.isArray(responseBody) && responseBody.length) { + let localData = {}; + responseBody.forEach(cpmPerSize => { + Object.keys(cpmPerSize).forEach(size => { + let obj = {}; + obj[size] = cpmPerSize[size]; + localData = Object.assign({}, localData, obj) + }) + }) + PRICE_MAPPING = localData; + return null; + } + + if (responseBody.version) { + const localPriceMapping = PRICE_MAPPING || DEFAULT_PRICE_MAPPING; + if (responseBody.tags && Array.isArray(responseBody.tags) && responseBody.tags.length) { + responseBody.tags.forEach((tag) => { + if (tag.ads && Array.isArray(tag.ads) && tag.ads.length) { + tag.ads.forEach(ad => { + if (ad.ad_type === 'banner') { + const size = `${ad.rtb.banner.width}x${ad.rtb.banner.height}`; + if (localPriceMapping[size]) { + ad.cpm = localPriceMapping[size]; + } else { + ad.cpm = localPriceMapping['0x0']; + } + } + }) + } + }); + } + } + return responseBody; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, { bidderRequest }) { + serverResponse = this.parseAndMapCpm(serverResponse); + if (!serverResponse) return []; + const bids = []; + if (serverResponse.error) { + let errorMessage = `in response for ${bidderRequest.bidderCode} adapter : ${serverResponse.error}`; + logError(errorMessage); + return bids; + } + + if (serverResponse.tags) { + serverResponse.tags.forEach(serverBid => { + const rtbBid = getRtbBid(serverBid); + if (rtbBid) { + if (rtbBid.cpm !== 0 && includes(this.supportedMediaTypes, rtbBid.ad_type)) { + const bid = newBid(serverBid, rtbBid, bidderRequest); + bid.mediaType = parseMediaType(rtbBid); + bids.push(bid); + } + } + }); + } + + if (serverResponse.debug && serverResponse.debug.debug_info) { + let debugHeader = 'AppNexus Debug Auction for Prebid\n\n' + let debugText = debugHeader + serverResponse.debug.debug_info + debugText = debugText + .replace(/(|)/gm, '\t') // Tables + .replace(/(<\/td>|<\/th>)/gm, '\n') // Tables + .replace(/^
/gm, '') // Remove leading
+ .replace(/(
\n|
)/gm, '\n') //
+ .replace(/

(.*)<\/h1>/gm, '\n\n===== $1 =====\n\n') // Header H1 + .replace(/(.*)<\/h[2-6]>/gm, '\n\n*** $1 ***\n\n') // Headers + .replace(/(<([^>]+)>)/igm, ''); // Remove any other tags + logMessage(debugText); + } + + return bids; + }, + + /** + * @typedef {Object} mappingFileInfo + * @property {string} url mapping file json url + * @property {number} refreshInDays prebid stores mapping data in localstorage so you can return in how many days you want to update value stored in localstorage. + * @property {string} localStorageKey unique key to store your mapping json in localstorage + */ + + /** + * Returns mapping file info. This info will be used by bidderFactory to preload mapping file and store data in local storage + * @returns {mappingFileInfo} + */ + getMappingFileInfo: function () { + return { + url: mappingFileUrl, + refreshInDays: 2 + } + }, + + getUserSyncs: function (syncOptions, responses, gdprConsent) { + if (syncOptions.iframeEnabled && hasPurpose1Consent({gdprConsent})) { + return [{ + type: 'iframe', + url: 'https://acdn.adnxs.com/dmp/async_usersync.html' + }]; + } + }, + + transformBidParams: function (params, isOpenRtb) { + params = convertTypes({ + 'member': 'string', + 'invCode': 'string', + 'placementId': 'number', + 'keywords': transformBidderParamKeywords, + 'publisherId': 'number' + }, params); + + if (isOpenRtb) { + params.use_pmt_rule = (typeof params.usePaymentRule === 'boolean') ? params.usePaymentRule : false; + if (params.usePaymentRule) { delete params.usePaymentRule; } + + if (isPopulatedArray(params.keywords)) { + params.keywords.forEach(deleteValues); + } + + Object.keys(params).forEach(paramKey => { + let convertedKey = convertCamelToUnderscore(paramKey); + if (convertedKey !== paramKey) { + params[convertedKey] = params[paramKey]; + delete params[paramKey]; + } + }); + } + + return params; + }, + + /** + * Add element selector to javascript tracker to improve native viewability + * @param {Bid} bid + */ + onBidWon: function (bid) { + if (bid.native) { + reloadViewabilityScriptWithCorrectParameters(bid); + } + } +} + +function isPopulatedArray(arr) { + return !!(isArray(arr) && arr.length > 0); +} + +function deleteValues(keyPairObj) { + if (isPopulatedArray(keyPairObj.value) && keyPairObj.value[0] === '') { + delete keyPairObj.value; + } +} + +function reloadViewabilityScriptWithCorrectParameters(bid) { + let viewJsPayload = getAppnexusViewabilityScriptFromJsTrackers(bid.native.javascriptTrackers); + + if (viewJsPayload) { + let prebidParams = 'pbjs_adid=' + bid.adId + ';pbjs_auc=' + bid.adUnitCode; + + let jsTrackerSrc = getViewabilityScriptUrlFromPayload(viewJsPayload) + + let newJsTrackerSrc = jsTrackerSrc.replace('dom_id=%native_dom_id%', prebidParams); + + // find iframe containing script tag + let frameArray = document.getElementsByTagName('iframe'); + + // boolean var to modify only one script. That way if there are muliple scripts, + // they won't all point to the same creative. + let modifiedAScript = false; + + // first, loop on all ifames + for (let i = 0; i < frameArray.length && !modifiedAScript; i++) { + let currentFrame = frameArray[i]; + try { + // IE-compatible, see https://stackoverflow.com/a/3999191/2112089 + let nestedDoc = currentFrame.contentDocument || currentFrame.contentWindow.document; + + if (nestedDoc) { + // if the doc is present, we look for our jstracker + let scriptArray = nestedDoc.getElementsByTagName('script'); + for (let j = 0; j < scriptArray.length && !modifiedAScript; j++) { + let currentScript = scriptArray[j]; + if (currentScript.getAttribute('data-src') == jsTrackerSrc) { + currentScript.setAttribute('src', newJsTrackerSrc); + currentScript.setAttribute('data-src', ''); + if (currentScript.removeAttribute) { + currentScript.removeAttribute('data-src'); + } + modifiedAScript = true; + } + } + } + } catch (exception) { + // trying to access a cross-domain iframe raises a SecurityError + // this is expected and ignored + if (!(exception instanceof DOMException && exception.name === 'SecurityError')) { + // all other cases are raised again to be treated by the calling function + throw exception; + } + } + } + } +} + +function strIsAppnexusViewabilityScript(str) { + let regexMatchUrlStart = str.match(VIEWABILITY_URL_START); + let viewUrlStartInStr = regexMatchUrlStart != null && regexMatchUrlStart.length >= 1; + + let regexMatchFileName = str.match(VIEWABILITY_FILE_NAME); + let fileNameInStr = regexMatchFileName != null && regexMatchFileName.length >= 1; + + return str.startsWith(SCRIPT_TAG_START) && fileNameInStr && viewUrlStartInStr; +} + +function getAppnexusViewabilityScriptFromJsTrackers(jsTrackerArray) { + let viewJsPayload; + if (isStr(jsTrackerArray) && strIsAppnexusViewabilityScript(jsTrackerArray)) { + viewJsPayload = jsTrackerArray; + } else if (isArray(jsTrackerArray)) { + for (let i = 0; i < jsTrackerArray.length; i++) { + let currentJsTracker = jsTrackerArray[i]; + if (strIsAppnexusViewabilityScript(currentJsTracker)) { + viewJsPayload = currentJsTracker; + } + } + } + return viewJsPayload; +} + +function getViewabilityScriptUrlFromPayload(viewJsPayload) { + // extracting the content of the src attribute + // -> substring between src=" and " + let indexOfFirstQuote = viewJsPayload.indexOf('src="') + 5; // offset of 5: the length of 'src=' + 1 + let indexOfSecondQuote = viewJsPayload.indexOf('"', indexOfFirstQuote); + let jsTrackerSrc = viewJsPayload.substring(indexOfFirstQuote, indexOfSecondQuote); + return jsTrackerSrc; +} + +function hasPurpose1Consent(bidderRequest) { + let result = true; + if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { + result = !!(deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); + } + } + return result; +} + +function formatRequest(payload, bidderRequest) { + let request = []; + let options = { + withCredentials: true + }; + + let endpointUrl = URL; + + if (!hasPurpose1Consent(bidderRequest)) { + endpointUrl = URL_SIMPLE; + } + + if (getParameterByName('apn_test').toUpperCase() === 'TRUE' || config.getConfig('apn_test') === true) { + options.customHeaders = { + 'X-Is-Test': 1 + } + } + + if (payload.tags.length > MAX_IMPS_PER_REQUEST) { + const clonedPayload = deepClone(payload); + + chunk(payload.tags, MAX_IMPS_PER_REQUEST).forEach(tags => { + clonedPayload.tags = tags; + const payloadString = JSON.stringify(clonedPayload); + request.push({ + method: 'POST', + url: endpointUrl, + data: payloadString, + bidderRequest, + options + }); + }); + } else { + const payloadString = JSON.stringify(payload); + request = { + method: 'POST', + url: endpointUrl, + data: payloadString, + bidderRequest, + options + }; + } + + return request; +} + +function newRenderer(adUnitCode, rtbBid, rendererOptions = {}) { + const renderer = Renderer.install({ + id: rtbBid.renderer_id, + url: rtbBid.renderer_url, + config: rendererOptions, + loaded: false, + adUnitCode + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + logError('Prebid Error calling setRender on renderer', err); + } + + renderer.setEventHandlers({ + impression: () => logMessage('Outstream video impression event'), + loaded: () => logMessage('Outstream video loaded event'), + ended: () => { + logMessage('Outstream renderer video event'); + document.querySelector(`#${adUnitCode}`).style.display = 'none'; + } + }); + return renderer; +} + +/** + * Unpack the Server's Bid into a Prebid-compatible one. + * @param serverBid + * @param rtbBid + * @param bidderRequest + * @return Bid + */ +function newBid(serverBid, rtbBid, bidderRequest) { + const bidRequest = getBidRequest(serverBid.uuid, [bidderRequest]); + const bid = { + requestId: serverBid.uuid, + cpm: rtbBid.cpm, + creativeId: rtbBid.creative_id, + dealId: rtbBid.deal_id, + currency: 'USD', + netRevenue: true, + ttl: 300, + adUnitCode: bidRequest.adUnitCode, + appnexus: { + buyerMemberId: rtbBid.buyer_member_id, + dealPriority: rtbBid.deal_priority, + dealCode: rtbBid.deal_code + } + }; + + // WE DON'T FULLY SUPPORT THIS ATM - future spot for adomain code; creating a stub for 5.0 compliance + if (rtbBid.adomain) { + bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [] }); + } + + if (rtbBid.advertiser_id) { + bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id }); + } + + if (rtbBid.rtb.video) { + // shared video properties used for all 3 contexts + Object.assign(bid, { + width: rtbBid.rtb.video.player_width, + height: rtbBid.rtb.video.player_height, + vastImpUrl: rtbBid.notify_url, + ttl: 3600 + }); + + const videoContext = deepAccess(bidRequest, 'mediaTypes.video.context'); + switch (videoContext) { + case ADPOD: + const primaryCatId = getIabSubCategory(bidRequest.bidder, rtbBid.brand_category_id); + bid.meta = Object.assign({}, bid.meta, { primaryCatId }); + const dealTier = rtbBid.deal_priority; + bid.video = { + context: ADPOD, + durationSeconds: Math.floor(rtbBid.rtb.video.duration_ms / 1000), + dealTier + }; + bid.vastUrl = rtbBid.rtb.video.asset_url; + break; + case OUTSTREAM: + bid.adResponse = serverBid; + bid.adResponse.ad = bid.adResponse.ads[0]; + bid.adResponse.ad.video = bid.adResponse.ad.rtb.video; + bid.vastXml = rtbBid.rtb.video.content; + + if (rtbBid.renderer_url) { + const videoBid = find(bidderRequest.bids, bid => bid.bidId === serverBid.uuid); + const rendererOptions = deepAccess(videoBid, 'renderer.options'); + bid.renderer = newRenderer(bid.adUnitCode, rtbBid, rendererOptions); + } + break; + case INSTREAM: + bid.vastUrl = rtbBid.notify_url + '&redir=' + encodeURIComponent(rtbBid.rtb.video.asset_url); + break; + } + } else if (rtbBid.rtb[NATIVE]) { + const nativeAd = rtbBid.rtb[NATIVE]; + + // setting up the jsTracker: + // we put it as a data-src attribute so that the tracker isn't called + // until we have the adId (see onBidWon) + let jsTrackerDisarmed = rtbBid.viewability.config.replace('src=', 'data-src='); + + let jsTrackers = nativeAd.javascript_trackers; + + if (jsTrackers == undefined) { + jsTrackers = jsTrackerDisarmed; + } else if (isStr(jsTrackers)) { + jsTrackers = [jsTrackers, jsTrackerDisarmed]; + } else { + jsTrackers.push(jsTrackerDisarmed); + } + + bid[NATIVE] = { + title: nativeAd.title, + body: nativeAd.desc, + body2: nativeAd.desc2, + cta: nativeAd.ctatext, + rating: nativeAd.rating, + sponsoredBy: nativeAd.sponsored, + privacyLink: nativeAd.privacy_link, + address: nativeAd.address, + downloads: nativeAd.downloads, + likes: nativeAd.likes, + phone: nativeAd.phone, + price: nativeAd.price, + salePrice: nativeAd.saleprice, + clickUrl: nativeAd.link.url, + displayUrl: nativeAd.displayurl, + clickTrackers: nativeAd.link.click_trackers, + impressionTrackers: nativeAd.impression_trackers, + javascriptTrackers: jsTrackers + }; + if (nativeAd.main_img) { + bid['native'].image = { + url: nativeAd.main_img.url, + height: nativeAd.main_img.height, + width: nativeAd.main_img.width, + }; + } + if (nativeAd.icon) { + bid['native'].icon = { + url: nativeAd.icon.url, + height: nativeAd.icon.height, + width: nativeAd.icon.width, + }; + } + } else { + Object.assign(bid, { + width: rtbBid.rtb.banner.width, + height: rtbBid.rtb.banner.height, + ad: rtbBid.rtb.banner.content + }); + try { + if (rtbBid.rtb.trackers) { + const url = rtbBid.rtb.trackers[0].impression_urls[0]; + const tracker = createTrackPixelHtml(url); + bid.ad += tracker; + } + } catch (error) { + logError('Error appending tracking pixel', error); + } + } + + return bid; +} + +function bidToTag(bid) { + const tag = {}; + tag.sizes = transformSizes(bid.sizes); + tag.primary_size = tag.sizes[0]; + tag.ad_types = []; + tag.uuid = bid.bidId; + if (bid.params.placementId) { + tag.id = parseInt(bid.params.placementId, 10); + } else { + tag.code = bid.params.invCode; + } + tag.allow_smaller_sizes = bid.params.allowSmallerSizes || false; + tag.use_pmt_rule = bid.params.usePaymentRule || false + tag.prebid = true; + tag.disable_psa = true; + let bidFloor = getBidFloor(bid); + if (bidFloor) { + tag.reserve = bidFloor; + } + if (bid.params.position) { + tag.position = { 'above': 1, 'below': 2 }[bid.params.position] || 0; + } + if (bid.params.trafficSourceCode) { + tag.traffic_source_code = bid.params.trafficSourceCode; + } + if (bid.params.privateSizes) { + tag.private_sizes = transformSizes(bid.params.privateSizes); + } + if (bid.params.supplyType) { + tag.supply_type = bid.params.supplyType; + } + if (bid.params.pubClick) { + tag.pubclick = bid.params.pubClick; + } + if (bid.params.extInvCode) { + tag.ext_inv_code = bid.params.extInvCode; + } + if (bid.params.publisherId) { + tag.publisher_id = parseInt(bid.params.publisherId, 10); + } + if (bid.params.externalImpId) { + tag.external_imp_id = bid.params.externalImpId; + } + if (!isEmpty(bid.params.keywords)) { + let keywords = transformBidderParamKeywords(bid.params.keywords); + + if (keywords.length > 0) { + keywords.forEach(deleteValues); + } + tag.keywords = keywords; + } + + let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { + tag.gpid = gpid; + } + + if (bid.mediaType === NATIVE || deepAccess(bid, `mediaTypes.${NATIVE}`)) { + tag.ad_types.push(NATIVE); + if (tag.sizes.length === 0) { + tag.sizes = transformSizes([1, 1]); + } + + if (bid.nativeParams) { + const nativeRequest = buildNativeRequest(bid.nativeParams); + tag[NATIVE] = { layouts: [nativeRequest] }; + } + } + + const videoMediaType = deepAccess(bid, `mediaTypes.${VIDEO}`); + const context = deepAccess(bid, 'mediaTypes.video.context'); + + if (videoMediaType && context === 'adpod') { + tag.hb_source = 7; + } else { + tag.hb_source = 1; + } + if (bid.mediaType === VIDEO || videoMediaType) { + tag.ad_types.push(VIDEO); + } + + // instream gets vastUrl, outstream gets vastXml + if (bid.mediaType === VIDEO || (videoMediaType && context !== 'outstream')) { + tag.require_asset_url = true; + } + + if (bid.params.video) { + tag.video = {}; + // place any valid video params on the tag + Object.keys(bid.params.video) + .filter(param => includes(VIDEO_TARGETING, param)) + .forEach(param => { + switch (param) { + case 'context': + case 'playback_method': + let type = bid.params.video[param]; + type = (isArray(type)) ? type[0] : type; + tag.video[param] = VIDEO_MAPPING[param][type]; + break; + // Deprecating tags[].video.frameworks in favor of tags[].video_frameworks + case 'frameworks': + break; + default: + tag.video[param] = bid.params.video[param]; + } + }); + + if (bid.params.video.frameworks && isArray(bid.params.video.frameworks)) { + tag['video_frameworks'] = bid.params.video.frameworks; + } + } + + // use IAB ORTB values if the corresponding values weren't already set by bid.params.video + if (videoMediaType) { + tag.video = tag.video || {}; + Object.keys(videoMediaType) + .filter(param => includes(VIDEO_RTB_TARGETING, param)) + .forEach(param => { + switch (param) { + case 'minduration': + case 'maxduration': + if (typeof tag.video[param] !== 'number') tag.video[param] = videoMediaType[param]; + break; + case 'skip': + if (typeof tag.video['skippable'] !== 'boolean') tag.video['skippable'] = (videoMediaType[param] === 1); + break; + case 'skipafter': + if (typeof tag.video['skipoffset'] !== 'number') tag.video['skippoffset'] = videoMediaType[param]; + break; + case 'playbackmethod': + if (typeof tag.video['playback_method'] !== 'number') { + let type = videoMediaType[param]; + type = (isArray(type)) ? type[0] : type; + + // we only support iab's options 1-4 at this time. + if (type >= 1 && type <= 4) { + tag.video['playback_method'] = type; + } + } + break; + case 'api': + if (!tag['video_frameworks'] && isArray(videoMediaType[param])) { + // need to read thru array; remove 6 (we don't support it), swap 4 <> 5 if found (to match our adserver mapping for these specific values) + let apiTmp = videoMediaType[param].map(val => { + let v = (val === 4) ? 5 : (val === 5) ? 4 : val; + + if (v >= 1 && v <= 5) { + return v; + } + }).filter(v => v); + tag['video_frameworks'] = apiTmp; + } + break; + } + }); + } + + if (bid.renderer) { + tag.video = Object.assign({}, tag.video, { custom_renderer_present: true }); + } + + if (bid.params.frameworks && isArray(bid.params.frameworks)) { + tag['banner_frameworks'] = bid.params.frameworks; + } + + let adUnit = find(auctionManager.getAdUnits(), au => bid.transactionId === au.transactionId); + if (adUnit && adUnit.mediaTypes && adUnit.mediaTypes.banner) { + tag.ad_types.push(BANNER); + } + + if (tag.ad_types.length === 0) { + delete tag.ad_types; + } + + return tag; +} + +/* Turn bid request sizes into ut-compatible format */ +function transformSizes(requestSizes) { + let sizes = []; + let sizeObj = {}; + + if (isArray(requestSizes) && requestSizes.length === 2 && + !isArray(requestSizes[0])) { + sizeObj.width = parseInt(requestSizes[0], 10); + sizeObj.height = parseInt(requestSizes[1], 10); + sizes.push(sizeObj); + } else if (typeof requestSizes === 'object') { + for (let i = 0; i < requestSizes.length; i++) { + let size = requestSizes[i]; + sizeObj = {}; + sizeObj.width = parseInt(size[0], 10); + sizeObj.height = parseInt(size[1], 10); + sizes.push(sizeObj); + } + } + + return sizes; +} + +function hasUserInfo(bid) { + return !!bid.params.user; +} + +function hasMemberId(bid) { + return !!parseInt(bid.params.member, 10); +} + +function hasAppDeviceInfo(bid) { + if (bid.params) { + return !!bid.params.app + } +} + +function hasAppId(bid) { + if (bid.params && bid.params.app) { + return !!bid.params.app.id + } + return !!bid.params.app +} + +function hasDebug(bid) { + return !!bid.debug +} + +function hasAdPod(bid) { + return ( + bid.mediaTypes && + bid.mediaTypes.video && + bid.mediaTypes.video.context === ADPOD + ); +} + +function hasOmidSupport(bid) { + let hasOmid = false; + const bidderParams = bid.params; + const videoParams = bid.params.video; + if (bidderParams.frameworks && isArray(bidderParams.frameworks)) { + hasOmid = includes(bid.params.frameworks, 6); + } + if (!hasOmid && videoParams && videoParams.frameworks && isArray(videoParams.frameworks)) { + hasOmid = includes(bid.params.video.frameworks, 6); + } + return hasOmid; +} + +/** + * Expand an adpod placement into a set of request objects according to the + * total adpod duration and the range of duration seconds. Sets minduration/ + * maxduration video property according to requireExactDuration configuration + */ +function createAdPodRequest(tags, adPodBid) { + const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; + + const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); + const maxDuration = getMaxValueFromArray(durationRangeSec); + + const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); + let request = fill(...tagToDuplicate, numberOfPlacements); + + if (requireExactDuration) { + const divider = Math.ceil(numberOfPlacements / durationRangeSec.length); + const chunked = chunk(request, divider); + + // each configured duration is set as min/maxduration for a subset of requests + durationRangeSec.forEach((duration, index) => { + chunked[index].map(tag => { + setVideoProperty(tag, 'minduration', duration); + setVideoProperty(tag, 'maxduration', duration); + }); + }); + } else { + // all maxdurations should be the same + request.map(tag => setVideoProperty(tag, 'maxduration', maxDuration)); + } + + return request; +} + +function getAdPodPlacementNumber(videoParams) { + const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; + const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); + + return requireExactDuration + ? Math.max(numberOfPlacements, durationRangeSec.length) + : numberOfPlacements; +} + +function setVideoProperty(tag, key, value) { + if (isEmpty(tag.video)) { tag.video = {}; } + tag.video[key] = value; +} + +function getRtbBid(tag) { + return tag && tag.ads && tag.ads.length && find(tag.ads, ad => ad.rtb); +} + +function buildNativeRequest(params) { + const request = {}; + + // map standard prebid native asset identifier to /ut parameters + // e.g., tag specifies `body` but /ut only knows `description`. + // mapping may be in form {tag: ''} or + // {tag: {serverName: '', requiredParams: {...}}} + Object.keys(params).forEach(key => { + // check if one of the forms is used, otherwise + // a mapping wasn't specified so pass the key straight through + const requestKey = + (NATIVE_MAPPING[key] && NATIVE_MAPPING[key].serverName) || + NATIVE_MAPPING[key] || + key; + + // required params are always passed on request + const requiredParams = NATIVE_MAPPING[key] && NATIVE_MAPPING[key].requiredParams; + request[requestKey] = Object.assign({}, requiredParams, params[key]); + + // convert the sizes of image/icon assets to proper format (if needed) + const isImageAsset = !!(requestKey === NATIVE_MAPPING.image.serverName || requestKey === NATIVE_MAPPING.icon.serverName); + if (isImageAsset && request[requestKey].sizes) { + let sizes = request[requestKey].sizes; + if (isArrayOfNums(sizes) || (isArray(sizes) && sizes.length > 0 && sizes.every(sz => isArrayOfNums(sz)))) { + request[requestKey].sizes = transformSizes(request[requestKey].sizes); + } + } + + if (requestKey === NATIVE_MAPPING.privacyLink) { + request.privacy_supported = true; + } + }); + + return request; +} + +/** + * This function hides google div container for outstream bids to remove unwanted space on page. Appnexus renderer creates a new iframe outside of google iframe to render the outstream creative. + * @param {string} elementId element id + */ +function hidedfpContainer(elementId) { + var el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']"); + if (el[0]) { + el[0].style.setProperty('display', 'none'); + } +} + +function hideSASIframe(elementId) { + try { + // find script tag with id 'sas_script'. This ensures it only works if you're using Smart Ad Server. + const el = document.getElementById(elementId).querySelectorAll("script[id^='sas_script']"); + if (el[0].nextSibling && el[0].nextSibling.localName === 'iframe') { + el[0].nextSibling.style.setProperty('display', 'none'); + } + } catch (e) { + // element not found! + } +} + +function outstreamRender(bid) { + hidedfpContainer(bid.adUnitCode); + hideSASIframe(bid.adUnitCode); + // push to render queue because ANOutstreamVideo may not be loaded yet + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + tagId: bid.adResponse.tag_id, + sizes: [bid.getSize().split('x')], + targetId: bid.adUnitCode, // target div id to render video + uuid: bid.adResponse.uuid, + adResponse: bid.adResponse, + rendererOptions: bid.renderer.getConfig() + }, handleOutstreamRendererEvents.bind(null, bid)); + }); +} + +function handleOutstreamRendererEvents(bid, id, eventName) { + bid.renderer.handleVideoEvent({ id, eventName }); +} + +function parseMediaType(rtbBid) { + const adType = rtbBid.ad_type; + if (adType === VIDEO) { + return VIDEO; + } else if (adType === NATIVE) { + return NATIVE; + } else { + return BANNER; + } +} + +function addUserId(eids, id, source, rti) { + if (id) { + if (rti) { + eids.push({ source, id, rti_partner: rti }); + } else { + eids.push({ source, id }); + } + } + return eids; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return (bid.params.reserve) ? bid.params.reserve : null; + } + + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + +registerBidder(spec); diff --git a/modules/goldbachBidAdapter.md b/modules/goldbachBidAdapter.md new file mode 100644 index 00000000000..f7c9479439b --- /dev/null +++ b/modules/goldbachBidAdapter.md @@ -0,0 +1,151 @@ +#Overview + +``` +Module Name: Goldbach Bid Adapter +Module Type: Bidder Adapter +Maintainer: dusan.veljovic@goldbach.com +``` + +# Description + +Connects to Xandr exchange for bids. + +Goldbach bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'goldbach', + params: { + placementId: 13144370 + } + }] + }, + // Native adUnit + { + code: 'native-div', + sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + image: { + required: true + }, + sponsoredBy: { + required: true + }, + icon: { + required: false + } + } + }, + bids: [{ + bidder: 'goldbach', + params: { + placementId: 13232354, + allowSmallerSizes: true + } + }] + }, + // Video instream adUnit + { + code: 'video-instream', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + }, + }, + bids: [{ + goldbach: 'goldbach', + params: { + placementId: 13232361, + video: { + skippable: true, + playback_methods: ['auto_play_sound_off'] + } + } + }] + }, + // Video outstream adUnit + { + code: 'video-outstream', + sizes: [[300, 250]], + mediaTypes: { + video: { + playerSize: [[300, 250]], + context: 'outstream', + // Certain ORTB 2.5 video values can be read from the mediatypes object; below are examples of supported params. + // To note - goldbach supports additional values for our system that are not part of the ORTB spec. If you want + // to use these values, they will have to be declared in the bids[].params.video object instead using the goldbach syntax. + // Between the corresponding values of the mediaTypes.video and params.video objects, the properties in params.video will + // take precedence if declared; eg in the example below, the `skippable: true` setting will be used instead of the `skip: 0`. + minduration: 1, + maxduration: 60, + skip: 0, // 1 - true, 0 - false + skipafter: 5, + playbackmethod: [2], // note - we only support options 1-4 at this time + api: [1,2,3] // note - option 6 is not supported at this time + } + }, + bids: [ + { + bidder: 'goldbach', + params: { + placementId: 13232385, + video: { + skippable: true, + playback_method: 'auto_play_sound_off' + } + } + } + ] + }, + // Banner adUnit in a App Webview + // Only use this for situations where prebid.js is in a webview of an App + // See Prebid Mobile for displaying ads via an SDK + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + } + bids: [{ + bidder: 'goldbach', + params: { + placementId: 13144370, + app: { + id: "B1O2W3M4AN.com.prebid.webview", + geo: { + lat: 40.0964439, + lng: -75.3009142 + }, + device_id: { + idfa: "4D12078D-3246-4DA4-AD5E-7610481E7AE", // Apple advertising identifier + aaid: "38400000-8cf0-11bd-b23e-10b96e40000d", // Android advertising identifier + md5udid: "5756ae9022b2ea1e47d84fead75220c8", // MD5 hash of the ANDROID_ID + sha1udid: "4DFAA92388699AC6539885AEF1719293879985BF", // SHA1 hash of the ANDROID_ID + windowsadid: "750c6be243f1c4b5c9912b95a5742fc5" // Windows advertising identifier + } + } + } + }] + } +]; +``` 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/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 8012afa2f30..697fd8c6103 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -286,7 +286,8 @@ function buildRequests(validBidRequests, bidderRequest) { schain, transactionId, userId = {}, - ortb2Imp + ortb2Imp, + adUnitCode = '' } = bidRequest; const { currency, floor } = _getFloor(mediaTypes, params.bidfloor, bidRequest); const eids = getEids(userId); @@ -295,13 +296,16 @@ function buildRequests(validBidRequests, bidderRequest) { let gpid = ''; const date = new Date(); - const lt = date && date.getTime(); - const to = date && date.getTimezoneOffset(); + const lt = date.getTime(); + const to = date.getTimezoneOffset(); if (to) { lt && (data.lt = lt); data.to = to; } + // ADTS-169 add adUnitCode to requests + if (adUnitCode) data.aun = adUnitCode + // ADTS-134 Retrieve ID envelopes for (const eid in eids) data[eid] = eids[eid]; 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/intersectionRtdProvider.js b/modules/intersectionRtdProvider.js new file mode 100644 index 00000000000..4404c4148fe --- /dev/null +++ b/modules/intersectionRtdProvider.js @@ -0,0 +1,114 @@ +import {submodule} from '../src/hook.js'; +import {isFn, logError} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import includes from 'core-js-pure/features/array/includes.js'; +import '../src/adapterManager.js'; +let observerAvailable = true; +function getIntersectionData(requestBidsObject, onDone, providerConfig, userConsent) { + const intersectionMap = {}; + const placeholdersMap = {}; + let done = false; + if (!observerAvailable) return complete(); + const observer = new IntersectionObserver(observerCallback, {threshold: 0.5}); + const adUnitCodes = requestBidsObject.adUnitCodes || []; + const auctionDelay = config.getConfig('realTimeData.auctionDelay') || 0; + const waitForIt = providerConfig.waitForIt; + let adUnits = requestBidsObject.adUnits || getGlobal().adUnits || []; + if (adUnitCodes.length) { + adUnits = adUnits.filter(unit => includes(adUnitCodes, unit.code)); + } + let checkTimeoutId; + findAndObservePlaceholders(); + if (auctionDelay > 0) { + setTimeout(complete, auctionDelay); + } + function findAndObservePlaceholders() { + const observed = adUnits.filter((unit) => { + const code = unit.code; + if (placeholdersMap[code]) return true; + const ph = document.getElementById(code); + if (ph) { + placeholdersMap[code] = ph; + observer.observe(ph); + return true; + } + }); + if ( + observed.length === adUnits.length || + !waitForIt || + auctionDelay <= 0 + ) { + return; + } + checkTimeoutId = setTimeout(findAndObservePlaceholders); + } + function observerCallback(entries) { + let entry = entries.pop(); + while (entry) { + const target = entry.target; + const id = target.getAttribute('id'); + if (id) { + const intersection = intersectionMap[id]; + if (!intersection || intersection.time < entry.time) { + intersectionMap[id] = { + 'boundingClientRect': cloneRect(entry.boundingClientRect), + 'intersectionRect': cloneRect(entry.intersectionRect), + 'rootRect': cloneRect(entry.rootRect), + 'intersectionRatio': entry.intersectionRatio, + 'isIntersecting': entry.isIntersecting, + 'time': entry.time + }; + if (adUnits.every(unit => !!intersectionMap[unit.code])) { + complete(); + } + } + } + entry = entries.pop(); + } + } + function complete() { + if (done) return; + if (checkTimeoutId) clearTimeout(checkTimeoutId); + done = true; + checkTimeoutId = null; + observer && observer.disconnect(); + adUnits && adUnits.forEach((unit) => { + const intersection = intersectionMap[unit.code]; + if (intersection && unit.bids) { + unit.bids.forEach(bid => bid.intersection = intersection); + } + }); + onDone(); + } +} +function init(moduleConfig) { + if (!isFn(window.IntersectionObserver)) { + logError('IntersectionObserver is not defined'); + observerAvailable = false; + } else { + observerAvailable = true; + } + return observerAvailable; +} +function cloneRect(rect) { + return rect ? { + 'left': rect.left, + 'top': rect.top, + 'right': rect.right, + 'bottom': rect.bottom, + 'width': rect.width, + 'height': rect.height, + 'x': rect.x, + 'y': rect.y, + } : rect; +} +export const intersectionSubmodule = { + name: 'intersection', + getBidRequestData: getIntersectionData, + init: init, +}; +function registerSubModule() { + submodule('realTimeData', intersectionSubmodule); +} +registerSubModule(); 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/ipromBidAdapter.js b/modules/ipromBidAdapter.js new file mode 100644 index 00000000000..46582ce95a1 --- /dev/null +++ b/modules/ipromBidAdapter.js @@ -0,0 +1,79 @@ +import { logError } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'iprom'; +const ENDPOINT_URL = 'https://core.iprom.net/programmatic'; +const VERSION = 'v1.0.2'; +const DEFAULT_CURRENCY = 'EUR'; +const DEFAULT_NETREVENUE = true; +const DEFAULT_TTL = 360; + +export const spec = { + code: BIDDER_CODE, + isBidRequestValid: function ({ bidder, params = {} } = {}) { + // id parameter checks + if (!params.id) { + logError(`${bidder}: Parameter 'id' missing`); + return false; + } else if (typeof params.id !== 'string') { + logError(`${bidder}: Parameter 'id' needs to be a string`); + return false; + } + // dimension parameter checks + if (!params.dimension) { + logError(`${bidder}: Required parameter 'dimension' missing`); + return false; + } else if (typeof params.dimension !== 'string') { + logError(`${bidder}: Parameter 'dimension' needs to be a string`); + return false; + } + + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const payload = { + bids: validBidRequests, + referer: bidderRequest.refererInfo, + version: VERSION + }; + const payloadString = JSON.stringify(payload); + + return { + method: 'POST', + url: ENDPOINT_URL, + data: payloadString + }; + }, + + interpretResponse: function (serverResponse, request) { + let bids = serverResponse.body; + + const bidResponses = []; + + bids.forEach(bid => { + const b = { + ad: bid.ad, + requestId: bid.requestId, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.creativeId, + currency: bid.currency || DEFAULT_CURRENCY, + netRevenue: bid.netRevenue || DEFAULT_NETREVENUE, + ttl: bid.ttl || DEFAULT_TTL, + meta: {}, + }; + + if (bid.aDomains && bid.aDomains.length) { + b.meta.advertiserDomains = bid.aDomains; + } + + bidResponses.push(b); + }); + + return bidResponses; + }, +} + +registerBidder(spec); diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 916e5c42ee9..af981a226c0 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -84,6 +84,14 @@ 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. * @@ -275,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; @@ -1066,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); + } }); } diff --git a/modules/kinessoIdSystem.js b/modules/kinessoIdSystem.js index ca8fe269a5e..632f3a669aa 100644 --- a/modules/kinessoIdSystem.js +++ b/modules/kinessoIdSystem.js @@ -180,7 +180,7 @@ function kinessoSyncUrl(accountId, consentData) { const usPrivacyString = uspDataHandler.getConsentData(); let kinessoSyncUrl = `${ID_SVC}?accountid=${accountId}`; if (usPrivacyString) { - kinessoSyncUrl = `${kinessoSyncUrl}?us_privacy=${usPrivacyString}`; + kinessoSyncUrl = `${kinessoSyncUrl}&us_privacy=${usPrivacyString}`; } if (!consentData || typeof consentData.gdprApplies !== 'boolean' || !consentData.gdprApplies) return kinessoSyncUrl; diff --git a/modules/limelightDigitalBidAdapter.js b/modules/limelightDigitalBidAdapter.js index fad4b6b96b3..65e744594cd 100644 --- a/modules/limelightDigitalBidAdapter.js +++ b/modules/limelightDigitalBidAdapter.js @@ -158,7 +158,8 @@ function buildPlacement(bidRequest) { height: size[1] } }), - type: bidRequest.params.adUnitType.toUpperCase() + type: bidRequest.params.adUnitType.toUpperCase(), + publisherId: bidRequest.params.publisherId } } } diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index 9abebb5533c..82503a57e9e 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -4,10 +4,20 @@ * @module modules/lotamePanoramaId * @requires module:modules/userId */ -import { timestamp, isStr, logError, isBoolean, buildUrl, isEmpty, isArray } from '../src/utils.js'; +import { + timestamp, + isStr, + logError, + isBoolean, + buildUrl, + isEmpty, + isArray, + isEmptyStr +} from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; +import { uspDataHandler } from '../src/adapterManager.js'; const KEY_ID = 'panoramaId'; const KEY_EXPIRY = `${KEY_ID}_expiry`; @@ -115,14 +125,23 @@ function saveLotameCache( /** * Retrieve all the cached values from cookies and/or local storage + * @param {Number} clientId */ -function getLotameLocalCache() { +function getLotameLocalCache(clientId = undefined) { let cache = { data: getFromStorage(KEY_ID), expiryTimestampMs: 0, + clientExpiryTimestampMs: 0, }; try { + if (clientId) { + const rawClientExpiry = getFromStorage(`${KEY_EXPIRY}_${clientId}`); + if (isStr(rawClientExpiry)) { + cache.clientExpiryTimestampMs = parseInt(rawClientExpiry, 10); + } + } + const rawExpiry = getFromStorage(KEY_EXPIRY); if (isStr(rawExpiry)) { cache.expiryTimestampMs = parseInt(rawExpiry, 10); @@ -191,11 +210,25 @@ export const lotamePanoramaIdSubmodule = { */ getId(config, consentData, cacheIdObj) { cookieDomain = lotamePanoramaIdSubmodule.findRootDomain(); - let localCache = getLotameLocalCache(); + const configParams = (config && config.params) || {}; + const clientId = configParams.clientId; + const hasCustomClientId = !isEmpty(clientId); + const localCache = getLotameLocalCache(clientId); - let refreshNeeded = Date.now() > localCache.expiryTimestampMs; + const hasExpiredPanoId = Date.now() > localCache.expiryTimestampMs; + + if (hasCustomClientId) { + const hasFreshClientNoConsent = Date.now() < localCache.clientExpiryTimestampMs; + if (hasFreshClientNoConsent) { + // There is no consent + return { + id: undefined, + reason: 'NO_CLIENT_CONSENT', + }; + } + } - if (!refreshNeeded) { + if (!hasExpiredPanoId) { return { id: localCache.data, }; @@ -203,6 +236,18 @@ export const lotamePanoramaIdSubmodule = { const storedUserId = getProfileId(); + // Add CCPA Consent data handling + const usp = uspDataHandler.getConsentData(); + + let usPrivacy; + if (typeof usp !== 'undefined' && !isEmpty(usp) && !isEmptyStr(usp)) { + usPrivacy = usp; + } + if (!usPrivacy) { + // fallback to 1st party cookie + usPrivacy = getFromStorage('us_privacy'); + } + const resolveIdFunction = function (callback) { let queryParams = {}; if (storedUserId) { @@ -226,6 +271,17 @@ export const lotamePanoramaIdSubmodule = { if (consentString) { queryParams.gdpr_consent = consentString; } + + // Add usPrivacy to the url + if (usPrivacy) { + queryParams.us_privacy = usPrivacy; + } + + // Add clientId to the url + if (hasCustomClientId) { + queryParams.c = clientId; + } + const url = buildUrl({ protocol: 'https', host: `id.crwdcntrl.net`, @@ -239,15 +295,31 @@ export const lotamePanoramaIdSubmodule = { if (response) { try { let responseObj = JSON.parse(response); - const shouldUpdateProfileId = !( + const hasNoConsentErrors = !( isArray(responseObj.errors) && responseObj.errors.indexOf(MISSING_CORE_CONSENT) !== -1 ); + if (hasCustomClientId) { + if (hasNoConsentErrors) { + clearLotameCache(`${KEY_EXPIRY}_${clientId}`); + } else if (isStr(responseObj.no_consent) && responseObj.no_consent === 'CLIENT') { + saveLotameCache( + `${KEY_EXPIRY}_${clientId}`, + responseObj.expiry_ts, + responseObj.expiry_ts + ); + + // End Processing + callback(); + return; + } + } + saveLotameCache(KEY_EXPIRY, responseObj.expiry_ts, responseObj.expiry_ts); if (isStr(responseObj.profile_id)) { - if (shouldUpdateProfileId) { + if (hasNoConsentErrors) { setProfileId(responseObj.profile_id); } @@ -262,7 +334,7 @@ export const lotamePanoramaIdSubmodule = { clearLotameCache(KEY_ID); } } else { - if (shouldUpdateProfileId) { + if (hasNoConsentErrors) { clearLotameCache(KEY_PROFILE); } clearLotameCache(KEY_ID); diff --git a/modules/luponmediaBidAdapter.js b/modules/luponmediaBidAdapter.js new file mode 100755 index 00000000000..897dc3c8825 --- /dev/null +++ b/modules/luponmediaBidAdapter.js @@ -0,0 +1,570 @@ +import {isArray, logMessage, deepAccess, logWarn, parseSizesInput, deepSetValue, generateUUID, isEmpty, logError, _each, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {BANNER} from '../src/mediaTypes.js'; +import { ajax } from '../src/ajax.js'; + +const BIDDER_CODE = 'luponmedia'; +const ENDPOINT_URL = 'https://rtb.adxpremium.services/openrtb2/auction'; + +const DIGITRUST_PROP_NAMES = { + PREBID_SERVER: { + id: 'id', + keyv: 'keyv' + } +}; + +var sizeMap = { + 1: '468x60', + 2: '728x90', + 5: '120x90', + 7: '125x125', + 8: '120x600', + 9: '160x600', + 10: '300x600', + 13: '200x200', + 14: '250x250', + 15: '300x250', + 16: '336x280', + 17: '240x400', + 19: '300x100', + 31: '980x120', + 32: '250x360', + 33: '180x500', + 35: '980x150', + 37: '468x400', + 38: '930x180', + 39: '750x100', + 40: '750x200', + 41: '750x300', + 42: '2x4', + 43: '320x50', + 44: '300x50', + 48: '300x300', + 53: '1024x768', + 54: '300x1050', + 55: '970x90', + 57: '970x250', + 58: '1000x90', + 59: '320x80', + 60: '320x150', + 61: '1000x1000', + 64: '580x500', + 65: '640x480', + 66: '930x600', + 67: '320x480', + 68: '1800x1000', + 72: '320x320', + 73: '320x160', + 78: '980x240', + 79: '980x300', + 80: '980x400', + 83: '480x300', + 85: '300x120', + 90: '548x150', + 94: '970x310', + 95: '970x100', + 96: '970x210', + 101: '480x320', + 102: '768x1024', + 103: '480x280', + 105: '250x800', + 108: '320x240', + 113: '1000x300', + 117: '320x100', + 125: '800x250', + 126: '200x600', + 144: '980x600', + 145: '980x150', + 152: '1000x250', + 156: '640x320', + 159: '320x250', + 179: '250x600', + 195: '600x300', + 198: '640x360', + 199: '640x200', + 213: '1030x590', + 214: '980x360', + 221: '1x1', + 229: '320x180', + 230: '2000x1400', + 232: '580x400', + 234: '6x6', + 251: '2x2', + 256: '480x820', + 257: '400x600', + 258: '500x200', + 259: '998x200', + 264: '970x1000', + 265: '1920x1080', + 274: '1800x200', + 278: '320x500', + 282: '320x400', + 288: '640x380', + 548: '500x1000', + 550: '980x480', + 552: '300x200', + 558: '640x640' +}; + +_each(sizeMap, (item, key) => sizeMap[item] = key); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.siteId && bid.params.keyId); // TODO: check for siteId and keyId + }, + buildRequests: function (bidRequests, bidderRequest) { + const bRequest = { + method: 'POST', + url: ENDPOINT_URL, + data: null, + options: {}, + bidderRequest + }; + + let currentImps = []; + + for (let i = 0, len = bidRequests.length; i < len; i++) { + let newReq = newOrtbBidRequest(bidRequests[i], bidderRequest, currentImps); + currentImps = newReq.imp; + bRequest.data = JSON.stringify(newReq); + } + + return bRequest; + }, + interpretResponse: (response, request) => { + const bidResponses = []; + var respCur = 'USD'; + let parsedRequest = JSON.parse(request.data); + let parsedReferrer = parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : ''; + try { + if (response.body && response.body.seatbid && isArray(response.body.seatbid)) { + // Supporting multiple bid responses for same adSize + respCur = response.body.cur || respCur; + response.body.seatbid.forEach(seatbidder => { + seatbidder.bid && + isArray(seatbidder.bid) && + seatbidder.bid.forEach(bid => { + let newBid = { + requestId: bid.impid, + cpm: (parseFloat(bid.price) || 0).toFixed(2), + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.id, + dealId: bid.dealid, + currency: respCur, + netRevenue: false, + ttl: 300, + referrer: parsedReferrer, + ad: bid.adm + }; + + bidResponses.push(newBid); + }); + }); + } + } catch (error) { + logError(error); + } + return bidResponses; + }, + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + let allUserSyncs = []; + if (!hasSynced && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { + responses.forEach(csResp => { + if (csResp.body && csResp.body.ext && csResp.body.ext.usersyncs) { + try { + let response = csResp.body.ext.usersyncs + let bidders = response.bidder_status; + for (let synci in bidders) { + let thisSync = bidders[synci]; + if (thisSync.no_cookie) { + let url = thisSync.usersync.url; + let type = thisSync.usersync.type; + + if (!url) { + logError(`No sync url for bidder luponmedia.`); + } else if ((type === 'image' || type === 'redirect') && syncOptions.pixelEnabled) { + logMessage(`Invoking image pixel user sync for luponmedia`); + allUserSyncs.push({type: 'image', url: url}); + } else if (type == 'iframe' && syncOptions.iframeEnabled) { + logMessage(`Invoking iframe user sync for luponmedia`); + allUserSyncs.push({type: 'iframe', url: url}); + } else { + logError(`User sync type "${type}" not supported for luponmedia`); + } + } + } + } catch (e) { + logError(e); + } + } + }); + } else { + logWarn('Luponmedia: Please enable iframe/pixel based user sync.'); + } + + hasSynced = true; + return allUserSyncs; + }, + onBidWon: bid => { + const bidString = JSON.stringify(bid); + spec.sendWinningsToServer(bidString); + }, + sendWinningsToServer: data => { + let mutation = `mutation {createWin(input: {win: {eventData: "${window.btoa(data)}"}}) {win {createTime } } }`; + let dataToSend = JSON.stringify({ query: mutation }); + + ajax('https://analytics.adxpremium.services/graphql', null, dataToSend, { + contentType: 'application/json', + method: 'POST' + }); + } +}; + +export function hasValidSupplyChainParams(schain) { + let isValid = false; + const requiredFields = ['asi', 'sid', 'hp']; + if (!schain.nodes) return isValid; + isValid = schain.nodes.reduce((status, node) => { + if (!status) return status; + return requiredFields.every(field => node[field]); + }, true); + if (!isValid) logError('LuponMedia: required schain params missing'); + return isValid; +} + +var hasSynced = false; + +export function resetUserSync() { + hasSynced = false; +} + +export function masSizeOrdering(sizes) { + const MAS_SIZE_PRIORITY = [15, 2, 9]; + + return sizes.sort((first, second) => { + // sort by MAS_SIZE_PRIORITY priority order + const firstPriority = MAS_SIZE_PRIORITY.indexOf(first); + const secondPriority = MAS_SIZE_PRIORITY.indexOf(second); + + if (firstPriority > -1 || secondPriority > -1) { + if (firstPriority === -1) { + return 1; + } + if (secondPriority === -1) { + return -1; + } + return firstPriority - secondPriority; + } + + // and finally ascending order + return first - second; + }); +} + +function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { + bidRequest.startTime = new Date().getTime(); + + const bannerParams = deepAccess(bidRequest, 'mediaTypes.banner'); + + let bannerSizes = []; + + if (bannerParams && bannerParams.sizes) { + const sizes = parseSizesInput(bannerParams.sizes); + + // get banner sizes in form [{ w: , h: }, ...] + const format = sizes.map(size => { + const [ width, height ] = size.split('x'); + const w = parseInt(width, 10); + const h = parseInt(height, 10); + return { w, h }; + }); + + bannerSizes = format; + } + + const data = { + id: bidRequest.transactionId, + test: config.getConfig('debug') ? 1 : 0, + source: { + tid: bidRequest.transactionId + }, + tmax: config.getConfig('timeout') || 1500, + imp: currentImps.concat([{ + id: bidRequest.bidId, + secure: 1, + ext: { + [bidRequest.bidder]: bidRequest.params + }, + banner: { + format: bannerSizes + } + }]), + ext: { + prebid: { + targeting: { + includewinners: true, + // includebidderkeys always false for openrtb + includebidderkeys: false + } + } + }, + user: { + } + } + + let bidFloor; + if (isFn(bidRequest.getFloor) && !config.getConfig('disableFloors')) { + let floorInfo; + try { + floorInfo = bidRequest.getFloor({ + currency: 'USD', + mediaType: 'video', + size: parseSizes(bidRequest, 'video') + }); + } catch (e) { + logError('LuponMedia: getFloor threw an error: ', e); + } + bidFloor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; + } else { + bidFloor = parseFloat(deepAccess(bidRequest, 'params.floor')); + } + if (!isNaN(bidFloor)) { + data.imp[0].bidfloor = bidFloor; + } + + appendSiteAppDevice(data, bidRequest, bidderRequest); + + const digiTrust = _getDigiTrustQueryParams(bidRequest, 'PREBID_SERVER'); + if (digiTrust) { + deepSetValue(data, 'user.ext.digitrust', digiTrust); + } + + if (bidderRequest.gdprConsent) { + // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module + let gdprApplies; + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + gdprApplies = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + + deepSetValue(data, 'regs.ext.gdpr', gdprApplies); + deepSetValue(data, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + } + + if (bidderRequest.uspConsent) { + deepSetValue(data, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + // Set user uuid + deepSetValue(data, 'user.id', generateUUID()); + + // set crumbs + if (bidRequest.crumbs && bidRequest.crumbs.pubcid) { + deepSetValue(data, 'user.buyeruid', bidRequest.crumbs.pubcid); + } else { + deepSetValue(data, 'user.buyeruid', generateUUID()); + } + + if (bidRequest.userId && typeof bidRequest.userId === 'object' && + (bidRequest.userId.tdid || bidRequest.userId.pubcid || bidRequest.userId.lipb || bidRequest.userId.idl_env)) { + deepSetValue(data, 'user.ext.eids', []); + + if (bidRequest.userId.tdid) { + data.user.ext.eids.push({ + source: 'adserver.org', + uids: [{ + id: bidRequest.userId.tdid, + ext: { + rtiPartner: 'TDID' + } + }] + }); + } + + if (bidRequest.userId.pubcid) { + data.user.ext.eids.push({ + source: 'pubcommon', + uids: [{ + id: bidRequest.userId.pubcid, + }] + }); + } + + // support liveintent ID + if (bidRequest.userId.lipb && bidRequest.userId.lipb.lipbid) { + data.user.ext.eids.push({ + source: 'liveintent.com', + uids: [{ + id: bidRequest.userId.lipb.lipbid + }] + }); + + data.user.ext.tpid = { + source: 'liveintent.com', + uid: bidRequest.userId.lipb.lipbid + }; + + if (Array.isArray(bidRequest.userId.lipb.segments) && bidRequest.userId.lipb.segments.length) { + deepSetValue(data, 'rp.target.LIseg', bidRequest.userId.lipb.segments); + } + } + + // support identityLink (aka LiveRamp) + if (bidRequest.userId.idl_env) { + data.user.ext.eids.push({ + source: 'liveramp.com', + uids: [{ + id: bidRequest.userId.idl_env + }] + }); + } + } + + if (config.getConfig('coppa') === true) { + deepSetValue(data, 'regs.coppa', 1); + } + + if (bidRequest.schain && hasValidSupplyChainParams(bidRequest.schain)) { + deepSetValue(data, 'source.ext.schain', bidRequest.schain); + } + + const siteData = Object.assign({}, bidRequest.params.inventory, config.getConfig('fpd.context')); + const userData = Object.assign({}, bidRequest.params.visitor, config.getConfig('fpd.user')); + + if (!isEmpty(siteData) || !isEmpty(userData)) { + const bidderData = { + bidders: [ bidderRequest.bidderCode ], + config: { + fpd: {} + } + }; + + if (!isEmpty(siteData)) { + bidderData.config.fpd.site = siteData; + } + + if (!isEmpty(userData)) { + bidderData.config.fpd.user = userData; + } + + deepSetValue(data, 'ext.prebid.bidderconfig.0', bidderData); + } + + const pbAdSlot = deepAccess(bidRequest, 'fpd.context.pbAdSlot'); + if (typeof pbAdSlot === 'string' && pbAdSlot) { + deepSetValue(data.imp[0].ext, 'context.data.adslot', pbAdSlot); + } + + return data; +} + +function _getDigiTrustQueryParams(bidRequest = {}, endpointName) { + if (!endpointName || !DIGITRUST_PROP_NAMES[endpointName]) { + return null; + } + const propNames = DIGITRUST_PROP_NAMES[endpointName]; + + function getDigiTrustId() { + const bidRequestDigitrust = deepAccess(bidRequest, 'userId.digitrustid.data'); + if (bidRequestDigitrust) { + return bidRequestDigitrust; + } + + let digiTrustUser = (window.DigiTrust && (config.getConfig('digiTrustId') || window.DigiTrust.getUser({member: 'T9QSFKPDN9'}))); + return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; + } + + let digiTrustId = getDigiTrustId(); + // Verify there is an ID and this user has not opted out + if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) { + return null; + } + + const digiTrustQueryParams = { + [propNames.id]: digiTrustId.id, + [propNames.keyv]: digiTrustId.keyv + }; + if (propNames.pref) { + digiTrustQueryParams[propNames.pref] = 0; + } + return digiTrustQueryParams; +} + +function _getPageUrl(bidRequest, bidderRequest) { + let pageUrl = config.getConfig('pageUrl'); + if (bidRequest.params.referrer) { + pageUrl = bidRequest.params.referrer; + } else if (!pageUrl) { + pageUrl = bidderRequest.refererInfo.referer; + } + return bidRequest.params.secure ? pageUrl.replace(/^http:/i, 'https:') : pageUrl; +} + +function appendSiteAppDevice(data, bidRequest, bidderRequest) { + if (!data) return; + + // ORTB specifies app OR site + if (typeof config.getConfig('app') === 'object') { + data.app = config.getConfig('app'); + } else { + data.site = { + page: _getPageUrl(bidRequest, bidderRequest) + } + } + if (typeof config.getConfig('device') === 'object') { + data.device = config.getConfig('device'); + } +} + +/** + * @param sizes + * @returns {*} + */ +function mapSizes(sizes) { + return parseSizesInput(sizes) + // map sizes while excluding non-matches + .reduce((result, size) => { + let mappedSize = parseInt(sizeMap[size], 10); + if (mappedSize) { + result.push(mappedSize); + } + return result; + }, []); +} + +function parseSizes(bid, mediaType) { + let params = bid.params; + if (mediaType === 'video') { + let size = []; + if (params.video && params.video.playerWidth && params.video.playerHeight) { + size = [ + params.video.playerWidth, + params.video.playerHeight + ]; + } else if (Array.isArray(deepAccess(bid, 'mediaTypes.video.playerSize')) && bid.mediaTypes.video.playerSize.length === 1) { + size = bid.mediaTypes.video.playerSize[0]; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0 && Array.isArray(bid.sizes[0]) && bid.sizes[0].length > 1) { + size = bid.sizes[0]; + } + return size; + } + + // Deprecated: temp legacy support + let sizes = []; + if (Array.isArray(params.sizes)) { + sizes = params.sizes; + } else if (typeof deepAccess(bid, 'mediaTypes.banner.sizes') !== 'undefined') { + sizes = mapSizes(bid.mediaTypes.banner.sizes); + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + sizes = mapSizes(bid.sizes) + } else { + logWarn('LuponMedia: no sizes are setup or found'); + } + + return masSizeOrdering(sizes); +} + +registerBidder(spec); diff --git a/modules/missenaBidAdapter.js b/modules/missenaBidAdapter.js new file mode 100644 index 00000000000..30749e977a8 --- /dev/null +++ b/modules/missenaBidAdapter.js @@ -0,0 +1,89 @@ +import { formatQS, logInfo } from '../src/utils.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const BIDDER_CODE = 'missena'; +const ENDPOINT_URL = 'https://bid.missena.io/'; + +export const spec = { + aliases: [BIDDER_CODE], + code: BIDDER_CODE, + gvlid: 687, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return typeof bid == 'object' && !!bid.params.apiKey; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + return validBidRequests.map((bidRequest) => { + const payload = { + request_id: bidRequest.bidId, + timeout: bidderRequest.timeout, + }; + + if (bidderRequest && bidderRequest.refererInfo) { + payload.referer = bidderRequest.refererInfo.referer; + payload.referer_canonical = bidderRequest.refererInfo.canonicalUrl; + } + + if (bidderRequest && bidderRequest.gdprConsent) { + payload.consent_string = bidderRequest.gdprConsent.consentString; + payload.consent_required = bidderRequest.gdprConsent.gdprApplies; + } + + return { + method: 'POST', + url: ENDPOINT_URL + '?' + formatQS({ t: bidRequest.params.apiKey }), + data: JSON.stringify(payload), + }; + }); + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + const response = serverResponse.body; + + if (response && !response.timeout && !!response.ad) { + bidResponses.push(response); + } + + return bidResponses; + }, + + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ + onTimeout: function onTimeout(timeoutData) { + logInfo('Missena - Timeout from adapter', timeoutData); + }, + + /** + * Register bidder specific code, which@ will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ + onBidWon: function (bid) { + logInfo('Missena - Bid won', bid); + }, +}; + +registerBidder(spec); diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index afc409c19f6..ff9ccdc6791 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -4,6 +4,7 @@ import { BANNER } from '../src/mediaTypes.js'; const BIDDER_CODE = 'nextMillennium'; const ENDPOINT = 'https://pbs.nextmillmedia.com/openrtb2/auction'; +const SYNC_ENDPOINT = 'https://statics.nextmillmedia.com/load-cookie.html?v=4'; const TIME_TO_LIVE = 360; export const spec = { @@ -89,7 +90,31 @@ export const spec = { }); return bidResponses; - } + }, + + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + if (!syncOptions.iframeEnabled) { + return + } + + let syncurl = gdprConsent && gdprConsent.gdprApplies ? `${SYNC_ENDPOINT}&gdpr=1&gdpr_consent=${gdprConsent.consentString}` : SYNC_ENDPOINT + + let bidders = [] + if (responses) { + _each(responses, (response) => { + _each(Object.keys(response.body.ext.responsetimemillis), b => bidders.push(b)) + }) + } + + if (bidders.length) { + syncurl += `&bidders=${bidders.join(',')}` + } + + return [{ + type: 'iframe', + url: syncurl + }]; + }, }; registerBidder(spec); 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/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index b5cd0232187..532a222bf85 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -56,6 +56,7 @@ let eidPermissions; * @property {string} [adapter='prebidServer'] adapter code to use for S2S * @property {boolean} [enabled=false] enables S2S bidding * @property {number} [timeout=1000] timeout for S2S bidders - should be lower than `pbjs.requestBids({timeout})` + * @property {number} [syncTimeout=1000] timeout for cookie sync iframe / image rendering * @property {number} [maxBids=1] * @property {AdapterOptions} [adapterOptions] adds arguments to resulting OpenRTB payload to Prebid Server * @property {Object} [syncUrlModifier] @@ -77,6 +78,7 @@ let eidPermissions; */ const s2sDefaultConfig = { timeout: 1000, + syncTimeout: 1000, maxBids: 1, adapter: 'prebidServer', adapterOptions: {}, @@ -274,11 +276,9 @@ function doAllSyncs(bidders, s2sConfig) { */ function doPreBidderSync(type, url, bidder, done, s2sConfig) { if (s2sConfig.syncUrlModifier && typeof s2sConfig.syncUrlModifier[bidder] === 'function') { - const newSyncUrl = s2sConfig.syncUrlModifier[bidder](type, url, bidder); - doBidderSync(type, newSyncUrl, bidder, done) - } else { - doBidderSync(type, url, bidder, done) + url = s2sConfig.syncUrlModifier[bidder](type, url, bidder); } + doBidderSync(type, url, bidder, done, s2sConfig.syncTimeout) } /** @@ -288,17 +288,18 @@ function doPreBidderSync(type, url, bidder, done, s2sConfig) { * @param {string} url the url to sync * @param {string} bidder name of bidder doing sync for * @param {function} done an exit callback; to signify this pixel has either: finished rendering or something went wrong + * @param {number} timeout: maximum time to wait for rendering in milliseconds */ -function doBidderSync(type, url, bidder, done) { +function doBidderSync(type, url, bidder, done, timeout) { if (!url) { logError(`No sync url for bidder "${bidder}": ${url}`); done(); } else if (type === 'image' || type === 'redirect') { logMessage(`Invoking image pixel user sync for bidder: "${bidder}"`); - triggerPixel(url, done); - } else if (type == 'iframe') { + triggerPixel(url, done, timeout); + } else if (type === 'iframe') { logMessage(`Invoking iframe user sync for bidder: "${bidder}"`); - insertUserSyncIframe(url, done); + insertUserSyncIframe(url, done, timeout); } else { logError(`User sync type "${type}" not supported for bidder: "${bidder}"`); done(); 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/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js index 16e01f80819..128fd72996b 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -6,7 +6,7 @@ import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'relaido'; const BIDDER_DOMAIN = 'api.relaido.jp'; -const ADAPTER_VERSION = '1.0.6'; +const ADAPTER_VERSION = '1.0.7'; const DEFAULT_TTL = 300; const UUID_KEY = 'relaido_uuid'; @@ -31,14 +31,14 @@ function isBidRequestValid(bid) { } function buildRequests(validBidRequests, bidderRequest) { - let bidRequests = []; + const bids = []; + let imuid = null; + let bidDomain = null; + let bidder = null; + let count = null; for (let i = 0; i < validBidRequests.length; i++) { const bidRequest = validBidRequests[i]; - const placementId = getBidIdParameter('placementId', bidRequest.params); - const bidDomain = bidRequest.params.domain || BIDDER_DOMAIN; - const bidUrl = `https://${bidDomain}/bid/v1/prebid/${placementId}`; - const uuid = getUuid(); let mediaType = ''; let width = 0; let height = 0; @@ -55,46 +55,63 @@ function buildRequests(validBidRequests, bidderRequest) { mediaType = BANNER; } - let payload = { - version: ADAPTER_VERSION, - timeout_ms: bidderRequest.timeout, - ad_unit_code: bidRequest.adUnitCode, - auction_id: bidRequest.auctionId, - bidder: bidRequest.bidder, - bidder_request_id: bidRequest.bidderRequestId, - bid_requests_count: bidRequest.bidRequestsCount, - bid_id: bidRequest.bidId, - transaction_id: bidRequest.transactionId, - media_type: mediaType, - uuid: uuid, - width: width, - height: height, - pv: '$prebid.version$' - }; + if (!imuid) { + const pickImuid = deepAccess(bidRequest, 'userId.imuid'); + if (pickImuid) { + imuid = pickImuid; + } + } + + if (!bidDomain) { + bidDomain = bidRequest.params.domain; + } + + if (!bidder) { + bidder = bidRequest.bidder + } + + if (!bidder) { + bidder = bidRequest.bidder + } - const imuid = deepAccess(bidRequest, 'userId.imuid'); - if (imuid) { - payload.imuid = imuid; + if (!count) { + count = bidRequest.bidRequestsCount; } - // It may not be encoded, so add it at the end of the payload - payload.ref = bidderRequest.refererInfo.referer; - - bidRequests.push({ - method: 'GET', - url: bidUrl, - data: payload, - options: { - withCredentials: true - }, - bidId: bidRequest.bidId, + bids.push({ + bid_id: bidRequest.bidId, + placement_id: getBidIdParameter('placementId', bidRequest.params), + transaction_id: bidRequest.transactionId, + bidder_request_id: bidRequest.bidderRequestId, + ad_unit_code: bidRequest.adUnitCode, + auction_id: bidRequest.auctionId, player: bidRequest.params.player, - width: payload.width, - height: payload.height, - mediaType: payload.media_type + width: width, + height: height, + media_type: mediaType }); } - return bidRequests; + + const data = JSON.stringify({ + version: ADAPTER_VERSION, + bids: bids, + timeout_ms: bidderRequest.timeout, + bidder: bidder, + bid_requests_count: count, + uuid: getUuid(), + pv: '$prebid.version$', + imuid: imuid, + ref: bidderRequest.refererInfo.referer + }) + + return { + method: 'POST', + url: `https://${bidDomain || BIDDER_DOMAIN}/bid/v1/sprebid`, + options: { + withCredentials: true + }, + data: data + }; } function interpretResponse(serverResponse, bidRequest) { @@ -105,34 +122,38 @@ function interpretResponse(serverResponse, bidRequest) { } const playerUrl = bidRequest.player || body.playerUrl; - const mediaType = bidRequest.mediaType || VIDEO; - - let bidResponse = { - requestId: bidRequest.bidId, - width: bidRequest.width, - height: bidRequest.height, - cpm: body.price, - currency: body.currency, - creativeId: body.creativeId, - dealId: body.dealId || '', - ttl: body.ttl || DEFAULT_TTL, - netRevenue: true, - mediaType: mediaType, - meta: { - advertiserDomains: body.adomain || [], - mediaType: VIDEO + + for (const res of body.ads) { + let bidResponse = { + requestId: res.bidId, + width: res.width, + height: res.height, + cpm: res.price, + currency: res.currency, + creativeId: res.creativeId, + dealId: body.dealId || '', + ttl: body.ttl || DEFAULT_TTL, + netRevenue: true, + mediaType: res.mediaType || VIDEO, + meta: { + advertiserDomains: res.adomain || [], + mediaType: VIDEO + } + }; + + if (bidResponse.mediaType === VIDEO) { + bidResponse.vastXml = res.vast; + bidResponse.renderer = newRenderer(res.bidId, playerUrl); + } else { + const playerTag = createPlayerTag(playerUrl); + const renderTag = createRenderTag(res.width, res.height, res.vast); + bidResponse.ad = `
${playerTag}${renderTag}
`; } - }; - if (mediaType === VIDEO) { - bidResponse.vastXml = body.vast; - bidResponse.renderer = newRenderer(bidRequest.bidId, playerUrl); - } else { - const playerTag = createPlayerTag(playerUrl); - const renderTag = createRenderTag(bidRequest.width, bidRequest.height, body.vast); - bidResponse.ad = `
${playerTag}${renderTag}
`; + bidResponses.push(bidResponse); } - bidResponses.push(bidResponse); + // eslint-disable-next-line no-console + console.log(JSON.stringify(bidResponses)); return bidResponses; } diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index eace460eb22..09bd0a4e72e 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -296,7 +296,7 @@ function raiGetFloor(bid, config) { } else if (typeof bid.getFloor == 'function') { let floorSpec = bid.getFloor({ currency: config.getConfig('currency.adServerCurrency'), - mediaType: bid.mediaType.banner ? 'banner' : 'video', + mediaType: typeof bid.mediaTypes['banner'] == 'object' ? 'banner' : 'video', size: '*' }) 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..68334aed0ab 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.6' 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 } @@ -299,6 +304,7 @@ function createBannerImp(bidRequest) { id: bidRequest.bidId, tagid: deepAccess(bidRequest, 'params.adspaceId'), bidfloor: getBidFloor(bidRequest, BANNER, adUnitSizes), + instl: deepAccess(bidRequest.ortb2Imp, 'instl'), banner: { w: sizes[0].w, h: sizes[0].h, @@ -314,6 +320,7 @@ function createVideoImp(bidRequest, videoMediaType) { id: bidRequest.bidId, tagid: deepAccess(bidRequest, 'params.adspaceId'), bidfloor: getBidFloor(bidRequest, VIDEO, videoMediaType.playerSize), + instl: deepAccess(bidRequest.ortb2Imp, 'instl'), video: { mimes: videoMediaType.mimes, minduration: videoMediaType.minduration, @@ -341,6 +348,7 @@ function createAdPodImp(bidRequest, videoMediaType) { id: bidRequest.bidId, tagid: tagid, bidfloor: getBidFloor(bidRequest, VIDEO, videoMediaType.playerSize), + instl: deepAccess(bidRequest.ortb2Imp, 'instl'), video: { w: videoMediaType.playerSize[0][0], h: videoMediaType.playerSize[0][1], 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/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/targetVideoBidAdapter.js b/modules/targetVideoBidAdapter.js new file mode 100644 index 00000000000..5714916c131 --- /dev/null +++ b/modules/targetVideoBidAdapter.js @@ -0,0 +1,187 @@ +import find from 'core-js-pure/features/array/find.js'; +import { getBidRequest } from '../src/utils.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; + +const SOURCE = 'pbjs'; +const BIDDER_CODE = 'targetVideo'; +const ENDPOINT_URL = 'https://ib.adnxs.com/ut/v3/prebid'; +const MARGIN = 1.35; + +export const spec = { + + code: BIDDER_CODE, + 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 && bid.params && bid.params.placementId); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(bidRequests, bidderRequest) { + const tags = bidRequests.map(createVideoTag); + const schain = bidRequests[0].schain; + const payload = { + tags: tags, + sdk: { + source: SOURCE, + version: '$prebid.version$' + }, + schain: schain + }; + return formatRequest(payload, bidderRequest); + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, { bidderRequest }) { + serverResponse = serverResponse.body; + const bids = []; + + if (serverResponse.tags) { + serverResponse.tags.forEach(serverBid => { + const rtbBid = getRtbBid(serverBid); + if (rtbBid && rtbBid.cpm !== 0 && rtbBid.ad_type == VIDEO) { + bids.push(newBid(serverBid, rtbBid, bidderRequest)); + } + }); + } + + return bids; + } + +} + +function getSizes(request) { + let sizes = request.sizes; + if (!sizes && request.mediaTypes && request.mediaTypes.banner && request.mediaTypes.banner.sizes) { + sizes = request.mediaTypes.banner.sizes; + } + if (Array.isArray(sizes) && !Array.isArray(sizes[0])) { + sizes = [sizes[0], sizes[1]]; + } + if (!Array.isArray(sizes) || !Array.isArray(sizes[0])) { + sizes = [[0, 0]]; + } + + return sizes; +} + +function formatRequest(payload, bidderRequest) { + const options = { + withCredentials: true + }; + const request = { + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(payload), + bidderRequest, + options + }; + + return request; +} + +/** + * Create video auction. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function createVideoTag(bid) { + const tag = {}; + tag.id = parseInt(bid.params.placementId, 10); + tag.gpid = 'targetVideo'; + tag.sizes = getSizes(bid); + tag.primary_size = tag.sizes[0]; + tag.ad_types = [VIDEO]; + tag.uuid = bid.bidId; + tag.allow_smaller_sizes = false; + tag.use_pmt_rule = false + tag.prebid = true; + tag.disable_psa = true; + tag.hb_source = 1; + tag.require_asset_url = true; + tag.video = { + playback_method: 2, + skippable: true + }; + + return tag; +} + +/** + * Unpack the Server's Bid into a Prebid-compatible one. + * @param serverBid + * @param rtbBid + * @param bidderRequest + * @return Bid + */ +function newBid(serverBid, rtbBid, bidderRequest) { + const bidRequest = getBidRequest(serverBid.uuid, [bidderRequest]); + const sizes = getSizes(bidRequest); + const bid = { + requestId: serverBid.uuid, + cpm: rtbBid.cpm * MARGIN, + creativeId: rtbBid.creative_id, + dealId: rtbBid.deal_id, + currency: 'USD', + netRevenue: true, + width: sizes[0][0], + height: sizes[0][1], + ttl: 300, + adUnitCode: bidRequest.adUnitCode, + appnexus: { + buyerMemberId: rtbBid.buyer_member_id, + dealPriority: rtbBid.deal_priority, + dealCode: rtbBid.deal_code + } + }; + + if (rtbBid.rtb.video) { + Object.assign(bid, { + vastImpUrl: rtbBid.notify_url, + ad: getBannerHtml(rtbBid.notify_url + '&redir=' + encodeURIComponent(rtbBid.rtb.video.asset_url)), + ttl: 3600 + }); + } + + return bid; +} + +function getRtbBid(tag) { + return tag && tag.ads && tag.ads.length && find(tag.ads, ad => ad.rtb); +} + +function getBannerHtml(vastUrl) { + return ` + + + + + + + +
+ + + + `; +} + +registerBidder(spec); diff --git a/modules/targetVideoBidAdapter.md b/modules/targetVideoBidAdapter.md new file mode 100644 index 00000000000..557c9f94410 --- /dev/null +++ b/modules/targetVideoBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: Target Video Bid Adapter +Module Type: Bidder Adapter +Maintainer: grajzer@gmail.com +``` + +# Description + +Connects to Appnexus exchange for bids. + +TargetVideo bid adapter supports Banner. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[640, 480], [300, 250]], + } + }, + bids: [{ + bidder: 'targetVideo', + params: { + placementId: 13232361 + } + }] + } +]; +``` 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/userId/index.js b/modules/userId/index.js index b0293a9c26a..42fff2cd16c 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -141,7 +141,7 @@ import { createEidsArray, buildEidPermissions } from './eids.js'; import { getCoreStorageManager } from '../../src/storageManager.js'; import { getPrebidInternal, isPlainObject, logError, isArray, cyrb53Hash, deepAccess, timestamp, delayExecution, logInfo, isFn, - logWarn, isEmptyStr, isNumber + logWarn, isEmptyStr, isNumber, isGptPubadsDefined } from '../../src/utils.js'; import includes from 'core-js-pure/features/array/includes.js'; @@ -184,6 +184,9 @@ export let syncDelay; /** @type {(number|undefined)} */ export let auctionDelay; +/** @type {(string|undefined)} */ +let ppidSource; + /** @param {Submodule[]} submodules */ export function setSubmoduleRegistry(submodules) { submoduleRegistry = submodules; @@ -557,6 +560,26 @@ export function requestBidsHook(fn, reqBidsConfigObj) { initializeSubmodulesAndExecuteCallbacks(function () { // pass available user id data to bid adapters addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, initializedSubmodules); + + // userSync.ppid should be one of the 'source' values in getUserIdsAsEids() eg pubcid.org or id5-sync.com + const matchingUserId = ppidSource && (getUserIdsAsEids() || []).find(userID => userID.source === ppidSource); + if (matchingUserId && typeof deepAccess(matchingUserId, 'uids.0.id') === 'string') { + const ppidValue = matchingUserId.uids[0].id.replace(/[\W_]/g, ''); + if (ppidValue.length >= 32 && ppidValue.length <= 150) { + if (isGptPubadsDefined()) { + window.googletag.pubads().setPublisherProvidedId(ppidValue); + } else { + window.googletag = window.googletag || {}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(function() { + window.googletag.pubads().setPublisherProvidedId(ppidValue); + }); + } + } else { + logWarn(`User ID - Googletag Publisher Provided ID for ${ppidSource} is not between 32 and 150 characters - ${ppidValue}`); + } + } + // calling fn allows prebid to continue processing fn.call(this, reqBidsConfigObj); }); @@ -817,6 +840,7 @@ export function attachIdSystem(submodule) { * @param {{getConfig:function}} config */ export function init(config) { + ppidSource = undefined; submodules = []; configRegistry = []; addedUserIdHook = false; @@ -839,9 +863,10 @@ export function init(config) { } // listen for config userSyncs to be set - config.getConfig(conf => { + config.getConfig('userSync', conf => { // Note: support for 'usersync' was dropped as part of Prebid.js 4.0 const userSync = conf.userSync; + ppidSource = userSync.ppid; if (userSync && userSync.userIds) { configRegistry = userSync.userIds; syncDelay = isNumber(userSync.syncDelay) ? userSync.syncDelay : DEFAULT_SYNC_DELAY; diff --git a/modules/ventes.md b/modules/ventes.md new file mode 100644 index 00000000000..5f0f571ecd8 --- /dev/null +++ b/modules/ventes.md @@ -0,0 +1,71 @@ +--- +layout: bidder +title: ventes +description: Prebid ventes Bidder Adapter +pbjs: false +biddercode: ventes +gdpr_supported: false +usp_supported: false +media_types: banner +coppa_supported: false +schain_supported: false +dchain_supported: false +prebid_member: false +--- + +### BidParams +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +|-----------------|----------|-----------------------------------------------------------|----------------------------------------------|---------------| +| `placementId` | required | Placement ID from Ventes Avenues | `'VA-062-0013-0183'` | `string` | +| `publisherId` | required | Publisher ID from Ventes Avenues | `'VA-062'` | `string` | +| `user` | optional | Object that specifies information about an external user. | `user: { age: 25, gender: 0, dnt: true}` | `object` | +| `app` | optional | Object containing mobile app parameters. | `app : { id: 'app-id'}` | `object` | + +#### User Object + +{: .table .table-bordered .table-striped } +| Name | Description | Example | Type | +|-------------------|-------------------------------------------------------------------------------------------|-----------------------|-----------------------| +| `age` | The age of the user. | `35` | `integer` | +| `externalUid` | Specifies a string that corresponds to an external user ID for this user. | `'1234567890abcdefg'` | `string` | +| `segments` | Specifies the segments to which the user belongs. | `[1, 2]` | `Array` | +| `gender` | Specifies the gender of the user. Allowed values: Unknown: `0`; Male: `1`; Female: `2` | `1` | `integer` | +| `dnt` | Do not track flag. Indicates if tracking cookies should be disabled for this auction | `true` | `boolean` | +| `language` | Two-letter ANSI code for this user's language. | `EN` | `string` | + + +### Ad Unit Setup for Banner +```javascript +var adUnits = [ +{ + code: 'test-hb-ad-11111-1', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bids: [{ + bidder: 'ventes', + params: { + placementId: 'VA-062-0013-0183', + publisherId: '5cebea3c9eea646c7b623d5e', + IABCategories: "['IAB1', 'IAB5']", + device:{ + ip: '123.145.167.189', + ifa:"AEBE52E7-03EE-455A-B3C4-E57283966239", + }, + app: { + id: "agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA", + name: "Yahoo Weather", + bundle: 'com.kiloo.subwaysurf', + storeurl: 'https://play.google.com/store/apps/details?id=com.kiloo.subwaysurf&hl=en', + domain: 'somoaudience.com', + } + } + }] + } +] +``` diff --git a/modules/ventesBidAdapter.js b/modules/ventesBidAdapter.js index 7a2b60d2ee2..a9de52a86ba 100644 --- a/modules/ventesBidAdapter.js +++ b/modules/ventesBidAdapter.js @@ -1,17 +1,24 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {convertCamelToUnderscore, isStr, isArray, isNumber, isPlainObject, replaceAuctionPrice} from '../src/utils.js'; +import { + BANNER, + NATIVE, + VIDEO +} from '../src/mediaTypes.js'; +import { + convertCamelToUnderscore, + isStr, + isArray, + isNumber, + isPlainObject, + replaceAuctionPrice +} from '../src/utils.js'; import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; const BID_METHOD = 'POST'; const BIDDER_URL = 'http://13.234.201.146:8088/va/ad'; -const FIRST_PRICE = 1; -const NET_REVENUE = true; -const TTL = 10; -const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language']; -const DEVICE_PARAMS = ['ua', 'geo', 'dnt', 'lmt', 'ip', 'ipv6', 'devicetype']; -const APP_DEVICE_PARAMS = ['geo', 'device_id']; // appid is collected separately + const DOMAIN_REGEX = new RegExp('//([^/]*)'); function groupBy(values, key) { @@ -26,7 +33,11 @@ function groupBy(values, key) { return Object .keys(groups) - .map(id => ({id, key, values: groups[id]})); + .map(id => ({ + id, + key, + values: groups[id] + })); } function validateMediaTypes(mediaTypes, allowedMediaTypes) { @@ -45,22 +56,22 @@ function isBanner(mediaTypes) { function validateBanner(banner) { return isPlainObject(banner) && - isArray(banner.sizes) && - (banner.sizes.length > 0) && - banner.sizes.every(validateMediaSizes); + isArray(banner.sizes) && + (banner.sizes.length > 0) && + banner.sizes.every(validateMediaSizes); } function validateMediaSizes(mediaSize) { return isArray(mediaSize) && - (mediaSize.length === 2) && - mediaSize.every(size => (isNumber(size) && size >= 0)); + (mediaSize.length === 2) && + mediaSize.every(size => (isNumber(size) && size >= 0)); } function hasUserInfo(bid) { return !!bid.params.user; } -function validateParameters(parameters, adUnit) { +function validateParameters(parameters) { if (!(parameters.placementId)) { return false; } @@ -101,8 +112,8 @@ function generateSiteFromAdUnitContext(bidRequests, adUnitContext) { function validateServerRequest(serverRequest) { return isPlainObject(serverRequest) && - isPlainObject(serverRequest.data) && - isArray(serverRequest.data.imp) + isPlainObject(serverRequest.data) && + isArray(serverRequest.data.imp) } function createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) { @@ -122,14 +133,15 @@ function generateBidRequestsFromAdUnits(bidRequests, bidRequestId, adUnitContext let userObj = {}; if (userObjBid) { Object.keys(userObjBid.params.user) - .filter(param => includes(USER_PARAMS, param)) .forEach((param) => { let uparam = convertCamelToUnderscore(param); if (param === 'segments' && isArray(userObjBid.params.user[param])) { let segs = []; userObjBid.params.user[param].forEach(val => { if (isNumber(val)) { - segs.push({'id': val}); + segs.push({ + 'id': val + }); } else if (isPlainObject(val)) { segs.push(val); } @@ -146,7 +158,6 @@ function generateBidRequestsFromAdUnits(bidRequests, bidRequestId, adUnitContext if (deviceObjBid && deviceObjBid.params && deviceObjBid.params.device) { deviceObj = {}; Object.keys(deviceObjBid.params.device) - .filter(param => includes(DEVICE_PARAMS, param)) .forEach(param => deviceObj[param] = deviceObjBid.params.device[param]); if (!deviceObjBid.hasOwnProperty('ua')) { deviceObj.ua = navigator.userAgent; @@ -159,37 +170,41 @@ function generateBidRequestsFromAdUnits(bidRequests, bidRequestId, adUnitContext deviceObj.ua = navigator.userAgent; deviceObj.language = navigator.language; } - const appDeviceObjBid = find(bidRequests, hasAppInfo); - let appIdObj; - if (appDeviceObjBid && appDeviceObjBid.params && appDeviceObjBid.params.app && appDeviceObjBid.params.app.id) { - Object.keys(appDeviceObjBid.params.app) - .filter(param => includes(APP_DEVICE_PARAMS, param)) - .forEach(param => appDeviceObjBid[param] = appDeviceObjBid.params.app[param]); - } const payload = {} payload.id = bidRequestId - payload.at = FIRST_PRICE + payload.at = 1 payload.cur = ['USD'] payload.imp = bidRequests.reduce(generateImpressionsFromAdUnit, []) - payload.site = generateSiteFromAdUnitContext(bidRequests, adUnitContext) - payload.device = deviceObj - if (appDeviceObjBid && payload.site != null) { + const appDeviceObjBid = find(bidRequests, hasAppInfo); + if (!appDeviceObjBid) { + payload.site = generateSiteFromAdUnitContext(bidRequests, adUnitContext) + } else { + let appIdObj; + if (appDeviceObjBid && appDeviceObjBid.params && appDeviceObjBid.params.app && appDeviceObjBid.params.app.id) { + appIdObj = {}; + Object.keys(appDeviceObjBid.params.app) + .forEach(param => appIdObj[param] = appDeviceObjBid.params.app[param]); + } payload.app = appIdObj; } + payload.device = deviceObj; payload.user = userObj - // payload.regs = getRegulationFromAdUnitContext(adUnitContext) - // payload.ext = generateBidRequestExtension() - return payload } function generateImpressionsFromAdUnit(acc, adUnit) { - const {bidId, mediaTypes, params} = adUnit; - const {placementId} = params; + const { + bidId, + mediaTypes, + params + } = adUnit; + const { + placementId + } = params; const pmp = {}; - if (placementId) pmp.deals = [{id: placementId}] + if (placementId) pmp.deals = [{ id: placementId }] const imps = Object .keys(mediaTypes) @@ -204,21 +219,40 @@ function generateImpressionsFromAdUnit(acc, adUnit) { } function generateBannerFromAdUnit(impId, data, params) { - const {position, placementId} = params; + const { + position, + placementId + } = params; const pos = position || 0; const pmp = {}; - const ext = {placementId}; - - if (placementId) pmp.deals = [{id: placementId}] + const ext = { + placementId + }; - return data.sizes.map(([w, h]) => ({id: `${impId}`, banner: {format: [{w, h}], w, h, pos}, pmp, ext, tagid: placementId})); + if (placementId) pmp.deals = [{ id: placementId }] + + return data.sizes.map(([w, h]) => ({ + id: `${impId}`, + banner: { + format: [{ + w, + h + }], + w, + h, + pos + }, + pmp, + ext, + tagid: placementId + })); } function validateServerResponse(serverResponse) { return isPlainObject(serverResponse) && - isPlainObject(serverResponse.body) && - isStr(serverResponse.body.cur) && - isArray(serverResponse.body.seatbid); + isPlainObject(serverResponse.body) && + isStr(serverResponse.body.cur) && + isArray(serverResponse.body.seatbid); } function seatBidsToAds(seatBid, bidResponse, serverRequest) { @@ -241,10 +275,8 @@ function validateBids(bid) { return true; } -const VAST_REGEXP = /VAST\s+version/; - function getMediaType(adm) { - const videoRegex = new RegExp(VAST_REGEXP); + const videoRegex = new RegExp(/VAST\s+version/); if (videoRegex.test(adm)) { return VIDEO; @@ -273,14 +305,16 @@ function generateAdFromBid(bid, bidResponse) { requestId: bid.impid, cpm: bid.price, currency: bidResponse.cur, - ttl: TTL, + ttl: 10, creativeId: bid.crid, mediaType: mediaType, - netRevenue: NET_REVENUE + netRevenue: true }; if (bid.adomain) { - base.meta = { advertiserDomains: bid.adomain }; + base.meta = { + advertiserDomains: bid.adomain + }; } const size = getSizeFromBid(bid); @@ -292,17 +326,21 @@ function generateAdFromBid(bid, bidResponse) { width: size.width, ad: creative.markup, adUrl: creative.markupUrl, - // vastXml: isVideo && !isStr(creative.markupUrl) ? creative.markup : null, - // vastUrl: isVideo && isStr(creative.markupUrl) ? creative.markupUrl : null, renderer: creative.renderer }; } function getSizeFromBid(bid) { if (isNumber(bid.w) && isNumber(bid.h)) { - return { width: bid.w, height: bid.h }; + return { + width: bid.w, + height: bid.h + }; } - return { width: null, height: null }; + return { + width: null, + height: null + }; } function getCreativeFromBid(bid) { @@ -333,12 +371,12 @@ const venavenBidderSpec = { const allowedBidderCodes = [this.code]; return isPlainObject(adUnit) && - allowedBidderCodes.indexOf(adUnit.bidder) !== -1 && - isStr(adUnit.adUnitCode) && - isStr(adUnit.bidderRequestId) && - isStr(adUnit.bidId) && - validateMediaTypes(adUnit.mediaTypes, this.supportedMediaTypes) && - validateParameters(adUnit.params, adUnit); + allowedBidderCodes.indexOf(adUnit.bidder) !== -1 && + isStr(adUnit.adUnitCode) && + isStr(adUnit.bidderRequestId) && + isStr(adUnit.bidId) && + validateMediaTypes(adUnit.mediaTypes, this.supportedMediaTypes) && + validateParameters(adUnit.params); }, buildRequests(bidRequests, bidderRequest) { if (!bidRequests) return null; @@ -367,4 +405,6 @@ const venavenBidderSpec = { registerBidder(venavenBidderSpec); -export {venavenBidderSpec as spec}; +export { + venavenBidderSpec as spec +}; diff --git a/modules/ventesBidAdapter.md b/modules/ventesBidAdapter.md index 479f6dd2898..c79ef080cd1 100644 --- a/modules/ventesBidAdapter.md +++ b/modules/ventesBidAdapter.md @@ -55,7 +55,6 @@ var adUnits = [ publisherId: '5cebea3c9eea646c7b623d5e', IABCategories: "['IAB1', 'IAB5']", device:{ - ip: '123.145.167.189', ifa:"AEBE52E7-03EE-455A-B3C4-E57283966239", }, app: { diff --git a/modules/vidoomyBidAdapter.js b/modules/vidoomyBidAdapter.js index 182284410e6..64145b2c6b4 100644 --- a/modules/vidoomyBidAdapter.js +++ b/modules/vidoomyBidAdapter.js @@ -1,4 +1,4 @@ -import { logError, deepAccess } from '../src/utils.js'; +import { logError, deepAccess, parseSizesInput } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -53,7 +53,7 @@ const isBidResponseValid = bid => { case BANNER: return Boolean(bid.width && bid.height && bid.ad); case VIDEO: - return Boolean(bid.vastUrl); + return Boolean(bid.vastUrl || bid.vastXml); default: return false; } @@ -62,14 +62,15 @@ const isBidResponseValid = bid => { const buildRequests = (validBidRequests, bidderRequest) => { const serverRequests = validBidRequests.map(bid => { let adType = BANNER; - let w, h; + let sizes; if (bid.mediaTypes && bid.mediaTypes[BANNER] && bid.mediaTypes[BANNER].sizes) { - [w, h] = bid.mediaTypes[BANNER].sizes[0]; + sizes = bid.mediaTypes[BANNER].sizes; adType = BANNER; } else if (bid.mediaTypes && bid.mediaTypes[VIDEO] && bid.mediaTypes[VIDEO].playerSize) { - [w, h] = bid.mediaTypes[VIDEO].playerSize; + sizes = bid.mediaTypes[VIDEO].playerSize; adType = VIDEO; } + const [w, h] = (parseSizesInput(sizes)[0] || '0x0').split('x'); const aElement = document.createElement('a'); aElement.href = (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) || top.location.href; @@ -80,6 +81,7 @@ const buildRequests = (validBidRequests, bidderRequest) => { const queryParams = { id: bid.params.id, adtype: adType, + auc: bid.adUnitCode, w, h, pos: parseInt(bid.params.position) || 1, @@ -88,7 +90,7 @@ const buildRequests = (validBidRequests, bidderRequest) => { dt: /Mobi/.test(navigator.userAgent) ? 2 : 1, pid: bid.params.pid, requestId: bid.bidId, - d: getDomainWithoutSubdomain(hostname), + d: getDomainWithoutSubdomain(hostname), // 'vidoomy.com', sp: encodeURIComponent(aElement.href), usp: bidderRequest.uspConsent || '', coppa: !!config.getConfig('coppa'), @@ -127,7 +129,7 @@ const interpretResponse = (serverResponse, bidRequest) => { let responseBody = serverResponse.body; if (!responseBody) return; if (responseBody.mediaType === 'video') { - responseBody.ad = responseBody.vastUrl; + responseBody.ad = responseBody.vastUrl || responseBody.vastXml; const videoContext = bidRequest.data.videoContext; if (videoContext === OUTSTREAM) { @@ -143,13 +145,12 @@ const interpretResponse = (serverResponse, bidRequest) => { responseBody.renderer = renderer; } catch (e) { - responseBody.ad = responseBody.vastUrl; + responseBody.ad = responseBody.vastUrl || responseBody.vastXml; logError(BIDDER_CODE + ': error while installing renderer to show outstream ad'); } } } const bid = { - vastUrl: responseBody.vastUrl, ad: responseBody.ad, renderer: responseBody.renderer, mediaType: responseBody.mediaType, @@ -178,6 +179,11 @@ const interpretResponse = (serverResponse, bidRequest) => { secondaryCatIds: responseBody.meta.secondaryCatIds } }; + if (responseBody.vastUrl) { + bid.vastUrl = responseBody.vastUrl; + } else if (responseBody.vastXml) { + bid.vastXml = responseBody.vastXml; + } const bids = []; @@ -202,7 +208,7 @@ function getUserSyncs (syncOptions, responses, gdprConsent, uspConsent) { return [].concat(urls).map(url => ({ type: pixelType, url: url - .replace('{{GDPR}}', gdprConsent ? gdprConsent.gdprApplies : '0') + .replace('{{GDPR}}', gdprConsent ? (gdprConsent.gdprApplies ? '1' : '0') : '0') .replace('{{GDPR_CONSENT}}', gdprConsent ? encodeURIComponent(gdprConsent.consentString) : '') .replace('{{USP_CONSENT}}', uspConsent ? encodeURIComponent(uspConsent) : '') })); diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index 3442cbc8dd8..af8672ea233 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -1,4 +1,4 @@ -import { triggerPixel, parseSizesInput, deepAccess, logError } from '../src/utils.js'; +import { triggerPixel, parseSizesInput, deepAccess, logError, getGptSlotInfoForAdUnitCode } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; @@ -241,7 +241,7 @@ function makeVideo(videoParams = {}) { } function buildImpObject(bid) { - const { params: { uid }, bidId, mediaTypes, sizes } = bid; + const { params: { uid }, bidId, mediaTypes, sizes, adUnitCode } = bid; const video = mediaTypes && _isVideoBid(bid) && _isValidVideoBid(bid) && makeVideo(mediaTypes.video); const banner = makeBanner((mediaTypes && mediaTypes.banner) || (!video && { sizes })); const impObject = { @@ -253,6 +253,10 @@ function buildImpObject(bid) { } }; + if (impObject.banner) { + impObject.ext.bidder.adslotExists = _isAdSlotExists(adUnitCode); + } + if (impObject.ext.bidder.uid && (impObject.banner || impObject.video)) { return impObject; } @@ -355,4 +359,17 @@ function _isValidVideoBid(bid, logErrors = false) { return result; } +function _isAdSlotExists(adUnitCode) { + if (document.getElementById(adUnitCode)) { + return true; + } + + const gptAdSlot = getGptSlotInfoForAdUnitCode(adUnitCode); + if (gptAdSlot && gptAdSlot.divId && document.getElementById(gptAdSlot.divId)) { + return true; + } + + return false; +} + registerBidder(spec); diff --git a/modules/visxBidAdapter.md b/modules/visxBidAdapter.md index 9578f7cc4a7..34ebe9bb937 100644 --- a/modules/visxBidAdapter.md +++ b/modules/visxBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: YOC VIS.X Bidder Adapter Module Type: Bidder Adapter -Maintainer: service@yoc.com +Maintainer: supply.partners@yoc.com ``` # Description @@ -47,16 +47,14 @@ var adUnits = [ } ] }, - // YOC In-stream adUnit + // In-stream video adUnit { code: 'instream-test-div', mediaTypes: { video: { context: 'instream', - playerSize: [400, 300], - mimes: ['video/mp4'], - protocols: [3, 6] - }, + playerSize: [400, 300] + } }, bids: [ { diff --git a/modules/weboramaRtdProvider.js b/modules/weboramaRtdProvider.js index 01086ad129f..70f258e1698 100644 --- a/modules/weboramaRtdProvider.js +++ b/modules/weboramaRtdProvider.js @@ -2,90 +2,326 @@ * This module adds Weborama provider to the real time data module * The {@link module:modules/realTimeData} module is required * The module will fetch contextual data (page-centric) from Weborama server + * and may access user-centric data from local storage * @module modules/weboramaRtdProvider * @requires module:modules/realTimeData */ /** -* @typedef {Object} ModuleParams -* @property {WeboCtxConf} weboCtxConf -*/ + * @typedef {Object} ModuleParams + * @property {WeboCtxConf} weboCtxConf + * @property {WeboUserDataConf} weboUserDataConf + */ /** -* @typedef {Object} WeboCtxConf -* @property {string} token required token to be used on bigsea contextual API requests -* @property {?string} targetURL specify the target url instead use the referer -* @property {?boolean} setTargeting if true will set the GAM targeting -* @property {?object} defaultProfile to be used if the profile is not found -*/ - -import { deepSetValue, logError, tryAppendQueryString, logMessage } from '../src/utils.js'; -import {submodule} from '../src/hook.js'; -import {ajax} from '../src/ajax.js'; -import {config} from '../src/config.js'; + * @typedef {Object} WeboCtxConf + * @property {string} token required token to be used on bigsea contextual API requests + * @property {?string} targetURL specify the target url instead use the referer + * @property {?Boolean} setPrebidTargeting if true will set the GAM targeting (default true) + * @property {?Boolean} sendToBidders if true, will send the contextual profile to all bidders (default true) + * @property {?object} defaultProfile to be used if the profile is not found + */ + +/** + * @typedef {Object} WeboUserDataConf + * @property {?string} localStorageProfileKey can be used to customize the local storage key (default is 'webo_wam2gam_entry') + * @property {?Boolean} setPrebidTargeting if true will set the GAM targeting (default true) + * @property {?Boolean} sendToBidders if true, will send the contextual profile to all bidders (default true) + * @property {?object} defaultProfile to be used if the profile is not found + */ + +import { + getGlobal +} from '../src/prebidGlobal.js'; +import { + deepSetValue, + deepAccess, + isEmpty, + mergeDeep, + logError, + tryAppendQueryString, + logMessage +} from '../src/utils.js'; +import { + submodule +} from '../src/hook.js'; +import { + ajax +} from '../src/ajax.js'; +import { + getStorageManager +} from '../src/storageManager.js'; + +const adapterManager = require('../src/adapterManager.js').default; /** @type {string} */ const MODULE_NAME = 'realTimeData'; /** @type {string} */ const SUBMODULE_NAME = 'weborama'; /** @type {string} */ -const WEBO_CTX = 'webo_ctx'; +export const DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY = 'webo_wam2gam_entry'; /** @type {string} */ -const WEBO_DS = 'webo_ds'; +const LOCAL_STORAGE_USER_TARGETING_SECTION = 'targeting'; +/** @type {number} */ +const GVLID = 284; +/** @type {object} */ +export const storage = getStorageManager(GVLID, SUBMODULE_NAME); /** @type {null|Object} */ -let _bigseaContextualProfile = null; +let _weboContextualProfile = null; -/** function that provides ad server targeting data to RTD-core -* @param {Array} adUnitsCodes -* @param {Object} moduleConfig -* @returns {Object} target data +/** @type {Boolean} */ +let _weboCtxInitialized = false; + +/** @type {null|Object} */ +let _weboUserDataUserProfile = null; + +/** @type {Boolean} */ +let _weboUserDataInitialized = false; + +/** Initialize module + * @param {object} moduleConfig + * @return {Boolean} true if module was initialized with success */ -function getTargetingData(adUnitsCodes, moduleConfig) { +function init(moduleConfig) { moduleConfig = moduleConfig || {}; const moduleParams = moduleConfig.params || {}; const weboCtxConf = moduleParams.weboCtxConf || {}; - const defaultContextualProfiles = weboCtxConf.defaultProfile || {} - const profile = _bigseaContextualProfile || defaultContextualProfiles; + const weboUserDataConf = moduleParams.weboUserDataConf; - if (weboCtxConf.setOrtb2) { - const ortb2 = config.getConfig('ortb2') || {}; - if (profile[WEBO_CTX]) { - deepSetValue(ortb2, 'site.ext.data.webo_ctx', profile[WEBO_CTX]); - } - if (profile[WEBO_DS]) { - deepSetValue(ortb2, 'site.ext.data.webo_ds', profile[WEBO_DS]); - } - config.setConfig({ortb2: ortb2}); - } + _weboCtxInitialized = initWeboCtx(weboCtxConf); + _weboUserDataInitialized = initWeboUserData(weboUserDataConf); - if (weboCtxConf.setTargeting === false) { - return {}; + return _weboCtxInitialized || _weboUserDataInitialized; +} + +/** Initialize contextual sub module + * @param {WeboCtxConf} weboCtxConf + * @return {Boolean} true if sub module was initialized with success + */ +function initWeboCtx(weboCtxConf) { + _weboCtxInitialized = false; + _weboContextualProfile = null; + + if (!weboCtxConf.token) { + logError('missing param "token" for weborama contextual sub module initialization'); + return false; } + return true; +} + +/** Initialize weboUserData sub module + * @param {WeboUserDataConf} weboUserDataConf + * @return {Boolean} true if sub module was initialized with success + */ +function initWeboUserData(weboUserDataConf) { + _weboUserDataInitialized = false; + _weboUserDataUserProfile = null; + + return !!weboUserDataConf; +} + +/** function that provides ad server targeting data to RTD-core + * @param {Array} adUnitsCodes + * @param {Object} moduleConfig + * @returns {Object} target data + */ +function getTargetingData(adUnitsCodes, moduleConfig) { + moduleConfig = moduleConfig || {}; + const moduleParams = moduleConfig.params || {}; + const weboCtxConf = moduleParams.weboCtxConf || {}; + const weboUserDataConf = moduleParams.weboUserDataConf || {}; + const weboCtxConfTargeting = weboCtxConf.setPrebidTargeting !== false; + const weboUserDataConfTargeting = weboUserDataConf.setPrebidTargeting !== false; + try { - const formattedProfile = profile; - const r = adUnitsCodes.reduce((rp, adUnitCode) => { + const profile = getCompleteProfile(moduleParams, weboCtxConfTargeting, weboUserDataConfTargeting); + + if (isEmpty(profile)) { + return {}; + } + + const td = adUnitsCodes.reduce((data, adUnitCode) => { if (adUnitCode) { - rp[adUnitCode] = formattedProfile; + data[adUnitCode] = profile; } - return rp; + return data; }, {}); - return r; + + return td; } catch (e) { logError('unable to format weborama rtd targeting data', e); return {}; } } +/** function that provides complete profile formatted to be used + * @param {ModuleParams} moduleParams + * @param {Boolean} weboCtxConfTargeting + * @param {Boolean} weboUserDataConfTargeting + * @returns {Object} complete profile + */ +function getCompleteProfile(moduleParams, weboCtxConfTargeting, weboUserDataConfTargeting) { + const profile = {}; + + if (weboCtxConfTargeting) { + const contextualProfile = getContextualProfile(moduleParams.weboCtxConf || {}); + mergeDeep(profile, contextualProfile); + } + + if (weboUserDataConfTargeting) { + const weboUserDataProfile = getWeboUserDataProfile(moduleParams.weboUserDataConf || {}); + mergeDeep(profile, weboUserDataProfile); + } + + return profile; +} + +/** return contextual profile + * @param {WeboCtxConf} weboCtxConf + * @returns {Object} contextual profile + */ +function getContextualProfile(weboCtxConf) { + const defaultContextualProfile = weboCtxConf.defaultProfile || {}; + return _weboContextualProfile || defaultContextualProfile; +} + +/** return weboUserData profile + * @param {WeboUserDataConf} weboUserDataConf + * @returns {Object} weboUserData profile + */ +function getWeboUserDataProfile(weboUserDataConf) { + const weboUserDataDefaultUserProfile = weboUserDataConf.defaultProfile || {}; + + if (storage.localStorageIsEnabled() && !_weboUserDataUserProfile) { + const localStorageProfileKey = weboUserDataConf.localStorageProfileKey || DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY; + + const entry = storage.getDataFromLocalStorage(localStorageProfileKey); + if (entry) { + const data = JSON.parse(entry); + if (data && Object.keys(data).length > 0) { + _weboUserDataUserProfile = data[LOCAL_STORAGE_USER_TARGETING_SECTION]; + } + } + } + + return _weboUserDataUserProfile || weboUserDataDefaultUserProfile; +} + +/** function that will allow RTD sub-modules to modify the AdUnit object for each auction + * @param {Object} reqBidsConfigObj + * @param {doneCallback} onDone + * @param {Object} moduleConfig + * @returns {void} + */ +export function getBidRequestData(reqBidsConfigObj, onDone, moduleConfig) { + moduleConfig = moduleConfig || {}; + const moduleParams = moduleConfig.params || {}; + const weboCtxConf = moduleParams.weboCtxConf || {}; + + const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + + if (!_weboCtxInitialized) { + handleBidRequestData(adUnits, moduleParams); + + onDone(); + + return; + } + + fetchContextualProfile(weboCtxConf, (data) => { + logMessage('fetchContextualProfile on getBidRequestData is done'); + + setWeboContextualProfile(data); + }, () => { + handleBidRequestData(adUnits, moduleParams); + + onDone(); + }); +} + +/** function that handles bid request data + * @param {Object[]} adUnits + * @param {ModuleParams} moduleParams + * @returns {void} + */ + +function handleBidRequestData(adUnits, moduleParams) { + const weboCtxConf = moduleParams.weboCtxConf || {}; + const weboUserDataConf = moduleParams.weboUserDataConf || {}; + const weboCtxConfTargeting = weboCtxConf.sendToBidders !== false; + const weboUserDataConfTargeting = weboUserDataConf.sendToBidders !== false; + const profile = getCompleteProfile(moduleParams, weboCtxConfTargeting, weboUserDataConfTargeting); + + if (isEmpty(profile)) { + return; + } + + adUnits.forEach(adUnit => { + if (adUnit.hasOwnProperty('bids')) { + adUnit.bids.forEach(bid => handleBid(adUnit, profile, bid)); + } + }); +} + +/** @type {string} */ +const SMARTADSERVER = 'smartadserver'; + +/** @type {Object} */ +const bidderAliasRegistry = adapterManager.aliasRegistry || {}; + +/** handle individual bid + * @param {Object} adUnit + * @param {Object} profile + * @param {Object} bid + * @returns {void} + */ +function handleBid(adUnit, profile, bid) { + const bidder = bidderAliasRegistry[bid.bidder] || bid.bidder; + + logMessage('handle bidder', bidder, bid); + + switch (bidder) { + case SMARTADSERVER: + handleSmartadserverBid(adUnit, profile, bid); + + break; + } +} + +/** handle smartadserver bid + * @param {Object} adUnit + * @param {Object} profile + * @param {Object} bid + * @returns {void} + */ +function handleSmartadserverBid(adUnit, profile, bid) { + const target = []; + + if (deepAccess(bid, 'params.target')) { + target.push(bid.params.target.split(';')); + } + + Object.keys(profile).forEach(key => { + profile[key].forEach(value => { + const keyword = `${key}=${value}`; + if (target.indexOf(keyword) === -1) { + target.push(keyword); + } + }); + }); + + deepSetValue(bid, 'params.target', target.join(';')); +} + /** set bigsea contextual profile on module state - * if the profile is empty, will store the default profile * @param {null|Object} data * @returns {void} */ -export function setBigseaContextualProfile(data) { +export function setWeboContextualProfile(data) { if (data && Object.keys(data).length > 0) { - _bigseaContextualProfile = data; + _weboContextualProfile = data; } } @@ -96,9 +332,9 @@ export function setBigseaContextualProfile(data) { */ /** onDone callback type - * @callback doneCallback - * @returns {void} - */ + * @callback doneCallback + * @returns {void} + */ /** Fetch Bigsea Contextual Profile * @param {WeboCtxConf} weboCtxConf @@ -117,7 +353,7 @@ function fetchContextualProfile(weboCtxConf, onSuccess, onDone) { const url = 'https://ctx.weborama.com/api/profile?' + queryString; ajax(url, { - success: function (response, req) { + success: function(response, req) { if (req.status === 200) { try { const data = JSON.parse(response); @@ -126,49 +362,28 @@ function fetchContextualProfile(weboCtxConf, onSuccess, onDone) { } catch (e) { onDone(); logError('unable to parse weborama data', e); + throw e; } } else if (req.status === 204) { onDone(); } }, - error: function () { + error: function() { onDone(); logError('unable to get weborama data'); } }, - null, - { + null, { method: 'GET', withCredentials: false, }); } -/** Initialize module - * @param {object} moduleConfig - * @return {boolean} true if module was initialized with success - */ -function init(moduleConfig) { - _bigseaContextualProfile = null; - - moduleConfig = moduleConfig || {}; - const moduleParams = moduleConfig.params || {}; - const weboCtxConf = moduleParams.weboCtxConf || {}; - - if (weboCtxConf.token) { - fetchContextualProfile(weboCtxConf, setBigseaContextualProfile, - () => logMessage('fetchContextualProfile on init is done')); - } else { - logError('missing param "token" for weborama rtd module initialization'); - return false; - } - - return true; -} - export const weboramaSubmodule = { name: SUBMODULE_NAME, init: init, getTargetingData: getTargetingData, + getBidRequestData: getBidRequestData, }; submodule(MODULE_NAME, weboramaSubmodule); diff --git a/modules/weboramaRtdProvider.md b/modules/weboramaRtdProvider.md index e7b9b96d668..06e5b4fb43b 100644 --- a/modules/weboramaRtdProvider.md +++ b/modules/weboramaRtdProvider.md @@ -10,8 +10,6 @@ Maintainer: prebid-support@weborama.com Weborama provides a Semantic AI Contextual API that classifies in Real-time a web page seen by a web user within generic and custom topics. It enables publishers to better monetize their inventory and unlock it to programmatic. -ORTB2 compliant and FPD support for Prebid versions < 4.29 - Contact prebid-support@weborama.com for information. ### Publisher Usage @@ -32,17 +30,31 @@ pbjs.setConfig( name: "weborama", waitForIt: true, params: { - weboCtxConf: { - setTargeting: true, - token: "<>", - targetURL: "..." // default is document.URL + weboCtxConf: { // contextual configuration + token: "<>", // mandatory + targetURL: "...", // default is document.URL + setPrebidTargeting: true, // default + sendToBidders: true, // default + defaultProfile: { // optional, default is none + webo_ctx: ['foo'], + webo_ds: ['bar'] + } + }, + weboUserDataConf: { // user-centric configuration + setPrebidTargeting: true, // default + sendToBidders: true, // default + defaultProfile: { // optional, default is none + webo_cs: ['baz'], + webo_audiences: ['bam'] + }, + localStorageProfileKey: 'webo_wam2gam_entry' // default } } } ] } ... -} +); ``` ### Parameter Descriptions for the Weborama Configuration Section @@ -55,15 +67,20 @@ pbjs.setConfig( | params.weboCtxConf | Object | Weborama Contextual Configuration | Optional | | params.weboCtxConf.token | String | Security Token provided by Weborama, unique per client | Mandatory | | params.weboCtxConf.targetURL | String | Url to be profiled in the contextual api | Optional. Defaults to `document.URL` | +| params.weboCtxConf.setPrebidTargeting|Boolean|If true, will use the contextual profile to set the prebid (GPT/GAM or AST) targeting of all adunits managed by prebid.js| Optional. Default is *true*.| +| params.weboCtxConf.sendToBidders|Boolean|If true, will send the contextual profile to all bidders (only smartadserver is supported now)| Optional. Default is *true*.| | params.weboCtxConf.defaultProfile | Object | default value of the profile to be used when there are no response from contextual api (such as timeout)| Optional. Default is `{}` | -| params.weboCtxConf.setTargeting|Boolean|If true, will use the contextual profile to set the gam targeting of all adunits managed by prebid.js| Optional. Default is *true*.| -| params.weboCtxConf.setOrtb2|Boolean|If true, will use the contextual profile to set the ortb2 configuration on `site.ext.data`| Optional. Default is *false*.| +| params.weboUserDataConf | Object | WeboUserData Configuration | Optional | +| params.weboUserDataConf.setPrebidTargeting|Boolean|If true, will use the contextual profile to set the prebid (GPT/GAM or AST) targeting of all adunits managed by prebid.js| Optional. Default is *true*.| +| params.weboUserDataConf.sendToBidders|Boolean|If true, will send the contextual profile to all bidders (only smartadserver is supported now)| Optional. Default is *true*.| +| params.weboUserDataConf.defaultProfile | Object | default value of the profile to be used when there are no response from contextual api (such as timeout)| Optional. Default is `{}` | +| params.weboUserDataConf.localStorageProfileKey| String | can be used to customize the local storage key | Optional | ### Testing To view an example of available segments returned by Weborama's backends: -`gulp serve --modules=rtdModule,weboramaRtdProvider,appnexusBidAdapter` +`gulp serve --modules=rtdModule,weboramaRtdProvider,smartadserverBidAdapter` and then point your browser at: diff --git a/modules/yahoosspBidAdapter.js b/modules/yahoosspBidAdapter.js index 101cb0ca9e3..73a991ec79a 100644 --- a/modules/yahoosspBidAdapter.js +++ b/modules/yahoosspBidAdapter.js @@ -6,7 +6,7 @@ import { Renderer } from '../src/Renderer.js'; const INTEGRATION_METHOD = 'prebid.js'; const BIDDER_CODE = 'yahoossp'; -const ADAPTER_VERSION = '1.0.1'; +const ADAPTER_VERSION = '1.0.2'; const PREBID_VERSION = '$prebid.version$'; const DEFAULT_BID_TTL = 300; const TEST_MODE_DCN = '8a969516017a7a396ec539d97f540011'; @@ -359,6 +359,10 @@ function appendImpObject(bid, openRtbObject) { impObject.ext.data = bid.ortb2Imp.ext.data; }; + if (deepAccess(bid, 'ortb2Imp.instl') && isNumber(bid.ortb2Imp.instl) && (bid.ortb2Imp.instl === 1)) { + impObject.instl = bid.ortb2Imp.instl; + }; + if (getPubIdMode(bid) === false) { impObject.tagid = bid.params.pos; impObject.ext.pos = bid.params.pos; diff --git a/modules/yahoosspBidAdapter.md b/modules/yahoosspBidAdapter.md index 5433e1fe3c9..7fb7a307192 100644 --- a/modules/yahoosspBidAdapter.md +++ b/modules/yahoosspBidAdapter.md @@ -57,7 +57,7 @@ At this time, only the following partners/publishers are eligble for pubId integ A. Do not have any display/banner inventory. B. Do not have any existing accounts on Yahoo SSP (aka: aol, oneMobile, oneDisplay). -# Mandaotory Bidder Parameters +# Mandatory Bidder Parameters ## dcn & pos (DEFAULT) The minimal requirements for the 'yahoossp' bid adapter to generate an outbound bid-request to our Yahoo SSP are: 1. At least 1 adUnit including mediaTypes: banner or video 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/modules/zetaSspBidAdapter.md b/modules/zetaSspBidAdapter.md index d2950bce6b9..00d8663586c 100644 --- a/modules/zetaSspBidAdapter.md +++ b/modules/zetaSspBidAdapter.md @@ -10,7 +10,7 @@ Maintainer: miakovlev@zetaglobal.com Module that connects to Zeta's SSP -# Test Parameters +# Banner Ad Unit: For Publishers ``` var adUnits = [ { @@ -40,3 +40,35 @@ Module that connects to Zeta's SSP } ]; ``` + +# Video Ad Unit: For Publishers +``` + var adUnits = [ + { + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'outstream' + } + }, + bids: [ + { + bidder: 'zeta_global_ssp', + bidId: 12345, + params: { + placement: 12345, + user: { + uid: 12345, + buyeruid: 12345 + }, + tags: { + someTag: 123, + sid: 'publisherId' + }, + test: 1 + } + } + ] + } + ]; +``` diff --git a/modules/zeta_global_sspBidAdapter.js b/modules/zeta_global_sspBidAdapter.js index f526a50e098..a0dade19b93 100644 --- a/modules/zeta_global_sspBidAdapter.js +++ b/modules/zeta_global_sspBidAdapter.js @@ -161,6 +161,9 @@ export const spec = { advertiserDomains: zetaBid.adomain }; } + if (deepAccess(zetaBid, 'ext.bidtype', '') === VIDEO) { + bid.vastXml = bid.ad; + } bidResponses.push(bid); }) }) diff --git a/package.json b/package.json index e67314ce3eb..6536fd206d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "6.1.0-pre", + "version": "6.3.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { diff --git a/plugins/pbjsGlobals.js b/plugins/pbjsGlobals.js index bf3c9033ee6..73912d8126e 100644 --- a/plugins/pbjsGlobals.js +++ b/plugins/pbjsGlobals.js @@ -1,11 +1,13 @@ let t = require('@babel/core').types; let prebid = require('../package.json'); +const path = require('path'); module.exports = function(api, options) { + const pbGlobal = options.globalVarName || prebid.globalVarName; let replace = { '$prebid.version$': prebid.version, - '$$PREBID_GLOBAL$$': options.globalVarName || prebid.globalVarName, + '$$PREBID_GLOBAL$$': pbGlobal, '$$REPO_AND_VERSION$$': `${prebid.repository.url.split('/')[3]}_prebid_${prebid.version}` }; @@ -13,8 +15,33 @@ module.exports = function(api, options) { '$$REPO_AND_VERSION$$' ]; + const PREBID_ROOT = path.resolve(__dirname, '..'); + + function getModuleName(filename) { + const modPath = path.parse(path.relative(PREBID_ROOT, filename)); + if (modPath.ext.toLowerCase() !== '.js') { + return null; + } + if (modPath.dir === 'modules') { + // modules/moduleName.js -> moduleName + return modPath.name; + } + if (modPath.name.toLowerCase() === 'index' && path.dirname(modPath.dir) === 'modules') { + // modules/moduleName/index.js -> moduleName + return path.basename(modPath.dir); + } + return null; + } + return { visitor: { + Program(path, state) { + const modName = getModuleName(state.filename); + if (modName != null) { + // append "registration" of module file to $$PREBID_GLOBAL$$.installedModules + path.node.body.push(...api.parse(`window.${pbGlobal}.installedModules.push('${modName}');`).program.body); + } + }, StringLiteral(path) { Object.keys(replace).forEach(name => { if (path.node.value.includes(name)) { diff --git a/src/auction.js b/src/auction.js index 059c09bc2ff..66692be1754 100644 --- a/src/auction.js +++ b/src/auction.js @@ -59,7 +59,7 @@ import { flatten, timestamp, adUnitsFilter, deepAccess, getBidRequest, getValue, parseUrl, generateUUID, - logMessage, bind, logError, logInfo, logWarn, isEmpty, _each, isFn, isEmptyStr + logMessage, bind, logError, logInfo, logWarn, isEmpty, _each, isFn, isEmptyStr, isAllowZeroCpmBidsEnabled } from './utils.js'; import { getPriceBucketString } from './cpmBucketManager.js'; import { getNativeTargeting } from './native.js'; @@ -567,7 +567,8 @@ function getPreparedBidForAuction({adUnitCode, bid, bidderRequest, auctionId}) { function setupBidTargeting(bidObject, bidderRequest) { let keyValues; - if (bidObject.bidderCode && (bidObject.cpm > 0 || bidObject.dealId)) { + const cpmCheck = (isAllowZeroCpmBidsEnabled(bidObject.bidderCode)) ? bidObject.cpm >= 0 : bidObject.cpm > 0; + if (bidObject.bidderCode && (cpmCheck || bidObject.dealId)) { let bidReq = find(bidderRequest.bids, bid => bid.adUnitCode === bidObject.adUnitCode); keyValues = getKeyValueTargetingPairs(bidObject.bidderCode, bidObject, bidReq); } diff --git a/src/prebid.js b/src/prebid.js index 855d53d7de0..ee20754bb62 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -47,8 +47,7 @@ $$PREBID_GLOBAL$$.libLoaded = true; $$PREBID_GLOBAL$$.version = 'v$prebid.version$'; logInfo('Prebid.js v$prebid.version$ loaded'); -// modules list generated from build -$$PREBID_GLOBAL$$.installedModules = ['v$prebid.modulesList$']; +$$PREBID_GLOBAL$$.installedModules = $$PREBID_GLOBAL$$.installedModules || []; // create adUnit array $$PREBID_GLOBAL$$.adUnits = $$PREBID_GLOBAL$$.adUnits || []; diff --git a/src/targeting.js b/src/targeting.js index a140c952230..423e946896e 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -1,6 +1,6 @@ import { uniques, isGptPubadsDefined, getHighestCpm, getOldestHighestCpmBid, groupBy, isAdUnitCodeMatchingSlot, timestamp, - deepAccess, deepClone, logError, logWarn, logInfo, isFn, isArray, logMessage, isStr + deepAccess, deepClone, logError, logWarn, logInfo, isFn, isArray, logMessage, isStr, isAllowZeroCpmBidsEnabled } from './utils.js'; import { config } from './config.js'; import { NATIVE_TARGETING_KEYS } from './native.js'; @@ -18,6 +18,10 @@ var pbTargetingKeys = []; const MAX_DFP_KEYLENGTH = 20; const TTL_BUFFER = 1000; +const CFG_ALLOW_TARGETING_KEYS = `targetingControls.allowTargetingKeys`; +const CFG_ADD_TARGETING_KEYS = `targetingControls.addTargetingKeys`; +const TARGETING_KEY_CONFIGURATION_ERROR_MSG = `Only one of "${CFG_ALLOW_TARGETING_KEYS}" or "${CFG_ADD_TARGETING_KEYS}" can be set`; + export const TARGETING_KEYS = Object.keys(CONSTANTS.TARGETING_KEYS).map( key => CONSTANTS.TARGETING_KEYS[key] ); @@ -261,7 +265,17 @@ export function newTargeting(auctionManager) { }); const defaultKeys = Object.keys(Object.assign({}, CONSTANTS.DEFAULT_TARGETING_KEYS, CONSTANTS.NATIVE_KEYS)); - const allowedKeys = config.getConfig('targetingControls.allowTargetingKeys') || defaultKeys; + let allowedKeys = config.getConfig(CFG_ALLOW_TARGETING_KEYS); + const addedKeys = config.getConfig(CFG_ADD_TARGETING_KEYS); + + if (addedKeys != null && allowedKeys != null) { + throw new Error(TARGETING_KEY_CONFIGURATION_ERROR_MSG); + } else if (addedKeys != null) { + allowedKeys = defaultKeys.concat(addedKeys); + } else { + allowedKeys = allowedKeys || defaultKeys; + } + if (Array.isArray(allowedKeys) && allowedKeys.length > 0) { targeting = getAllowedTargetingKeyValues(targeting, allowedKeys); } @@ -284,6 +298,13 @@ export function newTargeting(auctionManager) { return targeting; }; + // warn about conflicting configuration + config.getConfig('targetingControls', function (config) { + if (deepAccess(config, CFG_ALLOW_TARGETING_KEYS) != null && deepAccess(config, CFG_ADD_TARGETING_KEYS) != null) { + logError(TARGETING_KEY_CONFIGURATION_ERROR_MSG); + } + }); + // create an encoded string variant based on the keypairs of the provided object // - note this will encode the characters between the keys (ie = and &) function convertKeysToQueryForm(keyMap) { @@ -438,7 +459,7 @@ export function newTargeting(auctionManager) { const adUnitCodes = getAdUnitCodes(adUnitCode); return bidsReceived .filter(bid => includes(adUnitCodes, bid.adUnitCode)) - .filter(bid => bid.cpm > 0) + .filter(bid => (isAllowZeroCpmBidsEnabled(bid.bidderCode)) ? bid.cpm >= 0 : bid.cpm > 0) .map(bid => bid.adUnitCode) .filter(uniques) .map(adUnitCode => bidsReceived diff --git a/src/utils.js b/src/utils.js index 03c76529ddf..a0983da3bd8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,5 +1,6 @@ /* eslint-disable no-console */ import { config } from './config.js'; +import { getGlobal } from './prebidGlobal.js'; import clone from 'just-clone'; import find from 'core-js-pure/features/array/find.js'; import includes from 'core-js-pure/features/array/includes.js'; @@ -481,16 +482,43 @@ export function insertElement(elm, doc, target, asLastChildChild) { } catch (e) {} } +/** + * Returns a promise that completes when the given element triggers a 'load' or 'error' DOM event, or when + * `timeout` milliseconds have elapsed. + * + * @param {HTMLElement} element + * @param {Number} [timeout] + * @returns {Promise} + */ +export function waitForElementToLoad(element, timeout) { + let timer = null; + return new Promise((resolve) => { + const onLoad = function() { + element.removeEventListener('load', onLoad); + element.removeEventListener('error', onLoad); + if (timer != null) { + window.clearTimeout(timer); + } + resolve(); + }; + element.addEventListener('load', onLoad); + element.addEventListener('error', onLoad); + if (timeout != null) { + timer = window.setTimeout(onLoad, timeout); + } + }); +} + /** * Inserts an image pixel with the specified `url` for cookie sync * @param {string} url URL string of the image pixel to load * @param {function} [done] an optional exit callback, used when this usersync pixel is added during an async process + * @param {Number} [timeout] an optional timeout in milliseconds for the image to load before calling `done` */ -export function triggerPixel(url, done) { +export function triggerPixel(url, done, timeout) { const img = new Image(); if (done && internal.isFn(done)) { - img.addEventListener('load', done); - img.addEventListener('error', done); + waitForElementToLoad(img, timeout).then(done); } img.src = url; } @@ -538,18 +566,18 @@ export function insertHtmlIntoIframe(htmlCode) { * @param {string} url URL to be requested * @param {string} encodeUri boolean if URL should be encoded before inserted. Defaults to true * @param {function} [done] an optional exit callback, used when this usersync pixel is added during an async process + * @param {Number} [timeout] an optional timeout in milliseconds for the iframe to load before calling `done` */ -export function insertUserSyncIframe(url, done) { +export function insertUserSyncIframe(url, done, timeout) { let iframeHtml = internal.createTrackPixelIframeHtml(url, false, 'allow-scripts allow-same-origin'); let div = document.createElement('div'); div.innerHTML = iframeHtml; let iframe = div.firstChild; if (done && internal.isFn(done)) { - iframe.addEventListener('load', done); - iframe.addEventListener('error', done); + waitForElementToLoad(iframe, timeout).then(done); } internal.insertElement(iframe, document, 'html', true); -}; +} /** * Creates a snippet of HTML that retrieves the specified `url` @@ -1294,3 +1322,9 @@ export function cyrb53Hash(str, seed = 0) { h2 = imul(h2 ^ (h2 >>> 16), 2246822507) ^ imul(h1 ^ (h1 >>> 13), 3266489909); return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(); } + +export function isAllowZeroCpmBidsEnabled(bidderCode) { + const bidderSettings = getGlobal().bidderSettings; + return ((bidderSettings[bidderCode] && bidderSettings[bidderCode].allowZeroCpmBids === true) || + (bidderSettings.standard && bidderSettings.standard.allowZeroCpmBids === true)); +} diff --git a/test/spec/integration/faker/googletag.js b/test/spec/integration/faker/googletag.js index 1d1a7512153..a8676b500a5 100644 --- a/test/spec/integration/faker/googletag.js +++ b/test/spec/integration/faker/googletag.js @@ -51,9 +51,15 @@ export function enable() { window.googletag = { _slots: [], _callbackMap: {}, + _ppid: undefined, + cmd: [], pubads: function () { var self = this; return { + setPublisherProvidedId: function (ppid) { + self._ppid = ppid; + }, + getSlots: function () { return self._slots; }, diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index b5443cdd5c2..141edc1e61c 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -16,20 +16,21 @@ function validateBuiltServerRequest(builtReq, expectedReq) { describe('33acrossBidAdapter:', function () { const BIDDER_CODE = '33across'; const SITE_ID = 'sample33xGUID123456789'; - const PRODUCT_ID = 'siab'; const END_POINT = 'https://ssc.33across.com/api/v1/hb'; let element, win; let bidRequests; let sandbox; - function TtxRequestBuilder() { + function TtxRequestBuilder(siteId = SITE_ID) { const ttxRequest = { - imp: [{}], + imp: [{ + id: 'b1' + }], site: { - id: SITE_ID + id: siteId }, - id: 'b1', + id: 'r1', regs: { ext: { gdpr: 0 @@ -46,66 +47,83 @@ describe('33acrossBidAdapter:', function () { } }; + this.addImp = (id = 'b2') => { + ttxRequest.imp.push({ id }); + + return this; + } + this.withBanner = () => { - Object.assign(ttxRequest.imp[0], { - banner: { - format: [ - { - w: 300, - h: 250 - }, - { - w: 728, - h: 90 - } - ], - ext: { - ttx: { - viewability: { - amount: 100 + ttxRequest.imp.forEach((imp) => { + Object.assign(imp, { + banner: { + format: [ + { + w: 300, + h: 250 + }, + { + w: 728, + h: 90 + } + ], + ext: { + ttx: { + viewability: { + amount: 100 + } } } } - } + }); }); - return this; }; this.withBannerSizes = this.withSizes = sizes => { - Object.assign(ttxRequest.imp[0].banner, { format: sizes }); + ttxRequest.imp.forEach((imp) => { + Object.assign(imp.banner, { format: sizes }); + }); + return this; }; this.withVideo = (params = {}) => { - Object.assign(ttxRequest.imp[0], { - video: { - w: 300, - h: 250, - placement: 2, - ...params - } + ttxRequest.imp.forEach((imp) => { + Object.assign(imp, { + video: { + w: 300, + h: 250, + placement: 2, + ...params + } + }); }); return this; }; this.withViewability = (viewability, format = 'banner') => { - Object.assign(ttxRequest.imp[0][format], { - ext: { - ttx: { viewability } - } + ttxRequest.imp.forEach((imp) => { + Object.assign(imp[format], { + ext: { + ttx: { viewability } + } + }); }); + return this; }; - this.withProduct = (prod = PRODUCT_ID) => { - Object.assign(ttxRequest.imp[0], { - ext: { - ttx: { - prod + this.withProduct = (prod = 'siab') => { + ttxRequest.imp.forEach((imp) => { + Object.assign(imp, { + ext: { + ttx: { + prod + } } - } + }); }); return this; @@ -249,7 +267,7 @@ describe('33acrossBidAdapter:', function () { bidderRequestId: 'b1a', params: { siteId: SITE_ID, - productId: PRODUCT_ID + productId: 'siab' }, adUnitCode: 'div-id', auctionId: 'r1', @@ -258,35 +276,61 @@ describe('33acrossBidAdapter:', function () { } ]; + this.addBid = (bidParams = {}) => { + bidRequests.push({ + bidId: 'b2', + bidder: '33across', + bidderRequestId: 'b1b', + params: { + siteId: SITE_ID, + productId: 'siab' + }, + adUnitCode: 'div-id', + auctionId: 'r1', + mediaTypes: {}, + transactionId: 't2', + ...bidParams + }); + + return this; + }; + this.withBanner = () => { - bidRequests[0].mediaTypes.banner = { - sizes: [ - [300, 250], - [728, 90] - ] - }; + bidRequests.forEach((bid) => { + bid.mediaTypes.banner = { + sizes: [ + [300, 250], + [728, 90] + ] + }; + }); return this; }; this.withProduct = (prod) => { - bidRequests[0].params.productId = prod; - + bidRequests.forEach((bid) => { + bid.params.productId = prod; + }); return this; }; this.withVideo = (params) => { - bidRequests[0].mediaTypes.video = { - playerSize: [[300, 250]], - context: 'outstream', - ...params - }; + bidRequests.forEach((bid) => { + bid.mediaTypes.video = { + playerSize: [[300, 250]], + context: 'outstream', + ...params + }; + }); return this; } this.withUserIds = (eids) => { - bidRequests[0].userIdAsEids = eids; + bidRequests.forEach((bid) => { + bid.userIdAsEids = eids; + }); return this; }; @@ -315,6 +359,7 @@ describe('33acrossBidAdapter:', function () { } }; win = { + parent: null, document: { visibilityState: 'visible' }, @@ -331,7 +376,7 @@ describe('33acrossBidAdapter:', function () { sandbox = sinon.sandbox.create(); sandbox.stub(Date, 'now').returns(1); - sandbox.stub(document, 'getElementById').withArgs('div-id').returns(element); + sandbox.stub(document, 'getElementById').returns(element); sandbox.stub(utils, 'getWindowTop').returns(win); sandbox.stub(utils, 'getWindowSelf').returns(win); }); @@ -1376,10 +1421,146 @@ describe('33acrossBidAdapter:', function () { }); }); }); + + context('when SRA mode is enabled', function() { + it('builds a single request with multiple imps corresponding to each group {siteId, productId}', function() { + sandbox.stub(config, 'getConfig').callsFake(() => { + return { + enableSRAMode: true + } + }); + + const bidRequests = new BidRequestsBuilder() + .addBid() + .addBid({ + bidId: 'b3', + adUnitCode: 'div-id', + params: { + siteId: 'sample33xGUID123456780', + productId: 'siab' + } + }) + .addBid({ + bidId: 'b4', + adUnitCode: 'div-id', + params: { + siteId: 'sample33xGUID123456780', + productId: 'inview' + } + }) + .withBanner() + .withVideo({context: 'outstream'}) + .build(); + + const req1 = new TtxRequestBuilder() + .addImp() + .withProduct('siab') + .withBanner() + .withVideo() + .build(); + + const req2 = new TtxRequestBuilder('sample33xGUID123456780') + .withProduct('siab') + .withBanner() + .withVideo() + .build(); + + req2.imp[0].id = 'b3'; + + const req3 = new TtxRequestBuilder('sample33xGUID123456780') + .withProduct('inview') + .withBanner() + .withVideo() + .build(); + + req3.imp[0].id = 'b4'; + + const serverReq1 = new ServerRequestBuilder() + .withData(req1) + .build(); + + const serverReq2 = new ServerRequestBuilder() + .withData(req2) + .withUrl('https://ssc.33across.com/api/v1/hb?guid=sample33xGUID123456780') + .build(); + + const serverReq3 = new ServerRequestBuilder() + .withData(req3) + .withUrl('https://ssc.33across.com/api/v1/hb?guid=sample33xGUID123456780') + .build(); + + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests).to.deep.equal([serverReq1, serverReq2, serverReq3]); + }); + }); + + context('when SRA mode is not enabled', function() { + it('builds multiple requests, one corresponding to each Ad Unit', function() { + const bidRequests = new BidRequestsBuilder() + .addBid() + .addBid({ + bidId: 'b3', + adUnitCode: 'div-id', + params: { + siteId: 'sample33xGUID123456780', + productId: 'siab' + } + }) + .withBanner() + .withVideo({context: 'outstream'}) + .build(); + + const req1 = new TtxRequestBuilder() + .withProduct('siab') + .withBanner() + .withVideo() + .build(); + + const req2 = new TtxRequestBuilder() + .withProduct('siab') + .withBanner() + .withVideo() + .build(); + + req2.imp[0].id = 'b2'; + + const req3 = new TtxRequestBuilder('sample33xGUID123456780') + .withProduct('siab') + .withBanner() + .withVideo() + .build(); + + req3.imp[0].id = 'b3'; + + const serverReq1 = new ServerRequestBuilder() + .withData(req1) + .build(); + + const serverReq2 = new ServerRequestBuilder() + .withData(req2) + .build(); + + const serverReq3 = new ServerRequestBuilder() + .withData(req3) + .withUrl('https://ssc.33across.com/api/v1/hb?guid=sample33xGUID123456780') + .build(); + + const builtServerRequests = spec.buildRequests(bidRequests, {}); + + expect(builtServerRequests) + .to.deep.equal([ + serverReq1, + serverReq2, + serverReq3 + ]); + }); + }); }); describe('interpretResponse', function() { let ttxRequest, serverRequest; + const videoBid = ''; beforeEach(function() { ttxRequest = new TtxRequestBuilder() @@ -1390,6 +1571,7 @@ describe('33acrossBidAdapter:', function () { page: 'https://test-url.com' }) .build(); + serverRequest = new ServerRequestBuilder() .withUrl('https://staging-ssc.33across.com/api/v1/hb') .withData(ttxRequest) @@ -1405,11 +1587,12 @@ describe('33acrossBidAdapter:', function () { const serverResponse = { cur: 'USD', ext: {}, - id: 'b1', + id: 'r1', seatbid: [ { bid: [{ id: '1', + impid: 'b1', adm: '

I am an ad

', crid: 1, h: 250, @@ -1441,15 +1624,15 @@ describe('33acrossBidAdapter:', function () { }); it('interprets and returns the single video bid response', function() { - const videoBid = ''; const serverResponse = { cur: 'USD', ext: {}, - id: 'b1', + id: 'r1', seatbid: [ { bid: [{ id: '1', + impid: 'b1', adm: videoBid, ext: { ttx: { @@ -1497,6 +1680,7 @@ describe('33acrossBidAdapter:', function () { { bid: [{ id: '1', + impid: 'b1', adm: '

I am an ad

', crid: 1, h: 250, @@ -1533,7 +1717,7 @@ describe('33acrossBidAdapter:', function () { const serverResponse = { cur: 'USD', ext: {}, - id: 'b1', + id: 'r1', seatbid: [] }; @@ -1542,15 +1726,16 @@ describe('33acrossBidAdapter:', function () { }); context('when more than one bids are returned', function() { - it('interprets and returns the the first bid of the first seatbid', function() { + it('interprets and returns all bids', function() { const serverResponse = { cur: 'USD', ext: {}, - id: 'b1', + id: 'r1', seatbid: [ { bid: [{ id: '1', + impid: 'b1', adm: '

I am an ad

', crid: 1, h: 250, @@ -1559,6 +1744,7 @@ describe('33acrossBidAdapter:', function () { }, { id: '2', + impid: 'b2', adm: '

I am an ad

', crid: 2, h: 250, @@ -1570,7 +1756,14 @@ describe('33acrossBidAdapter:', function () { { bid: [{ id: '3', - adm: '

I am an ad

', + impid: 'b3', + adm: videoBid, + ext: { + ttx: { + mediaType: 'video', + vastType: 'xml' + } + }, crid: 3, h: 250, w: 300, @@ -1579,21 +1772,50 @@ describe('33acrossBidAdapter:', function () { } ] }; - const bidResponse = { - requestId: 'b1', - bidderCode: BIDDER_CODE, - cpm: 0.0940, - width: 300, - height: 250, - ad: '

I am an ad

', - ttl: 60, - creativeId: 1, - mediaType: 'banner', - currency: 'USD', - netRevenue: true - }; + const bidResponse = [ + { + requestId: 'b1', + bidderCode: BIDDER_CODE, + cpm: 0.0940, + width: 300, + height: 250, + ad: '

I am an ad

', + ttl: 60, + creativeId: 1, + mediaType: 'banner', + currency: 'USD', + netRevenue: true + }, + { + requestId: 'b2', + bidderCode: BIDDER_CODE, + cpm: 0.0938, + width: 300, + height: 250, + ad: '

I am an ad

', + ttl: 60, + creativeId: 2, + mediaType: 'banner', + currency: 'USD', + netRevenue: true + }, + { + requestId: 'b3', + bidderCode: BIDDER_CODE, + cpm: 0.0938, + width: 300, + height: 250, + ad: videoBid, + vastXml: '', + ttl: 60, + creativeId: 3, + mediaType: 'video', + currency: 'USD', + netRevenue: true + } + ]; - expect(spec.interpretResponse({ body: serverResponse }, serverRequest)).to.deep.equal([bidResponse]); + expect(spec.interpretResponse({ body: serverResponse }, serverRequest)).to.deep.equal(bidResponse); }); }); }); 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 8f87c73f1b4..0ecdb0e10c7 100644 --- a/test/spec/modules/adomikAnalyticsAdapter_spec.js +++ b/test/spec/modules/adomikAnalyticsAdapter_spec.js @@ -45,6 +45,8 @@ describe('Adomik Prebid Analytic', function () { const initOptions = { id: '123456', url: 'testurl', + testId: '12345', + testValue: '1000' }; const bid = { @@ -71,6 +73,8 @@ describe('Adomik Prebid Analytic', function () { expect(adomikAnalytics.currentContext).to.deep.equal({ uid: '123456', url: 'testurl', + testId: '12345', + testValue: '1000', id: '', timeouted: false }); @@ -81,6 +85,8 @@ describe('Adomik Prebid Analytic', function () { expect(adomikAnalytics.currentContext).to.deep.equal({ uid: '123456', url: 'testurl', + testId: '12345', + testValue: '1000', id: 'test-test-test', timeouted: false }); diff --git a/test/spec/modules/adplusBidAdapter_spec.js b/test/spec/modules/adplusBidAdapter_spec.js new file mode 100644 index 00000000000..840d86c80f1 --- /dev/null +++ b/test/spec/modules/adplusBidAdapter_spec.js @@ -0,0 +1,213 @@ +import {expect} from 'chai'; +import {spec, BIDDER_CODE, ADPLUS_ENDPOINT, } from 'modules/adplusBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; + +describe('AplusBidAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.be.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + let validRequest = { + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + inventoryId: '30', + adUnitId: '1', + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let validRequest = { + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + inventoryId: '30', + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + + it('should return false when required param types are wrong', function () { + let validRequest = { + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + inventoryId: 30, + adUnitId: '1', + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + + it('should return false when size is not exists', function () { + let validRequest = { + params: { + inventoryId: 30, + adUnitId: '1', + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + + it('should return false when size is wrong', function () { + let validRequest = { + mediaTypes: { + banner: { + sizes: [[300]] + } + }, + params: { + inventoryId: 30, + adUnitId: '1', + } + }; + expect(spec.isBidRequestValid(validRequest)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let validRequest = [ + { + bidder: BIDDER_CODE, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: { + inventoryId: '-1', + adUnitId: '-3', + }, + bidId: '2bdcb0b203c17d' + }, + ]; + + let bidderRequest = { + refererInfo: { + referer: 'https://test.domain' + } + }; + + it('bidRequest HTTP method', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request[0].method).to.equal('GET'); + }); + + it('bidRequest url', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request[0].url).to.equal(ADPLUS_ENDPOINT); + }); + + it('tests bidRequest data is clean and has the right values', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + + expect(request[0].data.bidId).to.equal('2bdcb0b203c17d'); + expect(request[0].data.inventoryId).to.equal('-1'); + expect(request[0].data.adUnitId).to.equal('-3'); + expect(request[0].data.adUnitWidth).to.equal(300); + expect(request[0].data.adUnitHeight).to.equal(250); + expect(request[0].data.sdkVersion).to.equal('1'); + expect(typeof request[0].data.session).to.equal('string'); + expect(request[0].data.session).length(36); + expect(request[0].data.interstitial).to.equal(0); + expect(request[0].data).to.not.have.deep.property('extraData'); + expect(request[0].data).to.not.have.deep.property('yearOfBirth'); + expect(request[0].data).to.not.have.deep.property('gender'); + expect(request[0].data).to.not.have.deep.property('categories'); + expect(request[0].data).to.not.have.deep.property('latitude'); + expect(request[0].data).to.not.have.deep.property('longitude'); + }); + }); + + describe('interpretResponse', function () { + const requestData = { + language: window.navigator.language, + screenWidth: 1440, + screenHeight: 900, + sdkVersion: '1', + inventoryId: '-1', + adUnitId: '-3', + adUnitWidth: 300, + adUnitHeight: 250, + domain: 'tassandigi.com', + pageUrl: 'https%3A%2F%2Ftassandigi.com%2Fserafettin%2Fads.html', + interstitial: 0, + session: '1c02db03-5289-932a-93af-7b4022611fec', + token: '1c02db03-5289-937a-93df-7b4022611fec', + secure: 1, + bidId: '2bdcb0b203c17d', + }; + const bidRequest = { + 'method': 'GET', + 'url': ADPLUS_ENDPOINT, + 'data': requestData, + }; + + const bidResponse = { + body: [ + { + 'ad': '
ad
', + 'advertiserDomains': [ + 'advertiser.com' + ], + 'categoryIDs': [ + 'IAB-111' + ], + 'cpm': 3.57, + 'creativeID': '1', + 'currency': 'TRY', + 'dealID': '1', + 'height': 300, + 'mediaType': 'banner', + 'netRevenue': true, + 'requestID': '2bdcb0b203c17d', + 'ttl': 300, + 'width': 250 + } + ], + headers: {} + }; + + const emptyBidResponse = { + body: null, + }; + + it('returns an empty array when the result body is not valid', function () { + const result = spec.interpretResponse(emptyBidResponse, bidRequest); + expect(result).to.deep.equal([]); + }); + + it('result is correct', function () { + const result = spec.interpretResponse(bidResponse, bidRequest); + expect(result[0].requestId).to.equal('2bdcb0b203c17d'); + expect(result[0].cpm).to.equal(3.57); + expect(result[0].width).to.equal(250); + expect(result[0].height).to.equal(300); + expect(result[0].creativeId).to.equal('1'); + expect(result[0].currency).to.equal('TRY'); + expect(result[0].dealId).to.equal('1'); + expect(result[0].mediaType).to.equal('banner'); + expect(result[0].netRevenue).to.equal(true); + expect(result[0].ttl).to.equal(300); + expect(result[0].meta.advertiserDomains).to.deep.equal(['advertiser.com']); + expect(result[0].meta.secondaryCatIds).to.deep.equal(['IAB-111']); + }); + }); +}); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index befc95e5f24..613d12ebcc0 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -590,6 +590,32 @@ describe('Adyoulike Adapter', function () { expect(payload.gdprConsent.consentRequired).to.be.null; }); + it('should add userid eids information to the request', function () { + let bidderRequest = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'userId': { + pubcid: '01EAJWWNEPN3CYMM5N8M5VXY22', + unsuported: '666' + } + }; + + bidderRequest.bids = bidRequestWithSinglePlacement; + + const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.userId).to.exist; + expect(payload.userId).to.deep.equal([{ + 'source': 'pubcid.org', + 'uids': [{ + 'atype': 1, + 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' + }] + }]); + }); + it('sends bid request to endpoint with single placement', function () { const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); const payload = JSON.parse(request.data); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index fbcce5a1322..e7724a4dc83 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -1004,12 +1004,16 @@ describe('AppNexusAdapter', function () { describe('interpretResponse', function () { let bfStub; + let bidderSettingsStorage; + before(function() { bfStub = sinon.stub(bidderFactory, 'getIabSubCategory'); + bidderSettingsStorage = $$PREBID_GLOBAL$$.bidderSettings; }); after(function() { bfStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = bidderSettingsStorage; }); let response = { @@ -1077,6 +1081,15 @@ describe('AppNexusAdapter', function () { 'adUnitCode': 'code', 'appnexus': { 'buyerMemberId': 958 + }, + 'meta': { + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [{ + 'bsid': '958' + }] + } } } ]; @@ -1085,11 +1098,46 @@ describe('AppNexusAdapter', function () { bidId: '3db3773286ee59', adUnitCode: 'code' }] - } + }; let result = spec.interpretResponse({ body: response }, {bidderRequest}); expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); + it('should reject 0 cpm bids', function () { + let zeroCpmResponse = deepClone(response); + zeroCpmResponse.tags[0].ads[0].cpm = 0; + + let bidderRequest = { + bidderCode: 'appnexus' + }; + + let result = spec.interpretResponse({ body: zeroCpmResponse }, { bidderRequest }); + expect(result.length).to.equal(0); + }); + + it('should allow 0 cpm bids if allowZeroCpmBids setConfig is true', function () { + $$PREBID_GLOBAL$$.bidderSettings = { + appnexus: { + allowZeroCpmBids: true + } + }; + + let zeroCpmResponse = deepClone(response); + zeroCpmResponse.tags[0].ads[0].cpm = 0; + + let bidderRequest = { + bidderCode: 'appnexus', + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + }; + + let result = spec.interpretResponse({ body: zeroCpmResponse }, { bidderRequest }); + expect(result.length).to.equal(1); + expect(result[0].cpm).to.equal(0); + }); + it('handles nobid responses', function () { let response = { 'version': '0.0.1', diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index e29994eba44..d9b8cac10b4 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec, VIDEO_ENDPOINT, BANNER_ENDPOINT, OUTSTREAM_SRC, DEFAULT_MIMES } from 'modules/beachfrontBidAdapter.js'; -import { parseUrl } from 'src/utils.js'; +import { config } from 'src/config.js'; +import { parseUrl, deepAccess } from 'src/utils.js'; describe('BeachfrontAdapter', function () { let bidRequests; @@ -556,6 +557,69 @@ describe('BeachfrontAdapter', function () { }); }); + describe('with first-party data', function () { + let sandbox + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('must add first-party data to the video bid request', function () { + sandbox.stub(config, 'getConfig').callsFake(key => { + const cfg = { + ortb2: { + site: { + keywords: 'test keyword' + }, + user: { + data: 'some user data' + } + } + }; + return deepAccess(cfg, key); + }); + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { video: {} }; + const bidderRequest = { + refererInfo: { + referer: 'http://example.com/page.html' + } + }; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.user.data).to.equal('some user data'); + expect(data.site.keywords).to.equal('test keyword'); + expect(data.site.page).to.equal('http://example.com/page.html'); + expect(data.site.domain).to.equal('example.com'); + }); + + it('must add first-party data to the banner bid request', function () { + sandbox.stub(config, 'getConfig').callsFake(key => { + const cfg = { + ortb2: { + site: { + keywords: 'test keyword' + }, + user: { + data: 'some user data' + } + } + }; + return deepAccess(cfg, key); + }); + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + const requests = spec.buildRequests([ bidRequest ]); + const data = requests[0].data; + expect(data.ortb2.user.data).to.equal('some user data'); + expect(data.ortb2.site.keywords).to.equal('test keyword'); + }); + }); + describe('for multi-format bids', function () { it('should create a POST request for each bid format', function () { const width = 300; diff --git a/test/spec/modules/cwireBidAdapter_spec.js b/test/spec/modules/cwireBidAdapter_spec.js index 4ed19cf7b74..3f922fc1471 100644 --- a/test/spec/modules/cwireBidAdapter_spec.js +++ b/test/spec/modules/cwireBidAdapter_spec.js @@ -36,11 +36,6 @@ const BidderRequestBuilder = function BidderRequestBuilder(options) { bidderRequestId: BID_DEFAULTS.request.bidderRequestId, transactionId: BID_DEFAULTS.request.transactionId, timeout: 3000, - refererInfo: { - numIframes: 0, - reachedTop: true, - referer: 'http://test.io/index.html?pbjs_debug=true' - } }; const request = { @@ -82,16 +77,13 @@ const BidRequestBuilder = function BidRequestBuilder(options, deleteKeys) { }; describe('C-WIRE bid adapter', () => { - let utilsMock; let sandbox; beforeEach(() => { - utilsMock = sinon.mock(utils); sandbox = sinon.createSandbox(); }); afterEach(() => { - utilsMock.restore(); sandbox.restore(); }); @@ -146,6 +138,12 @@ describe('C-WIRE bid adapter', () => { describe('C-WIRE - buildRequests()', function () { it('creates a valid request', function () { + // for whatever reason stub for getWindowLocation does not work + // so this was the closest way to test for get params + const params = sandbox.stub(utils, 'getParameterByName'); + params.withArgs('cwgroups').returns('group_1'); + params.withArgs('cwcreative').returns('54321'); + const bid01 = new BidRequestBuilder({ mediaTypes: { banner: { @@ -153,12 +151,16 @@ describe('C-WIRE bid adapter', () => { } } }).withParams().build(); + const bidderRequest01 = new BidderRequestBuilder().build(); const requests = spec.buildRequests([bid01], bidderRequest01); + expect(requests.data.slots.length).to.equal(1); expect(requests.data.cwid).to.be.null; expect(requests.data.slots[0].sizes[0]).to.equal('1x1'); + expect(requests.data.cwcreative).to.equal('54321'); + expect(requests.data.refgroups[0]).to.equal('group_1'); }); }); 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/dchain_spec.js b/test/spec/modules/dchain_spec.js new file mode 100644 index 00000000000..45061c539c1 --- /dev/null +++ b/test/spec/modules/dchain_spec.js @@ -0,0 +1,329 @@ +import { checkDchainSyntax, addBidResponseHook } from '../../../modules/dchain.js'; +import { config } from '../../../src/config.js'; +import { expect } from 'chai'; + +describe('dchain module', function () { + const STRICT = 'strict'; + const RELAX = 'relaxed'; + const OFF = 'off'; + + describe('checkDchainSyntax', function () { + let bid; + + beforeEach(function () { + bid = { + meta: { + dchain: { + 'ver': '1.0', + 'complete': 0, + 'ext': {}, + 'nodes': [{ + 'asi': 'domain.com', + 'bsid': '12345', + }, { + 'name': 'bidder', + 'domain': 'bidder.com', + 'ext': {} + }] + } + } + }; + }); + + it('Returns false if complete param is not 0 or 1', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.complete = 0; // integer + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.complete = 1; // integer + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.complete = '1'; // string + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.complete = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.complete = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.complete; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.complete = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.complete = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if ver param is not a String', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.ver = 1; // integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ver = '1'; // string + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.ver = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ver = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.ver; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ver = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ver = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if ext param is not an Object', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.ext = 1; // integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ext = '1'; // string + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ext = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ext = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.true; + delete dchainConfig.ext; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.ext = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.ext = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes param is not an Array', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes = 1; // integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes = '1'; // string + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if unknown field is used in main dchain', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.test = '1'; // String + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].asi is not a String', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].asi = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].asi = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].asi = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes[0].asi; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].asi = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].asi = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].bsid is not a String', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].bsid = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].bsid = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].bsid = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes[0].bsid; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].bsid = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].bsid = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].rid is not a String', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].rid = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].rid = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].rid = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes[0].rid; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].rid = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].rid = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].name is not a String', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].name = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].name = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].name = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes[0].name; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].name = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].name = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].domain is not a String', function () { + let dchainConfig = bid.meta.dchain; + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].domain = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].domain = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].domain = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.false; + delete dchainConfig.nodes[0].domain; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].domain = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].domain = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if nodes[].ext is not an Object', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.nodes[0].ext = '1'; // String + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].ext = 1; // Integer + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].ext = 1.1; // float + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].ext = {}; // object + expect(checkDchainSyntax(bid, STRICT)).to.true; + delete dchainConfig.nodes[0].ext; // undefined + expect(checkDchainSyntax(bid, STRICT)).to.true; + dchainConfig.nodes[0].ext = true; // boolean + expect(checkDchainSyntax(bid, STRICT)).to.false; + dchainConfig.nodes[0].ext = []; // array + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Returns false if unknown field is used in nodes[]', function () { + let dchainConfig = bid.meta.dchain; + dchainConfig.nodes[0].test = '1'; // String + expect(checkDchainSyntax(bid, STRICT)).to.false; + }); + + it('Relaxed mode: returns true even for invalid config', function () { + bid.meta.dchain = { + ver: 1.1, + complete: '0', + nodes: [{ + name: 'asdf', + domain: ['domain.com'] + }] + }; + + expect(checkDchainSyntax(bid, RELAX)).to.true; + }); + }); + + describe('addBidResponseHook', function () { + let bid; + let adUnitCode = 'adUnit1'; + + beforeEach(function () { + bid = { + bidderCode: 'bidderA', + meta: { + dchain: { + 'ver': '1.0', + 'complete': 0, + 'ext': {}, + 'nodes': [{ + 'asi': 'domain.com', + 'bsid': '12345', + }, { + 'name': 'bidder', + 'domain': 'bidder.com', + 'ext': {} + }] + }, + networkName: 'myNetworkName', + networkId: 8475 + } + }; + }); + + afterEach(function () { + config.resetConfig(); + }); + + it('good strict config should append a node object to existing bid.meta.dchain object', function () { + function testCallback(adUnitCode, bid) { + expect(bid.meta.dchain).to.exist; + expect(bid.meta.dchain.nodes[1]).to.exist.and.to.deep.equal({ + 'name': 'bidder', + 'domain': 'bidder.com', + 'ext': {} + }); + expect(bid.meta.dchain.nodes[2]).to.exist.and.to.deep.equal({ asi: 'bidderA' }); + } + + config.setConfig({ dchain: { validation: STRICT } }); + addBidResponseHook(testCallback, adUnitCode, bid); + }); + + it('bad strict config should delete the bid.meta.dchain object', function () { + function testCallback(adUnitCode, bid) { + expect(bid.meta.dchain).to.not.exist; + } + + config.setConfig({ dchain: { validation: STRICT } }); + bid.meta.dchain.complete = 3; + addBidResponseHook(testCallback, adUnitCode, bid); + }); + + it('relaxed config should allow bid.meta.dchain to proceed, even with bad values', function () { + function testCallback(adUnitCode, bid) { + expect(bid.meta.dchain).to.exist; + expect(bid.meta.dchain.complete).to.exist.and.to.equal(3); + expect(bid.meta.dchain.nodes[2]).to.exist.and.to.deep.equal({ asi: 'bidderA' }); + } + + config.setConfig({ dchain: { validation: RELAX } }); + bid.meta.dchain.complete = 3; + addBidResponseHook(testCallback, adUnitCode, bid); + }); + + it('off config should allow the bid.meta.dchain to proceed', function () { + // check for missing nodes + function testCallback(adUnitCode, bid) { + expect(bid.meta.dchain).to.exist; + expect(bid.meta.dchain.complete).to.exist.and.to.equal(0); + expect(bid.meta.dchain.nodes).to.exist.and.to.deep.equal({ test: 123 }); + } + + config.setConfig({ dchain: { validation: OFF } }); + bid.meta.dchain.nodes = { test: 123 }; + addBidResponseHook(testCallback, adUnitCode, bid); + }); + + it('no bidder dchain', function () { + function testCallback(adUnitCode, bid) { + expect(bid.meta.dchain).to.exist; + expect(bid.meta.dchain.ver).to.exist.and.to.equal('1.0'); + expect(bid.meta.dchain.complete).to.exist.and.to.equal(0); + expect(bid.meta.dchain.nodes).to.exist.and.to.deep.equal([{ name: 'myNetworkName', bsid: '8475' }, { name: 'bidderA' }]); + } + + delete bid.meta.dchain; + config.setConfig({ dchain: { validation: RELAX } }); + addBidResponseHook(testCallback, adUnitCode, bid); + }); + }); +}); diff --git a/test/spec/modules/emx_digitalBidAdapter_spec.js b/test/spec/modules/emx_digitalBidAdapter_spec.js index 5831a8506c1..043a8a3709e 100644 --- a/test/spec/modules/emx_digitalBidAdapter_spec.js +++ b/test/spec/modules/emx_digitalBidAdapter_spec.js @@ -432,6 +432,16 @@ describe('emx_digital Adapter', function () { }); }); + it('should add gpid to request if present', () => { + const gpid = '/12345/my-gpt-tag-0'; + let bid = utils.deepClone(bidderRequest.bids[0]); + bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; + bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; + let requestWithGPID = spec.buildRequests([bid], bidderRequest); + requestWithGPID = JSON.parse(requestWithGPID.data); + expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); + }); + it('should add UID 2.0 to request', () => { const uid2 = { id: '456' }; const bidRequestWithUID = utils.deepClone(bidderRequest); diff --git a/test/spec/modules/engageyaBidAdapter_spec.js b/test/spec/modules/engageyaBidAdapter_spec.js index ae22948994b..7f07e4b9e4a 100644 --- a/test/spec/modules/engageyaBidAdapter_spec.js +++ b/test/spec/modules/engageyaBidAdapter_spec.js @@ -4,18 +4,7 @@ import * as utils from 'src/utils.js'; const ENDPOINT_URL = 'https://recs.engageya.com/rec-api/getrecs.json'; -export const _getUrlVars = function (url) { - var hash; - var myJson = {}; - var hashes = url.slice(url.indexOf('?') + 1).split('&'); - for (var i = 0; i < hashes.length; i++) { - hash = hashes[i].split('='); - myJson[hash[0]] = hash[1]; - } - return myJson; -} - -describe('engageya adapter', function () { +describe('Engageya adapter', function () { let bidRequests; let nativeBidRequests; @@ -55,40 +44,57 @@ describe('engageya adapter', function () { } ] }) + describe('isBidRequestValid', function () { - it('valid bid case', function () { + it('Valid bid case', function () { let validBid = { bidder: 'engageya', params: { widgetId: 85610, websiteId: 91140, pageUrl: '[PAGE_URL]' - } + }, + sizes: [[300, 250]] } let isValid = spec.isBidRequestValid(validBid); - expect(isValid).to.equal(true); + expect(isValid).to.be.true; }); - it('invalid bid case: widgetId and websiteId is not passed', function () { + it('Invalid bid case: widgetId and websiteId is not passed', function () { let validBid = { bidder: 'engageya', params: {} } let isValid = spec.isBidRequestValid(validBid); - expect(isValid).to.equal(false); + expect(isValid).to.be.false; }) - it('invalid bid case: widget id must be number', function () { + it('Invalid bid case: widget id must be number', function () { let invalidBid = { bidder: 'engageya', params: { widgetId: '157746a', websiteId: 91140, pageUrl: '[PAGE_URL]' - } + }, + sizes: [[300, 250]] + } + let isValid = spec.isBidRequestValid(invalidBid); + expect(isValid).to.be.false; + }) + + it('Invalid bid case: unsupported sizes', function () { + let invalidBid = { + bidder: 'engageya', + params: { + widgetId: '157746a', + websiteId: 91140, + pageUrl: '[PAGE_URL]' + }, + sizes: [[250, 250]] } let isValid = spec.isBidRequestValid(invalidBid); - expect(isValid).to.equal(false); + expect(isValid).to.be.false; }) }) @@ -113,36 +119,30 @@ describe('engageya adapter', function () { it('Request params check', function () { let request = spec.buildRequests(bidRequests)[0]; - const data = _getUrlVars(request.url) - expect(parseInt(data.wid)).to.exist.and.to.equal(bidRequests[0].params.widgetId); - expect(parseInt(data.webid)).to.exist.and.to.equal(bidRequests[0].params.websiteId); - }) - }) - - describe('interpretResponse', function () { - it('should return empty array if no response', function () { - const result = spec.interpretResponse({}, []) - expect(result).to.be.an('array').that.is.empty + const urlParams = new URL(request.url).searchParams; + expect(parseInt(urlParams.get('wid'))).to.exist.and.to.equal(bidRequests[0].params.widgetId); + expect(parseInt(urlParams.get('webid'))).to.exist.and.to.equal(bidRequests[0].params.websiteId); }); - it('should return empty array if no valid bids', function () { - let response = { - recs: [], - imageWidth: 300, - imageHeight: 250, - ireqId: '1d236f7890b', - pbtypeId: 2 - }; - let request = spec.buildRequests(bidRequests)[0]; - const result = spec.interpretResponse({ body: response }, request) - expect(result).to.be.an('array').that.is.empty + it('Request pageUrl - use param', function () { + const pageUrl = 'https://url.test'; + bidRequests[0].params.pageUrl = pageUrl; + const request = spec.buildRequests(bidRequests)[0]; + const urlParams = new URL(request.url).searchParams; + expect(urlParams.get('url')).to.exist.and.to.equal(pageUrl); }); + }) - it('should interpret native response', function () { - let serverResponse = { + describe('interpretResponse', function () { + let nativeResponse; + let bannerResponse; + + beforeEach(() => { + const recsResponse = { recs: [ { - ecpm: 0.0920, + ecpm: 9.20, + pecpm: 0.0520, postId: '', thumbnail_path: 'https://engageya.live/wp-content/uploads/2019/05/images.png', domain: 'domain.test', @@ -159,8 +159,80 @@ describe('engageya adapter', function () { imageWidth: 300, imageHeight: 250, ireqId: '1d236f7890b', - pbtypeId: 1 + viewPxl: '//view.pixel', }; + + nativeResponse = { + ...recsResponse, + pbtypeId: 1, + } + + bannerResponse = { + ...recsResponse, + pbtypeId: 2, + widget: { + additionalData: '{"css":".eng_tag_ttl{display:block!important}"}' + }, + } + }) + + it('should return empty array if no response', function () { + const result = spec.interpretResponse({}, []) + expect(result).to.be.an('array').that.is.empty + }); + + it('should return empty array if no valid bids', function () { + let response = { + recs: [], + imageWidth: 300, + imageHeight: 250, + ireqId: '1d236f7890b', + pbtypeId: 2, + viewPxl: '//view.pixel', + }; + let request = spec.buildRequests(bidRequests)[0]; + const result = spec.interpretResponse({ body: response }, request) + expect(result).to.be.an('array').that.is.empty + }); + + it('should interpret native response', function () { + let expectedResult = [ + { + requestId: '1d236f7890b', + cpm: 0.0520, + width: 300, + height: 250, + netRevenue: true, + currency: 'USD', + creativeId: '', + ttl: 360, + meta: { + advertiserDomains: ['domain.test'] + }, + native: { + title: 'Test title', + body: '', + image: { + url: 'https://engageya.live/wp-content/uploads/2019/05/images.png', + width: 300, + height: 250 + }, + privacyLink: '', + clickUrl: '//click.test', + displayUrl: '//url.test', + cta: '', + sponsoredBy: 'Test displayName', + impressionTrackers: ['//impression.test', '//view.test', '//view.pixel'], + }, + } + ]; + let request = spec.buildRequests(bidRequests)[0]; + let result = spec.interpretResponse({ body: nativeResponse }, request); + expect(result).to.deep.equal(expectedResult); + }); + + it('should interpret native response - without pecpm', function () { + delete nativeResponse.recs[0].pecpm; let expectedResult = [ { requestId: '1d236f7890b', @@ -187,81 +259,76 @@ describe('engageya adapter', function () { displayUrl: '//url.test', cta: '', sponsoredBy: 'Test displayName', - impressionTrackers: ['//impression.test', '//view.test'], + impressionTrackers: ['//impression.test', '//view.test', '//view.pixel'], }, } ]; let request = spec.buildRequests(bidRequests)[0]; - let result = spec.interpretResponse({ body: serverResponse }, request); + let result = spec.interpretResponse({ body: nativeResponse }, request); expect(result).to.deep.equal(expectedResult); }); - it('should interpret display response', function () { - let serverResponse = { - recs: [ - { - ecpm: 0.0920, - postId: '', - thumbnail_path: 'https://engageya.live/wp-content/uploads/2019/05/images.png', - domain: 'domain.test', + it('should interpret native response - without trackers', function () { + delete nativeResponse.recs[0].trackers; + let expectedResult = [ + { + requestId: '1d236f7890b', + cpm: 0.0520, + width: 300, + height: 250, + netRevenue: true, + currency: 'USD', + creativeId: '', + ttl: 360, + meta: { + advertiserDomains: ['domain.test'] + }, + native: { title: 'Test title', + body: '', + image: { + url: 'https://engageya.live/wp-content/uploads/2019/05/images.png', + width: 300, + height: 250 + }, + privacyLink: '', clickUrl: '//click.test', - url: '//url.test', - displayName: 'Test displayName', - trackers: { - impressionPixels: ['//impression.test'], - viewPixels: ['//view.test'], - } - } - ], - imageWidth: 300, - imageHeight: 250, - ireqId: '1d236f7890b', - pbtypeId: 2, - widget: { - additionalData: '{"css":".eng_tag_ttl{display:block!important}"}' + displayUrl: '//url.test', + cta: '', + sponsoredBy: 'Test displayName', + impressionTrackers: ['//view.pixel'], + }, } - }; + ]; + let request = spec.buildRequests(bidRequests)[0]; + let result = spec.interpretResponse({ body: nativeResponse }, request); + expect(result).to.deep.equal(expectedResult); + }); + + it('should interpret display response', function () { let expectedResult = [ { requestId: '1d236f7890b', - cpm: 0.0920, + cpm: 0.0520, width: 300, height: 250, - netRevenue: false, + netRevenue: true, currency: 'USD', creativeId: '', ttl: 360, meta: { advertiserDomains: ['domain.test'] }, - ad: ``, + ad: ``, } ]; let request = spec.buildRequests(bidRequests)[0]; - let result = spec.interpretResponse({ body: serverResponse }, request); + let result = spec.interpretResponse({ body: bannerResponse }, request); expect(result).to.deep.equal(expectedResult); }); - it('should interpret display response without title', function () { - let serverResponse = { - recs: [ - { - ecpm: 0.0920, - postId: '', - thumbnail_path: 'https://engageya.live/wp-content/uploads/2019/05/images.png', - domain: 'domain.test', - title: ' ', - clickUrl: '//click.test', - url: '//url.test', - displayName: 'Test displayName', - } - ], - imageWidth: 300, - imageHeight: 250, - ireqId: '1d236f7890b', - pbtypeId: 2, - }; + it('should interpret display response - without pecpm', function () { + delete bannerResponse.recs[0].pecpm; let expectedResult = [ { requestId: '1d236f7890b', @@ -275,11 +342,80 @@ describe('engageya adapter', function () { meta: { advertiserDomains: ['domain.test'] }, - ad: `
`, + ad: ``, + } + ]; + let request = spec.buildRequests(bidRequests)[0]; + let result = spec.interpretResponse({ body: bannerResponse }, request); + expect(result).to.deep.equal(expectedResult); + }); + + it('should interpret display response - without title', function () { + bannerResponse.recs[0].title = ' '; + let expectedResult = [ + { + requestId: '1d236f7890b', + cpm: 0.0520, + width: 300, + height: 250, + netRevenue: true, + currency: 'USD', + creativeId: '', + ttl: 360, + meta: { + advertiserDomains: ['domain.test'] + }, + ad: `
`, + } + ]; + let request = spec.buildRequests(bidRequests)[0]; + let result = spec.interpretResponse({ body: bannerResponse }, request); + expect(result).to.deep.equal(expectedResult); + }); + + it('should interpret display response - without widget additional data', function () { + bannerResponse.widget.additionalData = null; + let expectedResult = [ + { + requestId: '1d236f7890b', + cpm: 0.0520, + width: 300, + height: 250, + netRevenue: true, + currency: 'USD', + creativeId: '', + ttl: 360, + meta: { + advertiserDomains: ['domain.test'] + }, + ad: ``, + } + ]; + let request = spec.buildRequests(bidRequests)[0]; + let result = spec.interpretResponse({ body: bannerResponse }, request); + expect(result).to.deep.equal(expectedResult); + }); + + it('should interpret display response - without trackers', function () { + bannerResponse.recs[0].trackers = null; + let expectedResult = [ + { + requestId: '1d236f7890b', + cpm: 0.0520, + width: 300, + height: 250, + netRevenue: true, + currency: 'USD', + creativeId: '', + ttl: 360, + meta: { + advertiserDomains: ['domain.test'] + }, + ad: ``, } ]; let request = spec.buildRequests(bidRequests)[0]; - let result = spec.interpretResponse({ body: serverResponse }, request); + let result = spec.interpretResponse({ body: bannerResponse }, request); expect(result).to.deep.equal(expectedResult); }); }) diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index c6104a23a1c..117208f9d00 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -328,26 +328,25 @@ describe('E-Planning Adapter', function () { describe('buildRequests', function () { let bidRequests = [validBid]; let sandbox; + let getWindowSelfStub; + let innerWidth; beforeEach(() => { sandbox = sinon.sandbox.create(); + getWindowSelfStub = sandbox.stub(utils, 'getWindowSelf'); + getWindowSelfStub.returns(createWindow(800)); }); afterEach(() => { sandbox.restore(); }); - const createWindow = () => { + const createWindow = (innerWidth) => { const win = {}; win.self = win; - win.innerWidth = 1025; + win.innerWidth = innerWidth; return win; }; - function setupSingleWindow(sandBox) { - const win = createWindow(); - sandBox.stub(utils, 'getWindowSelf').returns(win); - } - it('should create the url correctly', function () { const url = spec.buildRequests(bidRequests, bidderRequest).url; expect(url).to.equal('https://pbjs.e-planning.net/pbjs/1/' + CI + '/1/localhost/ROS'); @@ -516,7 +515,8 @@ describe('E-Planning Adapter', function () { it('should return the e parameter with a value according to the sizes in order corresponding to the desktop priority list of the ad units', function () { let bidRequestsPrioritySizes = [validBidExistingSizesInPriorityListForDesktop]; - setupSingleWindow(sandbox); + // overwrite default innerWdith for tests with a larger one we consider "Desktop" or NOT Mobile + getWindowSelfStub.returns(createWindow(1025)); const e = spec.buildRequests(bidRequestsPrioritySizes, bidderRequest).data.e; expect(e).to.equal('300x250_0:300x250,300x600,970x250'); }); diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index a5b4bd2a03f..81f4ecec074 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -98,6 +98,19 @@ describe('freewheelSSP BidAdapter Test', () => { 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'example.com', + 'sid': '0', + 'hp': 1, + 'rid': 'bidrequestid', + 'domain': 'example.com' + } + ] + } } ]; @@ -112,6 +125,12 @@ describe('freewheelSSP BidAdapter Test', () => { expect(payload.playerSize).to.equal('300x600'); }); + it('should return a properly formatted request with schain defined', function () { + const request = spec.buildRequests(bidRequests); + const payload = request[0].data; + expect(payload.schain).to.deep.equal(bidRequests[0].schain) + }); + it('sends bid request to ENDPOINT via GET', () => { const request = spec.buildRequests(bidRequests); expect(request[0].url).to.contain(ENDPOINT); diff --git a/test/spec/modules/glimpseBidAdapter_spec.js b/test/spec/modules/glimpseBidAdapter_spec.js index 98e1a1bb451..c139a7ab8d3 100644 --- a/test/spec/modules/glimpseBidAdapter_spec.js +++ b/test/spec/modules/glimpseBidAdapter_spec.js @@ -1,3 +1,4 @@ +import { BANNER } from '../../../src/mediaTypes' import { expect } from 'chai' import { newBidder } from 'src/adapters/bidderFactory.js' import { spec } from 'modules/glimpseBidAdapter.js' @@ -79,6 +80,20 @@ function getDeepCopy(object) { describe('GlimpseProtocolAdapter', () => { const glimpseAdapter = newBidder(spec) + describe('spec', () => { + it('Has defined the glimpse gvlid', () => { + expect(spec.gvlid).to.equal(1012) + }) + + it('Has defined glimpse as the bidder', () => { + expect(spec.code).to.equal('glimpse') + }) + + it('Has defined valid mediaTypes', () => { + expect(spec.supportedMediaTypes).to.deep.equal([BANNER]) + }) + }) + describe('Inherited functions', () => { it('Functions exist and are valid types', () => { expect(glimpseAdapter.callBids).to.exist.and.to.be.a('function') @@ -163,6 +178,24 @@ describe('GlimpseProtocolAdapter', () => { const bidRequests = [getBidRequest()] const bidderRequest = getBidderRequest() + it('Adds a demo flag', () => { + const request = spec.buildRequests(bidRequests, bidderRequest) + const payload = JSON.parse(request.data) + expect(payload.data.demo).to.be.false + }) + + it('Adds an account id', () => { + const request = spec.buildRequests(bidRequests, bidderRequest) + const payload = JSON.parse(request.data) + expect(payload.data.account).to.equal(-1) + }) + + it('Adds a demand provider', () => { + const request = spec.buildRequests(bidRequests, bidderRequest) + const payload = JSON.parse(request.data) + expect(payload.data.demand).to.equal('glimpse') + }) + it('Adds GDPR consent', () => { const request = spec.buildRequests(bidRequests, bidderRequest) const payload = JSON.parse(request.data) diff --git a/test/spec/modules/goldbachBidAdapter_spec.js b/test/spec/modules/goldbachBidAdapter_spec.js new file mode 100644 index 00000000000..459cda7958f --- /dev/null +++ b/test/spec/modules/goldbachBidAdapter_spec.js @@ -0,0 +1,1359 @@ +import { expect } from 'chai'; +import { spec } from 'modules/goldbachBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as bidderFactory from 'src/adapters/bidderFactory.js'; +import { auctionManager } from 'src/auctionManager.js'; +import { deepClone } from 'src/utils.js'; +import { config } from 'src/config.js'; + +const ENDPOINT = 'https://ib.adnxs.com/ut/v3/prebid'; +const PRICING_ENDPOINT = 'https://templates.da-services.ch/01_universal/burda_prebid/1.0/json/sizeCPMMapping.json'; + +describe('GoldbachXandrAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'goldbach', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true when required params found', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'member': '1234', + 'invCode': 'ABCD' + }; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placementId': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let getAdUnitsStub; + let bidRequests = [ + { + 'bidder': 'goldbach', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '04f2659e-c005-4eb1-a57c-fa93145e3843' + } + ]; + + beforeEach(function() { + getAdUnitsStub = sinon.stub(auctionManager, 'getAdUnits').callsFake(function() { + return []; + }); + }); + + afterEach(function() { + getAdUnitsStub.restore(); + }); + + it('should parse out private sizes', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + privateSizes: [300, 250] + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].private_sizes).to.exist; + expect(payload.tags[0].private_sizes).to.deep.equal([{width: 300, height: 250}]); + }); + + it('should add publisher_id in request', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + publisherId: '1231234' + } + }); + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].publisher_id).to.exist; + expect(payload.tags[0].publisher_id).to.deep.equal(1231234); + expect(payload.publisher_id).to.exist; + expect(payload.publisher_id).to.deep.equal(1231234); + }) + + it('should add source and verison to the tag', function () { + const request = spec.buildRequests(bidRequests)[1]; + const payload = JSON.parse(request.data); + expect(payload.sdk).to.exist; + expect(payload.sdk).to.deep.equal({ + source: 'pbjs', + version: '$prebid.version$' + }); + }); + + it('should populate the ad_types array on all requests', function () { + let adUnits = [{ + code: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [{ + bidder: 'goldbach', + params: { + placementId: '10433394' + } + }], + transactionId: '04f2659e-c005-4eb1-a57c-fa93145e3843' + }]; + + ['banner', 'video', 'native'].forEach(type => { + getAdUnitsStub.callsFake(function(...args) { + return adUnits; + }); + + const bidRequest = Object.assign({}, bidRequests[0]); + bidRequest.mediaTypes = {}; + bidRequest.mediaTypes[type] = {}; + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.deep.equal([type]); + + if (type === 'banner') { + delete adUnits[0].mediaTypes; + } + }); + }); + + it('should not populate the ad_types array when adUnit.mediaTypes is undefined', function() { + const bidRequest = Object.assign({}, bidRequests[0]); + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.not.exist; + }); + + it('should populate the ad_types array on outstream requests', function () { + const bidRequest = Object.assign({}, bidRequests[0]); + bidRequest.mediaTypes = {}; + bidRequest.mediaTypes.video = {context: 'outstream'}; + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.deep.equal(['video']); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests)[1]; + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('sends bid request to ENDPOINT via GET', function () { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.url).to.equal(PRICING_ENDPOINT); + expect(request.method).to.equal('GET'); + }); + + it('should attach valid video params to the tag', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + video: { + id: 123, + minduration: 100, + foobar: 'invalid' + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + id: 123, + minduration: 100 + }); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + + it('should include ORTB video values when video params were not set', function() { + let bidRequest = deepClone(bidRequests[0]); + bidRequest.params = { + placementId: '1234235', + video: { + skippable: true, + playback_method: ['auto_play_sound_off', 'auto_play_sound_unknown'], + context: 'outstream' + } + }; + bidRequest.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'outstream', + mimes: ['video/mp4'], + skip: 0, + minduration: 5, + api: [1, 5, 6], + playbackmethod: [2, 4] + } + }; + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].video).to.deep.equal({ + minduration: 5, + playback_method: 2, + skippable: true, + context: 4 + }); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 4]) + }); + + it('should add video property when adUnit includes a renderer', function () { + const videoData = { + mediaTypes: { + video: { + context: 'outstream', + mimes: ['video/mp4'] + } + }, + params: { + placementId: '10433394', + video: { + skippable: true, + playback_method: ['auto_play_sound_off'] + } + } + }; + + let bidRequest1 = deepClone(bidRequests[0]); + bidRequest1 = Object.assign({}, bidRequest1, videoData, { + renderer: { + url: 'https://test.renderer.url', + render: function () {} + } + }); + + let bidRequest2 = deepClone(bidRequests[0]); + bidRequest2.adUnitCode = 'adUnit_code_2'; + bidRequest2 = Object.assign({}, bidRequest2, videoData); + + const request = spec.buildRequests([bidRequest1, bidRequest2])[1]; + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + skippable: true, + playback_method: 2, + custom_renderer_present: true + }); + expect(payload.tags[1].video).to.deep.equal({ + skippable: true, + playback_method: 2 + }); + }); + + it('should attach valid user params to the tag', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + user: { + externalUid: '123', + segments: [123, { id: 987, value: 876 }], + foobar: 'invalid' + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.user).to.exist; + expect(payload.user).to.deep.equal({ + external_uid: '123', + segments: [{id: 123}, {id: 987, value: 876}] + }); + }); + + it('should attach reserve param when either bid param or getFloor function exists', function () { + let getFloorResponse = { currency: 'USD', floor: 3 }; + let request, payload = null; + let bidRequest = deepClone(bidRequests[0]); + + // 1 -> reserve not defined, getFloor not defined > empty + request = spec.buildRequests([bidRequest])[1]; + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.not.exist; + + // 2 -> reserve is defined, getFloor not defined > reserve is used + bidRequest.params = { + 'placementId': '10433394', + 'reserve': 0.5 + }; + request = spec.buildRequests([bidRequest])[1]; + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.exist.and.to.equal(0.5); + + // 3 -> reserve is defined, getFloor is defined > getFloor is used + bidRequest.getFloor = () => getFloorResponse; + + request = spec.buildRequests([bidRequest])[1]; + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.exist.and.to.equal(3); + }); + + it('should duplicate adpod placements into batches and set correct maxduration', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 300, + durationRangeSec: [15, 30], + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload1 = JSON.parse(request[0].data); + const payload2 = JSON.parse(request[1].data); + + // 300 / 15 = 20 total + expect(payload1.tags.length).to.equal(15); + expect(payload2.tags.length).to.equal(5); + + expect(payload1.tags[0]).to.deep.equal(payload1.tags[1]); + expect(payload1.tags[0].video.maxduration).to.equal(30); + + expect(payload2.tags[0]).to.deep.equal(payload1.tags[1]); + expect(payload2.tags[0].video.maxduration).to.equal(30); + }); + + it('should round down adpod placements when numbers are uneven', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 123, + durationRangeSec: [45], + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.tags.length).to.equal(2); + }); + + it('should duplicate adpod placements when requireExactDuration is set', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 300, + durationRangeSec: [15, 30], + requireExactDuration: true, + } + } + } + ); + + // 20 total placements with 15 max impressions = 2 requests + const request = spec.buildRequests([bidRequest])[1]; + expect(request.length).to.equal(2); + + // 20 spread over 2 requests = 15 in first request, 5 in second + const payload1 = JSON.parse(request[0].data); + const payload2 = JSON.parse(request[1].data); + expect(payload1.tags.length).to.equal(15); + expect(payload2.tags.length).to.equal(5); + + // 10 placements should have max/min at 15 + // 10 placemenst should have max/min at 30 + const payload1tagsWith15 = payload1.tags.filter(tag => tag.video.maxduration === 15); + const payload1tagsWith30 = payload1.tags.filter(tag => tag.video.maxduration === 30); + expect(payload1tagsWith15.length).to.equal(10); + expect(payload1tagsWith30.length).to.equal(5); + + // 5 placemenst with min/max at 30 were in the first request + // so 5 remaining should be in the second + const payload2tagsWith30 = payload2.tags.filter(tag => tag.video.maxduration === 30); + expect(payload2tagsWith30.length).to.equal(5); + }); + + it('should set durations for placements when requireExactDuration is set and numbers are uneven', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 105, + durationRangeSec: [15, 30, 60], + requireExactDuration: true, + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.tags.length).to.equal(7); + + const tagsWith15 = payload.tags.filter(tag => tag.video.maxduration === 15); + const tagsWith30 = payload.tags.filter(tag => tag.video.maxduration === 30); + const tagsWith60 = payload.tags.filter(tag => tag.video.maxduration === 60); + expect(tagsWith15.length).to.equal(3); + expect(tagsWith30.length).to.equal(3); + expect(tagsWith60.length).to.equal(1); + }); + + it('should break adpod request into batches', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 225, + durationRangeSec: [5], + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload1 = JSON.parse(request[0].data); + const payload2 = JSON.parse(request[1].data); + const payload3 = JSON.parse(request[2].data); + + expect(payload1.tags.length).to.equal(15); + expect(payload2.tags.length).to.equal(15); + expect(payload3.tags.length).to.equal(15); + }); + + // it('should contain hb_source value for adpod', function() { + // let bidRequest = Object.assign({}, + // bidRequests[0], + // { + // params: { placementId: '14542875' } + // }, + // { + // mediaTypes: { + // video: { + // context: 'adpod', + // playerSize: [640, 480], + // adPodDurationSec: 300, + // durationRangeSec: [15, 30], + // } + // } + // } + // ); + // const request = spec.buildRequests([bidRequest])[1]; + // const payload = JSON.parse(request.data); + // expect(payload.tags[0].hb_source).to.deep.equal(7); + // }); + + it('should contain hb_source value for other media', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'banner', + params: { + sizes: [[300, 250], [300, 600]], + placementId: 13144370 + } + } + ); + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + + it('adds brand_category_exclusion to request when set', function() { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon + .stub(config, 'getConfig') + .withArgs('adpod.brandCategoryExclusion') + .returns(true); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.brand_category_uniqueness).to.equal(true); + + config.getConfig.restore(); + }); + + it('should attach native params to the request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + title: {required: true}, + body: {required: true}, + body2: {required: true}, + image: {required: true, sizes: [100, 100]}, + icon: {required: true}, + cta: {required: false}, + rating: {required: true}, + sponsoredBy: {required: true}, + privacyLink: {required: true}, + displayUrl: {required: true}, + address: {required: true}, + downloads: {required: true}, + likes: {required: true}, + phone: {required: true}, + price: {required: true}, + salePrice: {required: true} + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].native.layouts[0]).to.deep.equal({ + title: {required: true}, + description: {required: true}, + desc2: {required: true}, + main_image: {required: true, sizes: [{ width: 100, height: 100 }]}, + icon: {required: true}, + ctatext: {required: false}, + rating: {required: true}, + sponsored_by: {required: true}, + privacy_link: {required: true}, + displayurl: {required: true}, + address: {required: true}, + downloads: {required: true}, + likes: {required: true}, + phone: {required: true}, + price: {required: true}, + saleprice: {required: true}, + privacy_supported: true + }); + expect(payload.tags[0].hb_source).to.equal(1); + }); + + it('should always populated tags[].sizes with 1,1 for native if otherwise not defined', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + image: { required: true } + } + } + ); + bidRequest.sizes = [[150, 100], [300, 250]]; + + let request = spec.buildRequests([bidRequest])[1]; + let payload = JSON.parse(request.data); + expect(payload.tags[0].sizes).to.deep.equal([{width: 150, height: 100}, {width: 300, height: 250}]); + + delete bidRequest.sizes; + + request = spec.buildRequests([bidRequest])[1]; + payload = JSON.parse(request.data); + + expect(payload.tags[0].sizes).to.deep.equal([{width: 1, height: 1}]); + }); + + it('should convert keyword params to proper form and attaches to request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + keywords: { + single: 'val', + singleArr: ['val'], + singleArrNum: [5], + multiValMixed: ['value1', 2, 'value3'], + singleValNum: 123, + emptyStr: '', + emptyArr: [''], + badValue: {'foo': 'bar'} // should be dropped + } + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].keywords).to.deep.equal([{ + 'key': 'single', + 'value': ['val'] + }, { + 'key': 'singleArr', + 'value': ['val'] + }, { + 'key': 'singleArrNum', + 'value': ['5'] + }, { + 'key': 'multiValMixed', + 'value': ['value1', '2', 'value3'] + }, { + 'key': 'singleValNum', + 'value': ['123'] + }, { + 'key': 'emptyStr' + }, { + 'key': 'emptyArr' + }]); + }); + + it('should add payment rules to the request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + usePaymentRule: true + } + } + ); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].use_pmt_rule).to.equal(true); + }); + + it('should add gpid to the request', function () { + let testGpid = '/12345/my-gpt-tag-0'; + let bidRequest = deepClone(bidRequests[0]); + bidRequest.ortb2Imp = { ext: { data: { pbadslot: testGpid } } }; + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.tags[0].gpid).to.exist.and.equal(testGpid) + }); + + it('should add gdpr consent information to the request', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'goldbach', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true, + addtlConsent: '1~7.12.35.62.66.70.89.93.108' + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest)[1]; + expect(request.options).to.deep.equal({withCredentials: true}); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_consent).to.exist; + expect(payload.gdpr_consent.consent_string).to.exist.and.to.equal(consentString); + expect(payload.gdpr_consent.consent_required).to.exist.and.to.be.true; + expect(payload.gdpr_consent.addtl_consent).to.exist.and.to.deep.equal([7, 12, 35, 62, 66, 70, 89, 93, 108]); + }); + + it('should add us privacy string to payload', function() { + let consentString = '1YA-'; + let bidderRequest = { + 'bidderCode': 'goldbach', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': consentString + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest)[1]; + const payload = JSON.parse(request.data); + + expect(payload.us_privacy).to.exist; + expect(payload.us_privacy).to.exist.and.to.equal(consentString); + }); + + it('supports sending hybrid mobile app parameters', function () { + let appRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + app: { + id: 'B1O2W3M4AN.com.prebid.webview', + geo: { + lat: 40.0964439, + lng: -75.3009142 + }, + device_id: { + idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', // Apple advertising identifier + aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', // Android advertising identifier + md5udid: '5756ae9022b2ea1e47d84fead75220c8', // MD5 hash of the ANDROID_ID + sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', // SHA1 hash of the ANDROID_ID + windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' // Windows advertising identifier + } + } + } + } + ); + const request = spec.buildRequests([appRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.app).to.exist; + expect(payload.app).to.deep.equal({ + appid: 'B1O2W3M4AN.com.prebid.webview' + }); + expect(payload.device.device_id).to.exist; + expect(payload.device.device_id).to.deep.equal({ + aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', + idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', + md5udid: '5756ae9022b2ea1e47d84fead75220c8', + sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', + windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' + }); + expect(payload.device.geo).to.exist; + expect(payload.device.geo).to.deep.equal({ + lat: 40.0964439, + lng: -75.3009142 + }); + }); + + it('should add referer info to payload', function () { + const bidRequest = Object.assign({}, bidRequests[0]) + const bidderRequest = { + refererInfo: { + referer: 'https://example.com/page.html', + reachedTop: true, + numIframes: 2, + stack: [ + 'https://example.com/page.html', + 'https://example.com/iframe1.html', + 'https://example.com/iframe2.html' + ] + } + } + const request = spec.buildRequests([bidRequest], bidderRequest)[1]; + const payload = JSON.parse(request.data); + + expect(payload.referrer_detection).to.exist; + expect(payload.referrer_detection).to.deep.equal({ + rd_ref: 'https%3A%2F%2Fexample.com%2Fpage.html', + rd_top: true, + rd_ifs: 2, + rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',') + }); + }); + + it('should populate schain if available', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'blob.com', + 'sid': '001', + 'hp': 1 + } + ] + } + }); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'blob.com', + 'sid': '001', + 'hp': 1 + } + ] + }); + }); + + it('should populate coppa if set in config', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + + expect(payload.user.coppa).to.equal(true); + + config.getConfig.restore(); + }); + + it('should set the X-Is-Test customHeader if test flag is enabled', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('apn_test') + .returns(true); + + const request = spec.buildRequests([bidRequest])[1]; + expect(request.options.customHeaders).to.deep.equal({'X-Is-Test': 1}); + + config.getConfig.restore(); + }); + + it('should always set withCredentials: true on the request.options', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + const request = spec.buildRequests([bidRequest])[1]; + expect(request.options.withCredentials).to.equal(true); + }); + + it('should set simple domain variant if purpose 1 consent is not given', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'goldbach', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true, + apiVersion: 2, + vendorData: { + purpose: { + consents: { + 1: false + } + } + } + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest)[1]; + expect(request.url).to.equal('https://ib.adnxs-simple.com/ut/v3/prebid'); + }); + + it('should populate eids when supported userIds are available', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + userId: { + tdid: 'sample-userid', + uid2: { id: 'sample-uid2-value' }, + criteoId: 'sample-criteo-userid', + netId: 'sample-netId-userid', + idl_env: 'sample-idl-userid', + flocId: { + id: 'sample-flocid-value', + version: 'chrome.1.0' + } + } + }); + + const request = spec.buildRequests([bidRequest])[1]; + const payload = JSON.parse(request.data); + expect(payload.eids).to.deep.include({ + source: 'adserver.org', + id: 'sample-userid', + rti_partner: 'TDID' + }); + + expect(payload.eids).to.deep.include({ + source: 'criteo.com', + id: 'sample-criteo-userid', + }); + + expect(payload.eids).to.deep.include({ + source: 'chrome.com', + id: 'sample-flocid-value' + }); + + expect(payload.eids).to.deep.include({ + source: 'netid.de', + id: 'sample-netId-userid', + }); + + expect(payload.eids).to.deep.include({ + source: 'liveramp.com', + id: 'sample-idl-userid' + }); + + expect(payload.eids).to.deep.include({ + source: 'uidapi.com', + id: 'sample-uid2-value', + rti_partner: 'UID2' + }); + }); + + it('should populate iab_support object at the root level if omid support is detected', function () { + // with bid.params.frameworks + let bidRequest_A = Object.assign({}, bidRequests[0], { + params: { + frameworks: [1, 2, 5, 6], + video: { + frameworks: [1, 2, 5, 6] + } + } + }); + let request = spec.buildRequests([bidRequest_A])[1]; + let payload = JSON.parse(request.data); + expect(payload.iab_support).to.be.an('object'); + expect(payload.iab_support).to.deep.equal({ + omidpn: 'Appnexus', + omidpv: '$prebid.version$' + }); + expect(payload.tags[0].banner_frameworks).to.be.an('array'); + expect(payload.tags[0].banner_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video_frameworks).to.be.an('array'); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video.frameworks).to.not.exist; + + // without bid.params.frameworks + const bidRequest_B = Object.assign({}, bidRequests[0]); + request = spec.buildRequests([bidRequest_B])[1]; + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; + + // with video.frameworks but it is not an array + const bidRequest_C = Object.assign({}, bidRequests[0], { + params: { + video: { + frameworks: "'1', '2', '3', '6'" + } + } + }); + request = spec.buildRequests([bidRequest_C])[1]; + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; + }); + }) + + describe('interpretResponse', function () { + let bfStub; + before(function() { + bfStub = sinon.stub(bidderFactory, 'getIabSubCategory'); + }); + + after(function() { + bfStub.restore(); + }); + + let response = { + 'version': '3.0.0', + 'tags': [ + { + 'uuid': '3db3773286ee59', + 'tag_id': 10433394, + 'auction_id': '4534722592064951574', + 'nobid': false, + 'no_ad_url': 'https://lax1-ib.adnxs.com/no-ad', + 'timeout_ms': 10000, + 'ad_profile_id': 27079, + 'ads': [ + { + 'content_source': 'rtb', + 'ad_type': 'banner', + 'buyer_member_id': 958, + 'creative_id': 29681110, + 'media_type_id': 1, + 'media_subtype_id': 1, + 'cpm': 0.5, + 'cpm_publisher_currency': 0.5, + 'publisher_currency_code': '$', + 'client_initiated_ad_counting': true, + 'viewability': { + 'config': '' + }, + 'rtb': { + 'banner': { + 'content': '', + 'width': 300, + 'height': 250 + }, + 'trackers': [ + { + 'impression_urls': [ + 'https://lax1-ib.adnxs.com/impression' + ], + 'video_events': {} + } + ] + } + } + ] + } + ] + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + 'requestId': '3db3773286ee59', + 'cpm': 0.5, + 'creativeId': 29681110, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'netRevenue': true, + 'adUnitCode': 'code', + 'appnexus': { + 'buyerMemberId': 958 + } + } + ]; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('handles nobid responses', function () { + let response = { + 'version': '0.0.1', + 'tags': [{ + 'uuid': '84ab500420319d', + 'tag_id': 5976557, + 'auction_id': '297492697822162468', + 'nobid': true + }] + }; + let bidderRequest; + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result.length).to.equal(0); + }); + + it('handles outstream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'content': '' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastXml'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles instream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'asset_url': 'https://sample.vastURL.com/here/vid' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'instream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles adpod responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'brand_category_id': 10, + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'asset_url': 'https://sample.vastURL.com/here/adpod', + 'duration_ms': 30000, + } + }, + 'viewability': { + 'config': '' + } + }] + }] + }; + + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'adpod' + } + } + }] + }; + bfStub.returns('1'); + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0].video.context).to.equal('adpod'); + expect(result[0].video.durationSeconds).to.equal(30); + }); + + it('handles native responses', function () { + let response1 = deepClone(response); + response1.tags[0].ads[0].ad_type = 'native'; + response1.tags[0].ads[0].rtb.native = { + 'title': 'Native Creative', + 'desc': 'Cool description great stuff', + 'desc2': 'Additional body text', + 'ctatext': 'Do it', + 'sponsored': 'AppNexus', + 'icon': { + 'width': 0, + 'height': 0, + 'url': 'https://cdn.adnxs.com/icon.png' + }, + 'main_img': { + 'width': 2352, + 'height': 1516, + 'url': 'https://cdn.adnxs.com/img.png' + }, + 'link': { + 'url': 'https://www.appnexus.com', + 'fallback_url': '', + 'click_trackers': ['https://nym1-ib.adnxs.com/click'] + }, + 'impression_trackers': ['https://example.com'], + 'rating': '5', + 'displayurl': 'https://AppNexus.com/?url=display_url', + 'likes': '38908320', + 'downloads': '874983', + 'price': '9.99', + 'saleprice': 'FREE', + 'phone': '1234567890', + 'address': '28 W 23rd St, New York, NY 10010', + 'privacy_link': 'https://appnexus.com/?url=privacy_url', + 'javascriptTrackers': '' + }; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + + let result = spec.interpretResponse({ body: response1 }, {bidderRequest}); + expect(result[0].native.title).to.equal('Native Creative'); + expect(result[0].native.body).to.equal('Cool description great stuff'); + expect(result[0].native.cta).to.equal('Do it'); + expect(result[0].native.image.url).to.equal('https://cdn.adnxs.com/img.png'); + }); + + it('supports configuring outstream renderers', function () { + const outstreamResponse = deepClone(response); + outstreamResponse.tags[0].ads[0].rtb.video = {}; + outstreamResponse.tags[0].ads[0].renderer_url = 'renderer.js'; + + const bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + renderer: { + options: { + adText: 'configured' + } + }, + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + }; + + const result = spec.interpretResponse({ body: outstreamResponse }, {bidderRequest}); + expect(result[0].renderer.config).to.deep.equal( + bidderRequest.bids[0].renderer.options + ); + }); + + it('should add deal_priority and deal_code', function() { + let responseWithDeal = deepClone(response); + responseWithDeal.tags[0].ads[0].ad_type = 'video'; + responseWithDeal.tags[0].ads[0].deal_priority = 5; + responseWithDeal.tags[0].ads[0].deal_code = '123'; + responseWithDeal.tags[0].ads[0].rtb.video = { + duration_ms: 1500, + player_width: 640, + player_height: 340, + }; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'adpod' + } + } + }] + } + let result = spec.interpretResponse({ body: responseWithDeal }, {bidderRequest}); + expect(Object.keys(result[0].appnexus)).to.include.members(['buyerMemberId', 'dealPriority', 'dealCode']); + expect(result[0].video.dealTier).to.equal(5); + }); + + it('should add advertiser id', function() { + let responseAdvertiserId = deepClone(response); + responseAdvertiserId.tags[0].ads[0].advertiser_id = '123'; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['advertiserId']); + }); + + it('should add advertiserDomains', function() { + let responseAdvertiserId = deepClone(response); + responseAdvertiserId.tags[0].ads[0].adomain = ['123']; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['advertiserDomains']); + expect(Object.keys(result[0].meta.advertiserDomains)).to.deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/gptPreAuction_spec.js b/test/spec/modules/gptPreAuction_spec.js index 3e8dbfe8d92..9e81aea80d1 100644 --- a/test/spec/modules/gptPreAuction_spec.js +++ b/test/spec/modules/gptPreAuction_spec.js @@ -197,15 +197,69 @@ describe('GPT pre-auction module', () => { code: 'slotCode3', }]; + // first two adUnits directly pass in pbadslot => gpid is same const expectedAdUnits = [{ code: 'adUnit1', - ortb2Imp: { ext: { data: { pbadslot: '12345' } } } - }, { + ortb2Imp: { + ext: { + data: { + pbadslot: '12345' + }, + gpid: '12345' + } + } + }, + // second adunit + { code: 'slotCode1', - ortb2Imp: { ext: { data: { pbadslot: '67890', adserver: { name: 'gam', adslot: 'slotCode1' } } } } + ortb2Imp: { + ext: { + data: { + pbadslot: '67890', + adserver: { + name: 'gam', + adslot: 'slotCode1' + } + }, + gpid: '67890' + } + } }, { code: 'slotCode3', - ortb2Imp: { ext: { data: { pbadslot: 'slotCode3', adserver: { name: 'gam', adslot: 'slotCode3' } } } } + ortb2Imp: { + ext: { + data: { + pbadslot: 'slotCode3', + adserver: { + name: 'gam', + adslot: 'slotCode3' + } + }, + gpid: 'slotCode3' + } + } + }]; + + window.googletag.pubads().setSlots(testSlots); + runMakeBidRequests(testAdUnits); + expect(returnedAdUnits).to.deep.equal(expectedAdUnits); + }); + + it('should not apply gpid if pbadslot was set by adUnitCode', () => { + const testAdUnits = [{ + code: 'noMatchCode', + }]; + + // first two adUnits directly pass in pbadslot => gpid is same + const expectedAdUnits = [{ + code: 'noMatchCode', + ortb2Imp: { + ext: { + data: { + pbadslot: 'noMatchCode' + }, + } + } }]; window.googletag.pubads().setSlots(testSlots); diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index dfd7db7d922..93b863bf116 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -152,6 +152,12 @@ describe('gumgumAdapter', function () { const zoneParam = { 'zone': '123a' }; const pubIdParam = { 'pubId': 123 }; + it('should set aun if the adUnitCode is available', function () { + const request = { ...bidRequests[0] }; + const bidRequest = spec.buildRequests([request])[0]; + expect(bidRequest.data.aun).to.equal(bidRequests[0].adUnitCode); + }); + it('should set pubId param if found', function () { const request = { ...bidRequests[0], params: pubIdParam }; const bidRequest = spec.buildRequests([request])[0]; diff --git a/test/spec/modules/imRtdProvider_spec.js b/test/spec/modules/imRtdProvider_spec.js index 58410dc0e38..e3365e8eaf2 100644 --- a/test/spec/modules/imRtdProvider_spec.js +++ b/test/spec/modules/imRtdProvider_spec.js @@ -1,6 +1,7 @@ import { imRtdSubmodule, storage, + getBidderFunction, getCustomBidderFunction, setRealTimeData, getRealTimeData, @@ -47,6 +48,30 @@ describe('imRtdProvider', function () { }) }) + describe('getBidderFunction', function () { + const assumedBidder = [ + 'ix', + 'pubmatic' + ]; + assumedBidder.forEach(bidderName => { + it(`should return bidderFunction with assumed bidder: ${bidderName}`, function () { + expect(getBidderFunction(bidderName)).to.exist.and.to.be.a('function'); + }); + + it(`should return bid with correct key data: ${bidderName}`, function () { + const bid = {bidder: bidderName}; + expect(getBidderFunction(bidderName)(bid, {'im_segments': ['12345', '67890']})).to.equal(bid); + }); + it(`should return bid without data: ${bidderName}`, function () { + const bid = {bidder: bidderName}; + expect(getBidderFunction(bidderName)(bid, '')).to.equal(bid); + }); + }); + it(`should return null with unexpected bidder`, function () { + expect(getBidderFunction('test')).to.equal(null); + }); + }) + describe('getCustomBidderFunction', function () { it('should return config function', function () { const config = { diff --git a/test/spec/modules/intersectionRtdProvider_spec.js b/test/spec/modules/intersectionRtdProvider_spec.js new file mode 100644 index 00000000000..eb223d81d8f --- /dev/null +++ b/test/spec/modules/intersectionRtdProvider_spec.js @@ -0,0 +1,141 @@ +import {config as _config, config} from 'src/config.js'; +import { expect } from 'chai'; +import events from 'src/events.js'; +import * as prebidGlobal from 'src/prebidGlobal.js'; +import { intersectionSubmodule } from 'modules/intersectionRtdProvider.js'; +import * as utils from 'src/utils.js'; +import {getGlobal} from 'src/prebidGlobal.js'; +import 'src/prebid.js'; + +describe('Intersection RTD Provider', function () { + let sandbox; + let placeholder; + const pbjs = getGlobal(); + const adUnit = { + code: 'ad-slot-1', + mediaTypes: { + banner: { + sizes: [ [300, 250] ] + } + }, + bids: [ + { + bidder: 'fake' + } + ] + }; + const providerConfig = {name: 'intersection', waitForIt: true}; + const rtdConfig = {realTimeData: {auctionDelay: 200, dataProviders: [providerConfig]}} + describe('IntersectionObserver not supported', function() { + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + afterEach(function() { + sandbox.restore(); + sandbox = undefined; + }); + it('init should return false', function () { + sandbox.stub(window, 'IntersectionObserver').value(undefined); + expect(intersectionSubmodule.init({})).is.false; + }); + }); + describe('IntersectionObserver supported', function() { + beforeEach(function() { + sandbox = sinon.sandbox.create(); + placeholder = createDiv(); + append(); + const __config = {}; + sandbox.stub(_config, 'getConfig').callsFake(function (path) { + return utils.deepAccess(__config, path); + }); + sandbox.stub(_config, 'setConfig').callsFake(function (obj) { + utils.mergeDeep(__config, obj); + }); + }); + afterEach(function() { + sandbox.restore(); + remove(); + sandbox = undefined; + placeholder = undefined; + pbjs.removeAdUnit(); + }); + it('init should return true', function () { + expect(intersectionSubmodule.init({})).is.true; + }); + it('should set intersection. (request with "adUnitCodes")', function(done) { + pbjs.addAdUnits([utils.deepClone(adUnit)]); + config.setConfig(rtdConfig); + const onDone = sandbox.stub(); + const requestBidObject = {adUnitCodes: [adUnit.code]}; + intersectionSubmodule.init({}); + intersectionSubmodule.getBidRequestData( + requestBidObject, + onDone, + providerConfig + ); + setTimeout(function() { + expect(pbjs.adUnits[0].bids[0]).to.have.property('intersection'); + done(); + }, 200); + }); + it('should set intersection. (request with "adUnits")', function(done) { + config.setConfig(rtdConfig); + const onDone = sandbox.stub(); + const requestBidObject = {adUnits: [utils.deepClone(adUnit)]}; + intersectionSubmodule.init(); + intersectionSubmodule.getBidRequestData( + requestBidObject, + onDone, + providerConfig + ); + setTimeout(function() { + expect(requestBidObject.adUnits[0].bids[0]).to.have.property('intersection'); + done(); + }, 200); + }); + it('should set intersection. (request all)', function(done) { + pbjs.addAdUnits([utils.deepClone(adUnit)]); + config.setConfig(rtdConfig); + const onDone = sandbox.stub(); + const requestBidObject = {}; + intersectionSubmodule.init({}); + intersectionSubmodule.getBidRequestData( + requestBidObject, + onDone, + providerConfig + ); + setTimeout(function() { + expect(pbjs.adUnits[0].bids[0]).to.have.property('intersection'); + done(); + }, 200); + }); + it('should call done due timeout', function(done) { + config.setConfig(rtdConfig); + remove(); + const onDone = sandbox.stub(); + const requestBidObject = {adUnits: [utils.deepClone(adUnit)]}; + intersectionSubmodule.init({}); + intersectionSubmodule.getBidRequestData( + requestBidObject, + onDone, + {...providerConfig, test: 1} + ); + setTimeout(function() { + sinon.assert.calledOnce(onDone); + expect(requestBidObject.adUnits[0].bids[0]).to.not.have.property('intersection'); + done(); + }, 300); + }); + }); + function createDiv() { + const div = document.createElement('div'); + div.id = adUnit.code; + return div; + } + function append() { + placeholder && document.body.appendChild(placeholder); + } + function remove() { + placeholder && placeholder.parentElement && placeholder.parentElement.removeChild(placeholder); + } +}); diff --git a/test/spec/modules/invibesBidAdapter_spec.js b/test/spec/modules/invibesBidAdapter_spec.js index 8b92e0ee81b..a61c4fa5267 100644 --- a/test/spec/modules/invibesBidAdapter_spec.js +++ b/test/spec/modules/invibesBidAdapter_spec.js @@ -15,7 +15,7 @@ describe('invibesBidAdapter:', function () { params: { placementId: PLACEMENT_ID }, - adUnitCode: 'test-div', + adUnitCode: 'test-div1', auctionId: 'a1', sizes: [ [300, 250], @@ -30,7 +30,7 @@ describe('invibesBidAdapter:', function () { params: { placementId: 'abcde' }, - adUnitCode: 'test-div', + adUnitCode: 'test-div2', auctionId: 'a2', sizes: [ [300, 250], @@ -48,7 +48,7 @@ describe('invibesBidAdapter:', function () { params: { placementId: PLACEMENT_ID }, - adUnitCode: 'test-div', + adUnitCode: 'test-div1', auctionId: 'a1', sizes: [ [300, 250], @@ -67,7 +67,7 @@ describe('invibesBidAdapter:', function () { params: { placementId: 'abcde' }, - adUnitCode: 'test-div', + adUnitCode: 'test-div2', auctionId: 'a2', sizes: [ [300, 250], @@ -223,6 +223,12 @@ describe('invibesBidAdapter:', function () { expect(JSON.parse(request.data.bidParamsJson).placementIds).to.contain(bidRequests[1].params.placementId); }); + it('sends all adUnitCodes', function () { + const request = spec.buildRequests(bidRequests); + expect(JSON.parse(request.data.bidParamsJson).adUnitCodes).to.contain(bidRequests[0].adUnitCode); + expect(JSON.parse(request.data.bidParamsJson).adUnitCodes).to.contain(bidRequests[1].adUnitCode); + }); + it('sends all Placement Ids and userId', function () { const request = spec.buildRequests(bidRequestsWithUserId); expect(JSON.parse(request.data.bidParamsJson).userId).to.exist; @@ -823,6 +829,20 @@ describe('invibesBidAdapter:', function () { } }; + let responseWithAdUnit = { + Ads: [{ + BidPrice: 0.5, + VideoExposedId: 123 + }], + BidModel: { + BidVersion: 1, + PlacementId: '12345_test-div1', + AuctionStartTime: Date.now(), + CreativeHtml: '' + }, + UseAdUnitCode: true + }; + var buildResponse = function(placementId, cid, blcids, creativeId) { return { MultipositionEnabled: true, @@ -911,6 +931,11 @@ describe('invibesBidAdapter:', function () { let secondResult = spec.interpretResponse({body: response}, {bidRequests}); expect(secondResult).to.be.empty; }); + + it('bids using the adUnitCode', function () { + let result = spec.interpretResponse({body: responseWithAdUnit}, {bidRequests}); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); }); context('when the response has meta', function () { diff --git a/test/spec/modules/ipromBidAdapter_spec.js b/test/spec/modules/ipromBidAdapter_spec.js new file mode 100644 index 00000000000..a3310a33cc2 --- /dev/null +++ b/test/spec/modules/ipromBidAdapter_spec.js @@ -0,0 +1,195 @@ +import {expect} from 'chai'; +import {spec} from 'modules/ipromBidAdapter.js'; + +describe('iPROM Adapter', function () { + let bidRequests; + let bidderRequest; + + beforeEach(function () { + bidRequests = [ + { + bidder: 'iprom', + params: { + id: '1234', + dimension: '300x250', + }, + adUnitCode: '/19966331/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bidId: '29a72b151f7bd3', + auctionId: 'e36abb27-g3b1-1ad6-8a4c-701c8919d3hh', + bidderRequestId: '2z76da40m1b3cb8', + transactionId: 'j51lhf58-1ad6-g3b1-3j6s-912c9493g0gu' + } + ]; + + bidderRequest = { + timeout: 3000, + refererInfo: { + referer: 'https://adserver.si/index.html', + reachedTop: true, + numIframes: 1, + stack: [ + 'https://adserver.si/index.html', + 'https://adserver.si/iframe1.html', + ] + } + } + }); + + describe('validating bids', function () { + it('should accept valid bid', function () { + let validBid = { + bidder: 'iprom', + params: { + id: '1234', + dimension: '300x250', + }, + }; + + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + + it('should reject bid if missing dimension and id', function () { + let invalidBid = { + bidder: 'iprom', + params: {} + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should reject bid if missing dimension', function () { + let invalidBid = { + bidder: 'iprom', + params: { + id: '1234', + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should reject bid if dimension is not a string', function () { + let invalidBid = { + bidder: 'iprom', + params: { + id: '1234', + dimension: 404, + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should reject bid if missing id', function () { + let invalidBid = { + bidder: 'iprom', + params: { + dimension: '300x250', + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should reject bid if id is not a string', function () { + let invalidBid = { + bidder: 'iprom', + params: { + id: 1234, + dimension: '300x250', + } + }; + + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + + describe('building requests', function () { + it('should go to correct endpoint', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.method).to.exist; + expect(request.method).to.equal('POST'); + expect(request.url).to.exist; + expect(request.url).to.equal('https://core.iprom.net/programmatic'); + }); + + it('should add referer info', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.referer).to.exist; + expect(requestparse.referer.referer).to.equal('https://adserver.si/index.html'); + }); + + it('should add adapter version', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.version).to.exist; + }); + + it('should contain id and dimension', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + const requestparse = JSON.parse(request.data); + + expect(requestparse.bids[0].params.id).to.equal('1234'); + expect(requestparse.bids[0].params.dimension).to.equal('300x250'); + }); + }); + + describe('handling responses', function () { + it('should return complete bid response', function () { + const serverResponse = { + body: [{ + requestId: '29a72b151f7bd3', + cpm: 0.5, + width: '300', + height: '250', + creativeId: 1234, + ad: 'Iprom Header bidding example', + aDomains: ['https://example.com'], + } + ]}; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(serverResponse, request); + + expect(bids).to.be.lengthOf(1); + expect(bids[0].requestId).to.equal('29a72b151f7bd3'); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].width).to.equal('300'); + expect(bids[0].height).to.equal('250'); + expect(bids[0].ad).to.have.length.above(1); + expect(bids[0].meta.advertiserDomains).to.deep.equal(['https://example.com']); + }); + + it('should return empty bid response', function () { + const emptyServerResponse = { + body: [] + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const bids = spec.interpretResponse(emptyServerResponse, request); + + expect(bids).to.be.lengthOf(0); + }); + }); +}); diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 4345aae8354..32ce40f25cb 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -353,6 +353,37 @@ describe('IndexexchangeAdapter', function () { ] }; + const DEFAULT_VIDEO_BID_RESPONSE_WITH_MTYPE_SET = { + cur: 'USD', + id: '1aa2bb3cc4de', + seatbid: [ + { + bid: [ + { + crid: '12346', + adomain: ['www.abcd.com'], + adid: '14851456', + impid: '1a2b3c4e', + cid: '3051267', + price: 110, + id: '2', + mtype: 2, + adm: ' Test In-Stream Video Test In-Stream Video { - expect(adUnit).to.have.all.keys('id', 'bidId', 'type', 'sizes', 'transactionId') + expect(adUnit).to.have.all.keys('id', 'bidId', 'type', 'sizes', 'transactionId', 'publisherId') expect(adUnit.id).to.be.a('number') expect(adUnit.bidId).to.be.a('string') expect(adUnit.type).to.be.a('string') @@ -456,10 +458,10 @@ describe('limelightDigitalAdapter', function () { }); function validateAdUnit(adUnit, bid) { - expect(adUnit.id).to.equal(bid.params.adUnitId) - expect(adUnit.bidId).to.equal(bid.bidId) - expect(adUnit.type).to.equal(bid.params.adUnitType.toUpperCase()) - expect(adUnit.transactionId).to.equal(bid.transactionId) + expect(adUnit.id).to.equal(bid.params.adUnitId); + expect(adUnit.bidId).to.equal(bid.bidId); + expect(adUnit.type).to.equal(bid.params.adUnitType.toUpperCase()); + expect(adUnit.transactionId).to.equal(bid.transactionId); let bidSizes = []; if (bid.mediaTypes) { if (bid.mediaTypes.video && bid.mediaTypes.video.playerSize) { @@ -478,4 +480,5 @@ function validateAdUnit(adUnit, bid) { height: size[1] } })); + expect(adUnit.publisherId).to.equal(bid.params.publisherId); } diff --git a/test/spec/modules/lotamePanoramaIdSystem_spec.js b/test/spec/modules/lotamePanoramaIdSystem_spec.js index 5b55adffa9e..090144ab158 100644 --- a/test/spec/modules/lotamePanoramaIdSystem_spec.js +++ b/test/spec/modules/lotamePanoramaIdSystem_spec.js @@ -2,6 +2,7 @@ import { lotamePanoramaIdSubmodule, storage, } from 'modules/lotamePanoramaIdSystem.js'; +import { uspDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; import sinon from 'sinon'; @@ -16,6 +17,7 @@ describe('LotameId', function() { let setLocalStorageStub; let removeFromLocalStorageStub; let timeStampStub; + let uspConsentDataStub; const nowTimestamp = new Date().getTime(); @@ -30,6 +32,7 @@ describe('LotameId', function() { 'removeDataFromLocalStorage' ); timeStampStub = sinon.stub(utils, 'timestamp').returns(nowTimestamp); + uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); }); afterEach(function () { @@ -40,6 +43,7 @@ describe('LotameId', function() { setLocalStorageStub.restore(); removeFromLocalStorageStub.restore(); timeStampStub.restore(); + uspConsentDataStub.restore(); }); describe('caching initial data received from the remote server', function () { @@ -726,4 +730,227 @@ describe('LotameId', function() { ); }); }); + + describe('with a custom client id', function () { + describe('with a client expiry set', function () { + beforeEach(function () { + getCookieStub + .withArgs('panoramaId_expiry_1234') + .returns(String(Date.now() + 500 * 1000)); + }); + + describe('and an existing pano id', function() { + let submoduleCallback; + beforeEach(function () { + getCookieStub + .withArgs('panoramaId_expiry') + .returns(String(Date.now() + 100000)); + getCookieStub + .withArgs('panoramaId') + .returns( + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87c' + ); + submoduleCallback = lotamePanoramaIdSubmodule.getId( + { + params: { + clientId: '1234', + }, + }, + { + gdprApplies: false, + } + ); + }); + + it('should not call the remote server when getId is called nor get an id', function () { + expect(submoduleCallback).to.be.eql({ + id: undefined, + reason: 'NO_CLIENT_CONSENT', + }); + }); + }); + + describe('and no existing pano id', function () { + let submoduleCallback; + + beforeEach(function () { + // Let the panoramaId_expiry be empty + + submoduleCallback = lotamePanoramaIdSubmodule.getId( + { + params: { + clientId: '1234', + }, + }, + { + gdprApplies: false, + } + ); + }); + + it('should not call the remote server nor return an id', function () { + expect(submoduleCallback).to.be.eql({ + id: undefined, + reason: 'NO_CLIENT_CONSENT', + }); + }); + }); + }); + + describe('with no client expiry set', function () { + describe('and no existing pano id', function () { + let request; + let callBackSpy = sinon.spy(); + + beforeEach(function () { + uspConsentDataStub.returns('1NNN'); + let submoduleCallback = lotamePanoramaIdSubmodule.getId( + { + params: { + clientId: '1234', + }, + }, + { + gdprApplies: false, + } + ).callback; + submoduleCallback(callBackSpy); + + request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ + profile_id: '4ec137245858469eb94a4e248f238694', + core_id: + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87f', + expiry_ts: 3600000, + }) + ); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should pass the usp consent string and client id back', function () { + expect(request.url).to.be.eq( + 'https://id.crwdcntrl.net/id?gdpr_applies=false&us_privacy=1NNN&c=1234' + ); + }); + + it('should NOT set an expiry for the client', function () { + sinon.assert.neverCalledWith( + setCookieStub, + 'panoramaId_expiry_1234', + sinon.match.number + ); + }); + }); + + describe('and an existing pano id', function () { + let submoduleCallback; + + beforeEach(function () { + getCookieStub + .withArgs('panoramaId_expiry') + .returns(String(Date.now() + 100000)); + getCookieStub + .withArgs('panoramaId') + .returns( + 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87c' + ); + submoduleCallback = lotamePanoramaIdSubmodule.getId( + { + params: { + clientId: '1234', + }, + }, + { + gdprApplies: false, + } + ); + }); + + it('should not call the remote server but use the cached value', function () { + expect(submoduleCallback).to.be.eql({ + id: 'ca22992567e3cd4d116a5899b88a55d0d857a23610db939ae6ac13ba2335d87c', + }); + }); + + it('should NOT set an expiry for the client', function () { + sinon.assert.neverCalledWith( + setCookieStub, + 'panoramaId_expiry_1234', + sinon.match.number + ); + }); + }); + }); + describe('when client consent has errors', function () { + let request; + let callBackSpy = sinon.spy(); + + beforeEach(function () { + let submoduleCallback = lotamePanoramaIdSubmodule.getId( + { + params: { + clientId: '1234', + }, + }, + { + gdprApplies: false, + } + ).callback; + submoduleCallback(callBackSpy); + + request = server.requests[0]; + + request.respond( + 200, + responseHeader, + JSON.stringify({ + expiry_ts: 3600000, + errors: [111], + no_consent: 'CLIENT', + }) + ); + }); + + it('should call the remote server when getId is called', function () { + expect(callBackSpy.calledOnce).to.be.true; + }); + + it('should pass client id back', function () { + expect(request.url).to.be.eq( + 'https://id.crwdcntrl.net/id?gdpr_applies=false&c=1234' + ); + }); + + it('should set the received expiry for the client', function() { + sinon.assert.calledWith( + setCookieStub, + 'panoramaId_expiry_1234', + 3600000 + ); + }); + + it('should not clear the cache for the panorama id', function() { + sinon.assert.neverCalledWith( + setCookieStub, + 'panoramaId', + sinon.match.any + ); + }); + + it('should not clear the cache for the panorama id expiry', function () { + sinon.assert.neverCalledWith( + setCookieStub, + 'panoramaId_expiry', + sinon.match.any + ); + }); + }); + }); }); diff --git a/test/spec/modules/luponmediaBidAdapter_spec.js b/test/spec/modules/luponmediaBidAdapter_spec.js new file mode 100755 index 00000000000..8aeecc87c98 --- /dev/null +++ b/test/spec/modules/luponmediaBidAdapter_spec.js @@ -0,0 +1,412 @@ +import { resetUserSync, spec, hasValidSupplyChainParams } from 'modules/luponmediaBidAdapter.js'; +const ENDPOINT_URL = 'https://rtb.adxpremium.services/openrtb2/auction'; + +describe('luponmediaBidAdapter', function () { + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'luponmedia', + 'params': { + 'siteId': 12345, + 'keyId': '4o2c4' + }, + 'adUnitCode': 'test-div', + 'sizes': [[300, 250]], + 'bidId': 'g1987234bjkads', + 'bidderRequestId': '290348ksdhkas89324', + 'auctionId': '20384rlek235', + }; + + it('should return true when required params are found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'siteId': 12345 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests = [ + { + 'bidder': 'luponmedia', + 'params': { + 'siteId': 303522, + 'keyId': '4o2c4' + }, + 'crumbs': { + 'pubcid': '8d8b16cb-1383-4a0f-b4bb-0be28464d974' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1533155193780-2', + 'transactionId': '585d96a5-bd93-4a89-b8ea-0f546f3aaa82', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '268a30af10dd6f', + 'bidderRequestId': '140411b5010a2a', + 'auctionId': '7376c117-b7aa-49f5-a661-488543deeefd', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'novi.ba', + 'sid': '199424', + 'hp': 1 + } + ] + } + } + ]; + + let bidderRequest = { + 'bidderCode': 'luponmedia', + 'auctionId': '7376c117-b7aa-49f5-a661-488543deeefd', + 'bidderRequestId': '140411b5010a2a', + 'bids': [ + { + 'bidder': 'luponmedia', + 'params': { + 'siteId': 303522, + 'keyId': '4o2c4' + }, + 'crumbs': { + 'pubcid': '8d8b16cb-1383-4a0f-b4bb-0be28464d974' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'div-gpt-ad-1533155193780-2', + 'transactionId': '585d96a5-bd93-4a89-b8ea-0f546f3aaa82', + 'sizes': [ + [ + 300, + 250 + ] + ], + 'bidId': '268a30af10dd6f', + 'bidderRequestId': '140411b5010a2a', + 'auctionId': '7376c117-b7aa-49f5-a661-488543deeefd', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'novi.ba', + 'sid': '199424', + 'hp': 1 + } + ] + } + } + ], + 'auctionStart': 1587413920820, + 'timeout': 2000, + 'refererInfo': { + 'referer': 'https://novi.ba/clanak/176067/fast-car-beginner-s-guide-to-tuning-turbo-engines', + 'reachedTop': true, + 'numIframes': 0, + 'stack': [ + 'https://novi.ba/clanak/176067/fast-car-beginner-s-guide-to-tuning-turbo-engines' + ] + }, + 'start': 1587413920835 + }; + + it('sends bid request to ENDPOINT via POST', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + let dynRes = JSON.parse(requests.data); + expect(requests.url).to.equal(ENDPOINT_URL); + expect(requests.method).to.equal('POST'); + expect(requests.data).to.equal('{"id":"585d96a5-bd93-4a89-b8ea-0f546f3aaa82","test":0,"source":{"tid":"585d96a5-bd93-4a89-b8ea-0f546f3aaa82","ext":{"schain":{"ver":"1.0","complete":1,"nodes":[{"asi":"novi.ba","sid":"199424","hp":1}]}}},"tmax":1500,"imp":[{"id":"268a30af10dd6f","secure":1,"ext":{"luponmedia":{"siteId":303522,"keyId":"4o2c4"}},"banner":{"format":[{"w":300,"h":250}]}}],"ext":{"prebid":{"targeting":{"includewinners":true,"includebidderkeys":false}}},"user":{"id":"' + dynRes.user.id + '","buyeruid":"8d8b16cb-1383-4a0f-b4bb-0be28464d974"},"site":{"page":"https://novi.ba/clanak/176067/fast-car-beginner-s-guide-to-tuning-turbo-engines"}}'); + }); + }); + + describe('interpretResponse', function () { + it('should get correct banner bid response', function () { + let response = { + 'id': '4776d680-15a2-45c3-bad5-db6bebd94a06', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2a122246ef72ea', + 'impid': '2a122246ef72ea', + 'price': 0.43, + 'adm': ' ', + 'adid': '56380110', + 'cid': '44724710', + 'crid': '443801010', + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'targeting': { + 'hb_bidder': 'luponmedia', + 'hb_pb': '0.40', + 'hb_size': '300x250' + }, + 'type': 'banner' + } + } + } + ], + 'seat': 'luponmedia' + } + ], + 'cur': 'USD', + 'ext': { + 'responsetimemillis': { + 'luponmedia': 233 + }, + 'tmaxrequest': 1500, + 'usersyncs': { + 'status': 'ok', + 'bidder_status': [] + } + } + }; + + let expectedResponse = [ + { + 'requestId': '2a122246ef72ea', + 'cpm': '0.43', + 'width': 300, + 'height': 250, + 'creativeId': '443801010', + 'currency': 'USD', + 'dealId': '23425', + 'netRevenue': false, + 'ttl': 300, + 'referrer': '', + 'ad': ' ' + } + ]; + + let bidderRequest = { + 'data': '{"site":{"page":"https://novi.ba/clanak/176067/fast-car-beginner-s-guide-to-tuning-turbo-engines"}}' + }; + + let result = spec.interpretResponse({ body: response }, bidderRequest); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('handles nobid responses', function () { + let noBidResponse = []; + + let noBidBidderRequest = { + 'data': '{"site":{"page":""}}' + } + let noBidResult = spec.interpretResponse({ body: noBidResponse }, noBidBidderRequest); + expect(noBidResult.length).to.equal(0); + }); + }); + + describe('getUserSyncs', function () { + const bidResponse1 = { + 'body': { + 'ext': { + 'responsetimemillis': { + 'luponmedia': 233 + }, + 'tmaxrequest': 1500, + 'usersyncs': { + 'status': 'ok', + 'bidder_status': [ + { + 'bidder': 'luponmedia', + 'no_cookie': true, + 'usersync': { + 'url': 'https://adxpremium.services/api/usersync', + 'type': 'redirect' + } + }, + { + 'bidder': 'luponmedia', + 'no_cookie': true, + 'usersync': { + 'url': 'https://adxpremium.services/api/iframeusersync', + 'type': 'iframe' + } + } + ] + } + } + } + }; + + const bidResponse2 = { + 'body': { + 'ext': { + 'responsetimemillis': { + 'luponmedia': 233 + }, + 'tmaxrequest': 1500, + 'usersyncs': { + 'status': 'no_cookie', + 'bidder_status': [] + } + } + } + }; + + it('should use a sync url from first response (pixel and iframe)', function () { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [bidResponse1, bidResponse2]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://adxpremium.services/api/usersync' + }, + { + type: 'iframe', + url: 'https://adxpremium.services/api/iframeusersync' + } + ]); + }); + + it('handle empty response (e.g. timeout)', function () { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('returns empty syncs when not pixel enabled and not iframe enabled', function () { + const syncs = spec.getUserSyncs({ pixelEnabled: false, iframeEnabled: false }, [bidResponse1]); + expect(syncs).to.deep.equal([]); + }); + + it('returns pixel syncs when pixel enabled and not iframe enabled', function() { + resetUserSync(); + + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: false }, [bidResponse1]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://adxpremium.services/api/usersync' + } + ]); + }); + + it('returns iframe syncs when not pixel enabled and iframe enabled', function() { + resetUserSync(); + + const syncs = spec.getUserSyncs({ pixelEnabled: false, iframeEnabled: true }, [bidResponse1]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://adxpremium.services/api/iframeusersync' + } + ]); + }); + }); + + describe('hasValidSupplyChainParams', function () { + it('returns true if schain is valid', function () { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'novi.ba', + 'sid': '199424', + 'hp': 1 + } + ] + }; + + const checkSchain = hasValidSupplyChainParams(schain); + expect(checkSchain).to.equal(true); + }); + + it('returns false if schain is invalid', function () { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'invalid': 'novi.ba' + } + ] + }; + + const checkSchain = hasValidSupplyChainParams(schain); + expect(checkSchain).to.equal(false); + }); + }); + + describe('onBidWon', function () { + const bidWonEvent = { + 'bidderCode': 'luponmedia', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '105bbf8c54453ff', + 'requestId': '934b8752185955', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 0.364, + 'creativeId': '443801010', + 'currency': 'USD', + 'netRevenue': false, + 'ttl': 300, + 'referrer': '', + 'ad': '', + 'auctionId': '926a8ea3-3dd4-4bf2-95ab-c85c2ce7e99b', + 'responseTimestamp': 1598527728026, + 'requestTimestamp': 1598527727629, + 'bidder': 'luponmedia', + 'adUnitCode': 'div-gpt-ad-1533155193780-5', + 'timeToRespond': 397, + 'size': '300x250', + 'status': 'rendered' + }; + + let ajaxStub; + + beforeEach(() => { + ajaxStub = sinon.stub(spec, 'sendWinningsToServer') + }) + + afterEach(() => { + ajaxStub.restore() + }) + + it('calls luponmedia\'s callback endpoint', () => { + const result = spec.onBidWon(bidWonEvent); + expect(result).to.equal(undefined); + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.deep.equal(JSON.stringify(bidWonEvent)); + }); + }); +}); diff --git a/test/spec/modules/missenaBidAdapter_spec.js b/test/spec/modules/missenaBidAdapter_spec.js new file mode 100644 index 00000000000..86b967cca5b --- /dev/null +++ b/test/spec/modules/missenaBidAdapter_spec.js @@ -0,0 +1,134 @@ +import { expect } from 'chai'; +import { spec, _getPlatform } from 'modules/missenaBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('Missena Adapter', function () { + const adapter = newBidder(spec); + + const bidId = 'abc'; + + const bid = { + bidder: 'missena', + bidId: bidId, + sizes: [[1, 1]], + params: { + apiKey: 'PA-34745704', + }, + }; + + describe('codes', function () { + it('should return a bidder code of missena', function () { + expect(spec.code).to.equal('missena'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true if the apiKey param is present', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if the apiKey is missing', function () { + expect( + spec.isBidRequestValid(Object.assign(bid, { params: {} })) + ).to.equal(false); + }); + + it('should return false if the apiKey is an empty string', function () { + expect( + spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })) + ).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const consentString = 'AAAAAAAAA=='; + + const bidderRequest = { + gdprConsent: { + consentString: consentString, + gdprApplies: true, + }, + refererInfo: { + referer: 'https://referer', + canonicalUrl: 'https://canonical', + }, + }; + + const requests = spec.buildRequests([bid, bid], bidderRequest); + const request = requests[0]; + const payload = JSON.parse(request.data); + + it('should return as many server requests as bidder requests', function () { + expect(requests.length).to.equal(2); + }); + + it('should have a post method', function () { + expect(request.method).to.equal('POST'); + }); + + it('should send the bidder id', function () { + expect(payload.request_id).to.equal(bidId); + }); + + it('should send referer information to the request', function () { + expect(payload.referer).to.equal('https://referer'); + expect(payload.referer_canonical).to.equal('https://canonical'); + }); + + it('should send gdpr consent information to the request', function () { + expect(payload.consent_string).to.equal(consentString); + expect(payload.consent_required).to.equal(true); + }); + }); + + describe('interpretResponse', function () { + const serverResponse = { + requestId: bidId, + cpm: 0.5, + currency: 'USD', + ad: '', + meta: { + advertiserDomains: ['missena.com'] + }, + }; + + const serverTimeoutResponse = { + requestId: bidId, + timeout: true, + ad: '', + }; + + const serverEmptyAdResponse = { + requestId: bidId, + cpm: 0.5, + currency: 'USD', + ad: '', + }; + + it('should return a proper bid response', function () { + const result = spec.interpretResponse({ body: serverResponse }, bid); + + expect(result.length).to.equal(1); + + expect(Object.keys(result[0])).to.have.members( + Object.keys(serverResponse) + ); + }); + + it('should return an empty response when the server answers with a timeout', function () { + const result = spec.interpretResponse( + { body: serverTimeoutResponse }, + bid + ); + expect(result).to.deep.equal([]); + }); + + it('should return an empty response when the server answers with an empty ad', function () { + const result = spec.interpretResponse( + { body: serverEmptyAdResponse }, + bid + ); + expect(result).to.deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index 1a24c6d0575..9b6364810e3 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -30,6 +30,18 @@ describe('nextMillenniumBidAdapterTests', function() { expect(JSON.parse(request[0].data).id).to.equal('b06c5141-fe8f-4cdf-9d7d-54415490a917'); }); + it('Test getUserSyncs function', function () { + const syncOptions = { + 'iframeEnabled': true + } + const userSync = spec.getUserSyncs(syncOptions); + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync[0].type).to.be.equal('iframe'); + expect(userSync[0].url).to.be.equal('https://statics.nextmillmedia.com/load-cookie.html?v=4'); + }); + it('validate_response_params', function() { const serverResponse = { body: { diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index 883119d2707..13bffc8075c 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -119,7 +119,7 @@ describe('OguryBidAdapter', function () { }; }); - it('should return sync array with two elements of type image', () => { + it('should return syncs array with two elements of type image', () => { const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); expect(userSyncs).to.have.lengthOf(2); @@ -129,7 +129,7 @@ describe('OguryBidAdapter', function () { expect(userSyncs[1].url).to.contain('https://ms-cookie-sync.presage.io/ttd/init-sync'); }); - it('should set the same source as query param', () => { + it('should set the source as query param', () => { const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); expect(userSyncs[0].url).to.contain('source=prebid'); expect(userSyncs[1].url).to.contain('source=prebid'); @@ -146,7 +146,7 @@ describe('OguryBidAdapter', function () { expect(spec.getUserSyncs(syncOptions, [], gdprConsent)).to.have.lengthOf(0); }); - it('should return sync array with two elements of type image when consentString is undefined', () => { + it('should return syncs array with two elements of type image when consentString is undefined', () => { gdprConsent = { gdprApplies: true, consentString: undefined @@ -160,7 +160,7 @@ describe('OguryBidAdapter', function () { expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') }); - it('should return sync array with two elements of type image when consentString is null', () => { + it('should return syncs array with two elements of type image when consentString is null', () => { gdprConsent = { gdprApplies: true, consentString: null @@ -174,7 +174,7 @@ describe('OguryBidAdapter', function () { expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') }); - it('should return sync array with two elements of type image when gdprConsent is undefined', () => { + it('should return syncs array with two elements of type image when gdprConsent is undefined', () => { gdprConsent = undefined; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); @@ -185,7 +185,7 @@ describe('OguryBidAdapter', function () { expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') }); - it('should return sync array with two elements of type image when gdprConsent is null', () => { + it('should return syncs array with two elements of type image when gdprConsent is null', () => { gdprConsent = null; const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); @@ -196,7 +196,7 @@ describe('OguryBidAdapter', function () { expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') }); - it('should return sync array with two elements of type image when gdprConsent is null and gdprApplies is false', () => { + it('should return syncs array with two elements of type image when gdprConsent is null and gdprApplies is false', () => { gdprConsent = { gdprApplies: false, consentString: null @@ -210,7 +210,7 @@ describe('OguryBidAdapter', function () { expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') }); - it('should return sync array with two elements of type image when gdprConsent is empty string and gdprApplies is false', () => { + it('should return syncs array with two elements of type image when gdprConsent is empty string and gdprApplies is false', () => { gdprConsent = { gdprApplies: false, consentString: '' @@ -240,7 +240,8 @@ describe('OguryBidAdapter', function () { w: 300, h: 250 }] - } + }, + ext: bidRequests[0].params }, { id: bidRequests[1].bidId, tagid: bidRequests[1].params.adUnitId, @@ -250,7 +251,8 @@ describe('OguryBidAdapter', function () { w: 600, h: 500 }] - } + }, + ext: bidRequests[1].params }], regs: { ext: { @@ -549,7 +551,7 @@ describe('OguryBidAdapter', function () { xhr.restore() }) - it('should send notification on bid timeout', function() { + it('should send on bid timeout notification', function() { const bid = { ad: 'cookies', cpm: 3 diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index be07e1dcc93..4be8c462144 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -2420,6 +2420,36 @@ describe('S2S Adapter', function () { utils.getBidRequest.restore(); }); + + describe('on sync requested with no cookie', () => { + let cfg, req, csRes; + + beforeEach(() => { + cfg = utils.deepClone(CONFIG); + req = utils.deepClone(REQUEST); + cfg.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; + req.s2sConfig = cfg; + config.setConfig({ s2sConfig: cfg }); + csRes = utils.deepClone(RESPONSE_NO_COOKIE); + }); + + afterEach(() => { + resetSyncedStatus(); + }) + + Object.entries({ + iframe: () => utils.insertUserSyncIframe, + image: () => utils.triggerPixel, + }).forEach(([type, syncer]) => { + it(`passes timeout to ${type} syncs`, () => { + cfg.syncTimeout = 123; + csRes.bidder_status[0].usersync.type = type; + adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(csRes)); + expect(syncer().args[0]).to.include.members([123]); + }); + }); + }); }); describe('bid won events', function () { diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 8905dfa5924..9696501437b 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -3457,7 +3457,7 @@ describe('PubMatic adapter', function () { 'h': 0, 'dealId': 'ASEA-MS-KLY-TTD-DESKTOP-ID-VID-6S-030420', 'ext': { - 'BidType': 1 + 'bidtype': 1 } }], 'ext': { diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 868b1856c34..51850e5f357 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -57,23 +57,34 @@ describe('RelaidoAdapter', function () { serverResponse = { body: { status: 'ok', - price: 500, - model: 'vcpm', - currency: 'JPY', - creativeId: 1000, - uuid: relaido_uuid, - vast: '', + ads: [{ + placementId: 100000, + width: 640, + height: 360, + bidId: '2ed93003f7bb99', + price: 500, + model: 'vcpm', + currency: 'JPY', + creativeId: 1000, + vast: '', + syncUrl: 'https://relaido/sync.html', + adomain: ['relaido.co.jp', 'www.cmertv.co.jp'], + mediaType: 'video' + }], playerUrl: 'https://relaido/player.js', - syncUrl: 'https://relaido/sync.html', - adomain: ['relaido.co.jp', 'www.cmertv.co.jp'] + syncUrl: 'https://api-dev.ulizaex.com/tr/v1/prebid/sync.html', + uuid: relaido_uuid, } }; serverRequest = { - method: 'GET', - bidId: bidRequest.bidId, - width: bidRequest.mediaTypes.video.playerSize[0][0], - height: bidRequest.mediaTypes.video.playerSize[0][1], - mediaType: 'video', + method: 'POST', + data: { + bids: [{ + bidId: bidRequest.bidId, + width: bidRequest.mediaTypes.video.playerSize[0][0], + height: bidRequest.mediaTypes.video.playerSize[0][1], + mediaType: 'video'}] + } }; }); @@ -195,28 +206,23 @@ describe('RelaidoAdapter', function () { describe('spec.buildRequests', function () { it('should build bid requests by video', function () { const bidRequests = spec.buildRequests([bidRequest], bidderRequest); - expect(bidRequests).to.have.lengthOf(1); - const request = bidRequests[0]; - expect(request.method).to.equal('GET'); - expect(request.url).to.equal('https://api.relaido.jp/bid/v1/prebid/100000'); - expect(request.bidId).to.equal(bidRequest.bidId); - expect(request.width).to.equal(bidRequest.mediaTypes.video.playerSize[0][0]); - expect(request.height).to.equal(bidRequest.mediaTypes.video.playerSize[0][1]); - expect(request.mediaType).to.equal('video'); - expect(request.data.ref).to.equal(bidderRequest.refererInfo.referer); - expect(request.data.timeout_ms).to.equal(bidderRequest.timeout); - expect(request.data.ad_unit_code).to.equal(bidRequest.adUnitCode); - expect(request.data.auction_id).to.equal(bidRequest.auctionId); - expect(request.data.bidder).to.equal(bidRequest.bidder); - expect(request.data.bidder_request_id).to.equal(bidRequest.bidderRequestId); - expect(request.data.bid_requests_count).to.equal(bidRequest.bidRequestsCount); - expect(request.data.bid_id).to.equal(bidRequest.bidId); - expect(request.data.transaction_id).to.equal(bidRequest.transactionId); - expect(request.data.media_type).to.equal('video'); - expect(request.data.uuid).to.equal(relaido_uuid); - expect(request.data.width).to.equal(bidRequest.mediaTypes.video.playerSize[0][0]); - expect(request.data.height).to.equal(bidRequest.mediaTypes.video.playerSize[0][1]); - expect(request.data.pv).to.equal('$prebid.version$'); + const data = JSON.parse(bidRequests.data); + expect(data.bids).to.have.lengthOf(1); + const request = data.bids[0]; + expect(bidRequests.method).to.equal('POST'); + expect(bidRequests.url).to.equal('https://api.relaido.jp/bid/v1/sprebid'); + expect(data.ref).to.equal(bidderRequest.refererInfo.referer); + expect(data.timeout_ms).to.equal(bidderRequest.timeout); + expect(request.ad_unit_code).to.equal(bidRequest.adUnitCode); + expect(request.auction_id).to.equal(bidRequest.auctionId); + expect(data.bidder).to.equal(bidRequest.bidder); + expect(request.bidder_request_id).to.equal(bidRequest.bidderRequestId); + expect(data.bid_requests_count).to.equal(bidRequest.bidRequestsCount); + expect(request.bid_id).to.equal(bidRequest.bidId); + expect(request.transaction_id).to.equal(bidRequest.transactionId); + expect(request.media_type).to.equal('video'); + expect(data.uuid).to.equal(relaido_uuid); + expect(data.pv).to.equal('$prebid.version$'); }); it('should build bid requests by banner', function () { @@ -236,9 +242,10 @@ describe('RelaidoAdapter', function () { } }; const bidRequests = spec.buildRequests([bidRequest], bidderRequest); - expect(bidRequests).to.have.lengthOf(1); - const request = bidRequests[0]; - expect(request.mediaType).to.equal('banner'); + const data = JSON.parse(bidRequests.data); + expect(data.bids).to.have.lengthOf(1); + const request = data.bids[0]; + expect(request.media_type).to.equal('banner'); }); it('should take 1x1 size', function () { @@ -258,17 +265,18 @@ describe('RelaidoAdapter', function () { } }; const bidRequests = spec.buildRequests([bidRequest], bidderRequest); - expect(bidRequests).to.have.lengthOf(1); - const request = bidRequests[0]; + const data = JSON.parse(bidRequests.data); + expect(data.bids).to.have.lengthOf(1); + const request = data.bids[0]; expect(request.width).to.equal(1); }); it('The referrer should be the last', function () { const bidRequests = spec.buildRequests([bidRequest], bidderRequest); - expect(bidRequests).to.have.lengthOf(1); - const request = bidRequests[0]; - const keys = Object.keys(request.data); + const data = JSON.parse(bidRequests.data); + expect(data.bids).to.have.lengthOf(1); + const keys = Object.keys(data); expect(keys[0]).to.equal('version'); expect(keys[keys.length - 1]).to.equal('ref'); }); @@ -277,9 +285,9 @@ describe('RelaidoAdapter', function () { bidRequest.userId = {} bidRequest.userId.imuid = 'i.tjHcK_7fTcqnbrS_YA2vaw'; const bidRequests = spec.buildRequests([bidRequest], bidderRequest); - expect(bidRequests).to.have.lengthOf(1); - const request = bidRequests[0]; - expect(request.data.imuid).to.equal('i.tjHcK_7fTcqnbrS_YA2vaw'); + const data = JSON.parse(bidRequests.data); + expect(data.bids).to.have.lengthOf(1); + expect(data.imuid).to.equal('i.tjHcK_7fTcqnbrS_YA2vaw'); }); }); @@ -288,29 +296,29 @@ describe('RelaidoAdapter', function () { const bidResponses = spec.interpretResponse(serverResponse, serverRequest); expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; - expect(response.requestId).to.equal(serverRequest.bidId); - expect(response.width).to.equal(serverRequest.width); - expect(response.height).to.equal(serverRequest.height); - expect(response.cpm).to.equal(serverResponse.body.price); - expect(response.currency).to.equal(serverResponse.body.currency); - expect(response.creativeId).to.equal(serverResponse.body.creativeId); - expect(response.vastXml).to.equal(serverResponse.body.vast); - expect(response.meta.advertiserDomains).to.equal(serverResponse.body.adomain); + expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.width).to.equal(serverRequest.data.bids[0].width); + expect(response.height).to.equal(serverRequest.data.bids[0].height); + expect(response.cpm).to.equal(serverResponse.body.ads[0].price); + expect(response.currency).to.equal(serverResponse.body.ads[0].currency); + expect(response.creativeId).to.equal(serverResponse.body.ads[0].creativeId); + expect(response.vastXml).to.equal(serverResponse.body.ads[0].vast); + expect(response.meta.advertiserDomains).to.equal(serverResponse.body.ads[0].adomain); expect(response.meta.mediaType).to.equal(VIDEO); expect(response.ad).to.be.undefined; }); it('should build bid response by banner', function () { - serverRequest.mediaType = 'banner'; + serverResponse.body.ads[0].mediaType = 'banner'; const bidResponses = spec.interpretResponse(serverResponse, serverRequest); expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; - expect(response.requestId).to.equal(serverRequest.bidId); - expect(response.width).to.equal(serverRequest.width); - expect(response.height).to.equal(serverRequest.height); - expect(response.cpm).to.equal(serverResponse.body.price); - expect(response.currency).to.equal(serverResponse.body.currency); - expect(response.creativeId).to.equal(serverResponse.body.creativeId); + expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.width).to.equal(serverRequest.data.bids[0].width); + expect(response.height).to.equal(serverRequest.data.bids[0].height); + expect(response.cpm).to.equal(serverResponse.body.ads[0].price); + expect(response.currency).to.equal(serverResponse.body.ads[0].currency); + expect(response.creativeId).to.equal(serverResponse.body.ads[0].creativeId); expect(response.vastXml).to.be.undefined; expect(response.ad).to.include(`
`); expect(response.ad).to.include(``); @@ -370,8 +378,8 @@ describe('RelaidoAdapter', function () { it('Should create nurl pixel if bid nurl', function () { let bid = { bidder: bidRequest.bidder, - creativeId: serverResponse.body.creativeId, - cpm: serverResponse.body.price, + creativeId: serverResponse.body.ads[0].creativeId, + cpm: serverResponse.body.ads[0].price, params: [bidRequest.params], auctionId: bidRequest.auctionId, requestId: bidRequest.bidId, diff --git a/test/spec/modules/seedtagBidAdapter_spec.js b/test/spec/modules/seedtagBidAdapter_spec.js index 3aa378379dd..159645ff6d6 100644 --- a/test/spec/modules/seedtagBidAdapter_spec.js +++ b/test/spec/modules/seedtagBidAdapter_spec.js @@ -274,6 +274,9 @@ describe('Seedtag Adapter', function() { expect(data.ga).to.equal(true) expect(data.cd).to.equal('consentString') }) + it('should expose gvlid', function() { + expect(spec.gvlid).to.equal(157) + }) }) }) diff --git a/test/spec/modules/sharedIdSystem_spec.js b/test/spec/modules/sharedIdSystem_spec.js index 534d0b3f381..fcfbe5f7c3f 100644 --- a/test/spec/modules/sharedIdSystem_spec.js +++ b/test/spec/modules/sharedIdSystem_spec.js @@ -50,10 +50,10 @@ describe('SharedId System', function () { expect(callbackSpy.calledOnce).to.be.true; expect(callbackSpy.lastCall.lastArg).to.equal(UUID); }); - it('should log message if coppa is set', function () { + it('should abort if coppa is set', function () { coppaDataHandlerDataStub.returns('true'); - sharedIdSystemSubmodule.getId({}); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); + const result = sharedIdSystemSubmodule.getId({}); + expect(result).to.be.undefined; }); }); describe('SharedId System extendId()', function () { @@ -85,10 +85,10 @@ describe('SharedId System', function () { let pubcommId = sharedIdSystemSubmodule.extendId(config, undefined, 'TestId').id; expect(pubcommId).to.equal('TestId'); }); - it('should log message if coppa is set', function () { + it('should abort if coppa is set', function () { coppaDataHandlerDataStub.returns('true'); - sharedIdSystemSubmodule.extendId({}, undefined, 'TestId'); - expect(utils.logInfo.args[0][0]).to.exist.and.to.equal('PubCommonId: IDs not provided for coppa requests, exiting PubCommonId'); + const result = sharedIdSystemSubmodule.extendId({params: {extend: true}}, undefined, 'TestId'); + expect(result).to.be.undefined; }); }); }); diff --git a/test/spec/modules/smaatoBidAdapter_spec.js b/test/spec/modules/smaatoBidAdapter_spec.js index faa288306a5..9803d1df103 100644 --- a/test/spec/modules/smaatoBidAdapter_spec.js +++ b/test/spec/modules/smaatoBidAdapter_spec.js @@ -287,6 +287,23 @@ describe('smaatoBidAdapterTest', () => { expect(req.regs.ext.us_privacy).to.equal('uspConsentString'); }); + it('sends no schain if no schain exists', () => { + const reqs = spec.buildRequests([singleBannerBidRequest], defaultBidderRequest); + + const req = extractPayloadOfFirstAndOnlyRequest(reqs); + expect(req.source.ext.schain).to.not.exist; + }); + + it('sends instl if instl exists', () => { + const instl = { instl: 1 }; + const bidRequestWithInstl = Object.assign({}, singleBannerBidRequest, {ortb2Imp: instl}); + + const reqs = spec.buildRequests([bidRequestWithInstl], defaultBidderRequest); + + const req = extractPayloadOfFirstAndOnlyRequest(reqs); + expect(req.imp[0].instl).to.equal(1); + }); + it('sends tmax', () => { const reqs = spec.buildRequests([singleBannerBidRequest], defaultBidderRequest); @@ -435,6 +452,16 @@ describe('smaatoBidAdapterTest', () => { expect(req.imp[0].bidfloor).to.be.equal(0.456); }); + it('sends instl if instl exists', () => { + const instl = { instl: 1 }; + const bidRequestWithInstl = Object.assign({}, singleVideoBidRequest, {ortb2Imp: instl}); + + const reqs = spec.buildRequests([bidRequestWithInstl], defaultBidderRequest); + + const req = extractPayloadOfFirstAndOnlyRequest(reqs); + expect(req.imp[0].instl).to.equal(1); + }); + it('splits multi format bid requests', () => { const combinedBannerAndVideoBidRequest = { bidder: 'smaato', @@ -516,6 +543,17 @@ describe('smaatoBidAdapterTest', () => { expect(req.imp[1].video.sequence).to.be.equal(2); }); + it('sends instl if instl exists', () => { + const instl = { instl: 1 }; + const bidRequestWithInstl = Object.assign({}, longFormVideoBidRequest, {ortb2Imp: instl}); + + const reqs = spec.buildRequests([bidRequestWithInstl], defaultBidderRequest); + + const req = extractPayloadOfFirstAndOnlyRequest(reqs); + expect(req.imp[0].instl).to.equal(1); + expect(req.imp[1].instl).to.equal(1); + }); + it('sends bidfloor when configured', () => { const longFormVideoBidRequestWithFloor = Object.assign({}, longFormVideoBidRequest); longFormVideoBidRequestWithFloor.getFloor = function(arg) { @@ -854,6 +892,29 @@ describe('smaatoBidAdapterTest', () => { expect(req.user.ext.eids).to.have.length(2); }); }); + + describe('schain in request', () => { + it('schain is added to source.ext.schain', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'asi', + 'sid': 'sid', + 'rid': 'rid', + 'hp': 1 + } + ] + }; + const bidRequestWithSchain = Object.assign({}, singleBannerBidRequest, {schain: schain}); + + const reqs = spec.buildRequests([bidRequestWithSchain], defaultBidderRequest); + + const req = extractPayloadOfFirstAndOnlyRequest(reqs); + expect(req.source.ext.schain).to.deep.equal(schain); + }); + }); }); describe('interpretResponse', () => { diff --git a/test/spec/modules/smartxBidAdapter_spec.js b/test/spec/modules/smartxBidAdapter_spec.js index 4e560c87df3..ddee2fa3347 100644 --- a/test/spec/modules/smartxBidAdapter_spec.js +++ b/test/spec/modules/smartxBidAdapter_spec.js @@ -189,6 +189,14 @@ describe('The smartx adapter', function () { domain: '', publisher: { id: '__name__' + }, + content: { + ext: { + prebid: { + name: 'pbjs', + version: '$prebid.version$' + } + } } }); }); @@ -525,6 +533,7 @@ describe('The smartx adapter', function () { bidderRequestObj.bidRequest.bids[0].params.outstream_options.title = 'abc'; bidderRequestObj.bidRequest.bids[0].params.outstream_options.skipOffset = 2; bidderRequestObj.bidRequest.bids[0].params.outstream_options.desiredBitrate = 123; + bidderRequestObj.bidRequest.bids[0].params.outstream_options.visibilityThreshold = 30; responses[0].renderer.render(responses[0]); diff --git a/test/spec/modules/tappxBidAdapter_spec.js b/test/spec/modules/tappxBidAdapter_spec.js index 3fe691847b6..49d739ed3be 100644 --- a/test/spec/modules/tappxBidAdapter_spec.js +++ b/test/spec/modules/tappxBidAdapter_spec.js @@ -183,6 +183,10 @@ describe('Tappx bid adapter', function () { badBidRequest_v.bids.mediaTypes.video.playerSize = [320, 250]; assert.isFalse(spec.isBidRequestValid(badBidRequest_v.bids)); }); + + it('should export the TCF vendor ID', function () { + expect(spec.gvlid).to.equal(628); + }) }); /** diff --git a/test/spec/modules/targetVideoBidAdapter_spec.js b/test/spec/modules/targetVideoBidAdapter_spec.js new file mode 100644 index 00000000000..0ce6f0fb70d --- /dev/null +++ b/test/spec/modules/targetVideoBidAdapter_spec.js @@ -0,0 +1,96 @@ +import { spec } from '../../../modules/targetVideoBidAdapter.js' + +describe('TargetVideo Bid Adapter', function() { + const bannerRequest = [{ + bidder: 'targetVideo', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + params: { + placementId: 12345, + } + }]; + + it('Test the bid validation function', function() { + const validBid = spec.isBidRequestValid(bannerRequest[0]); + const invalidBid = spec.isBidRequestValid(null); + + expect(validBid).to.be.true; + expect(invalidBid).to.be.false; + }); + + it('Test the request processing function', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + expect(request).to.not.be.empty; + + const payload = JSON.parse(request.data); + expect(payload).to.not.be.empty; + expect(payload.sdk).to.deep.equal({ + source: 'pbjs', + version: '$prebid.version$' + }); + expect(payload.tags[0].id).to.equal(12345); + expect(payload.tags[0].gpid).to.equal('targetVideo'); + expect(payload.tags[0].ad_types[0]).to.equal('video'); + }); + + it('Handle nobid responses', function () { + const responseBody = { + 'version': '0.0.1', + 'tags': [{ + 'uuid': '84ab500420319d', + 'tag_id': 5976557, + 'auction_id': '297492697822162468', + 'nobid': true + }] + }; + const bidderRequest = null; + + const bidResponse = spec.interpretResponse({ body: responseBody }, {bidderRequest}); + expect(bidResponse.length).to.equal(0); + }); + + it('Test the response parsing function', function () { + const responseBody = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'https://www.target-video.com/', + 'rtb': { + 'video': { + 'player_width': 640, + 'player_height': 360, + 'asset_url': 'https://www.target-video.com/' + } + } + }] + }] + }; + const bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }] + }; + + const bidResponse = spec.interpretResponse({ body: responseBody }, {bidderRequest}); + expect(bidResponse).to.not.be.empty; + + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.cpm).to.equal(0.675); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ad).to.include('') + expect(bid.ad).to.include('initPlayer') + }); +}); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 0190bceca70..29b5aca630e 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -49,6 +49,7 @@ import {flocIdSubmodule} from 'modules/flocIdSystem.js' import {amxIdSubmodule} from '../../../modules/amxIdSystem.js'; import {akamaiDAPIdSubmodule} from 'modules/akamaiDAPIdSystem.js' import {kinessoIdSubmodule} from 'modules/kinessoIdSystem.js' +import * as mockGpt from '../integration/faker/googletag.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -114,15 +115,21 @@ describe('User ID', function () { describe('Decorate Ad Units', function () { beforeEach(function () { + // reset mockGpt so nothing else interferes + mockGpt.disable(); + mockGpt.enable(); coreStorage.setCookie('pubcid', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('pubcid_alt', 'altpubcid200000', (new Date(Date.now() + 5000).toUTCString())); sinon.spy(coreStorage, 'setCookie'); + sinon.stub(utils, 'logWarn'); }); afterEach(function () { + mockGpt.enable(); $$PREBID_GLOBAL$$.requestBids.removeAll(); config.resetConfig(); coreStorage.setCookie.restore(); + utils.logWarn.restore(); }); after(function () { @@ -321,6 +328,50 @@ describe('User ID', function () { expect((getGlobal()).getUserIdsAsEids()).to.deep.equal(createEidsArray((getGlobal()).getUserIds())); }); + it('should set googletag ppid correctly', function () { + let adUnits = [getAdUnitMock()]; + setSubmoduleRegistry([amxIdSubmodule, sharedIdSystemSubmodule, identityLinkSubmodule]); + init(config); + + config.setConfig({ + userSync: { + ppid: 'pubcid.org', + userIds: [ + { name: 'amxId', value: {'amxId': 'amx-id-value-amx-id-value-amx-id-value'} }, + { name: 'pubCommonId', value: {'pubcid': 'pubCommon-id-value-pubCommon-id-value'} }, + { name: 'identityLink', value: {'idl_env': 'identityLink-id-value-identityLink-id-value'} }, + ] + } + }); + // before ppid should not be set + expect(window.googletag._ppid).to.equal(undefined); + requestBidsHook(() => {}, {adUnits}); + // ppid should have been set without dashes and stuff + expect(window.googletag._ppid).to.equal('pubCommonidvaluepubCommonidvalue'); + }); + + it('should log a warning if PPID too big or small', function () { + let adUnits = [getAdUnitMock()]; + setSubmoduleRegistry([sharedIdSystemSubmodule]); + init(config); + + config.setConfig({ + userSync: { + ppid: 'pubcid.org', + userIds: [ + { name: 'pubCommonId', value: {'pubcid': 'pubcommonIdValue'} }, + ] + } + }); + // before ppid should not be set + expect(window.googletag._ppid).to.equal(undefined); + requestBidsHook(() => {}, {adUnits}); + // ppid should NOT have been set + expect(window.googletag._ppid).to.equal(undefined); + // a warning should have been emmited + expect(utils.logWarn.args[0][0]).to.exist.and.to.contain('User ID - Googletag Publisher Provided ID for pubcid.org is not between 32 and 150 characters - pubcommonIdValue'); + }); + it('pbjs.refreshUserIds refreshes', function() { let sandbox = sinon.createSandbox(); diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index f0c3007b4c3..d29eb6c25dc 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -3,6 +3,7 @@ import { spec } from 'modules/visxBidAdapter.js'; import { config } from 'src/config.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; +import { makeSlot } from '../integration/faker/googletag.js'; describe('VisxAdapter', function () { const adapter = newBidder(spec); @@ -145,17 +146,17 @@ describe('VisxAdapter', function () { const expectedFullImps = [{ 'id': '30b31c1838de1e', 'banner': {'format': [{'w': 300, 'h': 250}, {'w': 300, 'h': 600}]}, - 'ext': {'bidder': {'uid': 903535}} + 'ext': {'bidder': {'uid': 903535, 'adslotExists': false}} }, { 'id': '3150ccb55da321', 'banner': {'format': [{'w': 728, 'h': 90}, {'w': 300, 'h': 250}]}, - 'ext': {'bidder': {'uid': 903535}} + 'ext': {'bidder': {'uid': 903535, 'adslotExists': false}} }, { 'id': '42dbe3a7168a6a', 'banner': {'format': [{'w': 300, 'h': 250}, {'w': 300, 'h': 600}]}, - 'ext': {'bidder': {'uid': 903536}} + 'ext': {'bidder': {'uid': 903536, 'adslotExists': false}} }, { 'id': '39a4e3a7168a6a', @@ -452,7 +453,122 @@ describe('VisxAdapter', function () { 'imp': [{ 'id': '39aff3a7169a6a', 'banner': {'format': [{'w': 300, 'h': 250}, {'w': 300, 'h': 600}]}, - 'ext': {'bidder': {'uid': 903538}} + 'ext': {'bidder': {'uid': 903538, 'adslotExists': false}} + }], + 'tmax': 3000, + 'cur': ['EUR'], + 'source': { + 'ext': { + 'wrapperType': 'Prebid_js', + 'wrapperVersion': '$prebid.version$' + } + }, + 'site': {'page': referrer} + }); + }); + }); + + describe('buildRequests (check ad slot exists)', function () { + function parseRequest(url) { + const res = {}; + (url.split('?')[1] || '').split('&').forEach((it) => { + const couple = it.split('='); + res[couple[0]] = decodeURIComponent(couple[1]); + }); + return res; + } + const bidderRequest = { + timeout: 3000, + refererInfo: { + referer: 'https://example.com' + } + }; + const referrer = bidderRequest.refererInfo.referer; + const bidRequests = [ + { + 'bidder': 'visx', + 'params': { + 'uid': 903535 + }, + 'adUnitCode': 'visx-adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'visx', + 'params': { + 'uid': 903535 + }, + 'adUnitCode': 'visx-adunit-code-2', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + let sandbox; + let documentStub; + + before(function() { + sandbox = sinon.sandbox.create(); + documentStub = sandbox.stub(document, 'getElementById'); + documentStub.withArgs('visx-adunit-code-1').returns({ + id: 'visx-adunit-code-1' + }); + documentStub.withArgs('visx-adunit-element-2').returns({ + id: 'visx-adunit-element-2' + }); + }); + + after(function() { + sandbox.restore(); + }); + + it('should find ad slot by ad unit code as element id', function () { + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + const payload = parseRequest(request.url); + expect(payload).to.be.an('object'); + expect(payload).to.have.property('auids', '903535'); + + const postData = request.data; + expect(postData).to.be.an('object'); + expect(postData).to.deep.equal({ + 'id': '22edbae2733bf6', + 'imp': [{ + 'id': '30b31c1838de1e', + 'banner': {'format': [{'w': 300, 'h': 250}, {'w': 300, 'h': 600}]}, + 'ext': {'bidder': {'uid': 903535, 'adslotExists': true}} + }], + 'tmax': 3000, + 'cur': ['EUR'], + 'source': { + 'ext': { + 'wrapperType': 'Prebid_js', + 'wrapperVersion': '$prebid.version$' + } + }, + 'site': {'page': referrer} + }); + }); + + it('should find ad slot by ad unit code as adUnitPath', function () { + makeSlot({code: 'visx-adunit-code-2', divId: 'visx-adunit-element-2'}); + + const request = spec.buildRequests([bidRequests[1]], bidderRequest); + const payload = parseRequest(request.url); + expect(payload).to.be.an('object'); + expect(payload).to.have.property('auids', '903535'); + + const postData = request.data; + expect(postData).to.be.an('object'); + expect(postData).to.deep.equal({ + 'id': '22edbae2733bf6', + 'imp': [{ + 'id': '30b31c1838de1e', + 'banner': {'format': [{'w': 300, 'h': 250}, {'w': 300, 'h': 600}]}, + 'ext': {'bidder': {'uid': 903535, 'adslotExists': true}} }], 'tmax': 3000, 'cur': ['EUR'], diff --git a/test/spec/modules/weboramaRtdProvider_spec.js b/test/spec/modules/weboramaRtdProvider_spec.js index 155f26990a7..081a6f06876 100644 --- a/test/spec/modules/weboramaRtdProvider_spec.js +++ b/test/spec/modules/weboramaRtdProvider_spec.js @@ -1,14 +1,21 @@ -import { setBigseaContextualProfile, weboramaSubmodule } from 'modules/weboramaRtdProvider.js'; -import { server } from 'test/mocks/xhr.js'; -import {config} from 'src/config.js'; - -const responseHeader = {'Content-Type': 'application/json'}; - -// TODO fix it +import { + weboramaSubmodule +} from 'modules/weboramaRtdProvider.js'; +import { + server +} from 'test/mocks/xhr.js'; +import { + storage, + DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY +} from '../../../modules/weboramaRtdProvider.js'; + +const responseHeader = { + 'Content-Type': 'application/json' +}; describe('weboramaRtdProvider', function() { describe('weboramaSubmodule', function() { - it('successfully instantiates and call contextual api', function () { + it('successfully instantiates and call contextual api', function() { const moduleConfig = { params: { weboCtxConf: { @@ -18,271 +25,458 @@ describe('weboramaRtdProvider', function() { } }; - expect(weboramaSubmodule.init(moduleConfig)).to.equal(true); - - let request = server.requests[0]; - - expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); - expect(request.method).to.equal('GET') + expect(weboramaSubmodule.init(moduleConfig)).to.equal(true); }); - it('instantiate without token should fail', function () { + + it('instantiate without contextual token should fail', function() { const moduleConfig = { params: { weboCtxConf: {} } }; - expect(weboramaSubmodule.init(moduleConfig)).to.equal(false); + expect(weboramaSubmodule.init(moduleConfig)).to.equal(false); + }); + + it('instantiate with empty weboUserData conf should return true', function() { + const moduleConfig = { + params: { + weboUserDataConf: {} + } + }; + expect(weboramaSubmodule.init(moduleConfig)).to.equal(true); }); }); - describe('Add Contextual Data', function() { + describe('Handle Set Targeting', function() { + let sandbox; + beforeEach(function() { - let conf = { - site: { - ext: { - data: { - inventory: ['value1'] + sandbox = sinon.sandbox.create(); + + storage.removeDataFromLocalStorage('webo_wam2gam_entry'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('Add Contextual Data', function() { + it('should set gam targeting and send to bidders by default', function() { + const moduleConfig = { + params: { + weboCtxConf: { + token: 'foo', + targetURL: 'https://prebid.org', } } - }, - user: { - ext: { - data: { - visitor: ['value2'] + }; + const data = { + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }; + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver' + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + let request = server.requests[0]; + + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); + expect(request.withCredentials).to.be.false; + + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_ctx=foo;webo_ctx=bar;webo_ds=baz'); + }); + + it('should set gam targeting but not send to bidders with setPrebidTargeting=true/sendToBidders=false', function() { + const moduleConfig = { + params: { + weboCtxConf: { + token: 'foo', + targetURL: 'https://prebid.org', + setPrebidTargeting: true, + sendToBidders: false, } } - }, - cur: ['USD'] - }; + }; + const data = { + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }; + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + let request = server.requests[0]; + + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); + expect(request.withCredentials).to.be.false; + + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar'); + }); - config.setConfig({ortb2: conf}); - }); - it('should set targeting and ortb2 if omit setTargeting', function() { - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setOrtb2: true, + it('should not set gam targeting with setPrebidTargeting=false but send to bidders', function() { + const moduleConfig = { + params: { + weboCtxConf: { + token: 'foo', + targetURL: 'https://prebid.org', + setPrebidTargeting: false, + } } + }; + const data = { + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }; + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }] + }] } - }; - const data = { - webo_ctx: ['foo', 'bar'], - webo_ds: ['baz'], - }; - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); + const onDoneSpy = sinon.spy(); - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + let request = server.requests[0]; - expect(targeting).to.deep.equal({ - 'adunit1': data, - 'adunit2': data, - }); + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); + expect(request.withCredentials).to.be.false; - const ortb2 = config.getConfig('ortb2'); + request.respond(200, responseHeader, JSON.stringify(data)); - expect(ortb2.site.ext.data.webo_ctx).to.deep.equal(data.webo_ctx); - expect(ortb2.site.ext.data.webo_ds).to.deep.equal(data.webo_ds); - }); - - it('should set targeting and ortb2 with setTargeting=true', function() { - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: true, - setOrtb2: true, - } - } - }; - const data = { - webo_ctx: ['foo', 'bar'], - webo_ds: ['baz'], - }; - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); + expect(onDoneSpy.calledOnce).to.be.true; - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + expect(targeting).to.deep.equal({}); - expect(targeting).to.deep.equal({ - 'adunit1': data, - 'adunit2': data, + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar;webo_ctx=foo;webo_ctx=bar;webo_ds=baz'); }); - const ortb2 = config.getConfig('ortb2'); - - expect(ortb2.site.ext.data.webo_ctx).to.deep.equal(data.webo_ctx); - expect(ortb2.site.ext.data.webo_ds).to.deep.equal(data.webo_ds); - }); - it('should set targeting and ortb2 only webo_ctx with setTargeting=true', function() { - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: true, - setOrtb2: true, + it('should use default profile in case of api error', function() { + const defaultProfile = { + webo_ctx: ['baz'], + }; + const moduleConfig = { + params: { + weboCtxConf: { + token: 'foo', + targetURL: 'https://prebid.org', + setPrebidTargeting: true, + defaultProfile: defaultProfile, + } } - } - }; - const data = { - webo_ctx: ['foo', 'bar'], - }; + }; - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver' + }] + }] + }; + const onDoneSpy = sinon.spy(); - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + let request = server.requests[0]; - expect(targeting).to.deep.equal({ - 'adunit1': data, - 'adunit2': data, - }); + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); + expect(request.withCredentials).to.be.false; - const ortb2 = config.getConfig('ortb2'); + request.respond(500, responseHeader); - expect(ortb2.site.ext.data.webo_ctx).to.deep.equal(data.webo_ctx); - expect(ortb2.site.ext.data).to.not.have.property('webo_ds'); - }); - it('should set only targeting and not ortb2 with setTargeting=true and setOrtb2=false', function() { - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: true, - setOrtb2: false, - } - } - }; - const data = { - webo_ctx: ['foo', 'bar'], - }; + expect(onDoneSpy.calledOnce).to.be.true; - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); + expect(targeting).to.deep.equal({ + 'adunit1': defaultProfile, + 'adunit2': defaultProfile, + }); - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_ctx=baz'); + }); + }); - expect(targeting).to.deep.equal({ - 'adunit1': data, - 'adunit2': data, + describe('Add WAM2GAM Data', function() { + it('should set gam targeting from local storage and send to bidders by default', function() { + const moduleConfig = { + params: { + weboUserDataConf: {} + } + }; + const data = { + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + }; + + const entry = { + targeting: data, + }; + + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage') + .withArgs(DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY) + .returns(JSON.stringify(entry)); + + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver' + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_cs=foo;webo_cs=bar;webo_audiences=baz'); }); - const ortb2 = config.getConfig('ortb2'); + it('should set gam targeting but not send to bidders with setPrebidTargeting=true/sendToBidders=false', function() { + const moduleConfig = { + params: { + weboUserDataConf: { + setPrebidTargeting: true, + sendToBidders: false + } + } + }; + const data = { + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + }; + + const entry = { + targeting: data, + }; + + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage') + .withArgs(DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY) + .returns(JSON.stringify(entry)); + + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar'); + }); - expect(ortb2.site.ext.data).to.not.have.property('webo_ctx'); - expect(ortb2.site.ext.data).to.not.have.property('webo_ds'); - }); - it('should set only targeting and not ortb2 with setTargeting=true and omit setOrtb2', function() { - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: true, + it('should not set gam targeting with setPrebidTargeting=false but send to bidders', function() { + const moduleConfig = { + params: { + weboUserDataConf: { + setPrebidTargeting: false, + } } - } - }; - const data = { - webo_ctx: ['foo', 'bar'], - }; + }; + const data = { + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + }; + + const entry = { + targeting: data, + }; + + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage') + .withArgs(DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY) + .returns(JSON.stringify(entry)); + + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({}); + + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar;webo_cs=foo;webo_cs=bar;webo_audiences=baz'); + }); - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); + it('should use default profile in case of nothing on local storage', function() { + const defaultProfile = { + webo_audiences: ['baz'] + }; + const moduleConfig = { + params: { + weboUserDataConf: { + setPrebidTargeting: true, + defaultProfile: defaultProfile, + } + } + }; - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver' + }] + }] + }; + const onDoneSpy = sinon.spy(); - expect(targeting).to.deep.equal({ - 'adunit1': data, - 'adunit2': data, - }); + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); - const ortb2 = config.getConfig('ortb2'); + expect(onDoneSpy.calledOnce).to.be.true; - expect(ortb2.site.ext.data).to.not.have.property('webo_ctx'); - expect(ortb2.site.ext.data).to.not.have.property('webo_ds'); - }); + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); - it('should set only ortb2 with setTargeting=false', function() { - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: false, - setOrtb2: true, - } - } - }; - const data = { - webo_ctx: ['foo', 'bar'], - }; - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); + expect(targeting).to.deep.equal({ + 'adunit1': defaultProfile, + 'adunit2': defaultProfile, + }); - let request = server.requests[0]; - request.respond(200, responseHeader, JSON.stringify(data)); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_audiences=baz'); + }); - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + it('should use default profile if cant read from local storage', function() { + const defaultProfile = { + webo_audiences: ['baz'] + }; + const moduleConfig = { + params: { + weboUserDataConf: { + setPrebidTargeting: true, + defaultProfile: defaultProfile, + } + } + }; - expect(targeting).to.deep.equal({}); + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); - const ortb2 = config.getConfig('ortb2'); + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver' + }] + }] + }; + const onDoneSpy = sinon.spy(); - expect(ortb2.site.ext.data.webo_ctx).to.deep.equal(data.webo_ctx); - expect(ortb2.site.ext.data).to.not.have.property('webo_ds'); - }); - it('should use default profile in case of api error', function() { - const defaultProfile = { - webo_ctx: ['baz'], - }; - const moduleConfig = { - params: { - weboCtxConf: { - token: 'foo', - targetURL: 'https://prebid.org', - setTargeting: true, - defaultProfile: defaultProfile, - } - } - }; + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); - const adUnitsCodes = ['adunit1', 'adunit2']; - weboramaSubmodule.init(moduleConfig); + expect(onDoneSpy.calledOnce).to.be.true; - let request = server.requests[0]; - request.respond(500, responseHeader); + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); - const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + expect(targeting).to.deep.equal({ + 'adunit1': defaultProfile, + 'adunit2': defaultProfile, + }); - expect(targeting).to.deep.equal({ - 'adunit1': defaultProfile, - 'adunit2': defaultProfile, + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_audiences=baz'); }); - - const ortb2 = config.getConfig('ortb2'); - - expect(ortb2.site.ext.data).to.not.have.property('webo_ctx'); - expect(ortb2.site.ext.data).to.not.have.property('webo_ds'); }); }); }); diff --git a/test/spec/modules/yahoosspBidAdapter_spec.js b/test/spec/modules/yahoosspBidAdapter_spec.js index 5eb82b399cc..e0af8784605 100644 --- a/test/spec/modules/yahoosspBidAdapter_spec.js +++ b/test/spec/modules/yahoosspBidAdapter_spec.js @@ -11,7 +11,7 @@ const DEFAULT_AD_UNIT_CODE = '/19968336/header-bid-tag-1'; const DEFAULT_AD_UNIT_TYPE = 'banner'; const DEFAULT_PARAMS_BID_OVERRIDE = {}; const DEFAULT_VIDEO_CONTEXT = 'instream'; -const ADAPTER_VERSION = '1.0.1'; +const ADAPTER_VERSION = '1.0.2'; const PREBID_VERSION = '$prebid.version$'; const INTEGRATION_METHOD = 'prebid.js'; @@ -656,6 +656,33 @@ describe('YahooSSP Bid Adapter:', () => { const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.imp[0].ext.data).to.deep.equal(validBidRequests[0].ortb2Imp.ext.data); }); + // adUnit.ortb2Imp.instl + it(`should allow adUnit.ortb2Imp.instl numeric boolean "1" to be added to the bid-request`, () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) + validBidRequests[0].ortb2Imp = { + instl: 1 + }; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].instl).to.deep.equal(validBidRequests[0].ortb2Imp.instl); + }); + + it(`should prevent adUnit.ortb2Imp.instl boolean "true" to be added to the bid-request`, () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) + validBidRequests[0].ortb2Imp = { + instl: true + }; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].instl).to.not.exist; + }); + + it(`should prevent adUnit.ortb2Imp.instl boolean "false" to be added to the bid-request`, () => { + let { validBidRequests, bidderRequest } = generateBuildRequestMock({}) + validBidRequests[0].ortb2Imp = { + instl: false + }; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].instl).to.not.exist; + }); }); describe('e2etest mode support:', () => { diff --git a/test/spec/modules/yieldoneBidAdapter_spec.js b/test/spec/modules/yieldoneBidAdapter_spec.js index 4186c5da41a..1b38bb94a8c 100644 --- a/test/spec/modules/yieldoneBidAdapter_spec.js +++ b/test/spec/modules/yieldoneBidAdapter_spec.js @@ -7,6 +7,8 @@ const ENDPOINT = 'https://y.one.impact-ad.jp/h_bid'; const USER_SYNC_URL = 'https://y.one.impact-ad.jp/push_sync'; const VIDEO_PLAYER_URL = 'https://img.ak.impact-ad.jp/ic/pone/ivt/firstview/js/dac-video-prebid.min.js'; +const DEFAULT_VIDEO_SIZE = {w: 640, h: 360}; + describe('yieldoneBidAdapter', function() { const adapter = newBidder(spec); @@ -40,32 +42,7 @@ describe('yieldoneBidAdapter', function() { }); describe('buildRequests', function () { - let bidRequests = [ - { - 'bidder': 'yieldone', - 'params': { - placementId: '36891' - }, - 'adUnitCode': 'adunit-code1', - 'sizes': [[300, 250], [336, 280]], - 'bidId': '23beaa6af6cdde', - 'bidderRequestId': '19c0c1efdf37e7', - 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', - }, - { - 'bidder': 'yieldone', - 'params': { - placementId: '47919' - }, - 'adUnitCode': 'adunit-code2', - 'sizes': [[300, 250]], - 'bidId': '382091349b149f"', - 'bidderRequestId': '"1f9c98192de251"', - 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', - } - ]; - - let bidderRequest = { + const bidderRequest = { refererInfo: { numIframes: 0, reachedTop: true, @@ -74,49 +51,318 @@ describe('yieldoneBidAdapter', function() { } }; - const request = spec.buildRequests(bidRequests, bidderRequest); + describe('Basic', function () { + const bidRequests = [ + { + 'bidder': 'yieldone', + 'params': {placementId: '36891'}, + 'adUnitCode': 'adunit-code1', + 'bidId': '23beaa6af6cdde', + 'bidderRequestId': '19c0c1efdf37e7', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + }, + { + 'bidder': 'yieldone', + 'params': {placementId: '47919'}, + 'adUnitCode': 'adunit-code2', + 'bidId': '382091349b149f"', + 'bidderRequestId': '"1f9c98192de251"', + 'auctionId': '61466567-d482-4a16-96f0-fe5f25ffbdf1', + } + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); - it('sends bid request to our endpoint via GET', function () { - expect(request[0].method).to.equal('GET'); - expect(request[1].method).to.equal('GET'); + it('sends bid request to our endpoint via GET', function () { + expect(request[0].method).to.equal('GET'); + expect(request[1].method).to.equal('GET'); + }); + it('attaches source and version to endpoint URL as query params', function () { + expect(request[0].url).to.equal(ENDPOINT); + expect(request[1].url).to.equal(ENDPOINT); + }); + it('adUnitCode should be sent as uc parameters on any requests', function () { + expect(request[0].data.uc).to.equal('adunit-code1'); + expect(request[1].data.uc).to.equal('adunit-code2'); + }); }); - it('attaches source and version to endpoint URL as query params', function () { - expect(request[0].url).to.equal(ENDPOINT); - expect(request[1].url).to.equal(ENDPOINT); - }); + describe('Old Format', function () { + const bidRequests = [ + { + params: {placementId: '0'}, + mediaType: 'banner', + sizes: [[300, 250], [336, 280]], + }, + { + params: {placementId: '1'}, + mediaType: 'banner', + sizes: [[336, 280]], + }, + { + // It doesn't actually exist. + params: {placementId: '2'}, + }, + { + params: {placementId: '3'}, + mediaType: 'video', + sizes: [[1280, 720], [1920, 1080]], + }, + { + params: {placementId: '4'}, + mediaType: 'video', + sizes: [[1920, 1080]], + }, + { + params: {placementId: '5'}, + mediaType: 'video', + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); - it('parameter sz has more than one size on banner requests', function () { - expect(request[0].data.sz).to.equal('300x250,336x280'); - expect(request[1].data.sz).to.equal('300x250'); + it('parameter sz has more than one size on banner requests', function () { + expect(request[0].data.sz).to.equal('300x250,336x280'); + expect(request[1].data.sz).to.equal('336x280'); + expect(request[2].data.sz).to.equal(''); + expect(request[3].data).to.not.have.property('sz'); + expect(request[4].data).to.not.have.property('sz'); + expect(request[5].data).to.not.have.property('sz'); + }); + + it('width and height should be set as separate parameters on outstream requests', function () { + expect(request[0].data).to.not.have.property('w'); + expect(request[1].data).to.not.have.property('w'); + expect(request[2].data).to.not.have.property('w'); + expect(request[3].data.w).to.equal(1280); + expect(request[3].data.h).to.equal(720); + expect(request[4].data.w).to.equal(1920); + expect(request[4].data.h).to.equal(1080); + expect(request[5].data.w).to.equal(DEFAULT_VIDEO_SIZE.w); + expect(request[5].data.h).to.equal(DEFAULT_VIDEO_SIZE.h); + }); }); - it('width and height should be set as separate parameters on outstream requests', function () { - const bidRequest = Object.assign({}, bidRequests[0]); - bidRequest.mediaTypes = {}; - bidRequest.mediaTypes.video = {context: 'outstream'}; - const request = spec.buildRequests([bidRequest], bidderRequest); - expect(request[0].data.w).to.equal('300'); - expect(request[0].data.h).to.equal('250'); + describe('New Format', function () { + const bidRequests = [ + { + params: {placementId: '0'}, + mediaTypes: { + banner: { + sizes: [[300, 250], [336, 280]], + }, + }, + }, + { + params: {placementId: '1'}, + mediaTypes: { + banner: { + sizes: [[336, 280]], + }, + }, + }, + { + // It doesn't actually exist. + params: {placementId: '2'}, + mediaTypes: { + banner: { + }, + }, + }, + { + params: {placementId: '3'}, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[1280, 720], [1920, 1080]], + }, + }, + }, + { + params: {placementId: '4'}, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [1920, 1080], + }, + }, + }, + { + params: {placementId: '5'}, + mediaTypes: { + video: { + context: 'outstream', + }, + }, + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + + it('parameter sz has more than one size on banner requests', function () { + expect(request[0].data.sz).to.equal('300x250,336x280'); + expect(request[1].data.sz).to.equal('336x280'); + expect(request[2].data.sz).to.equal(''); + expect(request[3].data).to.not.have.property('sz'); + expect(request[4].data).to.not.have.property('sz'); + expect(request[5].data).to.not.have.property('sz'); + }); + + it('width and height should be set as separate parameters on outstream requests', function () { + expect(request[0].data).to.not.have.property('w'); + expect(request[1].data).to.not.have.property('w'); + expect(request[2].data).to.not.have.property('w'); + expect(request[3].data.w).to.equal(1280); + expect(request[3].data.h).to.equal(720); + expect(request[4].data.w).to.equal(1920); + expect(request[4].data.h).to.equal(1080); + expect(request[5].data.w).to.equal(DEFAULT_VIDEO_SIZE.w); + expect(request[5].data.h).to.equal(DEFAULT_VIDEO_SIZE.h); + }); }); - it('adUnitCode should be sent as uc parameters on any requests', function () { - expect(request[0].data.uc).to.equal('adunit-code1'); - expect(request[1].data.uc).to.equal('adunit-code2'); + describe('Multiple Format', function () { + const bidRequests = [ + { + // It will be treated as a banner. + params: { + placementId: '0', + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [336, 280]], + }, + video: { + context: 'outstream', + playerSize: [1920, 1080], + }, + }, + }, + { + // It will be treated as a video. + params: { + placementId: '1', + playerParams: {}, + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [336, 280]], + }, + video: { + context: 'outstream', + playerSize: [1920, 1080], + }, + }, + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + + it('parameter sz has more than one size on banner requests', function () { + expect(request[0].data.sz).to.equal('300x250,336x280'); + expect(request[1].data).to.not.have.property('sz'); + }); + + it('width and height should be set as separate parameters on outstream requests', function () { + expect(request[0].data).to.not.have.property('w'); + expect(request[1].data.w).to.equal(1920); + expect(request[1].data.h).to.equal(1080); + }); }); - describe('userid idl_env should be passed to querystring', function () { - const bid = deepClone([bidRequests[0]]); + describe('FLUX Format', function () { + const bidRequests = [ + { + // It will be treated as a banner. + params: { + placementId: '0', + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [336, 280]], + }, + video: { + context: 'outstream', + playerSize: [[1, 1]], + }, + }, + }, + { + // It will be treated as a video. + params: { + placementId: '1', + playerParams: {}, + playerSize: [1920, 1080], + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [336, 280]], + }, + video: { + context: 'outstream', + playerSize: [[1, 1]], + }, + }, + }, + { + // It will be treated as a video. + params: { + placementId: '2', + playerParams: {}, + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [336, 280]], + }, + video: { + context: 'outstream', + playerSize: [[1, 1]], + }, + }, + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + + it('parameter sz has more than one size on banner requests', function () { + expect(request[0].data.sz).to.equal('300x250,336x280'); + expect(request[1].data).to.not.have.property('sz'); + expect(request[2].data).to.not.have.property('sz'); + }); + + it('width and height should be set as separate parameters on outstream requests', function () { + expect(request[0].data).to.not.have.property('w'); + expect(request[1].data.w).to.equal(1920); + expect(request[1].data.h).to.equal(1080); + expect(request[2].data.w).to.equal(DEFAULT_VIDEO_SIZE.w); + expect(request[2].data.h).to.equal(DEFAULT_VIDEO_SIZE.h); + }); + }); + describe('LiveRampID', function () { it('dont send LiveRampID if undefined', function () { - bid[0].userId = {}; - const request = spec.buildRequests(bid, bidderRequest); + const bidRequests = [ + { + params: {placementId: '0'}, + }, + { + params: {placementId: '1'}, + userId: {}, + }, + { + params: {placementId: '2'}, + userId: undefined, + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); expect(request[0].data).to.not.have.property('lr_env'); + expect(request[1].data).to.not.have.property('lr_env'); + expect(request[2].data).to.not.have.property('lr_env'); }); it('should send LiveRampID if available', function () { - bid[0].userId = {idl_env: 'idl_env_sample'}; - const request = spec.buildRequests(bid, bidderRequest); + const bidRequests = [ + { + params: {placementId: '0'}, + userId: {idl_env: 'idl_env_sample'}, + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); expect(request[0].data.lr_env).to.equal('idl_env_sample'); }); }); diff --git a/test/spec/modules/zeta_global_sspBidAdapter_spec.js b/test/spec/modules/zeta_global_sspBidAdapter_spec.js index dcb0183fb4c..b18c68febb8 100644 --- a/test/spec/modules/zeta_global_sspBidAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspBidAdapter_spec.js @@ -143,7 +143,10 @@ describe('Zeta Ssp Bid Adapter', function () { 'https://example2.com' ], h: 150, - w: 200 + w: 200, + ext: { + bidtype: 'video' + } } ] } @@ -159,6 +162,7 @@ describe('Zeta Ssp Bid Adapter', function () { const receivedBid1 = response.body.seatbid[0].bid[0]; expect(bid1).to.not.be.empty; expect(bid1.ad).to.equal(receivedBid1.adm); + expect(bid1.vastXml).to.be.undefined; expect(bid1.cpm).to.equal(receivedBid1.price); expect(bid1.height).to.equal(receivedBid1.h); expect(bid1.width).to.equal(receivedBid1.w); @@ -169,6 +173,7 @@ describe('Zeta Ssp Bid Adapter', function () { const receivedBid2 = response.body.seatbid[0].bid[1]; expect(bid2).to.not.be.empty; expect(bid2.ad).to.equal(receivedBid2.adm); + expect(bid2.vastXml).to.equal(receivedBid2.adm); expect(bid2.cpm).to.equal(receivedBid2.price); expect(bid2.height).to.equal(receivedBid2.h); expect(bid2.width).to.equal(receivedBid2.w); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 5b37f64688f..f414febcebe 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -1712,14 +1712,17 @@ describe('adapterManager tests', function () { }); describe('sizeMapping', function () { + let sandbox; beforeEach(function () { + sandbox = sinon.sandbox.create(); allS2SBidders.length = 0; clientTestAdapters.length = 0; - sinon.stub(window, 'matchMedia').callsFake(() => ({matches: true})); + // always have matchMedia return true for us + sandbox.stub(utils.getWindowTop(), 'matchMedia').callsFake(() => ({matches: true})); }); afterEach(function () { - matchMedia.restore(); + sandbox.restore(); config.resetConfig(); setSizeConfig([]); }); diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index 1064d7c0f7d..4eaf414bf85 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -5,6 +5,7 @@ import { createBidReceived } from 'test/fixtures/fixtures.js'; import CONSTANTS from 'src/constants.json'; import { auctionManager } from 'src/auctionManager.js'; import * as utils from 'src/utils.js'; +import {deepClone} from 'src/utils.js'; const bid1 = { 'bidderCode': 'rubicon', @@ -461,6 +462,50 @@ describe('targeting tests', function () { }); }); + describe('targetingControls.allowZeroCpmBids', function () { + let bid4; + let bidderSettingsStorage; + + before(function() { + bidderSettingsStorage = $$PREBID_GLOBAL$$.bidderSettings; + }); + + beforeEach(function () { + bid4 = utils.deepClone(bid1); + bid4.adserverTargeting = { + hb_pb: '0.0', + hb_adid: '567891011', + hb_bidder: 'appnexus', + }; + bid4.bidder = bid4.bidderCode = 'appnexus'; + bid4.cpm = 0; + bidsReceived = [bid4]; + }); + + after(function() { + bidsReceived = [bid1, bid2, bid3]; + $$PREBID_GLOBAL$$.bidderSettings = bidderSettingsStorage; + }) + + it('targeting should not include a 0 cpm by default', function() { + bid4.adserverTargeting = {}; + const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']); + expect(targeting['/123456/header-bid-tag-0']).to.deep.equal({}); + }); + + it('targeting should allow a 0 cpm with targetingControls.allowZeroCpmBids set to true', function () { + $$PREBID_GLOBAL$$.bidderSettings = { + standard: { + allowZeroCpmBids: true + } + }; + + const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']); + expect(targeting['/123456/header-bid-tag-0']).to.include.all.keys('hb_pb', 'hb_bidder', 'hb_adid', 'hb_bidder_appnexus', 'hb_adid_appnexus', 'hb_pb_appnexus'); + expect(targeting['/123456/header-bid-tag-0']['hb_pb']).to.equal('0.0') + }); + }); + describe('targetingControls.allowTargetingKeys', function () { let bid4; @@ -501,6 +546,77 @@ describe('targeting tests', function () { }); }); + describe('targetingControls.addTargetingKeys', function () { + let winningBid = null; + + beforeEach(function () { + bidsReceived = [bid1, bid2, nativeBid1, nativeBid2].map(deepClone); + bidsReceived.forEach((bid) => { + bid.adserverTargeting[CONSTANTS.TARGETING_KEYS.SOURCE] = 'test-source'; + bid.adUnitCode = 'adunit'; + if (winningBid == null || bid.cpm > winningBid.cpm) { + winningBid = bid; + } + }); + enableSendAllBids = true; + }); + + const expandKey = function (key) { + const keys = new Set(); + if (winningBid.adserverTargeting[key] != null) { + keys.add(key); + } + bidsReceived + .filter((bid) => bid.adserverTargeting[key] != null) + .map((bid) => bid.bidderCode) + .forEach((code) => keys.add(`${key}_${code}`.substr(0, 20))); + return new Array(...keys); + } + + const targetingResult = function () { + return targetingInstance.getAllTargeting(['adunit'])['adunit']; + } + + it('should include added keys', function () { + config.setConfig({ + targetingControls: { + addTargetingKeys: ['SOURCE'] + } + }); + expect(targetingResult()).to.include.all.keys(...expandKey(CONSTANTS.TARGETING_KEYS.SOURCE)); + }); + + it('should keep default and native keys', function() { + config.setConfig({ + targetingControls: { + addTargetingKeys: ['SOURCE'] + } + }); + const defaultKeys = new Set(Object.values(CONSTANTS.DEFAULT_TARGETING_KEYS)); + Object.values(CONSTANTS.NATIVE_KEYS).forEach((k) => defaultKeys.add(k)); + + const expectedKeys = new Set(); + bidsReceived + .map((bid) => Object.keys(bid.adserverTargeting)) + .reduce((left, right) => left.concat(right), []) + .filter((key) => defaultKeys.has(key)) + .map(expandKey) + .reduce((left, right) => left.concat(right), []) + .forEach((k) => expectedKeys.add(k)); + expect(targetingResult()).to.include.all.keys(...expectedKeys); + }); + + it('should not be allowed together with allowTargetingKeys', function () { + config.setConfig({ + targetingControls: { + allowTargetingKeys: [CONSTANTS.TARGETING_KEYS.BIDDER], + addTargetingKeys: [CONSTANTS.TARGETING_KEYS.SOURCE] + } + }); + expect(targetingResult).to.throw(); + }); + }); + describe('targetingControls.allowSendAllBidsTargetingKeys', function () { let bid4; diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index 6494ead78e7..898b79cdcb5 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -2,6 +2,7 @@ import { getAdServerTargeting } from 'test/fixtures/fixtures.js'; import { expect } from 'chai'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; +import {waitForElementToLoad} from 'src/utils.js'; var assert = require('assert'); @@ -1198,4 +1199,41 @@ describe('Utils', function () { }); }); }); + + describe('waitForElementToLoad', () => { + let element; + let callbacks; + + function callback() { + callbacks++; + } + + function delay(delay = 0) { + return new Promise((resolve) => { + window.setTimeout(resolve, delay); + }) + } + + beforeEach(() => { + callbacks = 0; + element = window.document.createElement('div'); + }); + + it('should respect timeout if set', () => { + waitForElementToLoad(element, 50).then(callback); + return delay(60).then(() => { + expect(callbacks).to.equal(1); + }); + }); + + ['load', 'error'].forEach((event) => { + it(`should complete on '${event} event'`, () => { + waitForElementToLoad(element).then(callback); + element.dispatchEvent(new Event(event)); + return delay().then(() => { + expect(callbacks).to.equal(1); + }) + }); + }); + }); });