diff --git a/gulpfile.js b/gulpfile.js index d2955f7d777..cbebc29e3bf 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,7 +39,7 @@ var port = 9999; // Tasks gulp.task('default', ['webpack']); -gulp.task('serve', ['lint', 'build-bundle-dev', 'watch', 'test']); +gulp.task('serve', ['build-bundle-dev', 'watch']); gulp.task('serve-nw', ['lint', 'watch', 'e2etest']); @@ -49,8 +49,8 @@ gulp.task('build', ['build-bundle-prod']); gulp.task('clean', function () { return gulp.src(['build'], { - read: false - }) + read: false + }) .pipe(clean()); }); @@ -78,13 +78,13 @@ var explicitModules = [ function bundle(dev, moduleArr) { var modules = moduleArr || helpers.getArgModules(), - allModules = helpers.getModuleNames(modules); + allModules = helpers.getModuleNames(modules); - if(modules.length === 0) { + if (modules.length === 0) { modules = allModules.filter(module => !explicitModules.includes(module)); } else { var diff = _.difference(modules, allModules); - if(diff.length !== 0) { + if (diff.length !== 0) { throw new gutil.PluginError({ plugin: 'bundle', message: 'invalid modules: ' + diff.join(', ') @@ -106,13 +106,13 @@ function bundle(dev, moduleArr) { gutil.log('Generating bundle:', outputFileName); return gulp.src( - entries - ) + entries + ) .pipe(gulpif(dev, sourcemaps.init({loadMaps: true}))) .pipe(concat(outputFileName)) .pipe(gulpif(!argv.manualEnable, footer('\n<%= global %>.processQueue();', { - global: prebid.globalVarName - } + global: prebid.globalVarName + } ))) .pipe(gulpif(dev, sourcemaps.write('.'))); } @@ -229,11 +229,11 @@ gulp.task('watch', function () { 'modules/**/*.js', 'test/spec/**/*.js', '!test/spec/loaders/**/*.js' - ], ['lint', 'build-bundle-dev', 'test']); + ], ['build-bundle-dev']); gulp.watch([ 'loaders/**/*.js', 'test/spec/loaders/**/*.js' - ], ['lint']); + ], []); connect.server({ https: argv.https, port: port, @@ -264,7 +264,7 @@ gulp.task('docs', ['clean-docs'], function () { gulp.task('e2etest', ['devpack', 'webpack'], function() { var cmdQueue = []; - if(argv.browserstack) { + if (argv.browserstack) { var browsers = require('./browsers.json'); delete browsers['bs_ie_9_windows_7']; @@ -276,11 +276,11 @@ gulp.task('e2etest', ['devpack', 'webpack'], function() { var startWith = 'bs'; - Object.keys(browsers).filter(function(v){ + Object.keys(browsers).filter(function(v) { return v.substring(0, startWith.length) === startWith && browsers[v].browser !== 'iphone'; - }).map(function(v,i,arr) { - var newArr = (i%2 === 0) ? arr.slice(i,i+2) : null; - if(newArr) { + }).map(function(v, i, arr) { + var newArr = (i % 2 === 0) ? arr.slice(i, i + 2) : null; + if (newArr) { var cmd = 'nightwatch --env ' + newArr.join(',') + cmdStr; cmdQueue.push(cmd); } diff --git a/karma.conf.maker.js b/karma.conf.maker.js index 2ff1d7d0880..5d075b6929c 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -142,7 +142,8 @@ module.exports = function(codeCoverage, browserstack, watchMode, file) { reporters: ['mocha'], mochaReporter: { - showDiff: true + showDiff: true, + output: 'minimal' }, // Continuous Integration mode diff --git a/modules/aardvarkBidAdapter.js b/modules/aardvarkBidAdapter.js new file mode 100644 index 00000000000..7d358864b35 --- /dev/null +++ b/modules/aardvarkBidAdapter.js @@ -0,0 +1,159 @@ +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'aardvark'; +const DEFAULT_ENDPOINT = 'bidder.rtk.io'; +const SYNC_ENDPOINT = 'sync.rtk.io'; +const AARDVARK_TTL = 300; +const AARDVARK_CURRENCY = 'USD'; + +let hasSynced = false; + +export function resetUserSync() { + hasSynced = false; +} + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + return ((typeof bid.params.ai === 'string') && !!bid.params.ai.length && + (typeof bid.params.sc === 'string') && !!bid.params.sc.length); + }, + + buildRequests: function(validBidRequests, bidderRequest) { + var auctionCodes = []; + var requests = []; + var requestsMap = {}; + var referer = utils.getTopWindowUrl(); + var pageCategories = []; + + if (window.top.rtkcategories && Array.isArray(window.top.rtkcategories)) { + pageCategories = window.top.rtkcategories; + } + + utils._each(validBidRequests, function(b) { + var rMap = requestsMap[b.params.ai]; + if (!rMap) { + rMap = { + shortCodes: [], + payload: { + version: 1, + jsonp: false, + rtkreferer: referer + }, + endpoint: DEFAULT_ENDPOINT + }; + + if (pageCategories && pageCategories.length) { + rMap.payload.categories = pageCategories.slice(0); + } + + if (b.params.categories && b.params.categories.length) { + rMap.payload.categories = rMap.payload.categories || [] + utils._each(b.params.categories, function(cat) { + rMap.payload.categories.push(cat); + }); + } + + if (bidderRequest && bidderRequest.gdprConsent) { + rMap.payload.gdpr = false; + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + rMap.payload.gdpr = bidderRequest.gdprConsent.gdprApplies; + } + if (rMap.payload.gdpr) { + rMap.payload.consent = bidderRequest.gdprConsent.consentString; + } + } + + requestsMap[b.params.ai] = rMap; + auctionCodes.push(b.params.ai); + } + + rMap.shortCodes.push(b.params.sc); + rMap.payload[b.params.sc] = b.bidId; + + if ((typeof b.params.host === 'string') && b.params.host.length && + (b.params.host !== rMap.endpoint)) { + rMap.endpoint = b.params.host; + } + }); + + utils._each(auctionCodes, function(auctionId) { + var req = requestsMap[auctionId]; + requests.push({ + method: 'GET', + url: `//${req.endpoint}/${auctionId}/${req.shortCodes.join('_')}/aardvark`, + data: req.payload, + bidderRequest + }); + }); + + return requests; + }, + + interpretResponse: function(serverResponse, bidRequest) { + var bidResponses = []; + + if (!Array.isArray(serverResponse.body)) { + serverResponse.body = [serverResponse.body]; + } + + utils._each(serverResponse.body, function(rawBid) { + var bidResponse = { + requestId: rawBid.cid, + cpm: rawBid.cpm || 0, + width: rawBid.width || 0, + height: rawBid.height || 0, + currency: rawBid.currency ? rawBid.currency : AARDVARK_CURRENCY, + netRevenue: rawBid.netRevenue ? rawBid.netRevenue : true, + ttl: rawBid.ttl ? rawBid.ttl : AARDVARK_TTL, + creativeId: rawBid.creativeId || 0 + }; + + if (rawBid.hasOwnProperty('dealId')) { + bidResponse.dealId = rawBid.dealId + } + + switch (rawBid.media) { + case 'banner': + bidResponse.ad = rawBid.adm + utils.createTrackPixelHtml(decodeURIComponent(rawBid.nurl)); + break; + + default: + return utils.logError('bad Aardvark response (media)', rawBid); + } + + bidResponses.push(bidResponse); + }); + + return bidResponses; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { + const syncs = []; + var url = '//' + SYNC_ENDPOINT + '/cs'; + var gdprApplies = false; + if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean')) { + gdprApplies = gdprConsent.gdprApplies; + } + + if (syncOptions.iframeEnabled) { + if (!hasSynced) { + hasSynced = true; + if (gdprApplies) { + url = url + '?g=1&c=' + encodeURIComponent(gdprConsent.consentString); + } + syncs.push({ + type: 'iframe', + url: url + }); + } + } else { + utils.logWarn('Aardvark: Please enable iframe based user sync.'); + } + return syncs; + } +}; + +registerBidder(spec); diff --git a/modules/aardvarkBidAdapter.md b/modules/aardvarkBidAdapter.md new file mode 100644 index 00000000000..9f7a128b6f3 --- /dev/null +++ b/modules/aardvarkBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +**Module Name**: Aardvark Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: chris@rtk.io + +# Description + +Module that connects to a RTK.io Ad Units to fetch bids. + +# Test Parameters +``` + var adUnits = [{ + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + code: 'div-gpt-ad-1460505748561-0', + + bids: [{ + bidder: 'aardvark', + params: { + ai: '0000', + sc: '1234' + } + }] + + }]; +``` diff --git a/modules/consentManagement.js b/modules/consentManagement.js index c7b6ac4df92..09eb938f314 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -37,8 +37,9 @@ const cmpCallMap = { * based on the appropriate result. * @param {function(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP * @param {function(string)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) + * @param {[objects]} adUnits used in the safeframe workflow to know what sizes to include in the $sf.ext.register call */ -function lookupIabConsent(cmpSuccess, cmpError) { +function lookupIabConsent(cmpSuccess, cmpError, adUnits) { let cmpCallbacks; // check if the CMP is located on the same window level as the prebid code. @@ -47,10 +48,37 @@ function lookupIabConsent(cmpSuccess, cmpError) { // in this case, use the IAB's iframe locator sample code (which is slightly cutomized) to try to find the CMP and use postMessage() to communicate with the CMP. if (utils.isFn(window.__cmp)) { window.__cmp('getVendorConsents', null, cmpSuccess); + } else if (inASafeFrame() && typeof window.$sf.ext.cmp === 'function') { + callCmpWhileInSafeFrame(); } else { callCmpWhileInIframe(); } + function inASafeFrame() { + return !!(window.$sf && window.$sf.ext); + } + + function callCmpWhileInSafeFrame() { + function sfCallback(msgName, data) { + if (msgName === 'cmpReturn') { + cmpSuccess(data.vendorConsents); + } + } + + // find sizes from adUnits object + let width = 1; + let height = 1; + + if (Array.isArray(adUnits) && adUnits.length > 0) { + let sizes = utils.getAdUnitSizes(adUnits[0]); + width = sizes[0][0]; + height = sizes[0][1]; + } + + window.$sf.ext.register(width, height, sfCallback); + window.$sf.ext.cmp('getVendorConsents'); + } + function callCmpWhileInIframe() { /** * START OF STOCK CODE FROM IAB 1.1 CMP SPEC @@ -134,6 +162,7 @@ export function requestBidsHook(config, fn) { args = arguments; nextFn = fn; haveExited = false; + let adUnits = config.adUnits || $$PREBID_GLOBAL$$.adUnits; // in case we already have consent (eg during bid refresh) if (consentData) { @@ -145,7 +174,7 @@ export function requestBidsHook(config, fn) { return nextFn.apply(context, args); } - cmpCallMap[userCMP].call(this, processCmpData, cmpFailed); + cmpCallMap[userCMP].call(this, processCmpData, cmpFailed, adUnits); // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished) if (!haveExited) { @@ -245,6 +274,7 @@ function exitModule(errMsg) { */ export function resetConsentData() { consentData = undefined; + gdprDataHandler.setConsentData(null); } /** diff --git a/modules/gxoneBidAdapter.js b/modules/gxoneBidAdapter.js new file mode 100644 index 00000000000..77c5ae2b1b7 --- /dev/null +++ b/modules/gxoneBidAdapter.js @@ -0,0 +1,141 @@ +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; +const BIDDER_CODE = 'gxone'; +const ENDPOINT_URL = '//ads.gx1as.com/hb'; +const TIME_TO_LIVE = 360; +const ADAPTER_SYNC_URL = '//ads.gx1as.com/push_sync'; +const LOG_ERROR_MESS = { + noAuid: 'Bid from response has no auid parameter - ', + noAdm: 'Bid from response has no adm parameter - ', + noBid: 'Array of bid objects is empty', + noPlacementCode: 'Can\'t find in requested bids the bid with auid - ', + emptyUids: 'Uids should be not empty', + emptySeatbid: 'Seatbid array from response has empty item', + emptyResponse: 'Response is empty', + hasEmptySeatbidArray: 'Response has empty seatbid array', + hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' +}; + +/** + * GXOne Bid Adapter. + * Contact: olivier@geronimo.co + * + */ +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + return !!bid.params.uid; + }, + + buildRequests: function(validBidRequests) { + const auids = []; + const bidsMap = {}; + const bids = validBidRequests || []; + let priceType = 'net'; + let reqId; + + bids.forEach(bid => { + if (bid.params.priceType === 'gross') { + priceType = 'gross'; + } + if (!bidsMap[bid.params.uid]) { + bidsMap[bid.params.uid] = [bid]; + auids.push(bid.params.uid); + } else { + bidsMap[bid.params.uid].push(bid); + } + reqId = bid.bidderRequestId; + }); + + const payload = { + u: utils.getTopWindowUrl(), + pt: priceType, + auids: auids.join(','), + r: reqId, + }; + + return { + method: 'GET', + url: ENDPOINT_URL, + data: utils.parseQueryStringParameters(payload).replace(/\&$/, ''), + bidsMap: bidsMap, + }; + }, + + interpretResponse: function(serverResponse, bidRequest) { + serverResponse = serverResponse && serverResponse.body + const bidResponses = []; + const bidsMap = bidRequest.bidsMap; + const priceType = bidRequest.data.pt; + + let errorMessage; + + if (!serverResponse) errorMessage = LOG_ERROR_MESS.emptyResponse; + else if (serverResponse.seatbid && !serverResponse.seatbid.length) { + errorMessage = LOG_ERROR_MESS.hasEmptySeatbidArray; + } + + if (!errorMessage && serverResponse.seatbid) { + serverResponse.seatbid.forEach(respItem => { + _addBidResponse(_getBidFromResponse(respItem), bidsMap, priceType, bidResponses); + }); + } + if (errorMessage) utils.logError(errorMessage); + return bidResponses; + }, + + getUserSyncs: function(syncOptions) { + if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: ADAPTER_SYNC_URL + }]; + } + } +} + +function _getBidFromResponse(respItem) { + if (!respItem) { + utils.logError(LOG_ERROR_MESS.emptySeatbid); + } else if (!respItem.bid) { + utils.logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); + } else if (!respItem.bid[0]) { + utils.logError(LOG_ERROR_MESS.noBid); + } + return respItem && respItem.bid && respItem.bid[0]; +} + +function _addBidResponse(serverBid, bidsMap, priceType, bidResponses) { + if (!serverBid) return; + let errorMessage; + if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid); + if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); + else { + const awaitingBids = bidsMap[serverBid.auid]; + if (awaitingBids) { + awaitingBids.forEach(bid => { + const bidResponse = { + requestId: bid.bidId, // bid.bidderRequestId, + cpm: serverBid.price, + width: serverBid.w, + height: serverBid.h, + creativeId: serverBid.auid, // bid.bidId, + currency: 'USD', + netRevenue: priceType !== 'gross', + ttl: TIME_TO_LIVE, + ad: serverBid.adm, + dealId: serverBid.dealid + }; + bidResponses.push(bidResponse); + }); + } else { + errorMessage = LOG_ERROR_MESS.noPlacementCode + serverBid.auid; + } + } + if (errorMessage) { + utils.logError(errorMessage); + } +} + +registerBidder(spec); diff --git a/modules/gxoneBidAdapter.md b/modules/gxoneBidAdapter.md new file mode 100755 index 00000000000..3168d297da3 --- /dev/null +++ b/modules/gxoneBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +Module Name: GXOne Bidder Adapter +Module Type: Bidder Adapter +Maintainer: olivier@geronimo.co + +# Description + +Module that connects to GXOne demand source to fetch bids. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "gxone", + params: { + uid: '2', + priceType: 'gross' // by default is 'net' + } + } + ] + },{ + code: 'test-div', + sizes: [[728, 90]], + bids: [ + { + bidder: "gxone", + params: { + uid: 9, + priceType: 'gross' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/lifestreetBidAdapter.js b/modules/lifestreetBidAdapter.js index 919e83576d3..17aeeb56f2e 100644 --- a/modules/lifestreetBidAdapter.js +++ b/modules/lifestreetBidAdapter.js @@ -12,7 +12,7 @@ const urlTemplate = template`//ads.lfstmedia.com/gate/${'adapter'}/${'slot'}?adk * * @param {BidRequest} bid The bid params to use for formatting a request */ -function formatBidRequest(bid) { +function formatBidRequest(bid, bidderRequest) { let url = urlTemplate({ adapter: 'prebid', slot: bid.params.slot, @@ -28,6 +28,16 @@ function formatBidRequest(bid) { hbver: ADAPTER_VERSION }); + if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + const gdpr = '&__gdpr=' + (bidderRequest.gdprConsent.gdprApplies ? '1' : '0'); + url += gdpr; + } + if (bidderRequest.gdprConsent.consentString !== undefined) { + url += '&__consent=' + bidderRequest.gdprConsent.consentString; + } + } + return { method: 'GET', url: url, @@ -95,9 +105,9 @@ export const spec = { * @param {validBidRequests[]} - an array of bids * @return ServerRequest Info describing the request to the server. */ - buildRequests: function(validBidRequests) { + buildRequests: function(validBidRequests, bidderRequest) { return validBidRequests.map(bid => { - return formatBidRequest(bid) + return formatBidRequest(bid, bidderRequest) }); }, diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index 8fe09ab74e6..08232231417 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -71,11 +71,16 @@ function getSize(size) { } } -function configuredParams(params) { - return { +function extParams(params, gdpr) { + let ext = { customer_id: params.cid, prebid_version: $$PREBID_GLOBAL$$.version + }; + ext.gdpr_applies = !!(gdpr && gdpr.gdprApplies); + if (ext.gdpr_applies) { + ext.gdpr_consent_string = gdpr.consentString || ''; } + return ext; } function slotParams(bidRequest) { @@ -100,13 +105,13 @@ function slotParams(bidRequest) { return params; } -function generatePayload(bidRequests, timeout) { +function generatePayload(bidRequests, bidderRequests) { return { site: siteDetails(bidRequests[0].params.site), - ext: configuredParams(bidRequests[0].params), + ext: extParams(bidRequests[0].params, bidderRequests.gdprConsent), id: bidRequests[0].auctionId, imp: bidRequests.map(request => slotParams(request)), - tmax: timeout + tmax: bidderRequests.timeout || config.getConfig('bidderTimeout') } } @@ -153,12 +158,11 @@ export const spec = { * Make a server request from the list of BidRequests. * * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @param {BidderRequests} bidderRequests * @return ServerRequest Info describing the request to the server. */ - buildRequests: function(bidRequests, auctionData) { - let timeout = auctionData.timeout || config.getConfig('bidderTimeout'); - let payload = generatePayload(bidRequests, timeout); - + buildRequests: function(bidRequests, bidderRequests) { + let payload = generatePayload(bidRequests, bidderRequests); return { method: 'POST', url: BID_URL, diff --git a/modules/playgroundxyzBidAdapter.js b/modules/playgroundxyzBidAdapter.js new file mode 100644 index 00000000000..527ecf00037 --- /dev/null +++ b/modules/playgroundxyzBidAdapter.js @@ -0,0 +1,374 @@ +import { Renderer } from 'src/Renderer'; +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { BANNER, VIDEO } from 'src/mediaTypes'; +import find from 'core-js/library/fn/array/find'; +import includes from 'core-js/library/fn/array/includes'; + +const BIDDER_CODE = 'playgroundxyz'; +const URL = 'https://ads.playground.xyz/host-config/prebid'; +const VIDEO_TARGETING = ['id', 'mimes', 'minduration', 'maxduration', + 'startdelay', 'skippable', 'playback_method', 'frameworks']; +const USER_PARAMS = ['age', 'external_uid', 'segments', 'gender', 'dnt', 'language']; +const SOURCE = 'pbjs'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['playgroundxyz'], + supportedMediaTypes: [BANNER, VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid.params.placementId || (bid.params.member && bid.params.invCode)); + }, + + /** + * 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(bidToTag); + const userObjBid = find(bidRequests, hasUserInfo); + let userObj; + if (userObjBid) { + userObj = {}; + Object.keys(userObjBid.params.user) + .filter(param => includes(USER_PARAMS, param)) + .forEach(param => userObj[param] = userObjBid.params.user[param]); + } + + const memberIdBid = find(bidRequests, hasMemberId); + const member = memberIdBid ? parseInt(memberIdBid.params.member, 10) : 0; + + const payload = { + tags: [...tags], + user: userObj, + sdk: { + source: SOURCE, + version: '$prebid.version$' + } + }; + if (member > 0) { + payload.member_id = member; + } + + if (bidderRequest && bidderRequest.gdprConsent) { + // note - objects for impbus use underscore instead of camelCase + payload.gdpr_consent = { + consent_string: bidderRequest.gdprConsent.consentString, + consent_required: bidderRequest.gdprConsent.gdprApplies + }; + } + const payloadString = JSON.stringify(payload); + + return { + method: 'POST', + url: URL, + data: payloadString, + 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 || serverResponse.error) { + let errorMessage = `in response for ${bidderRequest.bidderCode} adapter`; + if (serverResponse && serverResponse.error) { errorMessage += `: ${serverResponse.error}`; } + utils.logError(errorMessage); + return bids; + } + + if (serverResponse.tags) { + // sort tags by cpm + serverResponse.tags.sort(function (x, y) { + return y.cpm - x.cpm; + }); + + serverResponse.tags.forEach((serverBid, index) => { + 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); + // set cpm to 0 if it's not the first one + bid.cpm = index === 0 ? bid.cpm : 0; + bids.push(bid); + } + } + }); + } + return bids; + }, + + getUserSyncs: function (syncOptions) { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: '//acdn.adnxs.com/ib/static/usersync/v3/async_usersync.html' + }]; + } + } +} + +function newRenderer(adUnitCode, rtbBid, rendererOptions = {}) { + const renderer = Renderer.install({ + id: rtbBid.renderer_id, + url: rtbBid.renderer_url, + config: rendererOptions, + loaded: false, + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + renderer.setEventHandlers({ + impression: () => utils.logMessage('PlaygroundXYZ outstream video impression event'), + loaded: () => utils.logMessage('PlaygroundXYZ outstream video loaded event'), + ended: () => { + utils.logMessage('PlaygroundXYZ outstream renderer video event'); + document.querySelector(`#${adUnitCode}`).style.display = 'none'; + } + }); + return renderer; +} + +/* Turn keywords parameter into ut-compatible format */ +function getKeywords(keywords) { + let arrs = []; + + utils._each(keywords, (v, k) => { + if (utils.isArray(v)) { + let values = []; + utils._each(v, (val) => { + val = utils.getValueString('keywords.' + k, val); + if (val) { values.push(val); } + }); + v = values; + } else { + v = utils.getValueString('keywords.' + k, v); + if (utils.isStr(v)) { + v = [v]; + } else { + return; + } // unsuported types - don't send a key + } + arrs.push({ key: k, value: v }); + }); + + return arrs; +} + +/** + * Unpack the Server's Bid into a Prebid-compatible one. + * @param serverBid + * @param rtbBid + * @param bidderRequest + * @return Bid + */ +function newBid(serverBid, rtbBid, bidderRequest) { + const bid = { + requestId: serverBid.uuid, + cpm: rtbBid.cpm, + creativeId: rtbBid.creative_id, + dealId: rtbBid.deal_id, + currency: 'USD', + netRevenue: true, + ttl: 300, + playgroundxyz: { + buyerMemberId: rtbBid.buyer_member_id + } + }; + + if (rtbBid.rtb.video) { + Object.assign(bid, { + width: rtbBid.rtb.video.player_width, + height: rtbBid.rtb.video.player_height, + vastUrl: rtbBid.rtb.video.asset_url, + vastImpUrl: rtbBid.notify_url, + ttl: 3600 + }); + // This supports Outstream Video + if (rtbBid.renderer_url) { + const rendererOptions = utils.deepAccess( + bidderRequest.bids[0], + 'renderer.options' + ); + + Object.assign(bid, { + adResponse: serverBid, + renderer: newRenderer(bid.adUnitCode, rtbBid, rendererOptions) + }); + bid.adResponse.ad = bid.adResponse.ads[0]; + bid.adResponse.ad.video = bid.adResponse.ad.rtb.video; + } + } else { + Object.assign(bid, { + width: rtbBid.rtb.banner.width, + height: rtbBid.rtb.banner.height, + ad: rtbBid.rtb.banner.content + }); + try { + const url = rtbBid.rtb.trackers[0].impression_urls[0]; + const tracker = utils.createTrackPixelHtml(url); + bid.ad += tracker; + } catch (error) { + utils.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; + if (bid.params.reserve) { + tag.reserve = bid.params.reserve; + } + 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.externalImpId) { + tag.external_imp_id = bid.params.externalImpId; + } + if (!utils.isEmpty(bid.params.keywords)) { + tag.keywords = getKeywords(bid.params.keywords); + } + + const videoMediaType = utils.deepAccess(bid, `mediaTypes.${VIDEO}`); + const context = utils.deepAccess(bid, 'mediaTypes.video.context'); + + 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 => tag.video[param] = bid.params.video[param]); + } + + if ( + (utils.isEmpty(bid.mediaType) && utils.isEmpty(bid.mediaTypes)) || + (bid.mediaType === BANNER || (bid.mediaTypes && bid.mediaTypes[BANNER])) + ) { + tag.ad_types.push(BANNER); + } + + return tag; +} + +/* Turn bid request sizes into ut-compatible format */ +function transformSizes(requestSizes) { + let sizes = []; + let sizeObj = {}; + + if (utils.isArray(requestSizes) && requestSizes.length === 2 && + !utils.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 getRtbBid(tag) { + return tag && tag.ads && tag.ads.length && find(tag.ads, ad => ad.rtb); +} + +function outstreamRender(bid) { + // 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 { + return BANNER; + } +} + +registerBidder(spec); diff --git a/modules/playgroundxyzBidAdapter.md b/modules/playgroundxyzBidAdapter.md new file mode 100644 index 00000000000..1f675167b4a --- /dev/null +++ b/modules/playgroundxyzBidAdapter.md @@ -0,0 +1,103 @@ +# Overview + +``` +Module Name: Appnexus Bid Adapter +Module Type: Bidder Adapter +Maintainer: info@prebid.org +``` + +# Description + +Connects to playgroundxyz ad server for bids. + +Appnexus bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + sizes: [[300, 250], [300,600]], + bids: [{ + bidder: 'playgroundxyz', + params: { + placementId: '10433394' + } + }] + }, + // Native adUnit + { + code: 'native-div', + sizes: [[300, 250], [300,600]], + mediaTypes: { + native: { + title: { + required: true, + len: 80 + }, + body: { + required: true + }, + brand: { + required: true + }, + image: { + required: true + }, + clickUrl: { + required: true + }, + } + }, + bids: [{ + bidder: 'playgroundxyz', + params: { + placementId: '9880618' + } + }] + }, + // Video instream adUnit + { + code: 'video-instream', + sizes: [640, 480], + mediaTypes: { + video: { + context: 'instream' + }, + }, + bids: [{ + bidder: 'appnexus', + params: { + placementId: '9333431', + video: { + skippable: true, + playback_methods: ['auto_play_sound_off'] + } + } + }] + }, + // Video outstream adUnit + { + code: 'video-outstream', + sizes: [[640, 480]], + mediaTypes: { + video: { + context: 'outstream' + } + }, + bids: [ + { + bidder: 'appnexus', + params: { + placementId: '5768085', + video: { + skippable: true, + playback_method: ['auto_play_sound_off'] + } + } + } + ] + } +]; +``` diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index f499f5a0ae4..58a95ea366e 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -37,7 +37,7 @@ const availVendorDefaults = { adapter: 'prebidServer', cookieSet: false, enabled: true, - endpoint: '//prebid.adnxs.com/pbs/v1/auction', + endpoint: '//prebid.adnxs.com/pbs/v1/openrtb2/auction', syncEndpoint: '//prebid.adnxs.com/pbs/v1/cookie_sync', timeout: 1000 }, @@ -96,36 +96,56 @@ function setS2sConfig(options) { } _s2sConfig = options; - if (options.syncEndpoint) { - queueSync(options.bidders); - } } getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig)); +/** + * resets the _synced variable back to false, primiarily used for testing purposes +*/ +export function resetSyncedStatus() { + _synced = false; +} + /** * @param {Array} bidderCodes list of bidders to request user syncs for. */ -function queueSync(bidderCodes) { +function queueSync(bidderCodes, gdprConsent) { if (_synced) { return; } _synced = true; - const payload = JSON.stringify({ + + const payload = { uuid: utils.generateUUID(), bidders: bidderCodes - }); - ajax(_s2sConfig.syncEndpoint, (response) => { - try { - response = JSON.parse(response); - response.bidder_status.forEach(bidder => doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder)); - } catch (e) { - utils.logError(e); + }; + + if (gdprConsent) { + // only populate gdpr field if we know CMP returned consent information (ie didn't timeout or have an error) + if (gdprConsent.consentString) { + payload.gdpr = (gdprConsent.gdprApplies) ? 1 : 0; } - }, - payload, { - contentType: 'text/plain', - withCredentials: true - }); + // attempt to populate gdpr_consent if we know gdprApplies or it may apply + if (gdprConsent.gdprApplies !== false) { + payload.gdpr_consent = gdprConsent.consentString; + } + } + const jsonPayload = JSON.stringify(payload); + + ajax(_s2sConfig.syncEndpoint, + (response) => { + try { + response = JSON.parse(response); + response.bidder_status.forEach(bidder => doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder)); + } catch (e) { + utils.logError(e); + } + }, + jsonPayload, + { + contentType: 'text/plain', + withCredentials: true + }); } /** @@ -348,9 +368,6 @@ const LEGACY_PROTOCOL = { if (result.status === 'OK' || result.status === 'no_cookie') { if (result.bidder_status) { result.bidder_status.forEach(bidder => { - if (bidder.no_cookie) { - doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder); - } if (bidder.error) { utils.logWarn(`Prebid Server returned error: '${bidder.error}' for ${bidder.bidder}`); } @@ -637,7 +654,7 @@ const OPEN_RTB_PROTOCOL = { * const bids = protocol().interpretResponse(response, bidRequests, requestedBidders); */ const protocolAdapter = () => { - const OPEN_RTB_PATH = 'openrtb2/auction'; + const OPEN_RTB_PATH = '/openrtb2/'; const endpoint = (_s2sConfig && _s2sConfig.endpoint) || ''; const isOpenRtb = ~endpoint.indexOf(OPEN_RTB_PATH); @@ -666,6 +683,11 @@ export function PrebidServer() { .reduce(utils.flatten) .filter(utils.uniques); + if (_s2sConfig && _s2sConfig.syncEndpoint) { + let consent = (Array.isArray(bidRequests) && bidRequests.length > 0) ? bidRequests[0].gdprConsent : undefined; + queueSync(_s2sConfig.bidders, consent); + } + const request = protocolAdapter().buildRequest(s2sBidRequest, bidRequests, adUnitsWithSizes); const requestJson = JSON.stringify(request); diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 14da2c45164..989ce180463 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -268,27 +268,32 @@ export const spec = { interpretResponse: (response, request) => { const bidResponses = []; try { - if (response.body && response.body.seatbid && response.body.seatbid[0] && response.body.seatbid[0].bid) { - response.body.seatbid[0].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: CURRENCY, - netRevenue: NET_REVENUE, - ttl: 300, - referrer: utils.getTopWindowUrl(), - ad: bid.adm - }; - - if (bid.ext && bid.ext.deal_channel) { - newBid['dealChannel'] = dealChannelValues[bid.ext.deal_channel] || null; - } - - bidResponses.push(newBid); + if (response.body && response.body.seatbid && utils.isArray(response.body.seatbid)) { + // Supporting multiple bid responses for same adSize + response.body.seatbid.forEach(seatbidder => { + seatbidder.bid && + utils.isArray(seatbidder.bid) && + seatbidder.bid.forEach(bid => { + let newBid = { + requestId: bid.impid, + cpm: (parseFloat(bid.price) || 0).toFixed(2), + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.id, + dealId: bid.dealid, + currency: CURRENCY, + netRevenue: NET_REVENUE, + ttl: 300, + referrer: utils.getTopWindowUrl(), + ad: bid.adm + }; + + if (bid.ext && bid.ext.deal_channel) { + newBid['dealChannel'] = dealChannelValues[bid.ext.deal_channel] || null; + } + + bidResponses.push(newBid); + }); }); } } catch (error) { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index d6a69fda625..061440e7c0e 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -37,6 +37,7 @@ var sizeMap = { 43: '320x50', 44: '300x50', 48: '300x300', + 53: '1024x768', 54: '300x1050', 55: '970x90', 57: '970x250', @@ -232,8 +233,8 @@ export const spec = { 'p_screen_res', _getScreenResolution(), 'kw', keywords, 'tk_user_key', userId, - 'p_geo.latitude', parseFloat(latitude).toFixed(4), - 'p_geo.longitude', parseFloat(longitude).toFixed(4) + 'p_geo.latitude', isNaN(parseFloat(latitude)) ? undefined : parseFloat(latitude).toFixed(4), + 'p_geo.longitude', isNaN(parseFloat(longitude)) ? undefined : parseFloat(longitude).toFixed(4) ]; if (gdprConsent) { @@ -357,12 +358,24 @@ export const spec = { return bids; }, []); }, - getUserSyncs: function (syncOptions) { + getUserSyncs: function (syncOptions, responses, gdprConsent) { if (!hasSynced && syncOptions.iframeEnabled) { + // data is only assigned if params are available to pass to SYNC_ENDPOINT + let params = ''; + + if (gdprConsent && typeof gdprConsent.consentString === 'string') { + // add 'gdpr' only if 'gdprApplies' is defined + if (typeof gdprConsent.gdprApplies === 'boolean') { + params += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + params += `?gdpr_consent=${gdprConsent.consentString}`; + } + } + hasSynced = true; return { type: 'iframe', - url: SYNC_ENDPOINT + url: SYNC_ENDPOINT + params }; } } diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index 7db4747927a..0767a51e545 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -22,9 +22,10 @@ export const spec = { * Make a server request from the list of BidRequests. * * @param {validBidRequests[]} - an array of bids + * @param {bidderRequest} - bidder request object * @return ServerRequest Info describing the request to the server. */ - buildRequests: function (validBidRequests) { + buildRequests: function (validBidRequests, bidderRequest) { // use bidderRequest.bids[] to get bidder-dependent request info // if your bidder supports multiple currencies, use config.getConfig(currency) @@ -53,6 +54,12 @@ export const spec = { bidId: bid.bidId, prebidVersion: '$prebid.version$' }; + + if (bidderRequest && bidderRequest.gdprConsent) { + payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side + } + var payloadString = JSON.stringify(payload); return { method: 'POST', diff --git a/package.json b/package.json index 28cb310599e..7e036004973 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "1.11.0-pre", + "version": "1.12.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { diff --git a/src/auction.js b/src/auction.js index 8a23605bf0e..654b6dbb189 100644 --- a/src/auction.js +++ b/src/auction.js @@ -298,8 +298,8 @@ function getPreparedBidForAuction({adUnitCode, bid, bidRequest, auctionId}) { events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, bidObject); // a publisher-defined renderer can be used to render bids - const adUnitRenderer = - bidRequest.bids && bidRequest.bids[0] && bidRequest.bids[0].renderer; + const bidReq = bidRequest.bids && find(bidRequest.bids, bid => bid.adUnitCode == adUnitCode); + const adUnitRenderer = bidReq && bidReq.renderer; if (adUnitRenderer && adUnitRenderer.url) { bidObject.renderer = Renderer.install({ url: adUnitRenderer.url }); diff --git a/src/utils.js b/src/utils.js index cf977124dd1..c595101bae1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -107,6 +107,35 @@ exports.transformAdServerTargetingObj = function (targeting) { } }; +/** + * Read an adUnit object and return the sizes used in an [[728, 90]] format (even if they had [728, 90] defined) + * Preference is given to the `adUnit.mediaTypes.banner.sizes` object over the `adUnit.sizes` + * @param {object} adUnit one adUnit object from the normal list of adUnits + * @returns {array[array[number]]} array of arrays containing numeric sizes + */ +export function getAdUnitSizes(adUnit) { + if (!adUnit) { + return; + } + + let sizes = []; + if (adUnit.mediaTypes && adUnit.mediaTypes.banner && Array.isArray(adUnit.mediaTypes.banner.sizes)) { + let bannerSizes = adUnit.mediaTypes.banner.sizes; + if (Array.isArray(bannerSizes[0])) { + sizes = bannerSizes; + } else { + sizes.push(bannerSizes); + } + } else if (Array.isArray(adUnit.sizes)) { + if (Array.isArray(adUnit.sizes[0])) { + sizes = adUnit.sizes; + } else { + sizes.push(adUnit.sizes); + } + } + return sizes; +} + /** * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']' * @param {array[array|number]} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index ef50d2b6294..6fbc48b3cdc 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -7,6 +7,7 @@ import { newBidder, registerBidder } from 'src/adapters/bidderFactory'; import { config } from 'src/config'; import * as store from 'src/videoCache'; import * as ajaxLib from 'src/ajax'; +import find from 'core-js/library/fn/array/find'; const adloader = require('../../src/adloader'); var assert = require('assert'); @@ -643,6 +644,35 @@ describe('auctionmanager.js', function () { const addedBid = auction.getBidsReceived().pop(); assert.equal(addedBid.renderer.url, 'renderer.js'); }); + + it('bid for a regular unit and a video unit', function() { + let renderer = { + url: 'renderer.js', + render: (bid) => bid + }; + + // make sure that if the renderer is only on the second ad unit, prebid + // still correctly uses it + let bid = mockBid(); + let bidRequests = [mockBidRequest(bid)]; + + bidRequests[0].bids[1] = Object.assign({ + renderer, + bidId: utils.getUniqueIdentifierStr() + }, bidRequests[0].bids[0]); + bidRequests[0].bids[0].adUnitCode = ADUNIT_CODE1; + + makeRequestsStub.returns(bidRequests); + + // this should correspond with the second bid in the bidReq because of the ad unit code + bid.mediaType = 'video-outstream'; + spec.interpretResponse.returns(bid); + + auction.callBids(); + + const addedBid = find(auction.getBidsReceived(), bid => bid.adUnitCode == ADUNIT_CODE); + assert.equal(addedBid.renderer.url, 'renderer.js'); + }); }); describe('when auction timeout is 20', () => { diff --git a/test/spec/modules/aardvarkBidAdapter_spec.js b/test/spec/modules/aardvarkBidAdapter_spec.js new file mode 100644 index 00000000000..d2b9cbc0fa8 --- /dev/null +++ b/test/spec/modules/aardvarkBidAdapter_spec.js @@ -0,0 +1,264 @@ +import { expect } from 'chai'; +import { spec } from 'modules/aardvarkBidAdapter'; + +describe('aardvarkAdapterTest', () => { + describe('forming valid bidRequests', () => { + it('should accept valid bidRequests', () => { + expect(spec.isBidRequestValid({ + bidder: 'aardvark', + params: { + ai: 'xiby', + sc: 'TdAx', + }, + sizes: [[300, 250]] + })).to.equal(true); + }); + + it('should reject invalid bidRequests', () => { + expect(spec.isBidRequestValid({ + bidder: 'aardvark', + params: { + ai: 'xiby', + }, + sizes: [[300, 250]] + })).to.equal(false); + }); + }); + + describe('executing network requests', () => { + const bidRequests = [{ + bidder: 'aardvark', + params: { + ai: 'xiby', + sc: 'TdAx', + }, + adUnitCode: 'aaa', + transactionId: '1b8389fe-615c-482d-9f1a-177fb8f7d5b0', + sizes: [300, 250], + bidId: '1abgs362e0x48a8', + bidderRequestId: '70deaff71c281d', + auctionId: '5c66da22-426a-4bac-b153-77360bef5337' + }, + { + bidder: 'aardvark', + params: { + ai: 'xiby', + sc: 'RAZd', + host: 'adzone.pub.com' + }, + adUnitCode: 'bbb', + transactionId: '193995b4-7122-4739-959b-2463282a138b', + sizes: [[800, 600]], + bidId: '22aidtbx5eabd9', + bidderRequestId: '70deaff71c281d', + auctionId: 'e97cafd0-ebfc-4f5c-b7c9-baa0fd335a4a' + }]; + + it('should use HTTP GET method', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach(function(requestItem) { + expect(requestItem.method).to.equal('GET'); + }); + }); + + it('should call the correct bidRequest url', () => { + const requests = spec.buildRequests(bidRequests); + expect(requests.length).to.equal(1); + expect(requests[0].url).to.match(new RegExp('^\/\/adzone.pub.com/xiby/TdAx_RAZd/aardvark\?')); + }); + + it('should have correct data', () => { + const requests = spec.buildRequests(bidRequests); + expect(requests.length).to.equal(1); + expect(requests[0].data.version).to.equal(1); + expect(requests[0].data.jsonp).to.equal(false); + expect(requests[0].data.TdAx).to.equal('1abgs362e0x48a8'); + expect(requests[0].data.rtkreferer).to.not.be.undefined; + expect(requests[0].data.RAZd).to.equal('22aidtbx5eabd9'); + }); + }); + + describe('splitting multi-auction ad units into own requests', () => { + const bidRequests = [{ + bidder: 'aardvark', + params: { + ai: 'Toby', + sc: 'TdAx', + categories: ['cat1', 'cat2'] + }, + adUnitCode: 'aaa', + transactionId: '1b8389fe-615c-482d-9f1a-177fb8f7d5b0', + sizes: [300, 250], + bidId: '1abgs362e0x48a8', + bidderRequestId: '70deaff71c281d', + auctionId: '5c66da22-426a-4bac-b153-77360bef5337' + }, + { + bidder: 'aardvark', + params: { + ai: 'xiby', + sc: 'RAZd', + host: 'adzone.pub.com' + }, + adUnitCode: 'bbb', + transactionId: '193995b4-7122-4739-959b-2463282a138b', + sizes: [[800, 600]], + bidId: '22aidtbx5eabd9', + bidderRequestId: '70deaff71c281d', + auctionId: 'e97cafd0-ebfc-4f5c-b7c9-baa0fd335a4a' + }]; + + it('should use HTTP GET method', () => { + const requests = spec.buildRequests(bidRequests); + requests.forEach(function(requestItem) { + expect(requestItem.method).to.equal('GET'); + }); + }); + + it('should call the correct bidRequest urls for each auction', () => { + const requests = spec.buildRequests(bidRequests); + expect(requests[0].url).to.match(new RegExp('^\/\/bidder.rtk.io/Toby/TdAx/aardvark\?')); + expect(requests[0].data.categories.length).to.equal(2); + expect(requests[1].url).to.match(new RegExp('^\/\/adzone.pub.com/xiby/RAZd/aardvark\?')); + }); + + it('should have correct data', () => { + const requests = spec.buildRequests(bidRequests); + expect(requests.length).to.equal(2); + expect(requests[0].data.version).to.equal(1); + expect(requests[0].data.jsonp).to.equal(false); + expect(requests[0].data.TdAx).to.equal('1abgs362e0x48a8'); + expect(requests[0].data.rtkreferer).to.not.be.undefined; + expect(requests[0].data.RAZd).to.be.undefined; + expect(requests[1].data.version).to.equal(1); + expect(requests[1].data.jsonp).to.equal(false); + expect(requests[1].data.TdAx).to.be.undefined; + expect(requests[1].data.rtkreferer).to.not.be.undefined; + expect(requests[1].data.RAZd).to.equal('22aidtbx5eabd9'); + }); + }); + + describe('GDPR conformity', () => { + const bidRequests = [{ + bidder: 'aardvark', + params: { + ai: 'xiby', + sc: 'TdAx', + }, + adUnitCode: 'aaa', + transactionId: '1b8389fe-615c-482d-9f1a-177fb8f7d5b0', + sizes: [300, 250], + bidId: '1abgs362e0x48a8', + bidderRequestId: '70deaff71c281d', + auctionId: '5c66da22-426a-4bac-b153-77360bef5337' + }]; + + const bidderRequest = { + gdprConsent: { + consentString: 'awefasdfwefasdfasd', + gdprApplies: true + } + }; + + it('should transmit correct data', () => { + const requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests.length).to.equal(1); + expect(requests[0].data.gdpr).to.equal(true); + expect(requests[0].data.consent).to.equal('awefasdfwefasdfasd'); + }); + }); + + describe('GDPR absence conformity', () => { + const bidRequests = [{ + bidder: 'aardvark', + params: { + ai: 'xiby', + sc: 'TdAx', + }, + adUnitCode: 'aaa', + transactionId: '1b8389fe-615c-482d-9f1a-177fb8f7d5b0', + sizes: [300, 250], + bidId: '1abgs362e0x48a8', + bidderRequestId: '70deaff71c281d', + auctionId: '5c66da22-426a-4bac-b153-77360bef5337' + }]; + + const bidderRequest = { + gdprConsent: undefined + }; + + it('should transmit correct data', () => { + const requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests.length).to.equal(1); + expect(requests[0].data.gdpr).to.be.undefined; + expect(requests[0].data.consent).to.be.undefined; + }); + }); + + describe('interpretResponse', () => { + it('should handle bid responses', () => { + const serverResponse = { + body: [ + { + media: 'banner', + nurl: 'http://www.nurl.com/0', + cpm: 0.09, + width: 300, + height: 250, + cid: '22aidtbx5eabd9', + adm: '', + dealId: 'dealing', + ttl: 200, + }, + { + media: 'banner', + nurl: 'http://www.nurl.com/1', + cpm: 0.19, + width: 300, + height: 250, + cid: '1abgs362e0x48a8', + adm: '', + ttl: 200, + } + ], + headers: {} + }; + + const result = spec.interpretResponse(serverResponse, {}); + expect(result.length).to.equal(2); + + expect(result[0].requestId).to.equal('22aidtbx5eabd9'); + expect(result[0].cpm).to.equal(0.09); + expect(result[0].width).to.equal(300); + expect(result[0].height).to.equal(250); + expect(result[0].currency).to.equal('USD'); + expect(result[0].ttl).to.equal(200); + expect(result[0].dealId).to.equal('dealing'); + expect(result[0].ad).to.not.be.undefined; + + expect(result[1].requestId).to.equal('1abgs362e0x48a8'); + expect(result[1].cpm).to.equal(0.19); + expect(result[1].width).to.equal(300); + expect(result[1].height).to.equal(250); + expect(result[1].currency).to.equal('USD'); + expect(result[1].ttl).to.equal(200); + expect(result[1].ad).to.not.be.undefined; + }); + + it('should handle nobid responses', () => { + var emptyResponse = [{ + nurl: '', + cid: '9e5a09319e18f1', + media: 'banner', + error: 'No bids received for 9DgF', + adm: '', + id: '9DgF', + cpm: 0.00 + }]; + + var result = spec.interpretResponse({ body: emptyResponse }, {}); + expect(result.length).to.equal(1); + expect(result[0].cpm).to.equal(0.0); + }); + }); +}); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 5974ac79324..a837a0cce8a 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -77,7 +77,7 @@ describe('consentManagement', function () { utils.logWarn.restore(); config.resetConfig(); $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); - gdprDataHandler.consentData = null; + resetConsentData(); }); it('should return Warning message and return to hooked function', () => { @@ -113,7 +113,7 @@ describe('consentManagement', function () { $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); cmpStub.restore(); delete window.__cmp; - gdprDataHandler.consentData = null; + resetConsentData(); }); it('should bypass CMP and simply use previously stored consentData', () => { @@ -146,13 +146,66 @@ describe('consentManagement', function () { }); }); + describe('CMP workflow for safeframe page', () => { + let registerStub = sinon.stub(); + + beforeEach(() => { + didHookReturn = false; + window.$sf = { + ext: { + register: function() {}, + cmp: function() {} + } + }; + sinon.stub(utils, 'logError'); + sinon.stub(utils, 'logWarn'); + }); + + afterEach(() => { + delete window.$sf; + config.resetConfig(); + $$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook); + registerStub.restore(); + utils.logError.restore(); + utils.logWarn.restore(); + resetConsentData(); + }); + + it('should return the consent data from a safeframe callback', () => { + var testConsentData = { + data: { + msgName: 'cmpReturn', + vendorConsents: { + metadata: 'abc123def', + gdprApplies: true + } + } + }; + registerStub = sinon.stub(window.$sf.ext, 'register').callsFake((...args) => { + args[2](testConsentData.data.msgName, testConsentData.data); + }); + + setConfig(goodConfigWithAllowAuction); + debugger; //eslint-disable-line + requestBidsHook({adUnits: [{ sizes: [[300, 250]] }]}, () => { + didHookReturn = true; + }); + let consent = gdprDataHandler.getConsentData(); + + sinon.assert.notCalled(utils.logWarn); + sinon.assert.notCalled(utils.logError); + expect(didHookReturn).to.be.true; + expect(consent.consentString).to.equal('abc123def'); + expect(consent.gdprApplies).to.be.true; + }); + }); + describe('CMP workflow for iframed page', () => { let eventStub = sinon.stub(); let cmpStub = sinon.stub(); beforeEach(() => { didHookReturn = false; - resetConsentData(); window.__cmp = function() {}; sinon.stub(utils, 'logError'); sinon.stub(utils, 'logWarn'); @@ -166,7 +219,7 @@ describe('consentManagement', function () { delete window.__cmp; utils.logError.restore(); utils.logWarn.restore(); - gdprDataHandler.consentData = null; + resetConsentData(); }); it('should return the consent string from a postmessage + addEventListener response', () => { @@ -210,7 +263,6 @@ describe('consentManagement', function () { beforeEach(() => { didHookReturn = false; - resetConsentData(); sinon.stub(utils, 'logError'); sinon.stub(utils, 'logWarn'); window.__cmp = function() {}; @@ -223,7 +275,7 @@ describe('consentManagement', function () { utils.logError.restore(); utils.logWarn.restore(); delete window.__cmp; - gdprDataHandler.consentData = null; + resetConsentData(); }); it('performs lookup check and stores consentData for a valid existing user', () => { diff --git a/test/spec/modules/gxoneBidAdapter_spec.js b/test/spec/modules/gxoneBidAdapter_spec.js new file mode 100644 index 00000000000..f34f4358490 --- /dev/null +++ b/test/spec/modules/gxoneBidAdapter_spec.js @@ -0,0 +1,293 @@ +import { expect } from 'chai'; +import { spec } from 'modules/gxoneBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; + +describe('GXOne Adapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'gxone', + 'params': { + 'uid': '4' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'uid': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + function parseRequest(url) { + const res = {}; + url.split('&').forEach((it) => { + const couple = it.split('='); + res[couple[0]] = decodeURIComponent(couple[1]); + }); + return res; + } + let bidRequests = [ + { + 'bidder': 'gxone', + 'params': { + 'uid': '5' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'gxone', + 'params': { + 'uid': '5' + }, + 'adUnitCode': 'adunit-code-2', + 'sizes': [[728, 90]], + 'bidId': '3150ccb55da321', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }, + { + 'bidder': 'gxone', + 'params': { + 'uid': '6' + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '42dbe3a7168a6a', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('should attach valid params to the tag', () => { + const request = spec.buildRequests([bidRequests[0]]); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '5'); + }); + + it('auids must not be duplicated', () => { + const request = spec.buildRequests(bidRequests); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '5,6'); + }); + + it('pt parameter must be "gross" if params.priceType === "gross"', () => { + bidRequests[1].params.priceType = 'gross'; + const request = spec.buildRequests(bidRequests); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'gross'); + expect(payload).to.have.property('auids', '5,6'); + delete bidRequests[1].params.priceType; + }); + + it('pt parameter must be "net" or "gross"', () => { + bidRequests[1].params.priceType = 'some'; + const request = spec.buildRequests(bidRequests); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('u').that.is.a('string'); + expect(payload).to.have.property('pt', 'net'); + expect(payload).to.have.property('auids', '5,6'); + delete bidRequests[1].params.priceType; + }); + }); + + describe('interpretResponse', () => { + const responses = [ + {'bid': [{'price': 1.15, 'adm': '