diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 665395940ce..3621d9bb00e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -6,8 +6,12 @@ ## Steps to reproduce + +## Test page + diff --git a/gulpfile.js b/gulpfile.js index addbc445f01..2a7e8860f21 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -88,14 +88,22 @@ function bundle(dev, moduleArr) { var entries = [helpers.getBuiltPrebidCoreFile(dev)].concat(helpers.getBuiltModules(dev, modules)); + var outputFileName = argv.bundleName ? argv.bundleName : 'prebid.js'; + + // change output filename if argument --tag given + if (argv.tag && argv.tag.length) { + outputFileName = outputFileName.replace(/\.js$/, `.${argv.tag}.js`); + } + gutil.log('Concatenating files:\n', entries); gutil.log('Appending ' + prebid.globalVarName + '.processQueue();'); + gutil.log('Generating bundle:', outputFileName); return gulp.src( entries ) .pipe(gulpif(dev, sourcemaps.init({loadMaps: true}))) - .pipe(concat(argv.bundleName ? argv.bundleName : 'prebid.js')) + .pipe(concat(outputFileName)) .pipe(gulpif(!argv.manualEnable, footer('\n<%= global %>.processQueue();', { global: prebid.globalVarName } @@ -142,11 +150,6 @@ gulp.task('devpack', ['clean'], function () { gulp.task('webpack', ['clean'], function () { var cloned = _.cloneDeep(webpackConfig); - // change output filename if argument --tag given - if (argv.tag && argv.tag.length) { - cloned.output.filename = 'prebid.' + argv.tag + '.js'; - } - delete cloned.devtool; var externalModules = helpers.getArgModules(); diff --git a/integrationExamples/gpt/pbjs_example_gpt.html b/integrationExamples/gpt/pbjs_example_gpt.html index 1a23a235d40..77c875b9787 100644 --- a/integrationExamples/gpt/pbjs_example_gpt.html +++ b/integrationExamples/gpt/pbjs_example_gpt.html @@ -264,7 +264,7 @@ } }, { - bidder: 'huddledmasses', + bidder: 'colossusssp', params: { placement_id: 0 } @@ -340,7 +340,9 @@ }, { bidder: 'yieldmo', - params: {} + params: { + placementId: 'ym_12341234' // Optional Yieldmo Placement ID + } }, { bidder: 'adequant', @@ -382,7 +384,7 @@ } }, { - bidder: 'huddledmasses', + bidder: 'colossusssp', params: { placement_id: 0 } diff --git a/integrationExamples/gpt/pbjs_ucfunnel_gpt.html b/integrationExamples/gpt/pbjs_ucfunnel_gpt.html index 67881d7168d..cda2af03b18 100644 --- a/integrationExamples/gpt/pbjs_ucfunnel_gpt.html +++ b/integrationExamples/gpt/pbjs_ucfunnel_gpt.html @@ -13,9 +13,7 @@ bids: [{ bidder: 'ucfunnel', params: { - adid: "test-ad-83444226E44368D1E32E49EEBE6D29", //String - required - width: 300, - height: 250 + adid: "test-ad-83444226E44368D1E32E49EEBE6D29" //String - required } } ] diff --git a/integrationExamples/gpt/pbjs_video_adUnit.html b/integrationExamples/gpt/pbjs_video_adUnit.html index 0895e2a9622..080ca9be142 100644 --- a/integrationExamples/gpt/pbjs_video_adUnit.html +++ b/integrationExamples/gpt/pbjs_video_adUnit.html @@ -36,7 +36,7 @@ var videoAdUnit = { code: 'video1', sizes: [640,480], - mediaType: 'video', + mediaTypes: { video: {} }, bids: [ { bidder: 'appnexusAst', diff --git a/modules/adomikAnalyticsAdapter.js b/modules/adomikAnalyticsAdapter.js new file mode 100644 index 00000000000..c9bd7990ec8 --- /dev/null +++ b/modules/adomikAnalyticsAdapter.js @@ -0,0 +1,209 @@ +import adapter from 'src/AnalyticsAdapter'; +import CONSTANTS from 'src/constants.json'; +import adaptermanager from 'src/adaptermanager'; +// import utils from 'src/utils'; + +// Events used in adomik analytics adapter +const auctionInit = CONSTANTS.EVENTS.AUCTION_INIT; +const auctionEnd = CONSTANTS.EVENTS.AUCTION_END; +const bidRequested = CONSTANTS.EVENTS.BID_REQUESTED; +const bidResponse = CONSTANTS.EVENTS.BID_RESPONSE; +const bidWon = CONSTANTS.EVENTS.BID_WON; +const bidTimeout = CONSTANTS.EVENTS.BID_TIMEOUT; + +let bidwonTimeout = 1000; + +let adomikAdapter = Object.assign(adapter({}), + { + // Track every event needed + track({ eventType, args }) { + switch (eventType) { + case auctionInit: + adomikAdapter.currentContext.id = args.requestId + adomikAdapter.currentContext.timeout = args.timeout + if (args.config.bidwonTimeout !== undefined && typeof args.config.bidwonTimeout === 'number') { + bidwonTimeout = args.config.bidwonTimeout; + } + break; + + case bidTimeout: + adomikAdapter.currentContext.timeouted = true; + break; + + case bidResponse: + adomikAdapter.bucketEvents.push({ + type: 'response', + event: adomikAdapter.buildBidResponse(args) + }); + break; + + case bidWon: + adomikAdapter.bucketEvents.push({ + type: 'winner', + event: { + id: args.adId, + placementCode: args.adUnitCode + } + }); + break; + + case bidRequested: + args.bids.forEach(function(bid) { + adomikAdapter.bucketEvents.push({ + type: 'request', + event: { + bidder: bid.bidder.toUpperCase(), + placementCode: bid.placementCode + } + }); + }); + break; + + case auctionEnd: + setTimeout(() => { + if (adomikAdapter.bucketEvents.length > 0) { + adomikAdapter.sendTypedEvent(); + } + }, bidwonTimeout); + break; + } + } + } +); + +adomikAdapter.sendTypedEvent = function() { + const groupedTypedEvents = adomikAdapter.buildTypedEvents(); + + const bulkEvents = { + uid: adomikAdapter.currentContext.uid, + ahbaid: adomikAdapter.currentContext.id, + timeout: adomikAdapter.currentContext.timeout, + hostname: window.location.hostname, + eventsByPlacementCode: groupedTypedEvents.map(function(typedEventsByType) { + let sizes = []; + const eventKeys = ['request', 'response', 'winner']; + let events = {}; + + eventKeys.forEach((eventKey) => { + events[`${eventKey}s`] = []; + if (typedEventsByType[eventKey] !== undefined) { + typedEventsByType[eventKey].forEach((typedEvent) => { + if (typedEvent.event.size !== undefined) { + const size = adomikAdapter.sizeUtils.handleSize(sizes, typedEvent.event.size); + if (size !== null) { + sizes = [...sizes, size]; + } + } + events[`${eventKey}s`] = [...events[`${eventKey}s`], typedEvent.event]; + }); + } + }); + + return { + placementCode: typedEventsByType.placementCode, + sizes, + events + }; + }) + }; + + // Encode object in base64 + const encodedBuf = window.btoa(JSON.stringify(bulkEvents)); + + // Create final url and split it in 1600 characters max (+endpoint length) + const encodedUri = encodeURIComponent(encodedBuf); + const splittedUrl = encodedUri.match(/.{1,1600}/g); + + splittedUrl.forEach((split, i) => { + const partUrl = `${split}&id=${adomikAdapter.currentContext.id}&part=${i}&on=${splittedUrl.length - 1}`; + const img = new Image(1, 1); + img.src = 'https://' + adomikAdapter.currentContext.url + '/?q=' + partUrl; + }) +}; + +adomikAdapter.buildBidResponse = function (bid) { + return { + bidder: bid.bidderCode.toUpperCase(), + placementCode: bid.adUnitCode, + id: bid.adId, + status: (bid.statusMessage === 'Bid available') ? 'VALID' : 'EMPTY_OR_ERROR', + cpm: parseFloat(bid.cpm), + size: { + width: Number(bid.width), + height: Number(bid.height) + }, + timeToRespond: bid.timeToRespond, + afterTimeout: adomikAdapter.currentContext.timeouted + }; +} + +adomikAdapter.sizeUtils = { + sizeAlreadyExists: (sizes, typedEventSize) => { + return sizes.find((size) => size.height === typedEventSize.height && size.width === typedEventSize.width); + }, + formatSize: (typedEventSize) => { + return { + width: Number(typedEventSize.width), + height: Number(typedEventSize.height) + }; + }, + handleSize: (sizes, typedEventSize) => { + let formattedSize = null; + if (adomikAdapter.sizeUtils.sizeAlreadyExists(sizes, typedEventSize) === undefined) { + formattedSize = adomikAdapter.sizeUtils.formatSize(typedEventSize); + } + return formattedSize; + } +}; + +adomikAdapter.buildTypedEvents = function () { + const groupedTypedEvents = []; + adomikAdapter.bucketEvents.forEach(function(typedEvent, i) { + const [placementCode, type] = [typedEvent.event.placementCode, typedEvent.type]; + let existTypedEvent = groupedTypedEvents.findIndex((groupedTypedEvent) => groupedTypedEvent.placementCode === placementCode); + + if (existTypedEvent === -1) { + groupedTypedEvents.push({ + placementCode: placementCode, + [type]: [typedEvent] + }); + existTypedEvent = groupedTypedEvents.length - 1; + } + + if (groupedTypedEvents[existTypedEvent][type]) { + groupedTypedEvents[existTypedEvent][type] = [...groupedTypedEvents[existTypedEvent][type], typedEvent]; + } else { + groupedTypedEvents[existTypedEvent][type] = [typedEvent]; + } + }); + + return groupedTypedEvents; +} + +// Initialize adomik object +adomikAdapter.currentContext = {}; +adomikAdapter.bucketEvents = []; + +adomikAdapter.adapterEnableAnalytics = adomikAdapter.enableAnalytics; + +adomikAdapter.enableAnalytics = function (config) { + const initOptions = config.options; + if (initOptions) { + adomikAdapter.currentContext = { + uid: initOptions.id, + url: initOptions.url, + debug: initOptions.debug, + id: '', + timeouted: false, + timeout: 0, + } + adomikAdapter.adapterEnableAnalytics(config); + } +}; + +adaptermanager.registerAnalyticsAdapter({ + adapter: adomikAdapter, + code: 'adomik' +}); + +export default adomikAdapter; diff --git a/modules/adxcgAnalyticsAdapter.js b/modules/adxcgAnalyticsAdapter.js new file mode 100644 index 00000000000..5d2040c8e08 --- /dev/null +++ b/modules/adxcgAnalyticsAdapter.js @@ -0,0 +1,94 @@ +import {ajax} from 'src/ajax'; +import adapter from 'src/AnalyticsAdapter'; +import adaptermanager from 'src/adaptermanager'; +import * as url from 'src/url'; +import * as utils from 'src/utils'; + +const emptyUrl = ''; +const analyticsType = 'endpoint'; +const adxcgAnalyticsVersion = 'v1.04'; + +let initOptions; +let auctionTimestamp; +let events = { + bidRequests: [], + bidResponses: [] +}; + +var adxcgAnalyticsAdapter = Object.assign(adapter( + { + emptyUrl, + analyticsType + }), { + track({eventType, args}) { + if (typeof args !== 'undefined') { + if (eventType === 'bidTimeout') { + events.bidTimeout = args; + } else if (eventType === 'auctionInit') { + events.auctionInit = args; + auctionTimestamp = args.timestamp; + } else if (eventType === 'bidRequested') { + events.bidRequests.push(args); + } else if (eventType === 'bidResponse') { + events.bidResponses.push(mapBidResponse(args)); + } else if (eventType === 'bidWon') { + send({ + bidWon: mapBidResponse(args) + }); + } + } + + if (eventType === 'auctionEnd') { + send(events); + } + } +}); + +function mapBidResponse(bidResponse) { + return { + adUnitCode: bidResponse.adUnitCode, + statusMessage: bidResponse.statusMessage, + bidderCode: bidResponse.bidderCode, + adId: bidResponse.adId, + mediaType: bidResponse.mediaType, + creative_id: bidResponse.creative_id, + width: bidResponse.width, + height: bidResponse.height, + cpm: bidResponse.cpm, + timeToRespond: bidResponse.timeToRespond + }; +} + +function send(data) { + data.initOptions = initOptions; + data.auctionTimestamp = auctionTimestamp; + + let location = utils.getTopWindowLocation(); + let secure = location.protocol == 'https:'; + + let adxcgAnalyticsRequestUrl = url.format({ + protocol: secure ? 'https' : 'http', + hostname: secure ? 'hbarxs.adxcg.net' : 'hbarx.adxcg.net', + pathname: '/pbrx', + search: { + auctionTimestamp: auctionTimestamp, + adxcgAnalyticsVersion: adxcgAnalyticsVersion, + prebidVersion: $$PREBID_GLOBAL$$.version + } + }); + + ajax(adxcgAnalyticsRequestUrl, undefined, JSON.stringify(data), {method: 'POST'}); +} + +adxcgAnalyticsAdapter.originEnableAnalytics = adxcgAnalyticsAdapter.enableAnalytics; +adxcgAnalyticsAdapter.enableAnalytics = function (config) { + initOptions = config.options; + adxcgAnalyticsAdapter.originEnableAnalytics(config); +}; + +adaptermanager.registerAnalyticsAdapter({ + adapter: adxcgAnalyticsAdapter, + code: 'adxcg' +}); + +export default adxcgAnalyticsAdapter; diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index ccb0287a866..476cb5989e0 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -88,26 +88,26 @@ function AdxcgAdapter() { let nativeResponse = adxcgBidReponse.nativeResponse; - bid.native = { + bid['native'] = { clickUrl: escape(nativeResponse.link.url), impressionTrackers: nativeResponse.imptrackers }; nativeResponse.assets.forEach(asset => { if (asset.title && asset.title.text) { - bid.native.title = asset.title.text; + bid['native'].title = asset.title.text; } if (asset.img && asset.img.url) { - bid.native.image = asset.img.url; + bid['native'].image = asset.img.url; } if (asset.data && asset.data.label == 'DESC' && asset.data.value) { - bid.native.body = asset.data.value; + bid['native'].body = asset.data.value; } if (asset.data && asset.data.label == 'SPONSORED' && asset.data.value) { - bid.native.sponsoredBy = asset.data.value; + bid['native'].sponsoredBy = asset.data.value; } }); } diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 0ba497c5eed..5192270a94e 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -8,7 +8,7 @@ import { STATUS } from 'src/constants'; import adaptermanager from 'src/adaptermanager'; var AdyoulikeAdapter = function AdyoulikeAdapter() { - const _VERSION = '0.1'; + const _VERSION = '0.2'; const baseAdapter = new Adapter('adyoulike'); @@ -21,7 +21,7 @@ var AdyoulikeAdapter = function AdyoulikeAdapter() { const placements = validBids.map(bid => bid.params.placement); if (!utils.isEmpty(placements)) { - const body = createBody(placements); + const body = createBody(bidRequests, placements); const endpoint = createEndpoint(); ajax(endpoint, (response) => { @@ -61,10 +61,11 @@ var AdyoulikeAdapter = function AdyoulikeAdapter() { } /* Create request body */ - function createBody(placements) { + function createBody(bidRequests, placements) { const body = { Version: _VERSION, Placements: placements, + TransactionIds: {} }; // performance isn't supported by mobile safari iOS7. window.performance works, but @@ -80,6 +81,8 @@ var AdyoulikeAdapter = function AdyoulikeAdapter() { body.PageRefreshed = false; } + placements.forEach(placement => { body.TransactionIds[placement] = bidRequests[placement].transactionId; }); + return JSON.stringify(body); } diff --git a/modules/aolBidAdapter.js b/modules/aolBidAdapter.js index 4506ac1512e..5adba83e9df 100644 --- a/modules/aolBidAdapter.js +++ b/modules/aolBidAdapter.js @@ -165,12 +165,17 @@ const AolAdapter = function AolAdapter() { function _buildNexageApiUrl(bid) { let {dcn, pos} = bid.params; + let isSecure = (document.location.protocol === 'https:'); let nexageApi = nexageBaseApiTemplate({ - protocol: (document.location.protocol === 'https:') ? 'https' : 'http', + protocol: isSecure ? 'https' : 'http', host: bid.params.host || NEXAGE_SERVER }); if (dcn && pos) { let ext = ''; + if (isSecure) { + bid.params.ext = bid.params.ext || {}; + bid.params.ext.secure = 1; + } utils._each(bid.params.ext, (value, key) => { ext += `&${key}=${encodeURIComponent(value)}`; }); diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index 326d423d192..c4e2686db15 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -14,13 +14,15 @@ const NATIVE_MAPPING = { cta: 'ctatext', image: { serverName: 'main_image', - serverParams: { required: true, sizes: [{}] } + requiredParams: { required: true }, + minimumParams: { sizes: [{}] }, }, icon: { serverName: 'icon', - serverParams: { required: true, sizes: [{}] } + requiredParams: { required: true }, + minimumParams: { sizes: [{}] }, }, - sponsoredBy: 'sponsored_by' + sponsoredBy: 'sponsored_by', }; const SOURCE = 'pbjs'; @@ -44,7 +46,7 @@ export const spec = { * @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) { + buildRequests: function(bidRequests, bidderRequest) { const tags = bidRequests.map(bidToTag); const userObjBid = bidRequests.find(hasUserInfo); let userObj; @@ -74,6 +76,7 @@ export const spec = { method: 'POST', url: URL, data: payloadString, + bidderRequest }; }, @@ -83,18 +86,27 @@ export const spec = { * @param {*} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: function(serverResponse) { + interpretResponse: function(serverResponse, {bidderRequest}) { const bids = []; - serverResponse.tags.forEach(serverBid => { - const rtbBid = getRtbBid(serverBid); - if (rtbBid) { - if (rtbBid.cpm !== 0 && SUPPORTED_AD_TYPES.includes(rtbBid.ad_type)) { - const bid = newBid(serverBid, rtbBid); - bid.mediaType = parseMediaType(rtbBid); - bids.push(bid); + 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) { + serverResponse.tags.forEach(serverBid => { + const rtbBid = getRtbBid(serverBid); + if (rtbBid) { + if (rtbBid.cpm !== 0 && SUPPORTED_AD_TYPES.includes(rtbBid.ad_type)) { + const bid = newBid(serverBid, rtbBid); + bid.mediaType = parseMediaType(rtbBid); + bids.push(bid); + } } - } - }); + }); + } return bids; }, @@ -189,17 +201,18 @@ function newBid(serverBid, rtbBid) { bid.adResponse.ad = bid.adResponse.ads[0]; bid.adResponse.ad.video = bid.adResponse.ad.rtb.video; } - } else if (rtbBid.rtb.native) { - const native = rtbBid.rtb.native; - bid.native = { - title: native.title, - body: native.desc, - cta: native.ctatext, - sponsoredBy: native.sponsored, - image: native.main_img && native.main_img.url, - icon: native.icon && native.icon.url, - clickUrl: native.link.url, - impressionTrackers: native.impression_trackers, + } else if (rtbBid.rtb['native']) { + const nativeAd = rtbBid.rtb['native']; + bid['native'] = { + title: nativeAd.title, + body: nativeAd.desc, + cta: nativeAd.ctatext, + sponsoredBy: nativeAd.sponsored, + image: nativeAd.main_img && nativeAd.main_img.url, + icon: nativeAd.icon && nativeAd.icon.url, + clickUrl: nativeAd.link.url, + clickTrackers: nativeAd.link.click_trackers, + impressionTrackers: nativeAd.impression_trackers, }; } else { Object.assign(bid, { @@ -260,33 +273,12 @@ function bidToTag(bid) { tag.keywords = getKeywords(bid.params.keywords); } - if (bid.mediaType === 'native') { + if (bid.mediaType === 'native' || utils.deepAccess(bid, 'mediaTypes.native')) { tag.ad_types = ['native']; if (bid.nativeParams) { - const nativeRequest = {}; - - // 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: '', serverParams: {...}}} - Object.keys(bid.nativeParams).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; - - // if the mapping for this identifier specifies required server - // params via the `serverParams` object, merge that in - nativeRequest[requestKey] = Object.assign({}, - NATIVE_MAPPING[key] && NATIVE_MAPPING[key].serverParams, - bid.nativeParams[key] - ); - }); - - tag.native = {layouts: [nativeRequest]}; + const nativeRequest = buildNativeRequest(bid.nativeParams); + tag['native'] = {layouts: [nativeRequest]}; } } @@ -343,6 +335,44 @@ function getRtbBid(tag) { return tag && tag.ads && tag.ads.length && tag.ads.find(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]); + + // minimum params are passed if no non-required params given on adunit + const minimumParams = NATIVE_MAPPING[key] && NATIVE_MAPPING[key].minimumParams; + + if (requiredParams && minimumParams) { + // subtract required keys from adunit keys + const adunitKeys = Object.keys(params[key]); + const requiredKeys = Object.keys(requiredParams); + const remaining = adunitKeys.filter(key => !requiredKeys.includes(key)); + + // if none are left over, the minimum params needs to be sent + if (remaining.length === 0) { + request[requestKey] = Object.assign({}, request[requestKey], minimumParams); + } + } + }); + + return request; +} + function outstreamRender(bid) { // push to render queue because ANOutstreamVideo may not be loaded yet bid.renderer.push(() => { diff --git a/modules/appnexusAstBidAdapter.md b/modules/appnexusAstBidAdapter.md new file mode 100644 index 00000000000..2b370e11616 --- /dev/null +++ b/modules/appnexusAstBidAdapter.md @@ -0,0 +1,103 @@ +# Overview + +``` +Module Name: AppnexusAst Bid Adapter +Module Type: Bidder Adapter +Maintainer: info@prebid.org +``` + +# Description + +Connects to Appnexus exchange for bids. + +AppnexusAst 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: 'appnexusAst', + 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: 'appnexusAst', + params: { + placementId: '9880618' + } + }] + }, + // Video instream adUnit + { + code: 'video-instream', + sizes: [640, 480], + mediaTypes: { + video: { + context: 'instream' + }, + }, + bids: [{ + bidder: 'appnexusAst', + 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: 'appnexusAst', + params: { + placementId: '5768085', + video: { + skippable: true, + playback_method: ['auto_play_sound_off'] + } + } + } + ] + } +]; +``` diff --git a/modules/huddledmassesBidAdapter.js b/modules/colossussspBidAdapter.js similarity index 85% rename from modules/huddledmassesBidAdapter.js rename to modules/colossussspBidAdapter.js index 76511ee129d..8cf6239c8e5 100644 --- a/modules/huddledmassesBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -6,7 +6,7 @@ import {ajax} from 'src/ajax'; import {STATUS} from 'src/constants'; import adaptermanager from 'src/adaptermanager'; -var BIDDER_CODE = 'huddledmasses'; +var BIDDER_CODE = 'colossusssp'; var sizeObj = { 1: '468x60', @@ -39,7 +39,7 @@ var sizeObj = { utils._each(sizeObj, (item, key) => sizeObj[item] = key); -function HuddledMassesAdapter() { +function ColossusSspAdapter() { function _callBids(bidderRequest) { var bids = bidderRequest.bids || []; @@ -50,9 +50,9 @@ function HuddledMassesAdapter() { handleRpCB(responseText, bid); } catch (err) { if (typeof err === 'string') { - utils.logWarn(`${err} when processing huddledmasses response for placement code ${bid.placementCode}`); + utils.logWarn(`${err} when processing colossus response for placement code ${bid.placementCode}`); } else { - utils.logError('Error processing huddledmasses response for placement code ' + bid.placementCode, null, err); + utils.logError('Error processing colossus response for placement code ' + bid.placementCode, null, err); } var badBid = bidfactory.createBid(STATUS.NO_BID, bid); badBid.bidderCode = bid.bidder; @@ -64,7 +64,7 @@ function HuddledMassesAdapter() { try { ajax(buildOptimizedCall(bid), bidCallback, undefined, { withCredentials: true }); } catch (err) { - utils.logError('Error sending huddledmasses request for placement code ' + bid.placementCode, null, err); + utils.logError('Error sending colossus request for placement code ' + bid.placementCode, null, err); } }); } @@ -72,7 +72,7 @@ function HuddledMassesAdapter() { function buildOptimizedCall(bid) { bid.startTime = (new Date()).getTime(); - var parsedSizes = HuddledMassesAdapter.masSizeOrdering( + var parsedSizes = ColossusSspAdapter.masSizeOrdering( Array.isArray(bid.params.sizes) ? bid.params.sizes.map(size => (sizeObj[size] || '').split('x')) : bid.sizes ); @@ -117,7 +117,7 @@ function HuddledMassesAdapter() { index % 2 === 0 && queryString[index + 1] !== undefined ? memo + curr + '=' + encodeURIComponent(queryString[index + 1]) + '&' : memo, - '//huddledmassessupply.com/?' + '//colossusssp.com/?' ).slice(0, -1); } @@ -136,12 +136,12 @@ function HuddledMassesAdapter() { bidmanager.addBidResponse(bidRequest.placementCode, bid); } - return Object.assign(new Adapter(BIDDER_CODE), { // BIDDER_CODE huddledmasses + return Object.assign(new Adapter(BIDDER_CODE), { // BIDDER_CODE colossusssp callBids: _callBids }); } -HuddledMassesAdapter.masSizeOrdering = function (sizes) { +ColossusSspAdapter.masSizeOrdering = function (sizes) { var MAS_SIZE_PRIORITY = [15, 2, 9]; return utils.parseSizesInput(sizes) .reduce((result, size) => { @@ -169,6 +169,6 @@ HuddledMassesAdapter.masSizeOrdering = function (sizes) { }); }; -adaptermanager.registerBidAdapter(new HuddledMassesAdapter(), 'huddledmasses'); +adaptermanager.registerBidAdapter(new ColossusSspAdapter(), BIDDER_CODE); -module.exports = HuddledMassesAdapter; +module.exports = ColossusSspAdapter; diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index f46c3dc2fd1..f89aa077349 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -56,8 +56,8 @@ var CriteoAdapter = function CriteoAdapter() { var w = parseInt(sizeString.substring(0, xIndex)); var h = parseInt(sizeString.substring(xIndex + 1, sizeString.length)) return new Criteo.PubTag.DirectBidding.Size(w, h); - } - ) + }), + bid.params.publisherSubId ) ); @@ -148,13 +148,13 @@ var CriteoAdapter = function CriteoAdapter() { bidObject.cpm = bidResponse.cpm; // in case of native - if (slot.nativeCallback && bidResponse.native) { + if (slot.nativeCallback && bidResponse['native']) { if (typeof slot.nativeCallback !== 'function') { utils.logError('Criteo bid: nativeCallback parameter is not a function'); } else { // store the callbacks in a global object window.criteo_pubtag.native_slots = window.criteo_pubtag.native_slots || {}; - window.criteo_pubtag.native_slots['' + bidObject.adId] = { callback: slot.nativeCallback, nativeResponse: bidResponse.native }; + window.criteo_pubtag.native_slots['' + bidObject.adId] = { callback: slot.nativeCallback, nativeResponse: bidResponse['native'] }; // this code is executed in an iframe, we need to get a reference to the // publishertag in the main window to retrieve native responses and callbacks. diff --git a/modules/express.js b/modules/express.js index 5d3a91c6e8e..8a5dc095476 100644 --- a/modules/express.js +++ b/modules/express.js @@ -20,6 +20,8 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { utils.logWarn('no valid adUnits found, not loading ' + MODULE_NAME); } + // store gpt slots in a more performant hash lookup by elementId (adUnit code) + var gptSlotCache = {}; // put adUnits in a more performant hash lookup by code. var adUnitsCache = adUnits.reduce(function (cache, adUnit) { if (adUnit.code && adUnit.bids) { @@ -72,7 +74,7 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { const adUnit = adUnitsCache[elemId]; if (adUnit) { - adUnit._gptSlot = gptSlot; + gptSlotCache[elemId] = gptSlot; // store by elementId adUnit.sizes = adUnit.sizes || mapGptSlotSizes(gptSlot.getSizes()); adUnits.push(adUnit); gptSlots.splice(i, 1); @@ -141,7 +143,7 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { $$PREBID_GLOBAL$$.setTargetingForGPTAsync(); fGptRefresh.apply(pads(), [ adUnits.map(function (adUnit) { - return adUnit._gptSlot; + return gptSlotCache[adUnit.code]; }) ]); } @@ -157,7 +159,7 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { // get already displayed adUnits from aGptSlots if provided, else all defined gptSlots aGptSlots = defaultSlots(aGptSlots); var adUnits = pickAdUnits(/* mutated: */ aGptSlots).filter(function (adUnit) { - return adUnit._gptSlot._displayed; + return gptSlotCache[adUnit.code]._displayed; }); if (aGptSlots.length) { @@ -171,7 +173,7 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { $$PREBID_GLOBAL$$.setTargetingForGPTAsync(); fGptRefresh.apply(pads(), [ adUnits.map(function (adUnit) { - return adUnit._gptSlot + return gptSlotCache[adUnit.code]; }), options ]); diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 17cc04c9752..521dfabfb2d 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -63,7 +63,7 @@ const GumgumAdapter = function GumgumAdapter() { } = bidRequest; const timestamp = _getTimeStamp(); const trackingId = params.inScreen; - const nativeId = params.native; + const nativeId = params['native']; const slotId = params.inSlot; const bid = { tmax: $$PREBID_GLOBAL$$.cbTimeout }; @@ -72,7 +72,7 @@ const GumgumAdapter = function GumgumAdapter() { case !!(params.inImage): bid.pi = 1; break; case !!(params.inScreen): bid.pi = 2; break; case !!(params.inSlot): bid.pi = 3; break; - case !!(params.native): bid.pi = 5; break; + case !!(params['native']): bid.pi = 5; break; default: return utils.logWarn( `[GumGum] No product selected for the placement ${placementCode}` + ', please check your implementation.' diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 426362e0d9c..1b9766553c2 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -12,6 +12,7 @@ const OpenxAdapter = function OpenxAdapter() { const BIDDER_VERSION = '1.0.1'; let startTime; let timeout = config.getConfig('bidderTimeout'); + let shouldSendBoPixel = true; let pdNode = null; @@ -64,7 +65,9 @@ const OpenxAdapter = function OpenxAdapter() { beaconParams.bp = adUnit.pub_rev; beaconParams.ts = adUnit.ts; addBidResponse(adUnit, bid); - buildBoPixel(adUnit.creative[0], beaconParams); + if (shouldSendBoPixel === true) { + buildBoPixel(adUnit.creative[0], beaconParams); + } } }; @@ -182,6 +185,16 @@ const OpenxAdapter = function OpenxAdapter() { return found; } + function formatCustomParms(customKey, customParams) { + let value = customParams[customKey]; + if (Array.isArray(value)) { + // if value is an array, join them with commas first + value = value.join(','); + } + // return customKey=customValue format, escaping + to . and / to _ + return (customKey + '=' + value).replace('+', '.').replace('/', '_') + } + function buildRequest(bids, params, delDomain) { if (!utils.isArray(bids)) { return; @@ -193,13 +206,35 @@ const OpenxAdapter = function OpenxAdapter() { return utils.parseSizesInput(bid.sizes).join(','); }).join('|'); + let customParamsForAllBids = []; + let hasCustomParam = false; bids.forEach(function (bid) { - for (let customParam in bid.params.customParams) { - if (bid.params.customParams.hasOwnProperty(customParam)) { - params['c.' + customParam] = bid.params.customParams[customParam]; - } + if (bid.params.customParams) { + let customParamsForBid = utils._map(Object.keys(bid.params.customParams), customKey => formatCustomParms(customKey, bid.params.customParams)); + let formattedCustomParams = window.btoa(customParamsForBid.join('&')); + hasCustomParam = true; + customParamsForAllBids.push(formattedCustomParams); + } else { + customParamsForAllBids.push(''); + } + }); + if (hasCustomParam) { + params.tps = customParamsForAllBids.join('%2C'); + } + + let customFloorsForAllBids = []; + let hasCustomFloor = false; + bids.forEach(function (bid) { + if (bid.params.customFloor) { + customFloorsForAllBids.push(bid.params.customFloor * 1000); + hasCustomFloor = true; + } else { + customFloorsForAllBids.push(0); } }); + if (hasCustomFloor) { + params.aumfs = customFloorsForAllBids.join('%2C'); + } try { let queryString = buildQueryStringFromParams(params); @@ -227,11 +262,15 @@ const OpenxAdapter = function OpenxAdapter() { } let delDomain = bids[0].params.delDomain; + let bcOverride = bids[0].params.bc; startTime = new Date(params.start); if (params.timeout) { timeout = params.timeout; } + if (bids[0].params.hasOwnProperty('sendBoPixel') && typeof (bids[0].params.sendBoPixel) === 'boolean') { + shouldSendBoPixel = bids[0].params.sendBoPixel; + } buildRequest(bids, { ju: currentURL, @@ -243,7 +282,7 @@ const OpenxAdapter = function OpenxAdapter() { tws: getViewportDimensions(isIfr), ef: 'bt%2Cdb', be: 1, - bc: `${BIDDER_CONFIG}_${BIDDER_VERSION}`, + bc: bcOverride || `${BIDDER_CONFIG}_${BIDDER_VERSION}`, nocache: new Date().getTime() }, delDomain); diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index d68406b016c..0906a1a0b3d 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -4,16 +4,15 @@ import bidmanager from 'src/bidmanager'; import * as utils from 'src/utils'; import { ajax } from 'src/ajax'; import { STATUS, S2S } from 'src/constants'; -import { userSync } from 'src/userSync.js'; import { cookieSet } from 'src/cookie.js'; import adaptermanager from 'src/adaptermanager'; import { config } from 'src/config'; -import { StorageManager, pbjsSyncsKey } from 'src/storagemanager'; const getConfig = config.getConfig; const TYPE = S2S.SRC; const cookieSetUrl = 'https://acdn.adnxs.com/cookieset/cs.js'; +let _synced = false; /** * Try to convert a value to a type. @@ -106,15 +105,25 @@ function PrebidServer() { /* Prebid executes this function when the page asks to send out bid requests */ baseAdapter.callBids = function(bidRequest) { const isDebug = !!getConfig('debug'); - convertTypes(bidRequest.ad_units); + const adUnits = utils.cloneJson(bidRequest.ad_units); + adUnits.forEach(adUnit => { + let videoMediaType = utils.deepAccess(adUnit, 'mediaTypes.video'); + if (videoMediaType) { + // pbs expects a ad_unit.video attribute if the imp is video + adUnit.video = Object.assign({}, videoMediaType); + delete adUnit.mediaTypes.video; + } + }) + convertTypes(adUnits); let requestJson = { account_id: config.accountId, tid: bidRequest.tid, max_bids: config.maxBids, timeout_millis: config.timeout, + secure: config.secure, url: utils.getTopWindowUrl(), prebid_version: '$prebid.version$', - ad_units: bidRequest.ad_units.filter(hasSizes), + ad_units: adUnits.filter(hasSizes), is_debug: isDebug }; @@ -135,6 +144,27 @@ function PrebidServer() { return unit.sizes && unit.sizes.length; } + /** + * Run a cookie sync for the given type, url, and bidder + * + * @param {string} type the type of sync, "image", "redirect", "iframe" + * @param {string} url the url to sync + * @param {string} bidder name of bidder doing sync for + */ + function doBidderSync(type, url, bidder) { + if (!url) { + utils.logError(`No sync url for bidder "${bidder}": ${url}`); + } else if (type === 'image' || type === 'redirect') { + utils.logMessage(`Invoking image pixel user sync for bidder: "${bidder}"`); + utils.triggerPixel(url); + } else if (type == 'iframe') { + utils.logMessage(`Invoking iframe user sync for bidder: "${bidder}"`); + utils.insertUserSyncIframe(url); + } else { + utils.logError(`User sync type "${type}" not supported for bidder: "${bidder}"`); + } + } + /* Notify Prebid of bid responses so bids can get in the auction */ function handleResponse(response, requestedBidders) { let result; @@ -145,7 +175,7 @@ function PrebidServer() { if (result.bidder_status) { result.bidder_status.forEach(bidder => { if (bidder.no_cookie && !_cookiesQueued) { - userSync.registerSync(bidder.usersync.type, bidder.bidder, bidder.usersync.url); + doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder); } }); } @@ -214,12 +244,10 @@ function PrebidServer() { * @param {} {bidders} list of bidders to request user syncs for. */ baseAdapter.queueSync = function({bidderCodes}) { - let syncedList = StorageManager.get(pbjsSyncsKey) || []; - // filter synced bidders - https://github.com/prebid/Prebid.js/issues/1582 - syncedList = bidderCodes.filter(bidder => !syncedList.includes(bidder)); - if (syncedList.length === 0) { + if (_synced) { return; } + _synced = true; const payload = JSON.stringify({ uuid: utils.generateUUID(), bidders: bidderCodes @@ -227,10 +255,7 @@ function PrebidServer() { ajax(config.syncEndpoint, (response) => { try { response = JSON.parse(response); - if (response.status === 'ok') { - bidderCodes.forEach(code => StorageManager.add(pbjsSyncsKey, code, true)); - } - response.bidder_status.forEach(bidder => queueSync({bidder: bidder.bidder, url: bidder.usersync.url, type: bidder.usersync.type})); + response.bidder_status.forEach(bidder => doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder)); } catch (e) { utils.logError(e); } diff --git a/modules/pulsepointLiteBidAdapter.js b/modules/pulsepointLiteBidAdapter.js index a9485823efc..00b5c014e98 100644 --- a/modules/pulsepointLiteBidAdapter.js +++ b/modules/pulsepointLiteBidAdapter.js @@ -1,9 +1,14 @@ -import {createBid} from 'src/bidfactory'; -import {addBidResponse} from 'src/bidmanager'; +/* eslint dot-notation:0, quote-props:0 */ import {logError, getTopWindowLocation} from 'src/utils'; -import {ajax} from 'src/ajax'; -import {STATUS} from 'src/constants'; -import adaptermanager from 'src/adaptermanager'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const NATIVE_DEFAULTS = { + TITLE_LEN: 100, + DESCR_LEN: 200, + SPONSORED_BY_LEN: 50, + IMG_MIN: 150, + ICON_MIN: 50, +}; /** * PulsePoint "Lite" Adapter. This adapter implementation is lighter than the @@ -11,291 +16,298 @@ import adaptermanager from 'src/adaptermanager'; * dependencies and relies on a single OpenRTB request to the PulsePoint * bidder instead of separate requests per slot. */ -function PulsePointLiteAdapter() { - const bidUrl = window.location.protocol + '//bid.contextweb.com/header/ortb'; - const ajaxOptions = { - method: 'POST', - withCredentials: true, - contentType: 'text/plain' - }; - const NATIVE_DEFAULTS = { - TITLE_LEN: 100, - DESCR_LEN: 200, - SPONSORED_BY_LEN: 50, - IMG_MIN: 150, - ICON_MIN: 50, - }; +export const spec = { - /** - * Makes the call to PulsePoint endpoint and registers bids. - */ - function _callBids(bidRequest) { - try { - // construct the openrtb bid request from slots - const request = { - imp: bidRequest.bids.map(slot => impression(slot)), - site: site(bidRequest), - device: device(), - }; - ajax(bidUrl, (rawResponse) => { - bidResponseAvailable(bidRequest, rawResponse); - }, JSON.stringify(request), ajaxOptions); - } catch (e) { - // register passback on any exceptions while attempting to fetch response. - logError('pulsepoint.requestBid', 'ERROR', e); - bidResponseAvailable(bidRequest); - } - } + code: 'pulseLite', - /** - * Callback for bids, after the call to PulsePoint completes. - */ - function bidResponseAvailable(bidRequest, rawResponse) { - const idToSlotMap = {}; - const idToBidMap = {}; - // extract the request bids and the response bids, keyed by impr-id - bidRequest.bids.forEach((slot) => { - idToSlotMap[slot.bidId] = slot; - }); - const bidResponse = parse(rawResponse); - if (bidResponse) { - bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach((bid) => { - idToBidMap[bid.impid] = bid; - })); - } - // register the responses - Object.keys(idToSlotMap).forEach((id) => { - if (idToBidMap[id]) { - const size = adSize(idToSlotMap[id]); - const bid = createBid(STATUS.GOOD, bidRequest); - bid.bidderCode = bidRequest.bidderCode; - bid.cpm = idToBidMap[id].price; - bid.adId = id; - if (isNative(idToSlotMap[id])) { - bid.native = nativeResponse(idToSlotMap[id], idToBidMap[id]); - bid.mediaType = 'native'; - } else { - bid.ad = idToBidMap[id].adm; - bid.width = size[0]; - bid.height = size[1]; - } - addBidResponse(idToSlotMap[id].placementCode, bid); - } else { - const passback = createBid(STATUS.NO_BID, bidRequest); - passback.bidderCode = bidRequest.bidderCode; - passback.adId = id; - addBidResponse(idToSlotMap[id].placementCode, passback); - } - }); - } + aliases: ['pulsepointLite'], - /** - * Produces an OpenRTBImpression from a slot config. - */ - function impression(slot) { + supportedMediaTypes: ['native'], + + isBidRequestValid: bid => ( + !!(bid && bid.params && bid.params.cp && bid.params.ct) + ), + + buildRequests: bidRequests => { + const request = { + id: bidRequests[0].bidderRequestId, + imp: bidRequests.map(slot => impression(slot)), + site: site(bidRequests), + app: app(bidRequests), + device: device(), + }; return { - id: slot.bidId, - banner: banner(slot), - native: native(slot), - tagid: slot.params.ct.toString(), + method: 'POST', + url: '//bid.contextweb.com/header/ortb', + data: JSON.stringify(request), }; - } + }, - /** - * Produces an OpenRTB Banner object for the slot given. - */ - function banner(slot) { - const size = adSize(slot); - return slot.nativeParams ? null : { - w: size[0], - h: size[1], - }; - } + interpretResponse: (response, request) => ( + bidResponseAvailable(request, response) + ), - /** - * Produces an OpenRTB Native object for the slot given. - */ - function native(slot) { - if (slot.nativeParams) { - const assets = []; - addAsset(assets, titleAsset(assets.length + 1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); - addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); - addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); - addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN)); - addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); - return { - request: JSON.stringify({ assets }), - ver: '1.1', - }; + getUserSyncs: syncOptions => { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: '//bh.contextweb.com/visitormatch' + }]; + } else if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: '//bh.contextweb.com/visitormatch/prebid' + }]; } - return null; } - /** - * Helper method to add an asset to the assets list. - */ - function addAsset(assets, asset) { - if (asset) { - assets.push(asset); - } - } +}; - /** - * Produces a Native Title asset for the configuration given. - */ - function titleAsset(id, params, defaultLen) { - if (params) { - return { - id: id, - required: params.required ? 1 : 0, - title: { - len: params.len || defaultLen, - }, +/** + * Callback for bids, after the call to PulsePoint completes. + */ +function bidResponseAvailable(bidRequest, bidResponse) { + const idToImpMap = {}; + const idToBidMap = {}; + // extract the request bids and the response bids, keyed by impr-id + const ortbRequest = parse(bidRequest.data); + ortbRequest.imp.forEach(imp => { + idToImpMap[imp.id] = imp; + }); + if (bidResponse) { + bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => { + idToBidMap[bid.impid] = bid; + })); + } + const bids = []; + Object.keys(idToImpMap).forEach(id => { + if (idToBidMap[id]) { + const bid = { + requestId: id, + cpm: idToBidMap[id].price, + creative_id: id, + creativeId: id, + adId: id, }; + if (idToImpMap[id]['native']) { + bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); + bid.mediaType = 'native'; + } else { + bid.ad = idToBidMap[id].adm; + bid.width = idToImpMap[id].banner.w; + bid.height = idToImpMap[id].banner.h; + } + bids.push(bid); } - return null; + }); + return bids; +} + +/** + * Produces an OpenRTBImpression from a slot config. + */ +function impression(slot) { + return { + id: slot.bidId, + banner: banner(slot), + 'native': nativeImpression(slot), + tagid: slot.params.ct.toString(), + }; +} + +/** + * Produces an OpenRTB Banner object for the slot given. + */ +function banner(slot) { + const size = adSize(slot); + return slot.nativeParams ? null : { + w: size[0], + h: size[1], + }; +} + +/** + * Produces an OpenRTB Native object for the slot given. + */ +function nativeImpression(slot) { + if (slot.nativeParams) { + const assets = []; + addAsset(assets, titleAsset(assets.length + 1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); + addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); + addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); + addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN)); + addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); + return { + request: JSON.stringify({ assets }), + ver: '1.1', + }; } + return null; +} - /** - * Produces a Native Image asset for the configuration given. - */ - function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) { - return params ? { - id: id, - required: params.required ? 1 : 0, - img: { - type, - wmin: params.wmin || defaultMinWidth, - hmin: params.hmin || defaultMinHeight, - } - } : null; +/** + * Helper method to add an asset to the assets list. + */ +function addAsset(assets, asset) { + if (asset) { + assets.push(asset); } +} - /** - * Produces a Native Data asset for the configuration given. - */ - function dataAsset(id, params, type, defaultLen) { - return params ? { - id: id, +/** + * Produces a Native Title asset for the configuration given. + */ +function titleAsset(id, params, defaultLen) { + if (params) { + return { + id, required: params.required ? 1 : 0, - data: { - type, + title: { len: params.len || defaultLen, - } - } : null; + }, + }; } + return null; +} + +/** + * Produces a Native Image asset for the configuration given. + */ +function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) { + return params ? { + id, + required: params.required ? 1 : 0, + img: { + type, + wmin: params.wmin || defaultMinWidth, + hmin: params.hmin || defaultMinHeight, + } + } : null; +} + +/** + * Produces a Native Data asset for the configuration given. + */ +function dataAsset(id, params, type, defaultLen) { + return params ? { + id, + required: params.required ? 1 : 0, + data: { + type, + len: params.len || defaultLen, + } + } : null; +} - /** - * Produces an OpenRTB site object. - */ - function site(bidderRequest) { - const pubId = bidderRequest.bids.length > 0 ? bidderRequest.bids[0].params.cp : '0'; +/** + * Produces an OpenRTB site object. + */ +function site(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.cp : '0'; + const appParams = bidderRequest[0].params.app; + if (!appParams) { return { publisher: { id: pubId.toString(), }, ref: referrer(), page: getTopWindowLocation().href, - }; - } - - /** - * Attempts to capture the referrer url. - */ - function referrer() { - try { - return window.top.document.referrer; - } catch (e) { - return document.referrer; } } + return null; +} - /** - * Produces an OpenRTB Device object. - */ - function device() { +/** + * Produces an OpenRTB App object. + */ +function app(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.cp : '0'; + const appParams = bidderRequest[0].params.app; + if (appParams) { return { - ua: navigator.userAgent, - language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), - }; - } - - /** - * Safely parses the input given. Returns null on - * parsing failure. - */ - function parse(rawResponse) { - try { - if (rawResponse) { - return JSON.parse(rawResponse); - } - } catch (ex) { - logError('pulsepointLite.safeParse', 'ERROR', ex); + publisher: { + id: pubId.toString(), + }, + bundle: appParams.bundle, + storeurl: appParams.storeUrl, + domain: appParams.domain, } - return null; } + return null; +} - /** - * Determines the AdSize for the slot. - */ - function adSize(slot) { - if (slot.params.cf) { - const size = slot.params.cf.toUpperCase().split('X'); - const width = parseInt(slot.params.cw || size[0], 10); - const height = parseInt(slot.params.ch || size[1], 10); - return [width, height]; - } - return [1, 1]; +/** + * Attempts to capture the referrer url. + */ +function referrer() { + try { + return window.top.document.referrer; + } catch (e) { + return document.referrer; } +} - /** - * Parses the native response from the Bid given. - */ - function nativeResponse(slot, bid) { - if (slot.nativeParams) { - const nativeAd = parse(bid.adm); - const keys = {}; - if (nativeAd && nativeAd.native && nativeAd.native.assets) { - nativeAd.native.assets.forEach((asset) => { - keys.title = asset.title ? asset.title.text : keys.title; - keys.body = asset.data && asset.data.type === 2 ? asset.data.value : keys.body; - keys.sponsoredBy = asset.data && asset.data.type === 1 ? asset.data.value : keys.sponsoredBy; - keys.image = asset.img && asset.img.type === 3 ? asset.img.url : keys.image; - keys.icon = asset.img && asset.img.type === 1 ? asset.img.url : keys.icon; - }); - if (nativeAd.native.link) { - keys.clickUrl = encodeURIComponent(nativeAd.native.link.url); - } - keys.impressionTrackers = nativeAd.native.imptrackers; - return keys; - } +/** + * Produces an OpenRTB Device object. + */ +function device() { + return { + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + }; +} + +/** + * Safely parses the input given. Returns null on + * parsing failure. + */ +function parse(rawResponse) { + try { + if (rawResponse) { + return JSON.parse(rawResponse); } - return null; + } catch (ex) { + logError('pulsepointLite.safeParse', 'ERROR', ex); } + return null; +} - /** - * Parses the native response from the Bid given. - */ - function isNative(slot) { - return !!slot.nativeParams; +/** + * Determines the AdSize for the slot. + */ +function adSize(slot) { + if (slot.params.cf) { + const size = slot.params.cf.toUpperCase().split('X'); + const width = parseInt(slot.params.cw || size[0], 10); + const height = parseInt(slot.params.ch || size[1], 10); + return [width, height]; } - - return Object.assign(this, { - callBids: _callBids - }); + return [1, 1]; } /** - * "pulseLite" will be the adapter name going forward. "pulsepointLite" to be - * deprecated, but kept here for backwards compatibility. - * Reason is key truncation. When the Publisher opts for sending all bids to DFP, then - * the keys get truncated due to the limit in key-size (20 characters, detailed - * here https://support.google.com/dfp_premium/answer/1628457?hl=en). Here is an - * example, where keys got truncated when using the "pulsepointLite" alias - "hb_adid_pulsepointLi=1300bd87d59c4c2" -*/ -adaptermanager.registerBidAdapter(new PulsePointLiteAdapter(), 'pulseLite', { - supportedMediaTypes: [ 'native' ] -}); -adaptermanager.aliasBidAdapter('pulseLite', 'pulsepointLite'); + * Parses the native response from the Bid given. + */ +function nativeResponse(imp, bid) { + if (imp['native']) { + const nativeAd = parse(bid.adm); + const keys = {}; + if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) { + nativeAd['native'].assets.forEach(asset => { + keys.title = asset.title ? asset.title.text : keys.title; + keys.body = asset.data && asset.data.type === 2 ? asset.data.value : keys.body; + keys.sponsoredBy = asset.data && asset.data.type === 1 ? asset.data.value : keys.sponsoredBy; + keys.image = asset.img && asset.img.type === 3 ? asset.img.url : keys.image; + keys.icon = asset.img && asset.img.type === 1 ? asset.img.url : keys.icon; + }); + if (nativeAd['native'].link) { + keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url); + } + keys.impressionTrackers = nativeAd['native'].imptrackers; + return keys; + } + } + return null; +} -module.exports = PulsePointLiteAdapter; +registerBidder(spec); diff --git a/modules/pulsepointLiteBidAdapter.md b/modules/pulsepointLiteBidAdapter.md new file mode 100644 index 00000000000..23c96758ca0 --- /dev/null +++ b/modules/pulsepointLiteBidAdapter.md @@ -0,0 +1,43 @@ +# Overview + +**Module Name**: PulsePoint Lite Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: ExchangeTeam@pulsepoint.com + +# Description + +Connects to PulsePoint demand source to fetch bids. +Banner, Outstream and Native formats are supported. +Please use ```pulseLite``` as the bidder code. + +# Test Parameters +``` + var adUnits = [{ + code: 'banner-ad-div', + sizes: [[300, 250]], + bids: [{ + bidder: 'pulsepointLite', + params: { + cf: '300X250', + cp: 512379, + ct: 486653 + } + }] + },{ + code: 'native-ad-div', + sizes: [[1, 1]], + nativeParams: { + title: { required: true, len: 75 }, + image: { required: true }, + body: { len: 200 }, + sponsoredBy: { len: 20 } + }, + bids: [{ + bidder: 'pulseLite', + params: { + cp: 512379, + ct: 505642 + } + }] + }]; +``` diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 598319faca1..69981ba2b56 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -1,12 +1,5 @@ -import Adapter from 'src/adapter'; -import bidfactory from 'src/bidfactory'; -import bidmanager from 'src/bidmanager'; -import adaptermanager from 'src/adaptermanager'; import * as utils from 'src/utils'; -import { ajax } from 'src/ajax'; -import { STATUS } from 'src/constants'; -import { userSync } from 'src/userSync'; -const RUBICON_BIDDER_CODE = 'rubicon'; +import { registerBidder } from 'src/adapters/bidderFactory'; // use deferred function call since version isn't defined yet at this point function getIntegration() { @@ -20,6 +13,7 @@ function isSecure() { // use protocol relative urls for http or https const FASTLANE_ENDPOINT = '//fastlane.rubiconproject.com/a/api/fastlane.json'; const VIDEO_ENDPOINT = '//fastlane-adv.rubiconproject.com/v1/auction/video'; +const SYNC_ENDPOINT = 'https://tap-secure.rubiconproject.com/partner/scripts/rubicon/emily.html?rtb_ext=1'; const TIMEOUT_BUFFER = 500; @@ -72,284 +66,203 @@ var sizeMap = { }; utils._each(sizeMap, (item, key) => sizeMap[item] = key); -function RubiconAdapter() { - var baseAdapter = new Adapter(RUBICON_BIDDER_CODE); - var hasUserSyncFired = false; - - function _callBids(bidderRequest) { - var bids = bidderRequest.bids || []; - - bids.forEach(bid => { - try { - // Video endpoint only accepts POST calls - if (bid.mediaType === 'video') { - ajax( - VIDEO_ENDPOINT, - { - success: bidCallback, - error: bidError - }, - buildVideoRequestPayload(bid, bidderRequest), - { - withCredentials: true - } - ); - } else { - ajax( - buildOptimizedCall(bid), - { - success: bidCallback, - error: bidError - }, - undefined, - { - withCredentials: true - } - ); - } - } catch (err) { - utils.logError('Error sending rubicon request for placement code ' + bid.placementCode, null, err); - addErrorBid(); - } - - function bidCallback(responseText) { - try { - utils.logMessage('XHR callback function called for ad ID: ' + bid.bidId); - handleRpCB(responseText, bid); - } catch (err) { - if (typeof err === 'string') { - utils.logWarn(`${err} when processing rubicon response for placement code ${bid.placementCode}`); - } else { - utils.logError('Error processing rubicon response for placement code ' + bid.placementCode, null, err); - } - addErrorBid(); - } - } - - function bidError(err, xhr) { - utils.logError('Request for rubicon responded with:', xhr.status, err); - addErrorBid(); - } - - function addErrorBid() { - let badBid = bidfactory.createBid(STATUS.NO_BID, bid); - badBid.bidderCode = baseAdapter.getBidderCode(); - bidmanager.addBidResponse(bid.placementCode, badBid); - } - }); - } - - function _getScreenResolution() { - return [window.screen.width, window.screen.height].join('x'); - } - - function _getDigiTrustQueryParams() { - function getDigiTrustId() { - let digiTrustUser = window.DigiTrust && ($$PREBID_GLOBAL$$.getConfig('digiTrustId') || window.DigiTrust.getUser({member: 'T9QSFKPDN9'})); - return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; +export const spec = { + code: 'rubicon', + aliases: ['rubiconLite'], + supportedMediaTypes: ['video'], + /** + * @param {object} bid + * @return boolean + */ + isBidRequestValid: function(bid) { + if (typeof bid.params !== 'object') { + return false; } - let digiTrustId = getDigiTrustId(); - // Verify there is an ID and this user has not opted out - if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) { - return []; - } - return [ - 'dt.id', digiTrustId.id, - 'dt.keyv', digiTrustId.keyv, - 'dt.pref', 0 - ]; - } - - function buildVideoRequestPayload(bid, bidderRequest) { - bid.startTime = new Date().getTime(); - let params = bid.params; - if (!params || typeof params.video !== 'object') { - throw 'Invalid Video Bid'; - } - - let size; - if (params.video.playerWidth && params.video.playerHeight) { - size = [ - params.video.playerWidth, - params.video.playerHeight - ]; - } else if ( - Array.isArray(bid.sizes) && bid.sizes.length > 0 && - Array.isArray(bid.sizes[0]) && bid.sizes[0].length > 1 - ) { - size = bid.sizes[0]; - } else { - throw 'Invalid Video Bid - No size provided'; - } - - let postData = { - page_url: !params.referrer ? utils.getTopWindowUrl() : params.referrer, - resolution: _getScreenResolution(), - account_id: params.accountId, - integration: getIntegration(), - timeout: bidderRequest.timeout - (Date.now() - bidderRequest.auctionStart + TIMEOUT_BUFFER), - stash_creatives: true, - ae_pass_through_parameters: params.video.aeParams, - slots: [] - }; - - // Define the slot object - let slotData = { - site_id: params.siteId, - zone_id: params.zoneId, - position: params.position || 'btf', - floor: parseFloat(params.floor) > 0.01 ? params.floor : 0.01, - element_id: bid.placementCode, - name: bid.placementCode, - language: params.video.language, - width: size[0], - height: size[1] - }; - - // check and add inventory, keywords, visitor and size_id data - if (params.video.size_id) { - slotData.size_id = params.video.size_id; - } else { - throw 'Invalid Video Bid - Invalid Ad Type!'; - } - - if (params.inventory && typeof params.inventory === 'object') { - slotData.inventory = params.inventory; + if (!/^\d+$/.test(params.accountId)) { + return false; } - if (params.keywords && Array.isArray(params.keywords)) { - slotData.keywords = params.keywords; + let parsedSizes = parseSizes(bid); + if (parsedSizes.length < 1) { + return false; } - if (params.visitor && typeof params.visitor === 'object') { - slotData.visitor = params.visitor; + if (bid.mediaType === 'video') { + if (typeof params.video !== 'object' || !params.video.size_id) { + return false; + } } + return true; + }, + /** + * @param {BidRequest[]} bidRequests + * @param bidderRequest + * @return ServerRequest[] + */ + buildRequests: function(bidRequests, bidderRequest) { + return bidRequests.map(bidRequest => { + bidRequest.startTime = new Date().getTime(); - postData.slots.push(slotData); - - return (JSON.stringify(postData)); - } - - function buildOptimizedCall(bid) { - bid.startTime = new Date().getTime(); - - var { - accountId, - siteId, - zoneId, - position, - floor, - keywords, - visitor, - inventory, - userId, - referrer: pageUrl - } = bid.params; - - // defaults - floor = (floor = parseFloat(floor)) > 0.01 ? floor : 0.01; - position = position || 'btf'; - - // use rubicon sizes if provided, otherwise adUnit.sizes - var parsedSizes = RubiconAdapter.masSizeOrdering(Array.isArray(bid.params.sizes) - ? bid.params.sizes.map(size => (sizeMap[size] || '').split('x')) : bid.sizes - ); - - if (parsedSizes.length < 1) { - throw 'no valid sizes'; - } + if (bidRequest.mediaType === 'video') { + let params = bidRequest.params; + let size = parseSizes(bidRequest); + + let data = { + page_url: !params.referrer ? utils.getTopWindowUrl() : params.referrer, + resolution: _getScreenResolution(), + account_id: params.accountId, + integration: getIntegration(), + timeout: bidderRequest.timeout - (Date.now() - bidderRequest.auctionStart + TIMEOUT_BUFFER), + stash_creatives: true, + ae_pass_through_parameters: params.video.aeParams, + slots: [] + }; + + // Define the slot object + let slotData = { + site_id: params.siteId, + zone_id: params.zoneId, + position: params.position || 'btf', + floor: parseFloat(params.floor) > 0.01 ? params.floor : 0.01, + element_id: bidRequest.placementCode, + name: bidRequest.placementCode, + language: params.video.language, + width: size[0], + height: size[1], + size_id: params.video.size_id + }; + + if (params.inventory && typeof params.inventory === 'object') { + slotData.inventory = params.inventory; + } - if (!/^\d+$/.test(accountId)) { - throw 'invalid accountId provided'; - } + if (params.keywords && Array.isArray(params.keywords)) { + slotData.keywords = params.keywords; + } - // using array to honor ordering. if order isn't important (it shouldn't be), an object would probably be preferable - var queryString = [ - 'account_id', accountId, - 'site_id', siteId, - 'zone_id', zoneId, - 'size_id', parsedSizes[0], - 'alt_size_ids', parsedSizes.slice(1).join(',') || undefined, - 'p_pos', position, - 'rp_floor', floor, - 'rp_secure', isSecure() ? '1' : '0', - 'tk_flint', getIntegration(), - 'p_screen_res', _getScreenResolution(), - 'kw', keywords, - 'tk_user_key', userId - ]; - - if (visitor !== null && typeof visitor === 'object') { - utils._each(visitor, (item, key) => queryString.push(`tg_v.${key}`, item)); - } + if (params.visitor && typeof params.visitor === 'object') { + slotData.visitor = params.visitor; + } - if (inventory !== null && typeof inventory === 'object') { - utils._each(inventory, (item, key) => queryString.push(`tg_i.${key}`, item)); - } + data.slots.push(slotData); - queryString.push( - 'rand', Math.random(), - 'rf', !pageUrl ? utils.getTopWindowUrl() : pageUrl - ); + return { + method: 'POST', + url: VIDEO_ENDPOINT, + data, + bidRequest + } + } - queryString = queryString.concat(_getDigiTrustQueryParams()); + // non-video request builder + let { + accountId, + siteId, + zoneId, + position, + floor, + keywords, + visitor, + inventory, + userId, + referrer: pageUrl + } = bidRequest.params; + + // defaults + floor = (floor = parseFloat(floor)) > 0.01 ? floor : 0.01; + position = position || 'btf'; + + // use rubicon sizes if provided, otherwise adUnit.sizes + let parsedSizes = parseSizes(bidRequest); + + // using array to honor ordering. if order isn't important (it shouldn't be), an object would probably be preferable + let data = [ + 'account_id', accountId, + 'site_id', siteId, + 'zone_id', zoneId, + 'size_id', parsedSizes[0], + 'alt_size_ids', parsedSizes.slice(1).join(',') || undefined, + 'p_pos', position, + 'rp_floor', floor, + 'rp_secure', isSecure() ? '1' : '0', + 'tk_flint', getIntegration(), + 'tid', bidRequest.transactionId, + 'p_screen_res', _getScreenResolution(), + 'kw', keywords, + 'tk_user_key', userId + ]; - return queryString.reduce( - (memo, curr, index) => - index % 2 === 0 && queryString[index + 1] !== undefined - ? memo + curr + '=' + encodeURIComponent(queryString[index + 1]) + '&' : memo, - FASTLANE_ENDPOINT + '?' - ).slice(0, -1); // remove trailing & - } + if (visitor !== null && typeof visitor === 'object') { + utils._each(visitor, (item, key) => data.push(`tg_v.${key}`, item)); + } - let _renderCreative = (script, impId) => ` - - - -
- -
- -`; + if (inventory !== null && typeof inventory === 'object') { + utils._each(inventory, (item, key) => data.push(`tg_i.${key}`, item)); + } - function handleRpCB(responseText, bidRequest) { - const responseObj = JSON.parse(responseText); // can throw + data.push( + 'rand', Math.random(), + 'rf', !pageUrl ? utils.getTopWindowUrl() : pageUrl + ); + + data = data.concat(_getDigiTrustQueryParams()); + + data = data.reduce( + (memo, curr, index) => + index % 2 === 0 && data[index + 1] !== undefined + ? memo + curr + '=' + encodeURIComponent(data[index + 1]) + '&' : memo, + '' + ).slice(0, -1); // remove trailing & + + return { + method: 'GET', + url: FASTLANE_ENDPOINT, + data, + bidRequest + }; + }); + }, + /** + * @param {*} responseObj + * @param {BidRequest} bidRequest + * @return {Bid[]} An array of bids which + */ + interpretResponse: function(responseObj, {bidRequest}) { let ads = responseObj.ads; - const adResponseKey = bidRequest.placementCode; // check overall response if (typeof responseObj !== 'object' || responseObj.status !== 'ok') { - throw 'bad response'; + return []; } // video ads array is wrapped in an object - if (bidRequest.mediaType === 'video' && typeof ads === 'object') { - ads = ads[adResponseKey]; + if (typeof bidRequest === 'object' && bidRequest.mediaType === 'video' && typeof ads === 'object') { + ads = ads[bidRequest.placementCode]; } // check the ad response if (!Array.isArray(ads) || ads.length < 1) { - throw 'invalid ad response'; + return []; } // if there are multiple ads, sort by CPM ads = ads.sort(_adCpmSort); - ads.forEach(ad => { + return ads.reduce((bids, ad) => { if (ad.status !== 'ok') { - throw 'bad ad status'; + return []; } - // store bid response - // bid status is good (indicating 1) - var bid = bidfactory.createBid(STATUS.GOOD, bidRequest); - bid.currency = 'USD'; - bid.creative_id = ad.creative_id; - bid.bidderCode = baseAdapter.getBidderCode(); - bid.cpm = ad.cpm || 0; - bid.dealId = ad.deal; + let bid = { + requestId: bidRequest.bidId, + currency: 'USD', + creative_id: ad.creative_id, + bidderCode: spec.code, + cpm: ad.cpm || 0, + dealId: ad.deal + }; if (bidRequest.mediaType === 'video') { bid.width = bidRequest.params.video.playerWidth; bid.height = bidRequest.params.video.playerHeight; @@ -368,26 +281,82 @@ function RubiconAdapter() { return memo; }, {'rpfl_elemid': bidRequest.placementCode}); - try { - bidmanager.addBidResponse(bidRequest.placementCode, bid); - } catch (err) { - utils.logError('Error from addBidResponse', null, err); - } - }); - // Run the Emily user sync - hasUserSyncFired = syncEmily(hasUserSyncFired); + bids.push(bid); + + return bids; + }, []); + }, + getUserSyncs: function() { + if (!hasSynced) { + hasSynced = true; + return { + type: 'iframe', + url: SYNC_ENDPOINT + }; + } } +}; - function _adCpmSort(adA, adB) { - return (adB.cpm || 0.0) - (adA.cpm || 0.0); +function _adCpmSort(adA, adB) { + return (adB.cpm || 0.0) - (adA.cpm || 0.0); +} + +function _getScreenResolution() { + return [window.screen.width, window.screen.height].join('x'); +} + +function _getDigiTrustQueryParams() { + function getDigiTrustId() { + let digiTrustUser = window.DigiTrust && ($$PREBID_GLOBAL$$.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 []; } + return [ + 'dt.id', digiTrustId.id, + 'dt.keyv', digiTrustId.keyv, + 'dt.pref', 0 + ]; +} + +function _renderCreative(script, impId) { + return ` + + + +
+ +
+ +`; +} - return Object.assign(this, baseAdapter, { - callBids: _callBids - }); +function parseSizes(bid) { + let params = bid.params; + if (bid.mediaType === 'video') { + let size = []; + if (params.video.playerWidth && params.video.playerHeight) { + size = [ + params.video.playerWidth, + params.video.playerHeight + ]; + } 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; + } + return masSizeOrdering(Array.isArray(params.sizes) + ? params.sizes.map(size => (sizeMap[size] || '').split('x')) : bid.sizes + ); } -RubiconAdapter.masSizeOrdering = function(sizes) { +export function masSizeOrdering(sizes) { const MAS_SIZE_PRIORITY = [15, 2, 9]; return utils.parseSizesInput(sizes) @@ -417,41 +386,11 @@ RubiconAdapter.masSizeOrdering = function(sizes) { // and finally ascending order return first - second; }); -}; - -/** - * syncEmily - * @summary A user sync dependency for the Rubicon Project adapter - * Registers an Emily iframe user sync to be called/created later by Prebid - * Only registers once except that with each winning creative there will be additional, similar calls to the same service. Must enable iframe syncs which are off by default -@example - * // Config example for iframe user sync - * $$PREBID_GLOBAL$$.setConfig({ userSync: { - * syncEnabled: true, - * pixelEnabled: true, - * syncsPerBidder: 5, - * syncDelay: 3000, - * iframeEnabled: true - * }}); - * @return {boolean} Whether or not Emily synced - */ -function syncEmily(hasSynced) { - // Check that it has not already been triggered - only meant to fire once - if (hasSynced) { - return true; - } - - const iframeUrl = 'https://tap-secure.rubiconproject.com/partner/scripts/rubicon/emily.html?rtb_ext=1'; - - // register the sync with the Prebid (to be called later) - userSync.registerSync('iframe', 'rubicon', iframeUrl); - - return true; } -adaptermanager.registerBidAdapter(new RubiconAdapter(), RUBICON_BIDDER_CODE, { - supportedMediaTypes: ['video'] -}); -adaptermanager.aliasBidAdapter(RUBICON_BIDDER_CODE, 'rubiconLite'); +var hasSynced = false; +export function resetUserSync() { + hasSynced = false; +} -module.exports = RubiconAdapter; +registerBidder(spec); diff --git a/modules/s2sTesting.js b/modules/s2sTesting.js new file mode 100644 index 00000000000..a821383dc2d --- /dev/null +++ b/modules/s2sTesting.js @@ -0,0 +1,126 @@ +import { config } from 'src/config'; +import { setS2STestingModule } from 'src/adaptermanager'; + +var CONSTANTS = require('src/constants.json'); +const AST = CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING; +export const SERVER = 'server'; +export const CLIENT = 'client'; + +var testing = false; // whether testing is turned on +var bidSource = {}; // store bidder sources determined from s2sConfing bidderControl + +// load s2sConfig +config.getConfig('s2sConfig', config => { + testing = config.s2sConfig && config.s2sConfig.testing; + addBidderSourceTargeting(config.s2sConfig) + calculateBidSources(config.s2sConfig); +}); + +// function to add hb_source_ adServerTargeting (AST) kvp to bidder settings +function addBidderSourceTargeting(s2sConfig = {}) { + // bail if testing is not turned on + if (!testing) { + return; + } + var bidderSettings = $$PREBID_GLOBAL$$.bidderSettings || {}; + var bidderControl = s2sConfig.bidderControl || {}; + // for each configured bidder + (s2sConfig.bidders || []).forEach((bidder) => { + // remove any existing kvp setting + if (bidderSettings[bidder] && bidderSettings[bidder][AST]) { + bidderSettings[bidder][AST] = bidderSettings[bidder][AST].filter((kvp) => { + return kvp.key !== `hb_source_${bidder}`; + }); + } + // if includeSourceKvp === true add new kvp setting + if (bidderControl[bidder] && bidderControl[bidder].includeSourceKvp) { + bidderSettings[bidder] = bidderSettings[bidder] || {}; + bidderSettings[bidder][AST] = bidderSettings[bidder][AST] || []; + bidderSettings[bidder][AST].push({ + key: `hb_source_${bidder}`, + val: function (bidResponse) { + // default to client (currently only S2S sets this) + return bidResponse.source || CLIENT; + } + }); + // make sure "alwaysUseBid" is true so targeting is set + bidderSettings[bidder].alwaysUseBid = true; + } + }); +} + +export function getSourceBidderMap(adUnits = []) { + var sourceBidders = {[SERVER]: {}, [CLIENT]: {}}; + + // bail if testing is not turned on + if (!testing) { + return {[SERVER]: [], [CLIENT]: []}; + } + + adUnits.forEach((adUnit) => { + // if any adUnit bidders specify a bidSource, include them + (adUnit.bids || []).forEach((bid) => { + // calculate the source once and store on bid object + bid.calcSource = bid.calcSource || getSource(bid.bidSource); + // if no bidSource at bid level, default to bidSource from bidder + bid.finalSource = bid.calcSource || bidSource[bid.bidder] || CLIENT; // default to client + // add bidder to sourceBidders data structure + sourceBidders[bid.finalSource][bid.bidder] = true; + }); + }); + + // make sure all bidders in bidSource are in sourceBidders + Object.keys(bidSource).forEach((bidder) => { + sourceBidders[bidSource[bidder]][bidder] = true; + }); + + // return map of source => array of bidders + return { + [SERVER]: Object.keys(sourceBidders[SERVER]), + [CLIENT]: Object.keys(sourceBidders[CLIENT]) + }; +} + +/** + * @function calculateBidSources determines the source for each s2s bidder based on bidderControl weightings. these can be overridden at the adUnit level + * @param s2sConfig server-to-server configuration + */ +function calculateBidSources(s2sConfig = {}) { + // bail if testing is not turned on + if (!testing) { + return; + } + bidSource = {}; // reset bid sources + // calculate bid source (server/client) for each s2s bidder + var bidderControl = s2sConfig.bidderControl || {}; + (s2sConfig.bidders || []).forEach((bidder) => { + bidSource[bidder] = getSource(bidderControl[bidder] && bidderControl[bidder].bidSource) || SERVER; // default to server + }); +} + +/** + * @function getSource() gets a random source based on the given sourceWeights (export just for testing) + * @param sourceWeights mapping of relative weights of potential sources. for example {server: 1, client: 3} should do a server request 25% of the time and a client request 75% of the time. + * @param bidSources list of possible bid sources: "server", "client". In theory could get the sources from the sourceWeights keys, but this is publisher config defined, so bidSources let's us constrain that. + * @return the chosen source ("server" or "client"), or undefined if none chosen + */ +export function getSource(sourceWeights = {}, bidSources = [SERVER, CLIENT]) { + var srcIncWeight = {}; // store incremental weights of each source + var totWeight = 0; + bidSources.forEach((source) => { + totWeight += (sourceWeights[source] || 0); + srcIncWeight[source] = totWeight; + }); + if (!totWeight) return; // bail if no source weights + // choose a source randomly based on weights + var rndWeight = Math.random() * totWeight; + for (var i = 0; i < bidSources.length; i++) { + let source = bidSources[i]; + // choose the first source with an incremental weight > random weight + if (rndWeight < srcIncWeight[source]) return source; + } +} + +// inject the s2sTesting module into the adaptermanager rather than importing it +// importing it causes the packager to include it even when it's not explicitly included in the build +setS2STestingModule(exports); diff --git a/modules/serverbidBidAdapter.js b/modules/serverbidBidAdapter.js index 5a6fa385877..f5044fe4ae1 100644 --- a/modules/serverbidBidAdapter.js +++ b/modules/serverbidBidAdapter.js @@ -17,6 +17,10 @@ ServerBidAdapter = function ServerBidAdapter() { 'connectad': { 'BASE_URI': 'https://i.connectad.io/api/v2', 'SMARTSYNC_BASE_URI': 'https://s.zkcdn.net/ss' + }, + 'onefiftytwo': { + 'BASE_URI': 'https://e.serverbid.com/api/v2', + 'SMARTSYNC_BASE_URI': 'https://s.zkcdn.net/ss' } }; @@ -199,5 +203,6 @@ ServerBidAdapter.createNew = function() { adaptermanager.registerBidAdapter(new ServerBidAdapter(), 'serverbid'); adaptermanager.aliasBidAdapter('serverbid', 'connectad'); +adaptermanager.aliasBidAdapter('serverbid', 'onefiftytwo'); module.exports = ServerBidAdapter; diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index e8ed973fe5f..2e2831a028a 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -11,6 +11,20 @@ function Spotx() { let bidReq; let KVP_Object; + const _defaultBidderSettings = { + alwaysUseBid: true, + adserverTargeting: [ + { + key: 'hb_adid', + val: function (bidResponse) { + return bidResponse.spotx_ad_key; + } + } + ] + }; + + bidmanager.registerDefaultBidderSetting('spotx', _defaultBidderSettings); + baseAdapter.callBids = function(bidRequest) { if (!bidRequest || !bidRequest.bids || bidRequest.bids.length === 0) { return; @@ -85,7 +99,7 @@ function Spotx() { bid.cpm = KVP_Object.spotx_bid; bid.vastUrl = url; - bid.ad = url; + bid.spotx_ad_key = KVP_Object.spotx_ad_key; var sizes = utils.isArray(bidReq.sizes[0]) ? bidReq.sizes[0] : bidReq.sizes; bid.height = sizes[1]; diff --git a/modules/trustxBidAdapter.js b/modules/trustxBidAdapter.js index 44c00c2c0b3..13f893a841d 100644 --- a/modules/trustxBidAdapter.js +++ b/modules/trustxBidAdapter.js @@ -37,7 +37,7 @@ var TrustxAdapter = function TrustxAdapter() { query.push('u=' + encodeURIComponent(location.href)); query.push('auids=' + encodeURIComponent(auids.join(','))); query.push('cb=' + _makeHandler(auids, placementMap)); - query.push('pt=' + (window.globalPrebidTrustxPriceType === 'net' ? 'net' : 'gross')); + query.push('pt=' + (window.globalPrebidTrustxPriceType === 'gross' ? 'gross' : 'net')); adloader.loadScript(reqHost + path + query.join('&')); } diff --git a/modules/ucfunnelBidAdapter.js b/modules/ucfunnelBidAdapter.js index f171604db7c..978c7508002 100644 --- a/modules/ucfunnelBidAdapter.js +++ b/modules/ucfunnelBidAdapter.js @@ -58,8 +58,6 @@ function UcfunnelAdapter() { 'u', page, 'ru', refer, 'adid', bid.params.adid, - 'w', bid.params.width, - 'h', bid.params.height, 'ver', VER ]; diff --git a/modules/unrulyBidAdapter.js b/modules/unrulyBidAdapter.js index fd9e94859c9..0f6b6e40901 100644 --- a/modules/unrulyBidAdapter.js +++ b/modules/unrulyBidAdapter.js @@ -8,20 +8,20 @@ import adaptermanager from 'src/adaptermanager' function createRenderHandler({ bidResponseBid, rendererConfig }) { function createApi() { - parent.window.unruly.native.prebid = parent.window.unruly.native.prebid || {} - parent.window.unruly.native.prebid.uq = parent.window.unruly.native.prebid.uq || [] + parent.window.unruly['native'].prebid = parent.window.unruly['native'].prebid || {} + parent.window.unruly['native'].prebid.uq = parent.window.unruly['native'].prebid.uq || [] return { render(bidResponseBid) { - parent.window.unruly.native.prebid.uq.push(['render', bidResponseBid]) + parent.window.unruly['native'].prebid.uq.push(['render', bidResponseBid]) }, onLoaded(bidResponseBid) {} } } parent.window.unruly = parent.window.unruly || {} - parent.window.unruly.native = parent.window.unruly.native || {} - parent.window.unruly.native.siteId = parent.window.unruly.native.siteId || rendererConfig.siteId + parent.window.unruly['native'] = parent.window.unruly['native'] || {} + parent.window.unruly['native'].siteId = parent.window.unruly['native'].siteId || rendererConfig.siteId const api = createApi() return { diff --git a/modules/vertozBidAdapter.js b/modules/vertozBidAdapter.js index 3334a5a34c6..b6966dd62d1 100644 --- a/modules/vertozBidAdapter.js +++ b/modules/vertozBidAdapter.js @@ -33,7 +33,8 @@ function VertozAdapter() { _rqsrc: reqSrc, _cb: cb, _slotBidId: slotBidId, - _cpm: cpm + _cpm: cpm, + _cbn: '$$PREBID_GLOBAL$$' }; let queryParamValue = JSON.stringify(vzReq); diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 75077c65fd7..d311bb5722c 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -51,6 +51,10 @@ var YieldmoAdapter = function YieldmoAdapter() { placement.placement_id = bid.placementCode; placement.sizes = bid.sizes; + if (bid.params && bid.params.placementId) { + placement.ym_placement_id = bid.params.placementId; + } + placements.push(placement); } diff --git a/package.json b/package.json index 92d49e7a549..e3b61d4ea67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "0.29.0", + "version": "0.31.0", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { @@ -49,11 +49,11 @@ "gulp-clean": "^0.3.2", "gulp-concat": "^2.6.0", "gulp-connect": "^5.0.0", + "gulp-documentation": "^3.2.1", "gulp-eslint": "^4.0.0", "gulp-footer": "^1.0.5", "gulp-header": "^1.7.1", "gulp-if": "^2.0.2", - "gulp-documentation": "^3.2.1", "gulp-optimize-js": "^1.1.0", "gulp-rename": "^1.2.0", "gulp-replace": "^0.4.0", @@ -61,6 +61,7 @@ "gulp-uglify": "^3.0.0", "gulp-util": "^3.0.0", "gulp-webdriver": "^1.0.1", + "ignore-loader": "^0.1.2", "istanbul": "^0.4.5", "istanbul-instrumenter-loader": "^3.0.0", "json-loader": "^0.5.1", diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 38b7073478a..2204e997084 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -3,11 +3,12 @@ import { flatten, getBidderCodes, getDefinedParams, shuffle } from './utils'; import { mapSizes } from './sizeMapping'; import { processNativeAdUnitParams, nativeAdapters } from './native'; -import { StorageManager, pbjsSyncsKey } from './storagemanager'; +import { newBidder } from './adapters/bidderFactory'; var utils = require('./utils.js'); var CONSTANTS = require('./constants.json'); var events = require('./events'); +let s2sTestingModule; // store s2sTesting module if it's loaded var _bidderRegistry = {}; exports.bidderRegistry = _bidderRegistry; @@ -42,12 +43,6 @@ function getBids({bidderCode, requestId, bidderRequestId, adUnits}) { sizes = sizeMapping; } - if (adUnit.nativeParams) { - bid = Object.assign({}, bid, { - nativeParams: processNativeAdUnitParams(adUnit.nativeParams), - }); - } - if (adUnit.mediaTypes) { if (utils.isValidMediaTypes(adUnit.mediaTypes)) { bid = Object.assign({}, bid, { mediaTypes: adUnit.mediaTypes }); @@ -58,6 +53,14 @@ function getBids({bidderCode, requestId, bidderRequestId, adUnits}) { } } + const nativeParams = + adUnit.nativeParams || utils.deepAccess(adUnit, 'mediaTypes.native'); + if (nativeParams) { + bid = Object.assign({}, bid, { + nativeParams: processNativeAdUnitParams(nativeParams), + }); + } + bid = Object.assign({}, bid, getDefinedParams(adUnit, [ 'mediaType', 'renderer' @@ -88,7 +91,6 @@ exports.callBids = ({adUnits, cbTimeout}) => { events.emit(CONSTANTS.EVENTS.AUCTION_INIT, auctionInit); let bidderCodes = getBidderCodes(adUnits); - const syncedBidders = StorageManager.get(pbjsSyncsKey); if (_bidderSequence === RANDOM) { bidderCodes = shuffle(bidderCodes); } @@ -99,25 +101,34 @@ exports.callBids = ({adUnits, cbTimeout}) => { s2sAdapter.queueSync({bidderCodes}); } + let clientTestAdapters = []; + let s2sTesting = false; if (_s2sConfig.enabled) { + // if s2sConfig.bidderControl testing is turned on + s2sTesting = _s2sConfig.testing && typeof s2sTestingModule !== 'undefined'; + if (s2sTesting) { + // get all adapters doing client testing + clientTestAdapters = s2sTestingModule.getSourceBidderMap(adUnits)[s2sTestingModule.CLIENT]; + } + // these are called on the s2s adapter - let adaptersServerSide = _s2sConfig.bidders.filter(bidder => syncedBidders.includes(bidder)); + let adaptersServerSide = _s2sConfig.bidders; - // don't call these client side + // don't call these client side (unless client request is needed for testing) bidderCodes = bidderCodes.filter((elm) => { - return !adaptersServerSide.includes(elm); + return !adaptersServerSide.includes(elm) || clientTestAdapters.includes(elm); }); - let adUnitsCopy = utils.cloneJson(adUnits); + let adUnitsS2SCopy = utils.cloneJson(adUnits); // filter out client side bids - adUnitsCopy.forEach((adUnit) => { + adUnitsS2SCopy.forEach((adUnit) => { if (adUnit.sizeMapping) { adUnit.sizes = mapSizes(adUnit); delete adUnit.sizeMapping; } adUnit.sizes = transformHeightWidth(adUnit); adUnit.bids = adUnit.bids.filter((bid) => { - return adaptersServerSide.includes(bid.bidder); + return adaptersServerSide.includes(bid.bidder) && (!s2sTesting || bid.finalSource !== s2sTestingModule.CLIENT); }).map((bid) => { bid.bid_id = utils.getUniqueIdentifierStr(); return bid; @@ -125,7 +136,7 @@ exports.callBids = ({adUnits, cbTimeout}) => { }); // don't send empty requests - adUnitsCopy = adUnitsCopy.filter(adUnit => { + adUnitsS2SCopy = adUnitsS2SCopy.filter(adUnit => { return adUnit.bids.length !== 0; }); @@ -137,7 +148,7 @@ exports.callBids = ({adUnits, cbTimeout}) => { requestId, bidderRequestId, tid, - bids: getBids({bidderCode, requestId, bidderRequestId, 'adUnits': adUnitsCopy}), + bids: getBids({bidderCode, requestId, bidderRequestId, 'adUnits': adUnitsS2SCopy}), start: new Date().getTime(), auctionStart: auctionStart, timeout: _s2sConfig.timeout, @@ -148,13 +159,28 @@ exports.callBids = ({adUnits, cbTimeout}) => { } }); - let s2sBidRequest = {tid, 'ad_units': adUnitsCopy}; + let s2sBidRequest = {tid, 'ad_units': adUnitsS2SCopy}; utils.logMessage(`CALLING S2S HEADER BIDDERS ==== ${adaptersServerSide.join(',')}`); if (s2sBidRequest.ad_units.length) { s2sAdapter.callBids(s2sBidRequest); } } + let _bidderRequests = []; + // client side adapters + let adUnitsClientCopy = utils.cloneJson(adUnits); + // filter out s2s bids + adUnitsClientCopy.forEach((adUnit) => { + adUnit.bids = adUnit.bids.filter((bid) => { + return !s2sTesting || bid.finalSource !== s2sTestingModule.SERVER; + }) + }); + + // don't send empty requests + adUnitsClientCopy = adUnitsClientCopy.filter(adUnit => { + return adUnit.bids.length !== 0; + }); + bidderCodes.forEach(bidderCode => { const adapter = _bidderRegistry[bidderCode]; if (adapter) { @@ -163,21 +189,30 @@ exports.callBids = ({adUnits, cbTimeout}) => { bidderCode, requestId, bidderRequestId, - bids: getBids({bidderCode, requestId, bidderRequestId, adUnits}), - start: new Date().getTime(), + bids: getBids({bidderCode, requestId, bidderRequestId, 'adUnits': adUnitsClientCopy}), auctionStart: auctionStart, timeout: cbTimeout }; if (bidderRequest.bids && bidderRequest.bids.length !== 0) { - utils.logMessage(`CALLING BIDDER ======= ${bidderCode}`); $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidderRequest); - adapter.callBids(bidderRequest); + _bidderRequests.push(bidderRequest); } - } else { - utils.logError(`Adapter trying to be called which does not exist: ${bidderCode} adaptermanager.callBids`); } }); + + _bidderRequests.forEach(bidRequest => { + bidRequest.start = new Date().getTime(); + const adapter = _bidderRegistry[bidRequest.bidderCode]; + if (adapter) { + if (bidRequest.bids && bidRequest.bids.length !== 0) { + utils.logMessage(`CALLING BIDDER ======= ${bidRequest.bidderCode}`); + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + adapter.callBids(bidRequest); + } + } else { + utils.logError(`Adapter trying to be called which does not exist: ${bidRequest.bidderCode} adaptermanager.callBids`); + } + }) }; function transformHeightWidth(adUnit) { @@ -194,6 +229,13 @@ function transformHeightWidth(adUnit) { return sizesObj; } +function getSupportedMediaTypes(bidderCode) { + let result = []; + if (exports.videoAdapters.includes(bidderCode)) result.push('video'); + if (nativeAdapters.includes(bidderCode)) result.push('native'); + return result; +} + exports.videoAdapters = []; // added by adapterLoader for now exports.registerBidAdapter = function (bidAdaptor, bidderCode, {supportedMediaTypes = []} = {}) { @@ -220,14 +262,24 @@ exports.aliasBidAdapter = function (bidderCode, alias) { if (typeof existingAlias === 'undefined') { var bidAdaptor = _bidderRegistry[bidderCode]; - if (typeof bidAdaptor === 'undefined') { utils.logError('bidderCode "' + bidderCode + '" is not an existing bidder.', 'adaptermanager.aliasBidAdapter'); } else { try { - let newAdapter = new bidAdaptor.constructor(); - newAdapter.setBidderCode(alias); - this.registerBidAdapter(newAdapter, alias); + let newAdapter; + let supportedMediaTypes = getSupportedMediaTypes(bidderCode); + // Have kept old code to support backward compatibilitiy. + // Remove this if loop when all adapters are supporting bidderFactory. i.e When Prebid.js is 1.0 + if (bidAdaptor.constructor.prototype != Object.prototype) { + newAdapter = new bidAdaptor.constructor(); + newAdapter.setBidderCode(alias); + } else { + let spec = bidAdaptor.getSpec(); + newAdapter = newBidder(Object.assign({}, spec, { code: alias })); + } + this.registerBidAdapter(newAdapter, alias, { + supportedMediaTypes + }); } catch (e) { utils.logError(bidderCode + ' bidder does not currently support aliasing.', 'adaptermanager.aliasBidAdapter'); } @@ -278,3 +330,9 @@ exports.setBidderSequence = function (order) { exports.setS2SConfig = function (config) { _s2sConfig = config; }; + +// the s2sTesting module is injected when it's loaded rather than being imported +// importing it causes the packager to include it even when it's not explicitly included in the build +exports.setS2STestingModule = function (module) { + s2sTestingModule = module; +}; diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 6b5a0090ac3..e490e17c60d 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -24,7 +24,7 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * aliases: ['alias1', 'alias2'], * supportedMediaTypes: ['video', 'native'], * isBidRequestValid: function(paramsObject) { return true/false }, - * buildRequests: function(bidRequests) { return some ServerRequest(s) }, + * buildRequests: function(bidRequests, bidderRequest) { return some ServerRequest(s) }, * interpretResponse: function(oneServerResponse) { return some Bids, or throw an error. } * }); * @@ -41,7 +41,7 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @property {MediaType[]} [supportedMediaTypes]: A list of Media Types which the adapter supports. * @property {function(object): boolean} isBidRequestValid Determines whether or not the given bid has all the params * needed to make a valid request. - * @property {function(BidRequest[]): ServerRequest|ServerRequest[]} buildRequests Build the request to the Server + * @property {function(BidRequest[], bidderRequest): ServerRequest|ServerRequest[]} buildRequests Build the request to the Server * which requests Bids for the given array of Requests. Each BidRequest in the argument array is guaranteed to have * passed the isBidRequestValid() test. * @property {function(*, BidRequest): Bid[]} interpretResponse Given a successful response from the Server, @@ -66,6 +66,7 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @property {('GET'|'POST')} method The type of request which this is. * @property {string} url The endpoint for the request. For example, "//bids.example.com". * @property {string|object} data Data to be sent in the request. + * @property {object} options Content-Type set in the header of the bid request, overrides default 'text/plain'. * If this is a GET request, they'll become query params. If it's a POST request, they'll be added to the body. * Strings will be added as-is. Objects will be unpacked into query params based on key/value mappings, or * JSON-serialized into the Request body. @@ -79,6 +80,8 @@ import { logWarn, logError, parseQueryStringParameters, delayExecution } from 's * @property {string} ad A URL which can be used to load this ad, if it's chosen by the publisher. * @property {string} currency The currency code for the cpm value * @property {number} cpm The bid price, in US cents per thousand impressions. + * @property {number} ttl Time-to-live - how long (in seconds) Prebid can use this bid. + * @property {boolean} netRevenue Boolean defining whether the bid is Net or Gross. The default is true (Net). * @property {number} height The height of the ad, in pixels. * @property {number} width The width of the ad, in pixels. * @@ -136,6 +139,9 @@ export function registerBidder(spec) { */ export function newBidder(spec) { return Object.assign(new Adapter(spec.code), { + getSpec: function() { + return Object.freeze(spec); + }, callBids: function(bidderRequest) { if (!Array.isArray(bidderRequest.bids)) { return; @@ -152,18 +158,26 @@ export function newBidder(spec) { const adUnitCodesHandled = {}; function addBidWithCode(adUnitCode, bid) { adUnitCodesHandled[adUnitCode] = true; - bidmanager.addBidResponse(adUnitCode, bid); + addBid(adUnitCode, bid); } function fillNoBids() { bidderRequest.bids .map(bidRequest => bidRequest.placementCode) .forEach(adUnitCode => { if (adUnitCode && !adUnitCodesHandled[adUnitCode]) { - bidmanager.addBidResponse(adUnitCode, newEmptyBid()); + addBid(adUnitCode, newEmptyBid()); } }); } + function addBid(code, bid) { + try { + bidmanager.addBidResponse(code, bid); + } catch (err) { + logError('Error adding bid', code, err); + } + } + // After all the responses have come back, fill up the "no bid" bids and // register any required usersync pixels. const responses = []; @@ -185,17 +199,17 @@ export function newBidder(spec) { } } - const bidRequests = bidderRequest.bids.filter(filterAndWarn); - if (bidRequests.length === 0) { + const validBidRequests = bidderRequest.bids.filter(filterAndWarn); + if (validBidRequests.length === 0) { afterAllResponses(); return; } const bidRequestMap = {}; - bidRequests.forEach(bid => { + validBidRequests.forEach(bid => { bidRequestMap[bid.bidId] = bid; }); - let requests = spec.buildRequests(bidRequests, bidderRequest); + let requests = spec.buildRequests(validBidRequests, bidderRequest); if (!requests || requests.length === 0) { afterAllResponses(); return; @@ -220,10 +234,10 @@ export function newBidder(spec) { error: onFailure }, undefined, - { + Object.assign({ method: 'GET', withCredentials: true - } + }, request.options) ); break; case 'POST': @@ -234,11 +248,11 @@ export function newBidder(spec) { error: onFailure }, typeof request.data === 'string' ? request.data : JSON.stringify(request.data), - { + Object.assign({ method: 'POST', contentType: 'text/plain', withCredentials: true - } + }, request.options) ); break; default: diff --git a/src/bidmanager.js b/src/bidmanager.js index 40b3e0017cb..8eef5fbb9ac 100644 --- a/src/bidmanager.js +++ b/src/bidmanager.js @@ -1,6 +1,6 @@ import { uniques, flatten, adUnitsFilter, getBidderRequest } from './utils'; import { getPriceBucketString } from './cpmBucketManager'; -import { NATIVE_KEYS, nativeBidIsValid } from './native'; +import { nativeBidIsValid, getNativeTargeting } from './native'; import { isValidVideoBid } from './video'; import { getCacheUrl, store } from './videoCache'; import { Renderer } from 'src/Renderer'; @@ -288,13 +288,8 @@ function getKeyValueTargetingPairs(bidderCode, custBidObj) { custBidObj.sendStandardTargeting = defaultBidderSettingsMap[bidderCode].sendStandardTargeting; } - // set native key value targeting - if (custBidObj.native) { - Object.keys(custBidObj.native).forEach(asset => { - const key = NATIVE_KEYS[asset]; - const value = custBidObj.native[asset]; - if (key) { keyValues[key] = value; } - }); + if (custBidObj['native']) { + keyValues = Object.assign({}, keyValues, getNativeTargeting(custBidObj)); } return keyValues; @@ -444,10 +439,16 @@ events.on(CONSTANTS.EVENTS.BID_ADJUSTMENT, function (bid) { function adjustBids(bid) { var code = bid.bidderCode; var bidPriceAdjusted = bid.cpm; - if (code && $$PREBID_GLOBAL$$.bidderSettings && $$PREBID_GLOBAL$$.bidderSettings[code]) { - if (typeof $$PREBID_GLOBAL$$.bidderSettings[code].bidCpmAdjustment === 'function') { + let bidCpmAdjustment; + if ($$PREBID_GLOBAL$$.bidderSettings) { + if (code && $$PREBID_GLOBAL$$.bidderSettings[code] && typeof $$PREBID_GLOBAL$$.bidderSettings[code].bidCpmAdjustment === 'function') { + bidCpmAdjustment = $$PREBID_GLOBAL$$.bidderSettings[code].bidCpmAdjustment; + } else if ($$PREBID_GLOBAL$$.bidderSettings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD] && typeof $$PREBID_GLOBAL$$.bidderSettings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD].bidCpmAdjustment === 'function') { + bidCpmAdjustment = $$PREBID_GLOBAL$$.bidderSettings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD].bidCpmAdjustment; + } + if (bidCpmAdjustment) { try { - bidPriceAdjusted = $$PREBID_GLOBAL$$.bidderSettings[code].bidCpmAdjustment.call(null, bid.cpm, Object.assign({}, bid)); + bidPriceAdjusted = bidCpmAdjustment(bid.cpm, Object.assign({}, bid)); } catch (e) { utils.logError('Error during bid adjustment', 'bidmanager.js', e); } @@ -467,48 +468,49 @@ function getStandardBidderSettings() { let granularity = config.getConfig('priceGranularity'); let bidder_settings = $$PREBID_GLOBAL$$.bidderSettings; if (!bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD]) { - bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD] = { - adserverTargeting: [ - { - key: 'hb_bidder', - val: function (bidResponse) { - return bidResponse.bidderCode; - } - }, { - key: 'hb_adid', - val: function (bidResponse) { - return bidResponse.adId; - } - }, { - key: 'hb_pb', - val: function (bidResponse) { - if (granularity === CONSTANTS.GRANULARITY_OPTIONS.AUTO) { - return bidResponse.pbAg; - } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.DENSE) { - return bidResponse.pbDg; - } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.LOW) { - return bidResponse.pbLg; - } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.MEDIUM) { - return bidResponse.pbMg; - } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.HIGH) { - return bidResponse.pbHg; - } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.CUSTOM) { - return bidResponse.pbCg; - } - } - }, { - key: 'hb_size', - val: function (bidResponse) { - return bidResponse.size; - } - }, { - key: 'hb_deal', - val: function (bidResponse) { - return bidResponse.dealId; + bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD] = {}; + } + if (!bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD][CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING]) { + bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD][CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] = [ + { + key: 'hb_bidder', + val: function (bidResponse) { + return bidResponse.bidderCode; + } + }, { + key: 'hb_adid', + val: function (bidResponse) { + return bidResponse.adId; + } + }, { + key: 'hb_pb', + val: function (bidResponse) { + if (granularity === CONSTANTS.GRANULARITY_OPTIONS.AUTO) { + return bidResponse.pbAg; + } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.DENSE) { + return bidResponse.pbDg; + } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.LOW) { + return bidResponse.pbLg; + } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.MEDIUM) { + return bidResponse.pbMg; + } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.HIGH) { + return bidResponse.pbHg; + } else if (granularity === CONSTANTS.GRANULARITY_OPTIONS.CUSTOM) { + return bidResponse.pbCg; } } - ] - }; + }, { + key: 'hb_size', + val: function (bidResponse) { + return bidResponse.size; + } + }, { + key: 'hb_deal', + val: function (bidResponse) { + return bidResponse.dealId; + } + } + ]; } return bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD]; } diff --git a/src/config.js b/src/config.js index 704cb630982..41ba9d25301 100644 --- a/src/config.js +++ b/src/config.js @@ -23,12 +23,12 @@ const DEFAULT_USERSYNC = { }; const GRANULARITY_OPTIONS = { - 'LOW': 'low', - 'MEDIUM': 'medium', - 'HIGH': 'high', - 'AUTO': 'auto', - 'DENSE': 'dense', - 'CUSTOM': 'custom' + LOW: 'low', + MEDIUM: 'medium', + HIGH: 'high', + AUTO: 'auto', + DENSE: 'dense', + CUSTOM: 'custom' }; const ALL_TOPICS = '*'; @@ -174,6 +174,7 @@ export function newConfig() { function setConfig(options) { if (typeof options !== 'object') { utils.logError('setConfig options must be an object'); + return; } Object.assign(config, options); diff --git a/src/constants.json b/src/constants.json index a4649e09c31..806b3790c12 100644 --- a/src/constants.json +++ b/src/constants.json @@ -32,7 +32,8 @@ "BID_RESPONSE": "bidResponse", "BID_WON": "bidWon", "SET_TARGETING": "setTargeting", - "REQUEST_BIDS": "requestBids" + "REQUEST_BIDS": "requestBids", + "ADD_AD_UNITS": "addAdUnits" }, "EVENT_ID_PATHS": { "bidWon": "adUnitCode" diff --git a/src/native.js b/src/native.js index 8a46fa07dbc..c992cf9ad61 100644 --- a/src/native.js +++ b/src/native.js @@ -1,4 +1,4 @@ -import { getBidRequest, logError, triggerPixel } from './utils'; +import { deepAccess, getBidRequest, logError, triggerPixel } from './utils'; export const nativeAdapters = []; @@ -17,12 +17,12 @@ export const NATIVE_TARGETING_KEYS = Object.keys(NATIVE_KEYS).map( ); const IMAGE = { - image: {required: true}, - title: {required: true}, - sponsoredBy: {required: true}, - clickUrl: {required: true}, - body: {required: false}, - icon: {required: false}, + image: { required: true }, + title: { required: true }, + sponsoredBy: { required: true }, + clickUrl: { required: true }, + body: { required: false }, + icon: { required: false }, }; const SUPPORTED_TYPES = { @@ -59,7 +59,11 @@ function typeIsSupported(type) { * TODO: abstract this and the video helper functions into general * adunit validation helper functions */ -export const nativeAdUnit = adUnit => adUnit.mediaType === 'native'; +export const nativeAdUnit = adUnit => { + const mediaType = adUnit.mediaType === 'native'; + const mediaTypes = deepAccess(adUnit, 'mediaTypes.native'); + return mediaType || mediaTypes; +} export const nativeBidder = bid => nativeAdapters.includes(bid.bidder); export const hasNonNativeBidder = adUnit => adUnit.bids.filter(bid => !nativeBidder(bid)).length; @@ -70,28 +74,83 @@ export const hasNonNativeBidder = adUnit => */ export function nativeBidIsValid(bid) { const bidRequest = getBidRequest(bid.adId); - if (!bidRequest) { return false; } + if (!bidRequest) { + return false; + } + + // all native bid responses must define a landing page url + if (!deepAccess(bid, 'native.clickUrl')) { + return false; + } const requestedAssets = bidRequest.nativeParams; - if (!requestedAssets) { return true; } + if (!requestedAssets) { + return true; + } const requiredAssets = Object.keys(requestedAssets).filter( key => requestedAssets[key].required ); - const returnedAssets = Object.keys(bid.native).filter(key => bid.native[key]); + const returnedAssets = Object.keys(bid['native']).filter( + key => bid['native'][key] + ); return requiredAssets.every(asset => returnedAssets.includes(asset)); } /* - * Native responses may have impression trackers. This retrieves the - * impression tracker urls for the given ad object and fires them. + * Native responses may have associated impression or click trackers. + * This retrieves the appropriate tracker urls for the given ad object and + * fires them. As a native creatives may be in a cross-origin frame, it may be + * necessary to invoke this function via postMessage. secureCreatives is + * configured to fire this function when it receives a `message` of 'Prebid Native' + * and an `adId` with the value of the `bid.adId`. When a message is posted with + * these parameters, impression trackers are fired. To fire click trackers, the + * message should contain an `action` set to 'click'. + * + * // Native creative template example usage + * + * %%PATTERN:hb_native_title%% + * + * + * */ -export function fireNativeImpressions(adObject) { - const impressionTrackers = adObject.native && - adObject.native.impressionTrackers; +export function fireNativeTrackers(message, adObject) { + let trackers; - (impressionTrackers || []).forEach(tracker => { - triggerPixel(tracker); + if (message.action === 'click') { + trackers = adObject['native'] && adObject['native'].clickTrackers; + } else { + trackers = adObject['native'] && adObject['native'].impressionTrackers; + } + + (trackers || []).forEach(triggerPixel); +} + +/** + * Gets native targeting key-value paris + * @param {Object} bid + * @return {Object} targeting + */ +export function getNativeTargeting(bid) { + let keyValues = {}; + + Object.keys(bid['native']).forEach(asset => { + const key = NATIVE_KEYS[asset]; + const value = bid['native'][asset]; + if (key) { + keyValues[key] = value; + } }); + + return keyValues; } diff --git a/src/prebid.js b/src/prebid.js index 14f88021ca9..5e3168a829d 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -28,6 +28,7 @@ const { syncUsers, triggerUserSyncs } = userSync; var BID_WON = CONSTANTS.EVENTS.BID_WON; var SET_TARGETING = CONSTANTS.EVENTS.SET_TARGETING; +var ADD_AD_UNITS = CONSTANTS.EVENTS.ADD_AD_UNITS; var auctionRunning = false; var bidRequestQueue = []; @@ -447,6 +448,8 @@ $$PREBID_GLOBAL$$.addAdUnits = function (adUnitArr) { adUnitArr.transactionId = utils.generateUUID(); $$PREBID_GLOBAL$$.adUnits.push(adUnitArr); } + // emit event + events.emit(ADD_AD_UNITS); }; /** @@ -608,8 +611,17 @@ $$PREBID_GLOBAL$$.loadScript = function (tagSrc, callback, useCache) { }; /** - * Will enable sending a prebid.js to data provider specified - * @param {Object} config object {provider : 'string', options : {}} + * Enable sending analytics data to the analytics provider of your + * choice. + * + * For usage, see [Integrate with the Prebid Analytics + * API](http://prebid.org/dev-docs/integrate-with-the-prebid-analytics-api.html). + * + * For a list of supported analytics adapters, see [Analytics for + * Prebid](http://prebid.org/overview/analytics.html). + * @param {Object} config + * @param {string} config.provider The name of the provider, e.g., `"ga"` for Google Analytics. + * @param {Object} config.options The options for this particular analytics adapter. This will likely vary between adapters. */ $$PREBID_GLOBAL$$.enableAnalytics = function (config) { if (config && !utils.isEmpty(config)) { @@ -654,6 +666,45 @@ $$PREBID_GLOBAL$$.enableSendAllBids = function () { config.setConfig({ enableSendAllBids: true }); }; +/** + * The bid response object returned by an external bidder adapter during the auction. + * @typedef {Object} AdapterBidResponse + * @property {string} pbAg Auto granularity price bucket; CPM <= 5 ? increment = 0.05 : CPM > 5 && CPM <= 10 ? increment = 0.10 : CPM > 10 && CPM <= 20 ? increment = 0.50 : CPM > 20 ? priceCap = 20.00. Example: `"0.80"`. + * @property {string} pbCg Custom price bucket. For example setup, see {@link setPriceGranularity}. Example: `"0.84"`. + * @property {string} pbDg Dense granularity price bucket; CPM <= 3 ? increment = 0.01 : CPM > 3 && CPM <= 8 ? increment = 0.05 : CPM > 8 && CPM <= 20 ? increment = 0.50 : CPM > 20? priceCap = 20.00. Example: `"0.84"`. + * @property {string} pbLg Low granularity price bucket; $0.50 increment, capped at $5, floored to two decimal places. Example: `"0.50"`. + * @property {string} pbMg Medium granularity price bucket; $0.10 increment, capped at $20, floored to two decimal places. Example: `"0.80"`. + * @property {string} pbHg High granularity price bucket; $0.01 increment, capped at $20, floored to two decimal places. Example: `"0.84"`. + * + * @property {string} bidder The string name of the bidder. This *may* be the same as the `bidderCode`. For For a list of all bidders and their codes, see [Bidders' Params](http://prebid.org/dev-docs/bidders.html). + * @property {string} bidderCode The unique string that identifies this bidder. For a list of all bidders and their codes, see [Bidders' Params](http://prebid.org/dev-docs/bidders.html). + * + * @property {string} requestId The [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) representing the bid request. + * @property {number} requestTimestamp The time at which the bid request was sent out, expressed in milliseconds. + * @property {number} responseTimestamp The time at which the bid response was received, expressed in milliseconds. + * @property {number} timeToRespond How long it took for the bidder to respond with this bid, expressed in milliseconds. + * + * @property {string} size The size of the ad creative, expressed in `"AxB"` format, where A and B are numbers of pixels. Example: `"320x50"`. + * @property {string} width The width of the ad creative in pixels. Example: `"320"`. + * @property {string} height The height of the ad creative in pixels. Example: `"50"`. + * + * @property {string} ad The actual ad creative content, often HTML with CSS, JavaScript, and/or links to additional content. Example: `"",`. + * @property {number} ad_id The ad ID of the creative, as understood by the bidder's system. Used by the line item's [creative in the ad server](http://prebid.org/adops/send-all-bids-adops.html#step-3-add-a-creative). + * @property {string} adUnitCode The code used to uniquely identify the ad unit on the publisher's page. + * + * @property {string} statusMessage The status of the bid. Allowed values: `"Bid available"` or `"Bid returned empty or error response"`. + * @property {number} cpm The exact bid price from the bidder, expressed to the thousandths place. Example: `"0.849"`. + * + * @property {Object} adserverTargeting An object whose values represent the ad server's targeting on the bid. + * @property {string} adserverTargeting.hb_adid The ad ID of the creative, as understood by the ad server. + * @property {string} adserverTargeting.hb_pb The price paid to show the creative, as logged in the ad server. + * @property {string} adserverTargeting.hb_bidder The winning bidder whose ad creative will be served by the ad server. +*/ + +/** + * Get all of the bids that have won their respective auctions. Useful for [troubleshooting your integration](http://prebid.org/dev-docs/prebid-troubleshooting-guide.html). + * @return {Array} A list of bids that have won their respective auctions. +*/ $$PREBID_GLOBAL$$.getAllWinningBids = function () { return $$PREBID_GLOBAL$$._winningBids; }; @@ -758,8 +809,50 @@ $$PREBID_GLOBAL$$.setS2SConfig = function(options) { $$PREBID_GLOBAL$$.getConfig = config.getConfig; /** - * Set Prebid config options - * @param {Object} options + * Set Prebid config options. + * (Added in version 0.27.0). + * + * `setConfig` is designed to allow for advanced configuration while + * reducing the surface area of the public API. For more information + * about the move to `setConfig` (and the resulting deprecations of + * some other public methods), see [the Prebid 1.0 public API + * proposal](https://gist.github.com/mkendall07/51ee5f6b9f2df01a89162cf6de7fe5b6). + * + * #### Troubleshooting your configuration + * + * If you call `pbjs.setConfig` without an object, e.g., + * + * `pbjs.setConfig('debug', 'true'))` + * + * then Prebid.js will print an error to the console that says: + * + * ``` + * ERROR: setConfig options must be an object + * ``` + * + * If you don't see that message, you can assume the config object is valid. + * + * @param {Object} options Global Prebid configuration object. Must be JSON - no JavaScript functions are allowed. + * @param {string} options.bidderSequence The order in which bidders are called. Example: `pbjs.setConfig({ bidderSequence: "fixed" })`. Allowed values: `"fixed"` (order defined in `adUnit.bids` array on page), `"random"`. + * @param {boolean} options.debug Turn debug logging on/off. Example: `pbjs.setConfig({ debug: true })`. + * @param {string} options.priceGranularity The bid price granularity to use. Example: `pbjs.setConfig({ priceGranularity: "medium" })`. Allowed values: `"low"` ($0.50), `"medium"` ($0.10), `"high"` ($0.01), `"auto"` (sliding scale), `"dense"` (like `"auto"`, with smaller increments at lower CPMs), or a custom price bucket object, e.g., `{ "buckets" : [{"min" : 0,"max" : 20,"increment" : 0.1,"cap" : true}]}`. + * @param {boolean} options.enableSendAllBids Turn "send all bids" mode on/off. Example: `pbjs.setConfig({ enableSendAllBids: true })`. + * @param {number} options.bidderTimeout Set a global bidder timeout, in milliseconds. Example: `pbjs.setConfig({ bidderTimeout: 3000 })`. Note that it's still possible for a bid to get into the auction that responds after this timeout. This is due to how [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) works in JS: it queues the callback in the event loop in an approximate location that should execute after this time but it is not guaranteed. For more information about the asynchronous event loop and `setTimeout`, see [How JavaScript Timers Work](https://johnresig.com/blog/how-javascript-timers-work/). + * @param {string} options.publisherDomain The publisher's domain where Prebid is running, for cross-domain iFrame communication. Example: `pbjs.setConfig({ publisherDomain: "https://www.theverge.com" })`. + * @param {number} options.cookieSyncDelay A delay (in milliseconds) for requesting cookie sync to stay out of the critical path of page load. Example: `pbjs.setConfig({ cookieSyncDelay: 100 })`. + * @param {Object} options.s2sConfig The configuration object for [server-to-server header bidding](http://prebid.org/dev-docs/get-started-with-prebid-server.html). Example: + * ``` + * pbjs.setConfig({ + * s2sConfig: { + * accountId: '1', + * enabled: true, + * bidders: ['appnexus', 'pubmatic'], + * timeout: 1000, + * adapter: 'prebidServer', + * endpoint: 'https://prebid.adnxs.com/pbs/v1/auction' + * } + * }) + * ``` */ $$PREBID_GLOBAL$$.setConfig = config.setConfig; diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 2402ba755f4..efc1386fde3 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -4,7 +4,7 @@ */ import events from './events'; -import { fireNativeImpressions } from './native'; +import { fireNativeTrackers } from './native'; import { EVENTS } from './constants'; const BID_WON = EVENTS.BID_WON; @@ -42,7 +42,7 @@ function receiveMessage(ev) { // adId: '%%PATTERN:hb_adid%%' // }), '*'); if (data.message === 'Prebid Native') { - fireNativeImpressions(adObject); + fireNativeTrackers(data, adObject); $$PREBID_GLOBAL$$._winningBids.push(adObject); events.emit(BID_WON, adObject); } diff --git a/src/storagemanager.js b/src/storagemanager.js deleted file mode 100644 index cf6770dc2a7..00000000000 --- a/src/storagemanager.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Storage Manager aims to provide a consistent but concise API to persist data where conditions may require alternatives - * to localStorage (storing as cookie, in indexedDB, etc), or potentially a mechanism for x-domain storage - * - * Only html5 localStorage implemented currently. - * -*/ - -import { logWarn } from './utils'; - -export const pbjsSyncsKey = 'pbjsSyncs'; - -export function newStorageManager() { - function set(key, item) { - try { - localStorage.setItem(key, JSON.stringify(item)); - } catch (e) { - logWarn('could not set storage item: ', e); - } - } - - function get(key) { - try { - const item = JSON.parse(localStorage.getItem(key)); - return item && item.length ? item : []; - } catch (e) { - logWarn('could not get storage item: ', e); - return []; - } - } - - return { - get, - set, - - add(key, element, unique = false) { - set(key, get(key) - .concat([element]) - .filter((value, index, array) => unique ? array.indexOf(value) === index : true)); - }, - - remove(key, element) { - set(key, get(key).filter(value => value !== element)); - } - } -} - -export const StorageManager = newStorageManager(); diff --git a/src/url.js b/src/url.js index 5bfeb9c3151..502245f3abd 100644 --- a/src/url.js +++ b/src/url.js @@ -38,7 +38,7 @@ export function parse(url, options) { pathname: parsed.pathname.replace(/^(?!\/)/, '/'), search: parseQS(parsed.search || ''), hash: (parsed.hash || '').replace(/^#/, ''), - host: parsed.host + host: parsed.host || window.location.host }; } diff --git a/src/userSync.js b/src/userSync.js index eb8b3ef5e64..8fb8c04cd24 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -19,7 +19,11 @@ export function newUserSync(userSyncDependencies) { let numAdapterBids = {}; // Use what is in config by default - const config = userSyncDependencies.config; + let usConfig = userSyncDependencies.config; + // Update if it's (re)set + config.getConfig('userSync', (conf) => { + usConfig = Object.assign(usConfig, conf.userSync); + }); /** * @function getDefaultQueue @@ -40,7 +44,7 @@ export function newUserSync(userSyncDependencies) { * @private */ function fireSyncs() { - if (!config.syncEnabled || !userSyncDependencies.browserSupportsCookies || hasFired) { + if (!usConfig.syncEnabled || !userSyncDependencies.browserSupportsCookies || hasFired) { return; } @@ -63,7 +67,7 @@ export function newUserSync(userSyncDependencies) { * @private */ function fireImagePixels() { - if (!config.pixelEnabled) { + if (!usConfig.pixelEnabled) { return; } // Randomize the order of the pixels before firing @@ -83,14 +87,14 @@ export function newUserSync(userSyncDependencies) { * @private */ function loadIframes() { - if (!config.iframeEnabled) { + if (!usConfig.iframeEnabled) { return; } // Randomize the order of these syncs just like the pixels above utils.shuffle(queue.iframe).forEach((sync) => { let [bidderName, iframeUrl] = sync; utils.logMessage(`Invoking iframe user sync for bidder: ${bidderName}`); - // Create image object and add the src url + // Insert iframe into DOM utils.insertUserSyncIframe(iframeUrl); }); } @@ -125,18 +129,18 @@ export function newUserSync(userSyncDependencies) { * userSync.registerSync('image', 'rubicon', 'http://example.com/pixel') */ publicApi.registerSync = (type, bidder, url) => { - if (!config.syncEnabled || !utils.isArray(queue[type])) { - return utils.logWarn(`User sync type "{$type}" not supported`); + if (!usConfig.syncEnabled || !utils.isArray(queue[type])) { + return utils.logWarn(`User sync type "${type}" not supported`); } if (!bidder) { return utils.logWarn(`Bidder is required for registering sync`); } - if (Number(numAdapterBids[bidder]) >= config.syncsPerBidder) { + if (Number(numAdapterBids[bidder]) >= usConfig.syncsPerBidder) { return utils.logWarn(`Number of user syncs exceeded for "{$bidder}"`); } // All bidders are enabled by default. If specified only register for enabled bidders. - let hasEnabledBidders = config.enabledBidders && config.enabledBidders.length; - if (hasEnabledBidders && config.enabledBidders.indexOf(bidder) < 0) { + let hasEnabledBidders = usConfig.enabledBidders && usConfig.enabledBidders.length; + if (hasEnabledBidders && usConfig.enabledBidders.indexOf(bidder) < 0) { return utils.logWarn(`Bidder "${bidder}" not supported`); } queue[type].push([bidder, url]); @@ -162,7 +166,7 @@ export function newUserSync(userSyncDependencies) { * @public */ publicApi.triggerUserSyncs = () => { - if (config.enableOverride) { + if (usConfig.enableOverride) { publicApi.syncUsers(); } }; diff --git a/src/utils.js b/src/utils.js index 15e0ef30b8d..00a06fcb091 100644 --- a/src/utils.js +++ b/src/utils.js @@ -463,7 +463,7 @@ exports.triggerPixel = function (url) { * @param {string} encodeUri boolean if URL should be encoded before inserted. Defaults to true */ exports.insertUserSyncIframe = function(url) { - let iframeHtml = this.createTrackPixelIframeHtml(url, false, 'allow-scripts'); + let iframeHtml = this.createTrackPixelIframeHtml(url, false, 'allow-scripts allow-same-origin'); let div = document.createElement('div'); div.innerHTML = iframeHtml; let iframe = div.firstChild; diff --git a/test/pages/video.html b/test/pages/video.html index 755815111d1..8d28650cbfc 100644 --- a/test/pages/video.html +++ b/test/pages/video.html @@ -31,7 +31,9 @@ var videoAdUnit = { code: 'video1', sizes: [640,480], - mediaType: 'video', + mediaTypes: { + video: {context: 'instream'} + }, bids: [ { bidder: 'appnexusAst', diff --git a/test/spec/bidmanager_spec.js b/test/spec/bidmanager_spec.js index fd5f4fa2b31..967258a7fbc 100644 --- a/test/spec/bidmanager_spec.js +++ b/test/spec/bidmanager_spec.js @@ -198,7 +198,7 @@ describe('bidmanager.js', function () { assert.deepEqual(response, expected); }); - it('Custom bidCpmAdjustment for one bidder and inherit standard', function () { + it('Custom bidCpmAdjustment for one bidder and inherit standard but doesn\'t use standard bidCpmAdjustment', function () { $$PREBID_GLOBAL$$.bidderSettings = { appnexus: { @@ -207,6 +207,9 @@ describe('bidmanager.js', function () { }, }, standard: { + bidCpmAdjustment: function (bidCpm) { + return 200; + }, adserverTargeting: [ { key: 'hb_bidder', @@ -235,6 +238,27 @@ describe('bidmanager.js', function () { assert.deepEqual(response, expected); }); + it('Standard bidCpmAdjustment changes the bid of any bidder', function () { + const bid = Object.assign({}, + bidfactory.createBid(2), + fixtures.getBidResponses()[5] + ); + + assert.equal(bid.cpm, 0.5); + + $$PREBID_GLOBAL$$.bidderSettings = + { + standard: { + bidCpmAdjustment: function (bidCpm) { + return bidCpm * 0.5; + } + } + }; + + bidmanager.adjustBids(bid) + assert.equal(bid.cpm, 0.25); + }); + it('Custom bidCpmAdjustment AND custom configuration for one bidder and inherit standard settings', function () { $$PREBID_GLOBAL$$.bidderSettings = { @@ -549,10 +573,11 @@ describe('bidmanager.js', function () { sinon.stub(utils, 'getBidRequest', () => ({ start: timestamp(), bidder: 'appnexusAst', - nativeParams: { - title: {'required': true}, + mediaTypes: { + native: { + title: {required: true}, + } }, - mediaType: 'native', })); const bid = Object.assign({}, @@ -575,10 +600,11 @@ describe('bidmanager.js', function () { const bidRequest = () => ({ start: timestamp(), bidder: 'appnexusAst', - nativeParams: { - title: {'required': true}, + mediaTypes: { + native: { + title: {required: true}, + } }, - mediaType: 'native', }); sinon.stub(utils, 'getBidRequest', bidRequest); sinon.stub(utils, 'getBidderRequest', bidRequest); @@ -588,7 +614,10 @@ describe('bidmanager.js', function () { { bidderCode: 'appnexusAst', mediaType: 'native', - native: {title: 'foo'} + native: { + title: 'foo', + clickUrl: 'example.link' + } } ); diff --git a/test/spec/config_spec.js b/test/spec/config_spec.js index efdfd911728..14452987091 100644 --- a/test/spec/config_spec.js +++ b/test/spec/config_spec.js @@ -1,4 +1,4 @@ -import { excpet } from 'chai'; +import { expect } from 'chai'; import { assert } from 'chai'; import { newConfig } from 'src/config'; @@ -33,6 +33,11 @@ describe('config API', () => { expect(getConfig('baz')).to.equal('qux'); }); + it('only accepts objects', () => { + setConfig('invalid'); + expect(getConfig('0')).to.not.equal('i'); + }); + it('sets multiple config properties', () => { setConfig({ foo: 'bar' }); setConfig({ biz: 'buz' }); diff --git a/test/spec/modules/adomikAnalyticsAdapter_spec.js b/test/spec/modules/adomikAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..a3e0d214e5a --- /dev/null +++ b/test/spec/modules/adomikAnalyticsAdapter_spec.js @@ -0,0 +1,137 @@ +import adomikAnalytics from 'modules/adomikAnalyticsAdapter'; +import {expect} from 'chai'; +let events = require('src/events'); +let adaptermanager = require('src/adaptermanager'); +let constants = require('src/constants.json'); + +describe('Adomik Prebid Analytic', function () { + describe('enableAnalytics', function () { + beforeEach(() => { + sinon.spy(adomikAnalytics, 'track'); + sinon.spy(adomikAnalytics, 'sendTypedEvent'); + }); + + afterEach(() => { + adomikAnalytics.track.restore(); + adomikAnalytics.sendTypedEvent.restore(); + }); + + it('should catch all events', function (done) { + adaptermanager.registerAnalyticsAdapter({ + code: 'adomik', + adapter: adomikAnalytics + }); + + const initOptions = { + id: '123456', + url: 'testurl', + }; + + const bid = { + bidderCode: 'adomik_test_bid', + width: 10, + height: 10, + statusMessage: 'Bid available', + adId: '1234', + requestId: '', + responseTimestamp: 1496410856397, + requestTimestamp: 1496410856295, + cpm: 0.1, + bidder: 'biddertest', + adUnitCode: '0000', + timeToRespond: 100, + placementCode: 'placementtest' + } + + // Step 1: Initialize adapter + adaptermanager.enableAnalytics({ + provider: 'adomik', + options: initOptions + }); + expect(adomikAnalytics.currentContext).to.deep.equal({ + uid: '123456', + url: 'testurl', + debug: undefined, + id: '', + timeouted: false, + timeout: 0, + }); + + // Step 1: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, requestId: 'test-test-test', timeout: 3000}); + + expect(adomikAnalytics.currentContext).to.deep.equal({ + uid: '123456', + url: 'testurl', + debug: undefined, + id: 'test-test-test', + timeouted: false, + timeout: 3000, + }); + + // Step 2: Send bid requested event + events.emit(constants.EVENTS.BID_REQUESTED, { bids: [bid] }); + + expect(adomikAnalytics.bucketEvents.length).to.equal(1); + expect(adomikAnalytics.bucketEvents[0]).to.deep.equal({ + type: 'request', + event: { + bidder: 'BIDDERTEST', + placementCode: 'placementtest', + } + }); + + // Step 3: Send bid response event + events.emit(constants.EVENTS.BID_RESPONSE, bid); + + expect(adomikAnalytics.bucketEvents.length).to.equal(2); + expect(adomikAnalytics.bucketEvents[1]).to.deep.equal({ + type: 'response', + event: { + bidder: 'ADOMIK_TEST_BID', + placementCode: '0000', + id: '1234', + status: 'VALID', + cpm: 0.1, + size: { + width: 10, + height: 10 + }, + timeToRespond: 100, + afterTimeout: false, + } + }); + + // Step 4: Send bid won event + events.emit(constants.EVENTS.BID_WON, bid); + + expect(adomikAnalytics.bucketEvents.length).to.equal(3); + expect(adomikAnalytics.bucketEvents[2]).to.deep.equal({ + type: 'winner', + event: { + id: '1234', + placementCode: '0000', + } + }); + + // Step 5: Send bid timeout event + events.emit(constants.EVENTS.BID_TIMEOUT, {}); + + expect(adomikAnalytics.currentContext.timeouted).to.equal(true); + + // Step 6: Send auction end event + var clock = sinon.useFakeTimers(); + events.emit(constants.EVENTS.AUCTION_END, {}); + + setTimeout(function() { + sinon.assert.callCount(adomikAnalytics.sendTypedEvent, 1); + done(); + }, 3000); + + clock.tick(5000); + clock.restore(); + + sinon.assert.callCount(adomikAnalytics.track, 6); + }); + }); +}); diff --git a/test/spec/modules/adxcgAnalyticsAdapter_spec.js b/test/spec/modules/adxcgAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..790a39789b2 --- /dev/null +++ b/test/spec/modules/adxcgAnalyticsAdapter_spec.js @@ -0,0 +1,81 @@ +import adxcgAnalyticsAdapter from 'modules/adxcgAnalyticsAdapter'; +import { expect } from 'chai'; +let adaptermanager = require('src/adaptermanager'); +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('adxcg analytics adapter', () => { + let xhr; + let requests; + + beforeEach(() => { + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = request => requests.push(request); + }); + + afterEach(() => { + xhr.restore(); + }); + + describe('track', () => { + it('builds and sends auction data', () => { + let auctionTimestamp = 42; + let initOptions = { + publisherId: '42' + }; + let bidRequest = { + requestId: 'requestIdData' + }; + let bidResponse = { + adId: 'adIdData', + ad: 'adContent' + }; + + adaptermanager.registerAnalyticsAdapter({ + code: 'adxcg', + adapter: adxcgAnalyticsAdapter + }); + + adaptermanager.enableAnalytics({ + provider: 'adxcg', + options: initOptions + }); + + events.emit(constants.EVENTS.AUCTION_INIT, { + timestamp: auctionTimestamp + }); + events.emit(constants.EVENTS.BID_REQUESTED, bidRequest); + events.emit(constants.EVENTS.BID_RESPONSE, bidResponse); + events.emit(constants.EVENTS.AUCTION_END, {}); + + expect(requests.length).to.equal(1); + + let auctionEventData = JSON.parse(requests[0].requestBody); + + expect(auctionEventData.bidRequests.length).to.equal(1); + expect(auctionEventData.bidRequests[0]).to.deep.equal(bidRequest); + + expect(auctionEventData.bidResponses.length).to.equal(1); + expect(auctionEventData.bidResponses[0].adId).to.equal(bidResponse.adId); + expect(auctionEventData.bidResponses[0]).to.not.have.property('ad'); + + expect(auctionEventData.initOptions).to.deep.equal(initOptions); + expect(auctionEventData.auctionTimestamp).to.equal(auctionTimestamp); + + events.emit(constants.EVENTS.BID_WON, { + adId: 'adIdData', + ad: 'adContent' + }); + + expect(requests.length).to.equal(2); + + let winEventData = JSON.parse(requests[1].requestBody); + + expect(winEventData.bidWon.adId).to.equal(bidResponse.adId); + expect(winEventData.bidWon).to.not.have.property('ad'); + expect(winEventData.initOptions).to.deep.equal(initOptions); + expect(winEventData.auctionTimestamp).to.equal(auctionTimestamp); + }); + }); +}); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index 8911762b583..05c112c2a99 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -29,7 +29,8 @@ describe('Adyoulike Adapter', () => { 'placementCode': 'adunit/hb-0', 'params': { 'placement': 'placement_0' - } + }, + 'transactionId': 'bid_id_0_transaction_id' } ], }; @@ -43,7 +44,8 @@ describe('Adyoulike Adapter', () => { 'params': { 'placement': 'placement_0' }, - 'sizes': '300x250' + 'sizes': '300x250', + 'transactionId': 'bid_id_0_transaction_id' } ], }; @@ -57,7 +59,8 @@ describe('Adyoulike Adapter', () => { 'params': { 'placement': 'placement_0' }, - 'sizes': '300x250' + 'sizes': '300x250', + 'transactionId': 'bid_id_0_transaction_id' }, { 'bidId': 'bid_id_1', @@ -66,14 +69,16 @@ describe('Adyoulike Adapter', () => { 'params': { 'placement': 'placement_1' }, - 'sizes': [[300, 600]] + 'sizes': [[300, 600]], + 'transactionId': 'bid_id_1_transaction_id' }, { 'bidId': 'bid_id_2', 'bidder': 'adyoulike', 'placementCode': 'adunit/hb-2', 'params': {}, - 'sizes': '300x400' + 'sizes': '300x400', + 'transactionId': 'bid_id_2_transaction_id' }, { 'bidId': 'bid_id_3', @@ -81,7 +86,8 @@ describe('Adyoulike Adapter', () => { 'placementCode': 'adunit/hb-3', 'params': { 'placement': 'placement_3' - } + }, + 'transactionId': 'bid_id_3_transaction_id' } ], }; @@ -175,9 +181,10 @@ describe('Adyoulike Adapter', () => { expect(requests[0].url).to.contains('CanonicalUrl=' + encodeURIComponent(canonicalUrl)); let body = JSON.parse(requests[0].requestBody); - expect(body.Version).to.equal('0.1'); + expect(body.Version).to.equal('0.2'); expect(body.Placements).deep.equal(['placement_0']); expect(body.PageRefreshed).to.equal(false); + expect(body.TransactionIds).deep.equal({'placement_0': 'bid_id_0_transaction_id'}); }); it('sends bid request to endpoint with single placement without canonical', () => { @@ -190,9 +197,10 @@ describe('Adyoulike Adapter', () => { expect(requests[0].url).to.not.contains('CanonicalUrl=' + encodeURIComponent(canonicalUrl)); let body = JSON.parse(requests[0].requestBody); - expect(body.Version).to.equal('0.1'); + expect(body.Version).to.equal('0.2'); expect(body.Placements).deep.equal(['placement_0']); expect(body.PageRefreshed).to.equal(false); + expect(body.TransactionIds).deep.equal({'placement_0': 'bid_id_0_transaction_id'}); }); it('sends bid request to endpoint with multiple placements', () => { @@ -203,9 +211,10 @@ describe('Adyoulike Adapter', () => { expect(requests[0].url).to.contains('CanonicalUrl=' + encodeURIComponent(canonicalUrl)); let body = JSON.parse(requests[0].requestBody); - expect(body.Version).to.equal('0.1'); + expect(body.Version).to.equal('0.2'); expect(body.Placements).deep.equal(['placement_0', 'placement_1']); expect(body.PageRefreshed).to.equal(false); + expect(body.TransactionIds).deep.equal({'placement_0': 'bid_id_0_transaction_id', 'placement_1': 'bid_id_1_transaction_id'}); }); }); diff --git a/test/spec/modules/appnexusAnalyticsAdapter_spec.js b/test/spec/modules/appnexusAnalyticsAdapter_spec.js deleted file mode 100644 index 7f68359de07..00000000000 --- a/test/spec/modules/appnexusAnalyticsAdapter_spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import appnexusAnalytics from 'modules/appnexusAnalyticsAdapter'; -import { assert } from 'chai'; -import { getBidRequestedPayload } from 'test/fixtures/fixtures'; - -const spyEnqueue = sinon.spy(appnexusAnalytics, 'enqueue'); -const spyTrack = sinon.spy(appnexusAnalytics, 'track'); - -const bidRequestedPayload = getBidRequestedPayload(); - -// describe(` -// FEATURE: AppNexus Prebid Analytics Adapter (APA) -// STORY: As a publisher I use APA to collect data for auction events\n`, ()=> { -// describe(`SCENARIO: Bids are received from bidder -// GIVEN: A publisher page requests bids -// WHEN: The bidRequested event fires`, () => { -// appnexusAnalytics.enqueue('bidRequested', bidRequestedPayload); -// it(`THEN: APA enqueue is called with event payload -// AND: APA track does not get called`, () => { -// assert.ok(spyEnqueue.calledWith('bidRequested')); -// assert.deepEqual(spyEnqueue.args[0][1], bidRequestedPayload); -// assert.ok(!spyTrack.called); -// }); -// }); -// }); diff --git a/test/spec/modules/appnexusAstBidAdapter_spec.js b/test/spec/modules/appnexusAstBidAdapter_spec.js index eea8c55882b..d07ee6df543 100644 --- a/test/spec/modules/appnexusAstBidAdapter_spec.js +++ b/test/spec/modules/appnexusAstBidAdapter_spec.js @@ -1,206 +1,233 @@ import { expect } from 'chai'; import { spec } from 'modules/appnexusAstBidAdapter'; import { newBidder } from 'src/adapters/bidderFactory'; -import bidmanager from 'src/bidmanager'; const ENDPOINT = '//ib.adnxs.com/ut/v3/prebid'; -const REQUEST = { - 'bidderCode': 'appnexusAst', - 'requestId': 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6', - 'bidderRequestId': '7101db09af0db2', - 'bids': [ - { - 'bidder': 'appnexusAst', - 'params': { - 'placementId': '4799418', - }, - 'placementCode': '/19968336/header-bid-tag1', - 'sizes': [ - [728, 90], - [970, 90] - ], - 'bidId': '84ab500420319d', - 'bidderRequestId': '7101db09af0db2', - 'requestId': 'd3e07445-ab06-44c8-a9dd-5ef9af06d2a6' - } - ], - 'start': 1469479810130 -}; - -const RESPONSE = { - 'version': '0.0.1', - 'tags': [{ - 'uuid': '84ab500420319d', - 'tag_id': 4799418, - 'auction_id': '2256922143947979797', - 'no_ad_url': 'http://lax1-ib.adnxs.com/no-ad', - 'timeout_ms': 2500, - 'ads': [{ - 'content_source': 'rtb', - 'ad_type': 'banner', - 'buyer_member_id': 958, - 'creative_id': 33989846, - 'media_type_id': 1, - 'media_subtype_id': 1, - 'cpm': 0.500000, - 'cpm_publisher_currency': 0.500000, - 'publisher_currency_code': '$', - 'client_initiated_ad_counting': true, - 'rtb': { - 'banner': { - 'width': 728, - 'height': 90, - 'content': '' - }, - 'trackers': [{ - 'impression_urls': ['http://lax1-ib.adnxs.com/impression'] - }] - } - }] - }] -}; - describe('AppNexusAdapter', () => { const adapter = newBidder(spec); - describe('request function', () => { - let xhr; - let requests; - - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); - }); - - afterEach(() => xhr.restore()); - + describe('inherited functions', () => { it('exists and is a function', () => { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); + }); - it('requires paramters to make request', () => { - adapter.callBids({}); - expect(requests).to.be.empty; + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'appnexusAst', + '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', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('requires member && invCode', () => { - let backup = REQUEST.bids[0].params; - REQUEST.bids[0].params = {member: 1234}; - adapter.callBids(REQUEST); - expect(requests).to.be.empty; - REQUEST.bids[0].params = backup; + it('should return true when required params found', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'member': '1234', + 'invCode': 'ABCD' + }; + + expect(spec.isBidRequestValid(bid)).to.equal(true); }); - it('attaches valid video params to the tag', () => { - REQUEST.bids[0].params.video = { - id: 123, - minduration: 100, - foobar: 'invalid' + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placementId': 0 }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); - adapter.callBids(REQUEST); + describe('buildRequests', () => { + let bidRequests = [ + { + 'bidder': 'appnexusAst', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; - const request = JSON.parse(requests[0].requestBody).tags[0]; - expect(request.video).to.deep.equal({ - id: 123, - minduration: 100 + it('should add source and verison to the tag', () => { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.sdk).to.exist; + expect(payload.sdk).to.deep.equal({ + source: 'pbjs', + version: '$prebid.version$' }); - - delete REQUEST.bids[0].params.video; }); - it('attaches valid user params to the tag', () => { - REQUEST.bids[0].params.user = { - external_uid: '123', - foobar: 'invalid' - }; + it('sends bid request to ENDPOINT via POST', () => { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); - adapter.callBids(REQUEST); + it('should attach valid video params to the tag', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + video: { + id: 123, + minduration: 100, + foobar: 'invalid' + } + } + } + ); - const request = JSON.parse(requests[0].requestBody); - expect(request.user).to.exist; - expect(request.user).to.deep.equal({ - external_uid: '123', + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + id: 123, + minduration: 100 }); - - delete REQUEST.bids[0].params.user; }); - it('should add source and verison to the tag', () => { - adapter.callBids(REQUEST); + it('should attach valid user params to the tag', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + user: { + external_uid: '123', + foobar: 'invalid' + } + } + } + ); - const request = JSON.parse(requests[0].requestBody); - expect(request.sdk).to.exist; - expect(request.sdk).to.deep.equal({ - source: 'pbjs', - version: '$prebid.version$' + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.user).to.exist; + expect(payload.user).to.deep.equal({ + external_uid: '123', }); }); - it('attaches native params to the request', () => { - REQUEST.bids[0].mediaType = 'native'; - REQUEST.bids[0].nativeParams = { - title: {required: true}, - body: {required: true}, - image: {required: true, sizes: [{ width: 100, height: 100 }] }, - cta: {required: false}, - sponsoredBy: {required: true} - }; + it('should attache native params to the request', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + title: {required: true}, + body: {required: true}, + image: {required: true, sizes: [{ width: 100, height: 100 }] }, + cta: {required: false}, + sponsoredBy: {required: true} + } + } + ); - adapter.callBids(REQUEST); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); - const request = JSON.parse(requests[0].requestBody); - expect(request.tags[0].native.layouts[0]).to.deep.equal({ + expect(payload.tags[0].native.layouts[0]).to.deep.equal({ title: {required: true}, description: {required: true}, main_image: {required: true, sizes: [{ width: 100, height: 100 }] }, ctatext: {required: false}, sponsored_by: {required: true} }); - - delete REQUEST.bids[0].mediaType; - delete REQUEST.bids[0].params.nativeParams; }); - it('sets required native asset params when not provided on adunit', () => { - REQUEST.bids[0].mediaType = 'native'; - REQUEST.bids[0].nativeParams = { - image: {required: true}, - }; + it('sets minimum native asset params when not provided on adunit', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + image: {required: true}, + } + } + ); - adapter.callBids(REQUEST); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); - const request = JSON.parse(requests[0].requestBody); - expect(request.tags[0].native.layouts[0]).to.deep.equal({ + expect(payload.tags[0].native.layouts[0]).to.deep.equal({ main_image: {required: true, sizes: [{}] }, }); - - delete REQUEST.bids[0].mediaType; - delete REQUEST.bids[0].params.nativeParams; }); - it('sends bid request to ENDPOINT via POST', () => { - adapter.callBids(REQUEST); - expect(requests[0].url).to.equal(ENDPOINT); - expect(requests[0].method).to.equal('POST'); + it('does not overwrite native ad unit params with mimimum params', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + image: { + aspect_ratios: [{ + min_width: 100, + ratio_width: 2, + ratio_height: 3, + }] + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].native.layouts[0]).to.deep.equal({ + main_image: { + required: true, + aspect_ratios: [{ + min_width: 100, + ratio_width: 2, + ratio_height: 3, + }] + }, + }); }); - it('converts keyword params to proper form and attaches to request', () => { - REQUEST.bids[0].params.keywords = { - single: 'val', - singleArr: ['val'], - singleArrNum: [5], - multiValMixed: ['value1', 2, 'value3'], - singleValNum: 123, - badValue: {'foo': 'bar'} // should be dropped - }; + it('should convert keyword params to proper form and attaches to request', () => { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + keywords: { + single: 'val', + singleArr: ['val'], + singleArrNum: [5], + multiValMixed: ['value1', 2, 'value3'], + singleValNum: 123, + badValue: {'foo': 'bar'} // should be dropped + } + } + } + ); - adapter.callBids(REQUEST); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); - const request = JSON.parse(requests[0].requestBody).tags[0]; - expect(request.keywords).to.deep.equal([{ + expect(payload.tags[0].keywords).to.deep.equal([{ 'key': 'single', 'value': ['val'] }, { @@ -216,38 +243,75 @@ describe('AppNexusAdapter', () => { 'key': 'singleValNum', 'value': ['123'] }]); - - delete REQUEST.bids[0].params.keywords; - }); - }); - - describe('response handler', () => { - let server; - - beforeEach(() => { - server = sinon.fakeServer.create(); - sinon.stub(bidmanager, 'addBidResponse'); - }); - - afterEach(() => { - server.restore() - bidmanager.addBidResponse.restore(); }); - - it('registers bids', () => { - server.respondWith(JSON.stringify(RESPONSE)); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property('statusMessage', 'Bid available'); - expect(response).to.have.property('cpm', 0.5); + }) + + describe('interpretResponse', () => { + let response = { + 'version': '3.0.0', + 'tags': [ + { + 'uuid': '3db3773286ee59', + 'tag_id': 10433394, + 'auction_id': '4534722592064951574', + 'nobid': false, + 'no_ad_url': 'http://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, + 'rtb': { + 'banner': { + 'content': '', + 'width': 300, + 'height': 250 + }, + 'trackers': [ + { + 'impression_urls': [ + 'http://lax1-ib.adnxs.com/impression' + ], + 'video_events': {} + } + ] + } + } + ] + } + ] + }; + + it('should get correct bid response', () => { + let expectedResponse = [ + { + 'requestId': '3db3773286ee59', + 'cpm': 0.5, + 'creative_id': 29681110, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner' + } + ]; + let bidderRequest; + + let result = spec.interpretResponse(response, {bidderRequest}); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); }); it('handles nobid responses', () => { - server.respondWith(JSON.stringify({ + let response = { 'version': '0.0.1', 'tags': [{ 'uuid': '84ab500420319d', @@ -255,21 +319,15 @@ describe('AppNexusAdapter', () => { 'auction_id': '297492697822162468', 'nobid': true }] - })); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + }; + let bidderRequest; - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property( - 'statusMessage', - 'Bid returned empty or error response' - ); + let result = spec.interpretResponse(response, {bidderRequest}); + expect(result.length).to.equal(0); }); it('handles non-banner media responses', () => { - server.respondWith(JSON.stringify({ + let response = { 'tags': [{ 'uuid': '84ab500420319d', 'ads': [{ @@ -282,19 +340,19 @@ describe('AppNexusAdapter', () => { } }] }] - })); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); + }; + let bidderRequest; - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property('statusMessage', 'Bid available'); + let result = spec.interpretResponse(response, {bidderRequest}); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0]).to.have.property('descriptionUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); }); it('handles native responses', () => { - RESPONSE.tags[0].ads[0].ad_type = 'native'; - RESPONSE.tags[0].ads[0].rtb.native = { + let response1 = Object.assign({}, response); + response1.tags[0].ads[0].ad_type = 'native'; + response1.tags[0].ads[0].rtb.native = { 'title': 'Native Creative', 'desc': 'Cool description great stuff', 'ctatext': 'Do it', @@ -316,35 +374,13 @@ describe('AppNexusAdapter', () => { }, 'impression_trackers': ['http://example.com'], }; + let bidderRequest; - adapter.callBids(REQUEST); - server.respondWith(JSON.stringify(RESPONSE)); - server.respond(); - - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const response = bidmanager.addBidResponse.firstCall.args[1]; - - expect(response.native.title).to.equal('Native Creative'); - expect(response.native.body).to.equal('Cool description great stuff'); - expect(response.native.cta).to.equal('Do it'); - expect(response.native.image).to.equal('http://cdn.adnxs.com/img.png'); - - RESPONSE.tags[0].ads[0].ad_type = 'banner'; - }); - - it('handles JSON.parse errors', () => { - server.respondWith(''); - - adapter.callBids(REQUEST); - server.respond(); - sinon.assert.calledOnce(bidmanager.addBidResponse); - - const response = bidmanager.addBidResponse.firstCall.args[1]; - expect(response).to.have.property( - 'statusMessage', - 'Bid returned empty or error response' - ); + let result = spec.interpretResponse(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).to.equal('http://cdn.adnxs.com/img.png'); }); }); }); diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index e7f5310d90c..96916f3fa35 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -30,12 +30,23 @@ describe('AppNexus Adapter', () => { 'start': 1469479810130 }; - sinon.stub(bidManager, 'addBidResponse'); - const adLoaderStub = sinon.stub(adLoader, 'loadScript'); + let sandbox; + let adLoaderStub; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(bidManager, 'addBidResponse'); + adLoaderStub = sandbox.stub(adLoader, 'loadScript'); + }); + + afterEach(() => { + sandbox.restore(); + }); describe('callBids', () => { - adapter = new Adapter(); - adapter.callBids(REQUEST); - expect(adLoaderStub.getCall(0).args[0]).to.contain('traffic_source_code=source'); + it('should contain traffic_source_code', () => { + adapter = new Adapter(); + adapter.callBids(REQUEST); + expect(adLoaderStub.getCall(0).args[0]).to.contain('traffic_source_code=source'); + }); }); }); diff --git a/test/spec/modules/huddledmassesBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js similarity index 88% rename from test/spec/modules/huddledmassesBidAdapter_spec.js rename to test/spec/modules/colossussspBidAdapter_spec.js index f4cc12dde1b..e8435d90679 100644 --- a/test/spec/modules/huddledmassesBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -1,16 +1,16 @@ import { expect } from 'chai'; -import Adapter from '../../../modules/huddledmassesBidAdapter'; +import Adapter from '../../../modules/colossussspBidAdapter'; import adapterManager from 'src/adaptermanager'; import bidManager from 'src/bidmanager'; import CONSTANTS from 'src/constants.json'; -describe('HuddledMasses adapter tests', function () { +describe('ColossusSSP adapter tests', function () { let sandbox; const adUnit = { - code: 'huddledmasses', + code: 'colossusssp', sizes: [[300, 250], [300, 600]], bids: [{ - bidder: 'huddledmasses', + bidder: 'colossusssp', params: { placement_id: 0 } @@ -34,7 +34,7 @@ describe('HuddledMasses adapter tests', function () { sandbox.restore(); }); - describe('HuddledMasses callBids validation', () => { + describe('ColossusSSP callBids validation', () => { let bids, server; @@ -50,7 +50,7 @@ describe('HuddledMasses adapter tests', function () { server.restore(); }); - let adapter = adapterManager.bidderRegistry['huddledmasses']; + let adapter = adapterManager.bidderRegistry['colossusssp']; it('Valid bid-request', () => { sandbox.stub(adapter, 'callBids'); @@ -65,7 +65,7 @@ describe('HuddledMasses adapter tests', function () { .with.lengthOf(1); expect(bidderRequest).to.have.deep.property('bids[0]') - .to.have.property('bidder', 'huddledmasses'); + .to.have.property('bidder', 'colossusssp'); expect(bidderRequest).to.have.deep.property('bids[0]') .with.property('sizes') @@ -88,7 +88,7 @@ describe('HuddledMasses adapter tests', function () { expect(bids).to.be.lengthOf(1); expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bids[0].bidderCode).to.equal('huddledmasses'); + expect(bids[0].bidderCode).to.equal('colossusssp'); expect(bids[0].width).to.equal(300); expect(bids[0].height).to.equal(250); expect(bids[0].cpm).to.equal(0.712); diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index c8a49283c7a..68c554c7cb4 100644 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -131,6 +131,10 @@ describe('criteo adapter test', () => { server.respondWith(JSON.stringify(validResponse)); }); + afterEach(() => { + server.restore(); + }); + it('adds bid for valid request', (done) => { stubAddBidResponse = sinon.stub(bidManager, 'addBidResponse', function (adUnitCode, bid) { expect(bid).to.satisfy(bid => { return bid.getStatusCode() == CONSTANTS.STATUS.GOOD }); @@ -185,6 +189,10 @@ describe('criteo adapter test', () => { server.respondWith(JSON.stringify(validNativeResponse)); }); + afterEach(() => { + server.restore(); + }); + it('adds creative to the response of a native valid request', (done) => { stubAddBidResponse = sinon.stub( bidManager, 'addBidResponse', @@ -215,6 +223,10 @@ describe('criteo adapter test', () => { server = sinon.fakeServer.create({ autoRespond: true, respondImmediately: true }); }); + afterEach(() => { + server.restore(); + }); + it('no bid if cdb handler responds with no bid empty string response', (done) => { server.respondWith(''); diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index f11ef18a35a..937e6a084e4 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -15,6 +15,15 @@ var assert = require('chai').assert; var expect = require('chai').expect; describe('currency', function () { + let fakeCurrencyFileServer; + beforeEach(() => { + fakeCurrencyFileServer = sinon.fakeServer.create(); + }); + + afterEach(() => { + fakeCurrencyFileServer.restore(); + }); + describe('setConfig', () => { it('results in currencySupportEnabled = false when currency not configured', () => { setConfig({}); @@ -22,7 +31,6 @@ describe('currency', function () { }); it('results in currencySupportEnabled = true and currencyRates being loaded when configured', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); fakeCurrencyFileServer.respond(); @@ -103,7 +111,6 @@ describe('currency', function () { it('not run until currency rates file is loaded', () => { setConfig({}); - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); var marker = false; @@ -158,7 +165,6 @@ describe('currency', function () { }); it('should result in NO_BID when fromCurrency is not supported in file', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); fakeCurrencyFileServer.respond(); @@ -172,7 +178,6 @@ describe('currency', function () { }); it('should result in NO_BID when adServerCurrency is not supported in file', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'ABC' }); fakeCurrencyFileServer.respond(); @@ -186,7 +191,6 @@ describe('currency', function () { }); it('should return 1 when currency support is enabled and same currency code is requested as is set to adServerCurrency', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); fakeCurrencyFileServer.respond(); @@ -201,7 +205,6 @@ describe('currency', function () { }); it('should return direct conversion rate when fromCurrency is one of the configured bases', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'GBP' }); fakeCurrencyFileServer.respond(); @@ -216,7 +219,6 @@ describe('currency', function () { }); it('should return reciprocal conversion rate when adServerCurrency is one of the configured bases, but fromCurrency is not', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'GBP' }); fakeCurrencyFileServer.respond(); @@ -231,7 +233,6 @@ describe('currency', function () { }); it('should return intermediate conversion rate when neither fromCurrency nor adServerCurrency is one of the configured bases', () => { - var fakeCurrencyFileServer = sinon.fakeServer.create(); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'CNY' }); fakeCurrencyFileServer.respond(); diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index c781406da6e..5b0a9d37d57 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -387,7 +387,7 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript with correct parameters', () => { sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); }); }); @@ -402,7 +402,7 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript with correct parameters', () => { sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pubid%22%3A1032%2C%22pkey%22%3A%22data_team_test_hb_smoke_test%22%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pubid%22%3A1032%2C%22pkey%22%3A%22data_team_test_hb_smoke_test%22%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); }); }); @@ -417,7 +417,7 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript with correct parameters', () => { sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); }); }); @@ -432,7 +432,7 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript with correct parameters', () => { sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); }); }); @@ -447,8 +447,8 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript twice with correct parameters', () => { sinon.assert.calledTwice(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); }); }); @@ -463,9 +463,9 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript thrice with correct parameters', () => { sinon.assert.calledThrice(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543212%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%227g8h9i%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543211%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543212%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%227g8h9i%22%2C%22pid%22%3A1012546%2C%22kvw%22%3A%7B%22hbkv%22%3A%5B%2201%22%5D%7D%2C%22banner%22%3A%7B%7D%7D%5D%7D%7D', null); }); }); @@ -493,7 +493,7 @@ describe('improvedigital adapter tests', function () { }); it('should call loadScript twice with correct parameters', () => { sinon.assert.calledOnce(adloader.loadScript); - sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22pbjs.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%2C%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); + sinon.assert.calledWith(adloader.loadScript, 'http://ad.360yield.com/hb?jsonp=%7B%22bid_request%22%3A%7B%22id%22%3A%229876543210%22%2C%22callback%22%3A%22$$PREBID_GLOBAL$$.improveDigitalResponse%22%2C%22secure%22%3A0%2C%22version%22%3A%22' + improveDigitalAdapter.LIB_VERSION + '-' + improveDigitalAdapter.idClient.CONSTANTS.CLIENT_VERSION + '%22%2C%22imp%22%3A%5B%7B%22id%22%3A%221a2b3c%22%2C%22pid%22%3A1012544%2C%22banner%22%3A%7B%7D%7D%2C%7B%22id%22%3A%224d5e6f%22%2C%22pid%22%3A1012545%2C%22banner%22%3A%7B%22w%22%3A800%2C%22h%22%3A600%7D%7D%5D%7D%7D', null); }); }); diff --git a/test/spec/modules/justpremiumBidAdapter_spec.js b/test/spec/modules/justpremiumBidAdapter_spec.js index d4bb8547eba..8416d662572 100644 --- a/test/spec/modules/justpremiumBidAdapter_spec.js +++ b/test/spec/modules/justpremiumBidAdapter_spec.js @@ -324,8 +324,6 @@ describe('justpremium adapter', () => { expect(bidPlacementCode).to.equal('div-gpt-ad-1471513102552-1'); expect(bidResponse.getStatusCode()).to.equal(PREBID_CONSTANTS.STATUS.NO_BID); expect(bidResponse.bidderCode).to.equal('justpremium'); - - stubAddBidResponse.restore(); }); it('should add bid if tag contains any', () => { @@ -383,8 +381,6 @@ describe('justpremium adapter', () => { expect(bidResponse1.cpm).to.equal(responseData.cpm); expect(bidResponse1.format).to.equal(responseData.format); expect(bidResponse1.ad).to.equal(responseData.ad); - - stubAddBidResponse.restore(); }); }); }); diff --git a/test/spec/modules/komoonaBidAdapter_spec.js b/test/spec/modules/komoonaBidAdapter_spec.js index acb4981c1bd..2657c658ba2 100644 --- a/test/spec/modules/komoonaBidAdapter_spec.js +++ b/test/spec/modules/komoonaBidAdapter_spec.js @@ -99,7 +99,7 @@ describe('komoonaAdapter', () => { }); afterEach(() => { - server.restore() + server.restore(); bidmanager.addBidResponse.restore(); }); diff --git a/test/spec/modules/marsmediaBidAdapter_spec.js b/test/spec/modules/marsmediaBidAdapter_spec.js index 5db56cda976..c9381eb3c5f 100644 --- a/test/spec/modules/marsmediaBidAdapter_spec.js +++ b/test/spec/modules/marsmediaBidAdapter_spec.js @@ -75,6 +75,7 @@ describe('MarsMedia adapter implementation', () => { afterEach(() => { sandbox.restore(); + server.restore(); }); describe('should receive a valid request bid -', () => { diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index 21ceb5a6dec..c08e8c256e6 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -136,12 +136,15 @@ describe('openx adapter tests', function () { }); describe('test openx ad requests', () => { let spyAjax; + let spyBtoa; beforeEach(() => { spyAjax = sinon.spy(ajax, 'ajax'); + spyBtoa = sinon.spy(window, 'btoa'); sinon.stub(document.body, 'appendChild'); }); afterEach(() => { spyAjax.restore(); + spyBtoa.restore(); document.body.appendChild.restore(); }); @@ -179,19 +182,96 @@ describe('openx adapter tests', function () { params: { delDomain: 'testdelDomain', unit: 1234, - customParams: {'test1': 'testval1'} + customParams: {'test1': 'testval1+', 'test2': ['testval2/', 'testval3']} } } ] }; adapter.callBids(params); - sinon.assert.calledOnce(spyAjax); + sinon.assert.calledOnce(spyAjax); + sinon.assert.calledWith(spyBtoa, 'test1=testval1.&test2=testval2_,testval3'); let bidUrl = spyAjax.getCall(0).args[0]; expect(bidUrl).to.include('testdelDomain'); expect(bidUrl).to.include('1234'); expect(bidUrl).to.include('300x250,300x600'); - expect(bidUrl).to.include('c.test1=testval1'); + }); + + it('should send out custom floors on bids that have customFloors specified', () => { + let params = { + bids: [ + { + sizes: [[300, 250], [300, 600]], + params: { + delDomain: 'testdelDomain', + unit: 1234, + customFloor: 1 + } + }, + { + sizes: [[320, 50]], + params: { + delDomain: 'testdelDomain', + unit: 1234 + } + }, + { + sizes: [[728, 90]], + params: { + delDomain: 'testdelDomain', + unit: 1234, + customFloor: 1.5 + } + } + ] + }; + adapter.callBids(params); + + sinon.assert.calledOnce(spyAjax); + let bidUrl = spyAjax.getCall(0).args[0]; + expect(bidUrl).to.include('testdelDomain'); + expect(bidUrl).to.include('1234'); + expect(bidUrl).to.include('300x250,300x600|320x50|728x90'); + expect(bidUrl).to.include('aumfs=1000%2C0%2C1500'); + }); + + it('should change bc param if configureable bc is specified', () => { + let params = { + bids: [ + { + sizes: [[300, 250], [300, 600]], + params: { + delDomain: 'testdelDomain', + unit: 1234, + bc: 'hb_pb_test' + } + }, + { + sizes: [[320, 50]], + params: { + delDomain: 'testdelDomain', + unit: 1234, + bc: 'hb_pb_test' + } + }, + { + sizes: [[728, 90]], + params: { + delDomain: 'testdelDomain', + unit: 1234, + bc: 'hb_pb_test' + } + } + ] + }; + adapter.callBids(params); + + sinon.assert.calledOnce(spyAjax); + let bidUrl = spyAjax.getCall(0).args[0]; + expect(bidUrl).to.include('testdelDomain'); + expect(bidUrl).to.include('1234'); + expect(bidUrl).to.include('300x250,300x600|320x50|728x90'); + expect(bidUrl).to.include('bc=hb_pb_test'); }); }); }); diff --git a/test/spec/modules/orbitsoftBidAdapter_spec.js b/test/spec/modules/orbitsoftBidAdapter_spec.js index dc37be73483..4b24787f56b 100644 --- a/test/spec/modules/orbitsoftBidAdapter_spec.js +++ b/test/spec/modules/orbitsoftBidAdapter_spec.js @@ -14,7 +14,7 @@ describe('Orbitsoft Adapter tests', function () { describe('test orbitsoft callback response', function () { it('should exist and be a function', function () { - expect(pbjs.handleOASCB).to.exist.and.to.be.a('function'); + expect($$PREBID_GLOBAL$$.handleOASCB).to.exist.and.to.be.a('function'); }); it('should add empty bid responses if no bids returned', function () { @@ -43,8 +43,8 @@ describe('Orbitsoft Adapter tests', function () { cpm: 0 }; - pbjs._bidsRequested.push(bidderRequest); - pbjs.handleOASCB(response); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$.handleOASCB(response); let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; @@ -79,8 +79,8 @@ describe('Orbitsoft Adapter tests', function () { cpm: 0 }; - pbjs._bidsRequested.push(bidderRequest); - pbjs.handleOASCB(response); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$.handleOASCB(response); expect(stubAddBidResponse.getCall(0)).to.equal(null); stubAddBidResponse.restore(); @@ -116,8 +116,8 @@ describe('Orbitsoft Adapter tests', function () { height: 250 }; - pbjs._bidsRequested.push(bidderRequest); - pbjs.handleOASCB(response); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$.handleOASCB(response); let bidPlacementCode1 = stubAddBidResponse.getCall(0).args[0]; let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; @@ -277,9 +277,9 @@ describe('Orbitsoft Adapter tests', function () { height: 250 }; - pbjs._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); - pbjs.handleOASCB(response); + $$PREBID_GLOBAL$$.handleOASCB(response); let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; let adUrl = bidResponse1.adUrl; @@ -339,8 +339,8 @@ describe('Orbitsoft Adapter tests', function () { height: 250 }; - pbjs._bidsRequested.push(bidderRequest); - pbjs.handleOASCB(response); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$.handleOASCB(response); let bidResponse1 = stubAddBidResponse.getCall(0).args[1]; let adUrl = bidResponse1.adUrl; diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 44850249adc..21098a2859f 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -5,7 +5,6 @@ import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils'; import cookie from 'src/cookie'; import { userSync } from 'src/userSync'; -import { StorageManager } from 'src/storagemanager'; let CONFIG = { accountId: '1', @@ -20,8 +19,9 @@ const REQUEST = { 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', 'max_bids': 1, 'timeout_millis': 1000, + 'secure': 0, 'url': '', - 'prebid_version': '0.21.0-pre', + 'prebid_version': '0.30.0-pre', 'ad_units': [ { 'code': 'div-gpt-ad-1460505748561-0', @@ -153,42 +153,31 @@ const RESPONSE_NO_PBS_COOKIE = { }] }; +const RESPONSE_NO_PBS_COOKIE_ERROR = { + 'tid': '882fe33e-2981-4257-bd44-bd3b0394545f', + 'status': 'no_cookie', + 'bidder_status': [{ + 'bidder': 'rubicon', + 'no_cookie': true, + 'usersync': { + 'url': 'https://pixel.rubiconproject.com/exchange/sync.php?p=prebid', + 'type': 'jsonp' + } + }, { + 'bidder': 'pubmatic', + 'no_cookie': true, + 'usersync': { + 'url': '', + 'type': 'iframe' + } + }] +}; + describe('S2S Adapter', () => { let adapter; beforeEach(() => adapter = new Adapter()); - describe('queue sync function', () => { - let server; - let storageManagerAddStub; - - beforeEach(() => { - server = sinon.fakeServer.create(); - storageManagerAddStub = sinon.stub(StorageManager, 'add'); - }); - - afterEach(() => { - server.restore(); - storageManagerAddStub.restore(); - localStorage.removeItem('pbjsSyncs'); - }); - - it('exists and is a function', () => { - expect(adapter.queueSync).to.exist.and.to.be.a('function'); - }); - - it('requests only bidders that are not already synced', () => { - server.respondWith(JSON.stringify({status: 'ok', bidderCodes: ['rubicon'] })); - const reqBidderCodes = ['appnexus', 'newBidder']; - const syncedBidders = ['appnexus', 'rubicon']; - localStorage.setItem('pbjsSyncs', JSON.stringify(syncedBidders)); - adapter.setConfig(CONFIG); - adapter.queueSync({bidderCodes: reqBidderCodes}); - server.respond(); - sinon.assert.calledTwice(storageManagerAddStub); - }); - }); - describe('request function', () => { let xhr; let requests; @@ -219,7 +208,9 @@ describe('S2S Adapter', () => { beforeEach(() => { server = sinon.fakeServer.create(); - sinon.stub(userSync, 'registerSync'); + sinon.stub(utils, 'triggerPixel'); + sinon.stub(utils, 'insertUserSyncIframe'); + sinon.stub(utils, 'logError'); sinon.stub(cookie, 'cookieSet'); sinon.stub(bidmanager, 'addBidResponse'); sinon.stub(utils, 'getBidderRequestAllAdUnits').returns({ @@ -238,7 +229,9 @@ describe('S2S Adapter', () => { bidmanager.addBidResponse.restore(); utils.getBidderRequestAllAdUnits.restore(); utils.getBidRequest.restore(); - userSync.registerSync.restore(); + utils.triggerPixel.restore(); + utils.insertUserSyncIframe.restore(); + utils.logError.restore(); cookie.cookieSet.restore(); }); @@ -379,13 +372,29 @@ describe('S2S Adapter', () => { expect(bid_request_passed).to.have.property('adId', '123'); }); - it('queue cookie sync when no_cookie response', () => { + it('does cookie sync when no_cookie response', () => { server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE)); adapter.setConfig(CONFIG); adapter.callBids(REQUEST); server.respond(); - sinon.assert.calledTwice(userSync.registerSync); + + sinon.assert.calledOnce(utils.triggerPixel); + sinon.assert.calledWith(utils.triggerPixel, 'https://pixel.rubiconproject.com/exchange/sync.php?p=prebid'); + sinon.assert.calledOnce(utils.insertUserSyncIframe); + sinon.assert.calledWith(utils.insertUserSyncIframe, '//ads.pubmatic.com/AdServer/js/user_sync.html?predirect=https%3A%2F%2Fprebid.adnxs.com%2Fpbs%2Fv1%2Fsetuid%3Fbidder%3Dpubmatic%26uid%3D'); + }); + + it('logs error when no_cookie response is missing type or url', () => { + server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE_ERROR)); + + adapter.setConfig(CONFIG); + adapter.callBids(REQUEST); + server.respond(); + + sinon.assert.notCalled(utils.triggerPixel); + sinon.assert.notCalled(utils.insertUserSyncIframe); + sinon.assert.calledTwice(utils.logError); }); it('does not call cookieSet cookie sync when no_cookie response && not opted in', () => { diff --git a/test/spec/modules/pulsepointLiteBidAdapter_spec.js b/test/spec/modules/pulsepointLiteBidAdapter_spec.js index f7b7a790302..96f5c7a8d1f 100644 --- a/test/spec/modules/pulsepointLiteBidAdapter_spec.js +++ b/test/spec/modules/pulsepointLiteBidAdapter_spec.js @@ -1,71 +1,60 @@ +/* eslint dot-notation:0, quote-props:0 */ import {expect} from 'chai'; -import PulsePointAdapter from 'modules/pulsepointLiteBidAdapter'; +import {spec} from 'modules/pulsepointLiteBidAdapter'; import bidManager from 'src/bidmanager'; import {getTopWindowLocation} from 'src/utils'; -import * as ajax from 'src/ajax'; +import {newBidder} from 'src/adapters/bidderFactory'; describe('PulsePoint Lite Adapter Tests', () => { - let pulsepointAdapter = new PulsePointAdapter(); - let slotConfigs; - let nativeSlotConfig; - let ajaxStub; - - beforeEach(() => { - sinon.stub(bidManager, 'addBidResponse'); - ajaxStub = sinon.stub(ajax, 'ajax'); - - slotConfigs = { - bidderCode: 'pulseLite', - bids: [ - { - placementCode: '/DfpAccount1/slot1', - bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', - cf: '300x250' - } - }, { - placementCode: '/DfpAccount2/slot2', - bidId: 'bid23456', - params: { - cp: 'p10000', - ct: 't20000', - cf: '728x90' - } - } - ] - }; - nativeSlotConfig = { - bidderCode: 'pulseLite', - bids: [ - { - placementCode: '/DfpAccount1/slot3', - bidId: 'bid12345', - nativeParams: { - title: { required: true, len: 200 }, - image: { wmin: 100 }, - sponsoredBy: { } - }, - params: { - cp: 'p10000', - ct: 't10000' - } - } - ] - }; - }); - - afterEach(() => { - bidManager.addBidResponse.restore(); - ajaxStub.restore(); - }); + const slotConfigs = [{ + placementCode: '/DfpAccount1/slot1', + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '300x250' + } + }, { + placementCode: '/DfpAccount2/slot2', + bidId: 'bid23456', + params: { + cp: 'p10000', + ct: 't20000', + cf: '728x90' + } + }]; + const nativeSlotConfig = [{ + placementCode: '/DfpAccount1/slot3', + bidId: 'bid12345', + nativeParams: { + title: { required: true, len: 200 }, + image: { wmin: 100 }, + sponsoredBy: { } + }, + params: { + cp: 'p10000', + ct: 't10000' + } + }]; + const appSlotConfig = [{ + placementCode: '/DfpAccount1/slot3', + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + app: { + bundle: 'com.pulsepoint.apps', + storeUrl: 'http://pulsepoint.com/apps', + domain: 'pulsepoint.com', + } + } + }]; - it('Verify requests sent to PulsePoint', () => { - pulsepointAdapter.callBids(slotConfigs); - expect(ajaxStub.callCount).to.equal(1); - expect(ajaxStub.firstCall.args[0]).to.equal('http://bid.contextweb.com/header/ortb'); - const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]); + it('Verify build request', () => { + const request = spec.buildRequests(slotConfigs); + expect(request.url).to.equal('//bid.contextweb.com/header/ortb'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); // site object expect(ortbRequest.site).to.not.equal(null); expect(ortbRequest.site.publisher).to.not.equal(null); @@ -88,11 +77,10 @@ describe('PulsePoint Lite Adapter Tests', () => { expect(ortbRequest.imp[1].banner.h).to.equal(90); }); - it('Verify bid', () => { - pulsepointAdapter.callBids(slotConfigs); - // trigger a mock ajax callback with bid. - const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]); - ajaxStub.firstCall.args[1](JSON.stringify({ + it('Verify parse response', () => { + const request = spec.buildRequests(slotConfigs); + const ortbRequest = JSON.parse(request.data); + const ortbResponse = { seatbid: [{ bid: [{ impid: ortbRequest.imp[0].id, @@ -100,65 +88,40 @@ describe('PulsePoint Lite Adapter Tests', () => { adm: 'This is an Ad' }] }] - })); - expect(bidManager.addBidResponse.callCount).to.equal(2); + }; + const bids = spec.interpretResponse(ortbResponse, request); + expect(bids).to.have.lengthOf(1); // verify first bid - let placement = bidManager.addBidResponse.firstCall.args[0]; - let bid = bidManager.addBidResponse.firstCall.args[1]; - expect(placement).to.equal('/DfpAccount1/slot1'); - expect(bid.bidderCode).to.equal('pulseLite'); + const bid = bids[0]; expect(bid.cpm).to.equal(1.25); expect(bid.ad).to.equal('This is an Ad'); expect(bid.width).to.equal(300); expect(bid.height).to.equal(250); expect(bid.adId).to.equal('bid12345'); - // verify passback on 2nd impression. - placement = bidManager.addBidResponse.secondCall.args[0]; - bid = bidManager.addBidResponse.secondCall.args[1]; - expect(placement).to.equal('/DfpAccount2/slot2'); - expect(bid.adId).to.equal('bid23456'); - expect(bid.bidderCode).to.equal('pulseLite'); - expect(bid.cpm).to.be.undefined; + expect(bid.creative_id).to.equal('bid12345'); + expect(bid.creativeId).to.equal('bid12345'); }); it('Verify full passback', () => { - pulsepointAdapter.callBids(slotConfigs); - // trigger a mock ajax callback with no bid. - ajaxStub.firstCall.args[1](null); - let placement = bidManager.addBidResponse.firstCall.args[0]; - let bid = bidManager.addBidResponse.firstCall.args[1]; - expect(placement).to.equal('/DfpAccount1/slot1'); - expect(bid.bidderCode).to.equal('pulseLite'); - expect(bid).to.not.have.property('ad'); - expect(bid).to.not.have.property('cpm'); - expect(bid.adId).to.equal('bid12345'); - }); - - it('Verify passback when ajax call fails', () => { - ajaxStub.throws(); - pulsepointAdapter.callBids(slotConfigs); - let placement = bidManager.addBidResponse.firstCall.args[0]; - let bid = bidManager.addBidResponse.firstCall.args[1]; - expect(placement).to.equal('/DfpAccount1/slot1'); - expect(bid.bidderCode).to.equal('pulseLite'); - expect(bid).to.not.have.property('ad'); - expect(bid).to.not.have.property('cpm'); - expect(bid.adId).to.equal('bid12345'); + const request = spec.buildRequests(slotConfigs); + const bids = spec.interpretResponse(null, request) + expect(bids).to.have.lengthOf(0); }); it('Verify Native request', () => { - pulsepointAdapter.callBids(nativeSlotConfig); - expect(ajaxStub.callCount).to.equal(1); - expect(ajaxStub.firstCall.args[0]).to.equal('http://bid.contextweb.com/header/ortb'); - const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]); + const request = spec.buildRequests(nativeSlotConfig); + expect(request.url).to.equal('//bid.contextweb.com/header/ortb'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); // native impression expect(ortbRequest.imp[0].tagid).to.equal('t10000'); expect(ortbRequest.imp[0].banner).to.equal(null); - expect(ortbRequest.imp[0].native).to.not.equal(null); - expect(ortbRequest.imp[0].native.ver).to.equal('1.1'); - expect(ortbRequest.imp[0].native.request).to.not.equal(null); + const nativePart = ortbRequest.imp[0]['native']; + expect(nativePart).to.not.equal(null); + expect(nativePart.ver).to.equal('1.1'); + expect(nativePart.request).to.not.equal(null); // native request assets - const nativeRequest = JSON.parse(ortbRequest.imp[0].native.request); + const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); expect(nativeRequest).to.not.equal(null); expect(nativeRequest.assets).to.have.lengthOf(3); // title asset @@ -184,22 +147,22 @@ describe('PulsePoint Lite Adapter Tests', () => { }); it('Verify Native response', () => { - pulsepointAdapter.callBids(nativeSlotConfig); - expect(ajaxStub.callCount).to.equal(1); - expect(ajaxStub.firstCall.args[0]).to.equal('http://bid.contextweb.com/header/ortb'); - const ortbRequest = JSON.parse(ajaxStub.firstCall.args[2]); + const request = spec.buildRequests(nativeSlotConfig); + expect(request.url).to.equal('//bid.contextweb.com/header/ortb'); + expect(request.method).to.equal('POST'); + const ortbRequest = JSON.parse(request.data); const nativeResponse = { - native: { + 'native': { assets: [ { title: { text: 'Ad Title'} }, { data: { type: 1, value: 'Sponsored By: Brand' }}, { img: { type: 3, url: 'http://images.cdn.brand.com/123' } } ], link: { url: 'http://brand.clickme.com/' }, - imptrackers: [ 'http://imp1.trackme.com/', 'http://imp1.contextweb.com/' ] + imptrackers: ['http://imp1.trackme.com/', 'http://imp1.contextweb.com/'] } }; - ajaxStub.firstCall.args[1](JSON.stringify({ + const ortbResponse = { seatbid: [{ bid: [{ impid: ortbRequest.imp[0].id, @@ -207,28 +170,78 @@ describe('PulsePoint Lite Adapter Tests', () => { adm: JSON.stringify(nativeResponse) }] }] - })); + }; + const bids = spec.interpretResponse(ortbResponse, request); // verify bid - let placement = bidManager.addBidResponse.firstCall.args[0]; - let bid = bidManager.addBidResponse.firstCall.args[1]; - expect(placement).to.equal('/DfpAccount1/slot3'); - expect(bid.bidderCode).to.equal('pulseLite'); + const bid = bids[0]; expect(bid.cpm).to.equal(1.25); expect(bid.adId).to.equal('bid12345'); expect(bid.ad).to.be.undefined; expect(bid.mediaType).to.equal('native'); - expect(bid.native).to.not.equal(null); - expect(bid.native.title).to.equal('Ad Title'); - expect(bid.native.sponsoredBy).to.equal('Sponsored By: Brand'); - expect(bid.native.image).to.equal('http://images.cdn.brand.com/123'); - expect(bid.native.clickUrl).to.equal(encodeURIComponent('http://brand.clickme.com/')); - expect(bid.native.impressionTrackers).to.have.lengthOf(2); - expect(bid.native.impressionTrackers[0]).to.equal('http://imp1.trackme.com/'); - expect(bid.native.impressionTrackers[1]).to.equal('http://imp1.contextweb.com/'); + const nativeBid = bid['native']; + expect(nativeBid).to.not.equal(null); + expect(nativeBid.title).to.equal('Ad Title'); + expect(nativeBid.sponsoredBy).to.equal('Sponsored By: Brand'); + expect(nativeBid.image).to.equal('http://images.cdn.brand.com/123'); + expect(nativeBid.clickUrl).to.equal(encodeURIComponent('http://brand.clickme.com/')); + expect(nativeBid.impressionTrackers).to.have.lengthOf(2); + expect(nativeBid.impressionTrackers[0]).to.equal('http://imp1.trackme.com/'); + expect(nativeBid.impressionTrackers[1]).to.equal('http://imp1.contextweb.com/'); }); - it('Verify adapter interface', function () { - const adapter = new PulsePointAdapter(); - expect(adapter).to.have.property('callBids'); + it('Verifies bidder code', () => { + expect(spec.code).to.equal('pulseLite'); + }); + + it('Verifies bidder aliases', () => { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('pulsepointLite'); + }); + + it('Verifies supported media types', () => { + expect(spec.supportedMediaTypes).to.have.lengthOf(1); + expect(spec.supportedMediaTypes[0]).to.equal('native'); + }); + + it('Verifies if bid request valid', () => { + expect(spec.isBidRequestValid(slotConfigs[0])).to.equal(true); + expect(spec.isBidRequestValid(slotConfigs[1])).to.equal(true); + expect(spec.isBidRequestValid(nativeSlotConfig[0])).to.equal(true); + expect(spec.isBidRequestValid({})).to.equal(false); + expect(spec.isBidRequestValid({ params: {} })).to.equal(false); + expect(spec.isBidRequestValid({ params: { ct: 123 } })).to.equal(false); + expect(spec.isBidRequestValid({ params: { cp: 123 } })).to.equal(false); + expect(spec.isBidRequestValid({ params: { ct: 123, cp: 234 }})).to.equal(true); + }); + + it('Verifies sync options', () => { + expect(spec.getUserSyncs({})).to.be.undefined; + expect(spec.getUserSyncs({ iframeEnabled: false})).to.be.undefined; + const options = spec.getUserSyncs({ iframeEnabled: true}); + expect(options).to.not.be.undefined; + expect(options).to.have.lengthOf(1); + expect(options[0].type).to.equal('iframe'); + expect(options[0].url).to.equal('//bh.contextweb.com/visitormatch'); + }); + + it('Verifies image pixel sync', () => { + const options = spec.getUserSyncs({ pixelEnabled: true}); + expect(options).to.not.be.undefined; + expect(options).to.have.lengthOf(1); + expect(options[0].type).to.equal('image'); + expect(options[0].url).to.equal('//bh.contextweb.com/visitormatch/prebid'); + }); + + it('Verify app requests', () => { + const request = spec.buildRequests(appSlotConfig); + const ortbRequest = JSON.parse(request.data); + // site object + expect(ortbRequest.site).to.equal(null); + expect(ortbRequest.app).to.not.be.null; + expect(ortbRequest.app.publisher).to.not.equal(null); + expect(ortbRequest.app.publisher.id).to.equal('p10000'); + expect(ortbRequest.app.bundle).to.equal('com.pulsepoint.apps'); + expect(ortbRequest.app.storeurl).to.equal('http://pulsepoint.com/apps'); + expect(ortbRequest.app.domain).to.equal('pulsepoint.com'); }); }); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index 183b79f7ec9..620fc56e516 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -1,8 +1,8 @@ import { expect } from 'chai'; import adapterManager from 'src/adaptermanager'; -import bidManager from 'src/bidmanager'; -import RubiconAdapter from 'modules/rubiconBidAdapter'; +import { spec, masSizeOrdering, resetUserSync } from 'modules/rubiconBidAdapter'; import { parse as parseQuery } from 'querystring'; +import { newBidder } from 'src/adapters/bidderFactory'; import { userSync } from 'src/userSync'; var CONSTANTS = require('src/constants.json'); @@ -11,7 +11,6 @@ const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be s describe('the rubicon adapter', () => { let sandbox, - adUnit, bidderRequest; function createVideoBidderRequest() { @@ -55,34 +54,6 @@ describe('the rubicon adapter', () => { beforeEach(() => { sandbox = sinon.sandbox.create(); - adUnit = { - code: '/19968336/header-bid-tag-0', - sizes: [[300, 250], [320, 50]], - mediaType: 'video', - bids: [ - { - bidder: 'rubicon', - params: { - accountId: '14062', - siteId: '70608', - zoneId: '335918', - userId: '12346', - keywords: ['a', 'b', 'c'], - inventory: { - rating: '5-star', - prodtype: 'tech' - }, - visitor: { - ucat: 'new', - lastsearch: 'iphone' - }, - position: 'atf', - referrer: 'localhost' - } - } - ] - }; - bidderRequest = { bidderCode: 'rubicon', requestId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', @@ -111,7 +82,8 @@ describe('the rubicon adapter', () => { sizes: [[300, 250], [320, 50]], bidId: '2ffb201a808da7', bidderRequestId: '178e34bad3658f', - requestId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a' + requestId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' } ], start: 1472239426002, @@ -124,46 +96,7 @@ describe('the rubicon adapter', () => { sandbox.restore(); }); - describe('callBids public interface', () => { - let rubiconAdapter = adapterManager.bidderRegistry['rubicon']; - - it('should receive a well-formed bidRequest from the adaptermanager', () => { - sandbox.stub(rubiconAdapter, 'callBids'); - - adapterManager.callBids({ - adUnits: [clone(adUnit)] - }); - - let bidderRequest = rubiconAdapter.callBids.getCall(0).args[0]; - - expect(bidderRequest).to.have.property('bids') - .that.is.an('array') - .with.lengthOf(1); - - expect(bidderRequest).to.have.deep.property('bids[0]') - .to.have.property('bidder', 'rubicon'); - - expect(bidderRequest).to.have.deep.property('bids[0]') - .to.have.property('mediaType', 'video'); - - expect(bidderRequest).to.have.deep.property('bids[0]') - .to.have.property('placementCode', adUnit.code); - - expect(bidderRequest).to.have.deep.property('bids[0]') - .with.property('sizes') - .that.is.an('array') - .with.lengthOf(2) - .that.deep.equals(adUnit.sizes); - - expect(bidderRequest).to.have.deep.property('bids[0]') - .with.property('params') - .that.deep.equals(adUnit.bids[0].params); - }); - }); - describe('MAS mapping / ordering', () => { - let masSizeOrdering = RubiconAdapter.masSizeOrdering; - it('should not include values without a proper mapping', () => { // two invalid sizes included: [42, 42], [1, 1] let ordering = masSizeOrdering([[320, 50], [42, 42], [300, 250], [640, 480], [1, 1], [336, 280]]); @@ -189,41 +122,16 @@ describe('the rubicon adapter', () => { }); }); - describe('callBids implementation', () => { - let rubiconAdapter; - + describe('buildRequests implementation', () => { describe('for requests', () => { - let xhr, - bids; - - beforeEach(() => { - rubiconAdapter = new RubiconAdapter(); - - bids = []; - - xhr = sandbox.useFakeXMLHttpRequest(); - - sandbox.stub(bidManager, 'addBidResponse', (elemId, bid) => { - bids.push(bid); - }); - }); - - afterEach(() => { - xhr.restore(); - }); - describe('to fastlane', () => { - it('should make a well-formed request', () => { - rubiconAdapter.callBids(bidderRequest); - - let request = xhr.requests[0]; + it('should make a well-formed request objects', () => { + sandbox.stub(Math, 'random', () => 0.1); - let [path, query] = request.url.split('?'); - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); - expect(path).to.equal( - '//fastlane.rubiconproject.com/a/api/fastlane.json' - ); + expect(request.url).to.equal('//fastlane.rubiconproject.com/a/api/fastlane.json'); let expectedQuery = { 'account_id': '14062', @@ -234,7 +142,9 @@ describe('the rubicon adapter', () => { 'p_pos': 'atf', 'rp_floor': '0.01', 'rp_secure': /[01]/, + 'rand': '0.1', 'tk_flint': INTEGRATION, + 'tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', 'p_screen_res': /\d+x\d+/, 'tk_user_key': '12346', 'kw': 'a,b,c', @@ -249,61 +159,50 @@ describe('the rubicon adapter', () => { Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; if (value instanceof RegExp) { - expect(query[key]).to.match(value); + expect(data[key]).to.match(value); } else { - expect(query[key]).to.equal(value); + expect(data[key]).to.equal(value); } }); - - expect(query).to.have.property('rand'); }); it('should use rubicon sizes if present', () => { var sizesBidderRequest = clone(bidderRequest); sizesBidderRequest.bids[0].params.sizes = [55, 57, 59]; - rubiconAdapter.callBids(sizesBidderRequest); - - let query = parseQuery(xhr.requests[0].url.split('?')[1]); + let [request] = spec.buildRequests(sizesBidderRequest.bids, sizesBidderRequest); + let data = parseQuery(request.data); - expect(query['size_id']).to.equal('55'); - expect(query['alt_size_ids']).to.equal('57,59'); + expect(data['size_id']).to.equal('55'); + expect(data['alt_size_ids']).to.equal('57,59'); }); - it('should not send a request and register an error bid if no valid sizes', () => { + it('should not validate bid request if no valid sizes', () => { var sizesBidderRequest = clone(bidderRequest); sizesBidderRequest.bids[0].sizes = [[620, 250], [300, 251]]; - rubiconAdapter.callBids(sizesBidderRequest); - - expect(xhr.requests.length).to.equal(0); + let result = spec.isBidRequestValid(sizesBidderRequest.bids[0]); - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); + expect(result).to.equal(false); }); - it('should not send a request and register an error if no account id is present', () => { + it('should not validate bid request if no account id is present', () => { var noAccountBidderRequest = clone(bidderRequest); delete noAccountBidderRequest.bids[0].params.accountId; - rubiconAdapter.callBids(noAccountBidderRequest); + let result = spec.isBidRequestValid(noAccountBidderRequest.bids[0]); - expect(xhr.requests.length).to.equal(0); - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); + expect(result).to.equal(false); }); it('should allow a floor override', () => { var floorBidderRequest = clone(bidderRequest); floorBidderRequest.bids[0].params.floor = 2; - rubiconAdapter.callBids(floorBidderRequest); - - let query = parseQuery(xhr.requests[0].url.split('?')[1]); + let [request] = spec.buildRequests(floorBidderRequest.bids, floorBidderRequest); + let data = parseQuery(request.data); - expect(query['rp_floor']).to.equal('2'); + expect(data['rp_floor']).to.equal('2'); }); it('should send digitrust params', () => { @@ -321,12 +220,8 @@ describe('the rubicon adapter', () => { }) ); - rubiconAdapter.callBids(bidderRequest); - - let request = xhr.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let expectedQuery = { 'dt.id': 'testId', @@ -337,25 +232,21 @@ describe('the rubicon adapter', () => { // test that all values above are both present and correct Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; - expect(query[key]).to.equal(value); + expect(data[key]).to.equal(value); }); delete window.DigiTrust; }); it('should not send digitrust params when DigiTrust not loaded', () => { - rubiconAdapter.callBids(bidderRequest); - - let request = xhr.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); }); @@ -374,18 +265,14 @@ describe('the rubicon adapter', () => { }) ); - rubiconAdapter.callBids(bidderRequest); - - let request = xhr.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); delete window.DigiTrust; @@ -406,18 +293,14 @@ describe('the rubicon adapter', () => { }) ); - rubiconAdapter.callBids(bidderRequest); - - let request = xhr.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); delete window.DigiTrust; @@ -427,7 +310,7 @@ describe('the rubicon adapter', () => { var origGetConfig; beforeEach(() => { window.DigiTrust = { - getUser: sinon.spy() + getUser: sandbox.spy() }; origGetConfig = window.$$PREBID_GLOBAL$$.getConfig; }); @@ -438,7 +321,7 @@ describe('the rubicon adapter', () => { }); it('should send digiTrustId config params', () => { - sinon.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { var config = { digiTrustId: { success: true, @@ -452,12 +335,8 @@ describe('the rubicon adapter', () => { return config[key]; }); - rubiconAdapter.callBids(bidderRequest); - - let request = xhr.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let expectedQuery = { 'dt.id': 'testId', @@ -467,7 +346,7 @@ describe('the rubicon adapter', () => { // test that all values above are both present and correct Object.keys(expectedQuery).forEach(key => { let value = expectedQuery[key]; - expect(query[key]).to.equal(value); + expect(data[key]).to.equal(value); }); // should not have called DigiTrust.getUser() @@ -475,7 +354,7 @@ describe('the rubicon adapter', () => { }); it('should not send digiTrustId config params due to optout', () => { - sinon.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { var config = { digiTrustId: { success: true, @@ -489,18 +368,14 @@ describe('the rubicon adapter', () => { return config[key]; }); - rubiconAdapter.callBids(bidderRequest); - - let request = xhr.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); // should not have called DigiTrust.getUser() @@ -508,7 +383,7 @@ describe('the rubicon adapter', () => { }); it('should not send digiTrustId config params due to failure', () => { - sinon.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { var config = { digiTrustId: { success: false, @@ -522,18 +397,14 @@ describe('the rubicon adapter', () => { return config[key]; }); - rubiconAdapter.callBids(bidderRequest); - - let request = xhr.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); // should not have called DigiTrust.getUser() @@ -541,23 +412,19 @@ describe('the rubicon adapter', () => { }); it('should not send digiTrustId config params if they do not exist', () => { - sinon.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { + sandbox.stub(window.$$PREBID_GLOBAL$$, 'getConfig', (key) => { var config = {}; return config[key]; }); - rubiconAdapter.callBids(bidderRequest); - - let request = xhr.requests[0]; - - let query = request.url.split('?')[1]; - query = parseQuery(query); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); let undefinedKeys = ['dt.id', 'dt.keyv']; // Test that none of the DigiTrust keys are part of the query undefinedKeys.forEach(key => { - expect(typeof query[key]).to.equal('undefined'); + expect(typeof data[key]).to.equal('undefined'); }); // should have called DigiTrust.getUser() once @@ -567,16 +434,6 @@ describe('the rubicon adapter', () => { }); describe('for video requests', () => { - /* - beforeEach(() => { - createVideoBidderRequest(); - - sandbox.stub(Date, 'now', () => - bidderRequest.auctionStart + 100 - ); - }); - */ - it('should make a well-formed video request', () => { createVideoBidderRequest(); @@ -584,12 +441,10 @@ describe('the rubicon adapter', () => { bidderRequest.auctionStart + 100 ); - rubiconAdapter.callBids(bidderRequest); - - let request = xhr.requests[0]; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; let url = request.url; - let post = JSON.parse(request.requestBody); expect(url).to.equal('//fastlane-adv.rubiconproject.com/v1/auction/video'); @@ -651,17 +506,15 @@ describe('the rubicon adapter', () => { // enter an explicit floor price // floorBidderRequest.bids[0].params.floor = 3.25; - rubiconAdapter.callBids(floorBidderRequest); - - let request = xhr.requests[0]; - let post = JSON.parse(request.requestBody); + let [request] = spec.buildRequests(floorBidderRequest.bids, floorBidderRequest); + let post = request.data; let floor = post.slots[0].floor; expect(floor).to.equal(3.25); }); - it('should trap when no video object is passed in', () => { + it('should not validate bid request when no video object is passed in', () => { createVideoBidderRequestNoVideo(); sandbox.stub(Date, 'now', () => bidderRequest.auctionStart + 100 @@ -669,9 +522,9 @@ describe('the rubicon adapter', () => { var floorBidderRequest = clone(bidderRequest); - rubiconAdapter.callBids(floorBidderRequest); + let result = spec.isBidRequestValid(floorBidderRequest.bids[0]); - expect(xhr.requests.length).to.equal(0); + expect(result).to.equal(false); }); it('should get size from bid.sizes too', () => { @@ -682,10 +535,8 @@ describe('the rubicon adapter', () => { var floorBidderRequest = clone(bidderRequest); - rubiconAdapter.callBids(floorBidderRequest); - - let request = xhr.requests[0]; - let post = JSON.parse(request.requestBody); + let [request] = spec.buildRequests(floorBidderRequest.bids, floorBidderRequest); + let post = request.data; expect(post.slots[0].width).to.equal(300); expect(post.slots[0].height).to.equal(250); @@ -693,32 +544,10 @@ describe('the rubicon adapter', () => { }); }); - describe('response handler', () => { - let bids, - server, - addBidResponseAction; - - beforeEach(() => { - bids = []; - - server = sinon.fakeServer.create(); - - sandbox.stub(bidManager, 'addBidResponse', (elemId, bid) => { - bids.push(bid); - if (addBidResponseAction) { - addBidResponseAction(); - addBidResponseAction = undefined; - } - }); - }); - - afterEach(() => { - server.restore(); - }); - + describe('interpretResponse', () => { describe('for fastlane', () => { it('should handle a success response and sort by cpm', () => { - server.respondWith(JSON.stringify({ + let response = { 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -773,34 +602,32 @@ describe('the rubicon adapter', () => { ] } ] - })); - - rubiconAdapter.callBids(bidderRequest); - - server.respond(); + }; - expect(bidManager.addBidResponse.calledTwice).to.equal(true); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); expect(bids).to.be.lengthOf(2); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); expect(bids[0].bidderCode).to.equal('rubicon'); expect(bids[0].width).to.equal(320); expect(bids[0].height).to.equal(50); expect(bids[0].cpm).to.equal(0.911); expect(bids[0].creative_id).to.equal('crid-9'); + expect(bids[0].currency).to.equal('USD'); expect(bids[0].ad).to.contain(`alert('foo')`) .and.to.contain(``) .and.to.contain(`
`); expect(bids[0].rubiconTargeting.rpfl_elemid).to.equal('/19968336/header-bid-tag-0'); expect(bids[0].rubiconTargeting.rpfl_14062).to.equal('43_tier_all_test'); - expect(bids[1].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); expect(bids[1].bidderCode).to.equal('rubicon'); expect(bids[1].width).to.equal(300); expect(bids[1].height).to.equal(250); expect(bids[1].cpm).to.equal(0.811); expect(bids[1].creative_id).to.equal('crid-9'); + expect(bids[1].currency).to.equal('USD'); expect(bids[1].ad).to.contain(`alert('foo')`) .and.to.contain(``) .and.to.contain(`
`); @@ -809,7 +636,7 @@ describe('the rubicon adapter', () => { }); it('should be fine with a CPM of 0', () => { - server.respondWith(JSON.stringify({ + let response = { 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -825,48 +652,18 @@ describe('the rubicon adapter', () => { 'cpm': 0, 'size_id': 15 }] - })); - - rubiconAdapter.callBids(bidderRequest); - - server.respond(); - - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - }); - - it('should return currency "USD"', () => { - server.respondWith(JSON.stringify({ - 'status': 'ok', - 'account_id': 14062, - 'site_id': 70608, - 'zone_id': 530022, - 'size_id': 15, - 'alt_size_ids': [ - 43 - ], - 'tracking': '', - 'inventory': {}, - 'ads': [{ - 'status': 'ok', - 'cpm': 0, - 'size_id': 15 - }] - })); - - rubiconAdapter.callBids(bidderRequest); + }; - server.respond(); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); - expect(bidManager.addBidResponse.calledOnce).to.equal(true); expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); - expect(bids[0].currency).to.equal('USD'); + expect(bids[0].cpm).to.be.equal(0); }); it('should handle an error with no ads returned', () => { - server.respondWith(JSON.stringify({ + let response = { 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -878,19 +675,17 @@ describe('the rubicon adapter', () => { 'tracking': '', 'inventory': {}, 'ads': [] - })); - - rubiconAdapter.callBids(bidderRequest); + }; - server.respond(); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); + expect(bids).to.be.lengthOf(0); }); - it('should handle an error with bad status', () => { - server.respondWith(JSON.stringify({ + it('should handle an error', () => { + let response = { 'status': 'ok', 'account_id': 14062, 'site_id': 70608, @@ -904,71 +699,23 @@ describe('the rubicon adapter', () => { 'ads': [{ 'status': 'not_ok', }] - })); - - rubiconAdapter.callBids(bidderRequest); + }; - server.respond(); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); + expect(bids).to.be.lengthOf(0); }); it('should handle an error because of malformed json response', () => { - server.respondWith('{test{'); - - rubiconAdapter.callBids(bidderRequest); - - server.respond(); - - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); - }); - - it('should handle error contacting endpoint', () => { - server.respondWith([404, {}, '']); - - rubiconAdapter.callBids(bidderRequest); - - server.respond(); + let response = '{test{'; - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.NO_BID); - }); - - it('should not register an error bid when a success call to addBidResponse throws an error', () => { - server.respondWith(JSON.stringify({ - 'status': 'ok', - 'account_id': 14062, - 'site_id': 70608, - 'zone_id': 530022, - 'size_id': 15, - 'alt_size_ids': [ - 43 - ], - 'tracking': '', - 'inventory': {}, - 'ads': [{ - 'status': 'ok', - 'cpm': 0.8, - 'size_id': 15 - }] - })); - - addBidResponseAction = function() { - throw new Error('test error'); - }; - - rubiconAdapter.callBids(bidderRequest); - - server.respond(); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); - // was calling twice for same bid, but should only call once - expect(bidManager.addBidResponse.calledOnce).to.equal(true); - expect(bids).to.be.lengthOf(1); + expect(bids).to.be.lengthOf(0); }); }); @@ -978,7 +725,7 @@ describe('the rubicon adapter', () => { }); it('should register a successful bid', () => { - server.respondWith(JSON.stringify({ + let response = { 'status': 'ok', 'ads': { '/19968336/header-bid-tag-0': [ @@ -1003,18 +750,14 @@ describe('the rubicon adapter', () => { ] }, 'account_id': 7780 - })); - - rubiconAdapter.callBids(bidderRequest); - - server.respond(); + }; - // was calling twice for same bid, but should only call once - expect(bidManager.addBidResponse.calledOnce).to.equal(true); + let bids = spec.interpretResponse(response, { + bidRequest: bidderRequest.bids[0] + }); expect(bids).to.be.lengthOf(1); - expect(bids[0].getStatusCode()).to.equal(CONSTANTS.STATUS.GOOD); expect(bids[0].bidderCode).to.equal('rubicon'); expect(bids[0].creative_id).to.equal('crid-999999'); expect(bids[0].cpm).to.equal(1); @@ -1029,95 +772,25 @@ describe('the rubicon adapter', () => { }); describe('user sync', () => { - let bids; - let server; - let addBidResponseAction; - let rubiconAdapter; - let userSyncStub; const emilyUrl = 'https://tap-secure.rubiconproject.com/partner/scripts/rubicon/emily.html?rtb_ext=1'; beforeEach(() => { - bids = []; - - server = sinon.fakeServer.create(); - // monitor userSync registrations - userSyncStub = sinon.stub(userSync, 'registerSync'); - - sandbox.stub(bidManager, 'addBidResponse', (elemId, bid) => { - bids.push(bid); - if (addBidResponseAction) { - addBidResponseAction(); - addBidResponseAction = undefined; - } - }); - - server.respondWith(JSON.stringify({ - 'status': 'ok', - 'account_id': 14062, - 'site_id': 70608, - 'zone_id': 530022, - 'size_id': 15, - 'alt_size_ids': [ - 43 - ], - 'tracking': '', - 'inventory': {}, - 'ads': [ - { - 'status': 'ok', - 'impression_id': '153dc240-8229-4604-b8f5-256933b9374c', - 'size_id': '15', - 'ad_id': '6', - 'advertiser': 7, - 'network': 8, - 'creative_id': 9, - 'type': 'script', - 'script': 'alert(\'foo\')', - 'campaign_id': 10, - 'cpm': 0.811, - 'targeting': [ - { - 'key': 'rpfl_14062', - 'values': [ - '15_tier_all_test' - ] - } - ] - } - ] - })); - - // Remove all Emily iframes for a fresh start - let iframes = document.querySelectorAll('[src="' + emilyUrl + '"]'); - for (let i = 0; i < iframes.length; i += 1) { - iframes[i].outerHTML = ''; - } - - rubiconAdapter = new RubiconAdapter(); - }); - - afterEach(() => { - server.restore(); - userSyncStub.restore(); + resetUserSync(); }); it('should register the Emily iframe', () => { - expect(userSyncStub.calledOnce).to.be.false; - rubiconAdapter.callBids(bidderRequest); - server.respond(); - expect(userSyncStub.calledOnce).to.be.true; - expect(userSyncStub.getCall(0).args).to.eql(['iframe', 'rubicon', emilyUrl]); + let syncs = spec.getUserSyncs(); + + expect(syncs).to.deep.equal({type: 'iframe', url: emilyUrl}); }); it('should not register the Emily iframe more than once', () => { - expect(userSyncStub.calledOnce).to.be.false; - rubiconAdapter.callBids(bidderRequest); - server.respond(); - expect(userSyncStub.calledOnce).to.be.true; - // run another auction, should still have only been called once - rubiconAdapter.callBids(bidderRequest); - server.respond(); - expect(userSyncStub.calledOnce).to.be.true; + let syncs = spec.getUserSyncs(); + expect(syncs).to.deep.equal({type: 'iframe', url: emilyUrl}); + + // when called again, should still have only been called once + syncs = spec.getUserSyncs(); + expect(syncs).to.equal(undefined); }); }); }); diff --git a/test/spec/modules/s2sTesting_spec.js b/test/spec/modules/s2sTesting_spec.js new file mode 100644 index 00000000000..f829087a967 --- /dev/null +++ b/test/spec/modules/s2sTesting_spec.js @@ -0,0 +1,437 @@ +import { getSourceBidderMap, calculateBidSources, getSource } from 'modules/s2sTesting'; +import { config } from 'src/config'; + +var events = require('src/events'); +var CONSTANTS = require('src/constants.json'); +const BID_ADJUSTMENT = CONSTANTS.EVENTS.BID_ADJUSTMENT; + +var expect = require('chai').expect; + +describe('s2sTesting', function () { + let mathRandomStub; + let randomNumber = 0; + + beforeEach(() => { + mathRandomStub = sinon.stub(Math, 'random', () => { return randomNumber; }); + }); + + afterEach(() => { + mathRandomStub.restore(); + }); + + describe('getSource', () => { + // helper function to set random number and get the source + function getExpectedSource(randNumber, sourceWeights, sources) { + // set random number for testing + randomNumber = randNumber; + return getSource(sourceWeights, sources); + } + + it('returns undefined if no sources', () => { + expect(getExpectedSource(0, {})).to.be.undefined; + expect(getExpectedSource(0.5, {})).to.be.undefined; + expect(getExpectedSource(0.9999, {})).to.be.undefined; + }); + + it('returns undefined if no weights', () => { + expect(getExpectedSource(0, {server: 0, client: 0})).to.be.undefined; + expect(getExpectedSource(0.5, {client: 0})).to.be.undefined; + }); + + it('gets the expected source from 3 sources', () => { + var sources = ['server', 'client', 'both']; + expect(getExpectedSource(0, {server: 1, client: 1, both: 2}, sources)).to.equal('server'); + expect(getExpectedSource(0.2499999, {server: 1, client: 1, both: 2}, sources)).to.equal('server'); + expect(getExpectedSource(0.25, {server: 1, client: 1, both: 2}, sources)).to.equal('client'); + expect(getExpectedSource(0.49999, {server: 1, client: 1, both: 2}, sources)).to.equal('client'); + expect(getExpectedSource(0.5, {server: 1, client: 1, both: 2}, sources)).to.equal('both'); + expect(getExpectedSource(0.99999, {server: 1, client: 1, both: 2}, sources)).to.equal('both'); + }); + + it('gets the expected source from 2 sources', () => { + expect(getExpectedSource(0, {server: 2, client: 3})).to.equal('server'); + expect(getExpectedSource(0.39999, {server: 2, client: 3})).to.equal('server'); + expect(getExpectedSource(0.4, {server: 2, client: 3})).to.equal('client'); + expect(getExpectedSource(0.9, {server: 2, client: 3})).to.equal('client'); + var sources = ['server', 'client', 'both']; + expect(getExpectedSource(0, {server: 2, client: 3}, sources)).to.equal('server'); + expect(getExpectedSource(0.39999, {server: 2, client: 3}, sources)).to.equal('server'); + expect(getExpectedSource(0.4, {server: 2, client: 3}, sources)).to.equal('client'); + expect(getExpectedSource(0.9, {server: 2, client: 3}, sources)).to.equal('client'); + }); + + it('gets the expected source from 1 source', () => { + expect(getExpectedSource(0, {client: 2})).to.equal('client'); + expect(getExpectedSource(0.5, {client: 2})).to.equal('client'); + expect(getExpectedSource(0.99999, {client: 2})).to.equal('client'); + }); + + it('ignores an invalid source', () => { + expect(getExpectedSource(0, {client: 2, cache: 2})).to.equal('client'); + expect(getExpectedSource(0.3333, {server: 1, cache: 1, client: 2})).to.equal('server'); + expect(getExpectedSource(0.34, {server: 1, cache: 1, client: 2})).to.equal('client'); + }); + + it('ignores order of sources', () => { + var sources = ['server', 'client', 'both']; + expect(getExpectedSource(0, {client: 1, server: 1, both: 2}, sources)).to.equal('server'); + expect(getExpectedSource(0.2499999, {both: 2, client: 1, server: 1}, sources)).to.equal('server'); + expect(getExpectedSource(0.25, {client: 1, both: 2, server: 1}, sources)).to.equal('client'); + expect(getExpectedSource(0.49999, {server: 1, both: 2, client: 1}, sources)).to.equal('client'); + expect(getExpectedSource(0.5, {both: 2, server: 1, client: 1}, sources)).to.equal('both'); + }); + + it('accepts an array of sources', () => { + expect(getExpectedSource(0.3333, {second: 2, first: 1}, ['first', 'second'])).to.equal('first'); + expect(getExpectedSource(0.34, {second: 2, first: 1}, ['first', 'second'])).to.equal('second'); + expect(getExpectedSource(0.9999, {second: 2, first: 1}, ['first', 'second'])).to.equal('second'); + }); + }); + + describe('getSourceBidderMap', () => { + describe('setting source through s2sConfig', () => { + beforeEach(() => { + // set random number for testing + randomNumber = 0.7; + }); + + it('does not work if testing is "false"', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: false, + bidderControl: {rubicon: {bidSource: {server: 1, client: 1}}} + }}); + expect(getSourceBidderMap()).to.eql({ + server: [], + client: [] + }); + }); + + it('sets one client bidder', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: true, + bidderControl: {rubicon: {bidSource: {server: 1, client: 1}}} + }}); + expect(getSourceBidderMap()).to.eql({ + server: [], + client: ['rubicon'] + }); + }); + + it('sets one server bidder', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: true, + bidderControl: {rubicon: {bidSource: {server: 4, client: 1}}} + }}); + expect(getSourceBidderMap()).to.eql({ + server: ['rubicon'], + client: [] + }); + }); + + it('defaults to server', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon'], + testing: true + }}); + expect(getSourceBidderMap()).to.eql({ + server: ['rubicon'], + client: [] + }); + }); + + it('sets two bidders', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {bidSource: {server: 3, client: 1}}, + appnexus: {bidSource: {server: 1, client: 1}} + }}}); + var serverClientBidders = getSourceBidderMap(); + expect(serverClientBidders.server).to.eql(['rubicon']); + expect(serverClientBidders.client).to.have.members(['appnexus']); + }); + }); + + describe('setting source through adUnits', () => { + beforeEach(() => { + // reset s2sconfig bid sources + config.setConfig({s2sConfig: {testing: true}}); + // set random number for testing + randomNumber = 0.7; + }); + + it('sets one bidder source from one adUnit', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 4, client: 1}} + ]} + ]; + expect(getSourceBidderMap(adUnits)).to.eql({ + server: ['rubicon'], + client: [] + }); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('server'); + expect(adUnits[0].bids[0].finalSource).to.equal('server'); + + adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 1, client: 1}} + ]} + ]; + expect(getSourceBidderMap(adUnits)).to.eql({ + server: [], + client: ['rubicon'] + }); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + }); + + it('defaults to client if no bidSource', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {}} + ]} + ]; + expect(getSourceBidderMap(adUnits)).to.eql({ + server: [], + client: ['rubicon'] + }); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.be.undefined; + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + }); + + it('sets multiple bidders sources from one adUnit', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 2, client: 1}}, + {bidder: 'appnexus', bidSource: {server: 3, client: 1}} + ]} + ]; + var serverClientBidders = getSourceBidderMap(adUnits); + expect(serverClientBidders.server).to.eql(['appnexus']); + expect(serverClientBidders.client).to.have.members(['rubicon']); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + expect(adUnits[0].bids[1].calcSource).to.equal('server'); + expect(adUnits[0].bids[1].finalSource).to.equal('server'); + }); + + it('sets multiple bidders sources from multiple adUnits', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 2, client: 1}}, + {bidder: 'appnexus', bidSource: {server: 1, client: 1}} + ]}, + {bids: [ + {bidder: 'rubicon', bidSource: {server: 4, client: 1}}, + {bidder: 'bidder3', bidSource: {client: 1}} + ]} + ]; + var serverClientBidders = getSourceBidderMap(adUnits); + expect(serverClientBidders.server).to.have.members(['rubicon']); + expect(serverClientBidders.server).to.not.have.members(['appnexus', 'bidder3']); + expect(serverClientBidders.client).to.have.members(['rubicon', 'appnexus', 'bidder3']); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + expect(adUnits[0].bids[1].calcSource).to.equal('client'); + expect(adUnits[0].bids[1].finalSource).to.equal('client'); + expect(adUnits[1].bids[0].calcSource).to.equal('server'); + expect(adUnits[1].bids[0].finalSource).to.equal('server'); + expect(adUnits[1].bids[1].calcSource).to.equal('client'); + expect(adUnits[1].bids[1].finalSource).to.equal('client'); + }); + + it('should reuse calculated sources', () => { + var adUnits = [ + {bids: [ + {bidder: 'rubicon', calcSource: 'client', bidSource: {server: 4, client: 1}}, + {bidder: 'appnexus', calcSource: 'server', bidSource: {server: 1, client: 1}}, + {bidder: 'bidder3', calcSource: 'server', bidSource: {client: 1}} + ]} + ]; + var serverClientBidders = getSourceBidderMap(adUnits); + + expect(serverClientBidders.server).to.have.members(['appnexus', 'bidder3']); + expect(serverClientBidders.server).to.not.have.members(['rubicon']); + expect(serverClientBidders.client).to.have.members(['rubicon']); + expect(serverClientBidders.client).to.not.have.members(['appnexus', 'bidder3']); + // should have saved the source on the bid + expect(adUnits[0].bids[0].calcSource).to.equal('client'); + expect(adUnits[0].bids[0].finalSource).to.equal('client'); + expect(adUnits[0].bids[1].calcSource).to.equal('server'); + expect(adUnits[0].bids[1].finalSource).to.equal('server'); + expect(adUnits[0].bids[2].calcSource).to.equal('server'); + expect(adUnits[0].bids[2].finalSource).to.equal('server'); + }); + }); + + describe('setting source through s2sconfig and adUnits', () => { + beforeEach(() => { + // reset s2sconfig bid sources + config.setConfig({s2sConfig: {testing: true}}); + // set random number for testing + randomNumber = 0.7; + }); + + it('should get sources from both', () => { + // set rubicon: server and appnexus: client + var adUnits = [ + {bids: [ + {bidder: 'rubicon', bidSource: {server: 4, client: 1}}, + {bidder: 'appnexus', bidSource: {client: 1}} + ]} + ]; + + // set rubicon: client and appnexus: server + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {bidSource: {server: 2, client: 1}}, + appnexus: {bidSource: {server: 1}} + } + }}); + + var serverClientBidders = getSourceBidderMap(adUnits); + expect(serverClientBidders.server).to.have.members(['rubicon', 'appnexus']); + expect(serverClientBidders.client).to.have.members(['rubicon', 'appnexus']); + }); + }); + }); + + describe('addBidderSourceTargeting', () => { + const AST = CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING; + + function checkTargeting(bidder) { + var targeting = window.pbjs.bidderSettings[bidder][AST]; + var srcTargeting = targeting[targeting.length - 1]; + expect(srcTargeting.key).to.equal(`hb_source_${bidder}`); + expect(srcTargeting.val).to.be.a('function'); + expect(window.pbjs.bidderSettings[bidder].alwaysUseBid).to.be.true; + } + + function checkNoTargeting(bidder) { + var bs = window.pbjs.bidderSettings; + var targeting = bs[bidder] && bs[bidder][AST]; + if (!targeting) { + expect(targeting).to.be.undefined; + return; + } + expect(targeting.find((kvp) => { + return kvp.key === `hb_source_${bidder}`; + })).to.be.undefined; + } + + function checkTargetingVal(bidResponse, expectedVal) { + var targeting = window.pbjs.bidderSettings[bidResponse.bidderCode][AST]; + var targetingFunc = targeting[targeting.length - 1].val; + expect(targetingFunc(bidResponse)).to.equal(expectedVal); + } + + beforeEach(() => { + // set bidderSettings + window.pbjs.bidderSettings = {}; + }); + + it('should not set hb_source_ unless testing is on and includeSourceKvp is set', () => { + config.setConfig({s2sConfig: {bidders: ['rubicon', 'appnexus']}}); + expect(window.pbjs.bidderSettings).to.eql({}); + + config.setConfig({s2sConfig: {bidders: ['rubicon', 'appnexus'], testing: true}}); + expect(window.pbjs.bidderSettings).to.eql({}); + + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {bidSource: {server: 2, client: 1}}, + appnexus: {bidSource: {server: 1}} + } + }}); + expect(window.pbjs.bidderSettings).to.eql({}); + + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: false, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + expect(window.pbjs.bidderSettings).to.eql({}); + }); + + it('should set hb_source_ if includeSourceKvp is set', () => { + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + checkTargeting('rubicon'); + checkTargeting('appnexus'); + checkTargetingVal({bidderCode: 'rubicon', source: 'server'}, 'server'); + checkTargetingVal({bidderCode: 'appnexus', source: 'client'}, 'client'); + + // turn off appnexus + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: false} + } + }}); + checkTargeting('rubicon'); + checkNoTargeting('appnexus'); + checkTargetingVal({bidderCode: 'rubicon', source: 'client'}, 'client'); + + // should default to "client" + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + checkTargeting('rubicon'); + checkTargeting('appnexus'); + checkTargetingVal({bidderCode: 'rubicon'}, 'client'); + checkTargetingVal({bidderCode: 'appnexus'}, 'client'); + }); + + it('should reset adServerTargeting when a new config is set', () => { + // set config with targeting + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true, + bidderControl: { + rubicon: {includeSourceKvp: true}, + appnexus: {includeSourceKvp: true} + } + }}); + checkTargeting('rubicon'); + checkTargeting('appnexus'); + + // set config without targeting + config.setConfig({s2sConfig: { + bidders: ['rubicon', 'appnexus'], + testing: true + }}); + checkNoTargeting('rubicon'); + checkNoTargeting('appnexus'); + }); + }); +}); diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 5453e594155..b92dfe4d493 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -120,7 +120,6 @@ describe('sharethrough adapter', () => { afterEach(() => { server.restore(); - stubAddBidResponse.reset(); }); it('should add a bid object for each bid', () => { @@ -174,7 +173,7 @@ describe('sharethrough adapter', () => { describe('when bidResponse string cannot be JSON parsed', () => { beforeEach(() => { - pbjs._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); adapter.str.placementCodeSet['foo'] = {}; server.respondWith(/aaaa1111/, 'non JSON string'); @@ -200,7 +199,7 @@ describe('sharethrough adapter', () => { describe('when no fill', () => { beforeEach(() => { - pbjs._bidsRequested.push(bidderRequest); + $$PREBID_GLOBAL$$._bidsRequested.push(bidderRequest); adapter.str.placementCodeSet['foo'] = {}; let bidderResponse1 = { diff --git a/test/spec/modules/trustxBidAdapter_spec.js b/test/spec/modules/trustxBidAdapter_spec.js index e559a16d71a..7208ebef343 100644 --- a/test/spec/modules/trustxBidAdapter_spec.js +++ b/test/spec/modules/trustxBidAdapter_spec.js @@ -8,10 +8,10 @@ describe('trustx adapter tests', function () { var bidmanager = require('src/bidmanager'); var adLoader = require('src/adloader'); var utils = require('src/utils'); - window.pbjs = window.pbjs || {}; + window.$$PREBID_GLOBAL$$ = window.$$PREBID_GLOBAL$$ || {}; if (typeof (pbjs) === 'undefined') { - var pbjs = window.pbjs; + var pbjs = window.$$PREBID_GLOBAL$$; } let stubLoadScript; beforeEach(function () { @@ -73,7 +73,7 @@ describe('trustx adapter tests', function () { sinon.assert.calledWith(stubLoadScript, bidUrl); var parsedBidUrl = urlParse(bidUrl); var parsedBidUrlQueryString = querystringify.parse(parsedBidUrl.query); - var generatedCallback = 'pbjs.trustx_callback_wrapper_5_6'; + var generatedCallback = '$$PREBID_GLOBAL$$.trustx_callback_wrapper_5_6'; expect(parsedBidUrl.hostname).to.equal('sofia.trustx.org'); expect(parsedBidUrl.pathname).to.equal('/hb'); expect(parsedBidUrlQueryString).to.have.property('auids').and.to.equal('5,6'); diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js new file mode 100644 index 00000000000..977575a4d19 --- /dev/null +++ b/test/spec/native_spec.js @@ -0,0 +1,46 @@ +import { expect } from 'chai'; +import { fireNativeTrackers, getNativeTargeting } from 'src/native'; +const utils = require('src/utils'); + +const bid = { + native: { + title: 'Native Creative', + body: 'Cool description great stuff', + cta: 'Do it', + sponsoredBy: 'AppNexus', + clickUrl: 'https://www.link.example', + clickTrackers: ['https://tracker.example'], + impressionTrackers: ['https://impression.example'], + } +}; + +describe('native.js', () => { + let triggerPixelStub; + + beforeEach(() => { + triggerPixelStub = sinon.stub(utils, 'triggerPixel'); + }); + + afterEach(() => { + utils.triggerPixel.restore(); + }); + + it('gets native targeting keys', () => { + const targeting = getNativeTargeting(bid); + expect(targeting.hb_native_title).to.equal(bid.native.title); + expect(targeting.hb_native_body).to.equal(bid.native.body); + expect(targeting.hb_native_linkurl).to.equal(bid.native.clickUrl); + }); + + it('fires impression trackers', () => { + fireNativeTrackers({}, bid); + sinon.assert.calledOnce(triggerPixelStub); + sinon.assert.calledWith(triggerPixelStub, bid.native.impressionTrackers[0]); + }); + + it('fires click trackers', () => { + fireNativeTrackers({ action: 'click' }, bid); + sinon.assert.calledOnce(triggerPixelStub); + sinon.assert.calledWith(triggerPixelStub, bid.native.clickTrackers[0]); + }); +}); diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index 034700d5cbc..6da22ed8984 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -3,7 +3,8 @@ import AdapterManager from 'src/adaptermanager'; import { getAdUnits } from 'test/fixtures/fixtures'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils'; -import { StorageManager } from 'src/storagemanager'; +import { registerBidder } from 'src/adapters/bidderFactory'; +var s2sTesting = require('../../../../modules/s2sTesting'); const CONFIG = { enabled: true, @@ -19,33 +20,28 @@ var prebidServerAdapterMock = { setConfig: sinon.stub(), queueSync: sinon.stub() }; +var adequantAdapterMock = { + bidder: 'adequant', + callBids: sinon.stub(), + setConfig: sinon.stub(), + queueSync: sinon.stub() +}; +var appnexusAdapterMock = { + bidder: 'appnexus', + callBids: sinon.stub(), + setConfig: sinon.stub(), + queueSync: sinon.stub() +}; describe('adapterManager tests', () => { describe('S2S tests', () => { - var stubGetStorageItem; - var stubSetStorageItem; - beforeEach(() => { AdapterManager.setS2SConfig(CONFIG); AdapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; - stubGetStorageItem = sinon.stub(StorageManager, 'get'); - stubSetStorageItem = sinon.stub(StorageManager, 'set'); - stubSetStorageItem = sinon.stub(StorageManager, 'add'); - stubSetStorageItem = sinon.stub(StorageManager, 'remove'); - - stubGetStorageItem.returns(['appnexus']); - prebidServerAdapterMock.callBids.reset(); }); - afterEach(() => { - StorageManager.get.restore(); - StorageManager.set.restore(); - StorageManager.add.restore(); - StorageManager.remove.restore(); - }); - it('invokes callBids on the S2S adapter', () => { AdapterManager.callBids({adUnits: getAdUnits()}); sinon.assert.calledOnce(prebidServerAdapterMock.callBids); @@ -96,4 +92,231 @@ describe('adapterManager tests', () => { expect(spy.called).to.equal(false); }); }) + + describe('s2sTesting', () => { + function getTestAdUnits() { + // copy adUnits + return JSON.parse(JSON.stringify(getAdUnits())); + } + + function checkServerCalled(numAdUnits, numBids) { + sinon.assert.calledOnce(prebidServerAdapterMock.callBids); + var requestObj = prebidServerAdapterMock.callBids.firstCall.args[0]; + expect(requestObj.ad_units.length).to.equal(numAdUnits); + for (let i = 0; i < numAdUnits; i++) { + expect(requestObj.ad_units[i].bids.filter((bid) => { + return bid.bidder === 'appnexus' || bid.bidder === 'adequant'; + }).length).to.equal(numBids); + } + } + + function checkClientCalled(adapter, numBids) { + sinon.assert.calledOnce(adapter.callBids); + expect(adapter.callBids.firstCall.args[0].bids.length).to.equal(numBids); + } + + var TESTING_CONFIG; + var stubGetSourceBidderMap; + + beforeEach(() => { + TESTING_CONFIG = Object.assign(CONFIG, { + bidders: ['appnexus', 'adequant'], + testing: true + }); + + AdapterManager.setS2SConfig(CONFIG); + AdapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; + AdapterManager.bidderRegistry['adequant'] = adequantAdapterMock; + AdapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; + + stubGetSourceBidderMap = sinon.stub(s2sTesting, 'getSourceBidderMap'); + + prebidServerAdapterMock.callBids.reset(); + adequantAdapterMock.callBids.reset(); + appnexusAdapterMock.callBids.reset(); + }); + + afterEach(() => { + s2sTesting.getSourceBidderMap.restore(); + }); + + it('calls server adapter if no sources defined', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: [], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + AdapterManager.callBids({adUnits: getTestAdUnits()}); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + sinon.assert.notCalled(appnexusAdapterMock.callBids); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client adapter if one client source defined', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + AdapterManager.callBids({adUnits: getTestAdUnits()}); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client adapters if client sources defined', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + AdapterManager.callBids({adUnits: getTestAdUnits()}); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('does not call server adapter for bidders that go to client', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[0].bids[1].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[1].finalSource = s2sTesting.CLIENT; + AdapterManager.callBids({adUnits}); + + // server adapter + sinon.assert.notCalled(prebidServerAdapterMock.callBids); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('does not call client adapters for bidders that go to server', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.SERVER; + adUnits[0].bids[1].finalSource = s2sTesting.SERVER; + adUnits[1].bids[0].finalSource = s2sTesting.SERVER; + adUnits[1].bids[1].finalSource = s2sTesting.SERVER; + AdapterManager.callBids({adUnits}); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + sinon.assert.notCalled(appnexusAdapterMock.callBids); + + // adequant + sinon.assert.notCalled(adequantAdapterMock.callBids); + }); + + it('calls client and server adapters for bidders that go to both', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.BOTH; + adUnits[0].bids[1].finalSource = s2sTesting.BOTH; + adUnits[1].bids[0].finalSource = s2sTesting.BOTH; + adUnits[1].bids[1].finalSource = s2sTesting.BOTH; + AdapterManager.callBids({adUnits}); + + // server adapter + checkServerCalled(2, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 2); + + // adequant + checkClientCalled(adequantAdapterMock, 2); + }); + + it('makes mixed client/server adapter calls for mixed bidder sources', () => { + stubGetSourceBidderMap.returns({[s2sTesting.CLIENT]: ['appnexus', 'adequant'], [s2sTesting.SERVER]: []}); + AdapterManager.setS2SConfig(TESTING_CONFIG); + var adUnits = getTestAdUnits(); + adUnits[0].bids[0].finalSource = s2sTesting.CLIENT; + adUnits[0].bids[1].finalSource = s2sTesting.CLIENT; + adUnits[1].bids[0].finalSource = s2sTesting.SERVER; + adUnits[1].bids[1].finalSource = s2sTesting.SERVER; + AdapterManager.callBids({adUnits}); + + // server adapter + checkServerCalled(1, 2); + + // appnexus + checkClientCalled(appnexusAdapterMock, 1); + + // adequant + checkClientCalled(adequantAdapterMock, 1); + }); + }); + + describe('aliasBidderAdaptor', function() { + const CODE = 'sampleBidder'; + + // Note: remove this describe once Prebid is 1.0 + describe('old way', function() { + let originalRegistry; + + function SampleAdapter() { + return Object.assign(this, { + callBids: sinon.stub(), + setBidderCode: sinon.stub() + }); + } + + before(() => { + originalRegistry = AdapterManager.bidderRegistry; + AdapterManager.bidderRegistry[CODE] = new SampleAdapter(); + }); + + after(() => { + AdapterManager.bidderRegistry = originalRegistry; + }); + + it('should add alias to registry', () => { + const alias = 'testalias'; + AdapterManager.aliasBidAdapter(CODE, alias); + expect(AdapterManager.bidderRegistry).to.have.property(alias); + }); + }); + + describe('using bidderFactory', function() { + let spec; + + beforeEach(() => { + spec = { + code: CODE, + isBidRequestValid: () => {}, + buildRequests: () => {}, + interpretResponse: () => {}, + getUserSyncs: () => {} + }; + }); + + it('should add alias to registry when original adapter is using bidderFactory', function() { + let thisSpec = Object.assign(spec, { supportedMediaTypes: ['video'] }); + registerBidder(thisSpec); + const alias = 'aliasBidder'; + AdapterManager.aliasBidAdapter(CODE, alias); + expect(AdapterManager.bidderRegistry).to.have.property(alias); + expect(AdapterManager.videoAdapters).to.include(alias); + }); + }); + }); }); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 04e4c051365..e91ddcf39a4 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -147,6 +147,31 @@ describe('bidders created by newBidder', () => { }); }); + it('should make the appropriate POST request when options are passed', () => { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const options = { contentType: 'application/json'}; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: url, + data: data, + options: options + }); + + bidder.callBids(MOCK_BIDS_REQUEST); + + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(url); + expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + method: 'POST', + contentType: 'application/json', + withCredentials: true + }); + }); + it('should make the appropriate GET request', () => { const bidder = newBidder(spec); const url = 'test.url.com'; @@ -169,6 +194,30 @@ describe('bidders created by newBidder', () => { }); }); + it('should make the appropriate GET request when options are passed', () => { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const opt = { withCredentials: false } + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'GET', + url: url, + data: data, + options: opt + }); + + bidder.callBids(MOCK_BIDS_REQUEST); + + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2&`); + expect(ajaxStub.firstCall.args[2]).to.be.undefined; + expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + method: 'GET', + withCredentials: false + }); + }); + it('should make multiple calls if the spec returns them', () => { const bidder = newBidder(spec); const url = 'test.url.com'; diff --git a/test/spec/userSync_spec.js b/test/spec/userSync_spec.js index 4eabe254113..d6ae525f6d7 100644 --- a/test/spec/userSync_spec.js +++ b/test/spec/userSync_spec.js @@ -1,6 +1,5 @@ import { expect } from 'chai'; import { config } from 'src/config'; -import { StorageManager, pbjsSyncsKey } from 'src/storagemanager'; // Use require since we need to be able to write to these vars const utils = require('../../src/utils'); let { newUserSync } = require('../../src/userSync'); @@ -174,4 +173,15 @@ describe('user sync', () => { expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('http://example.com/'); expect(triggerPixelStub.getCall(1)).to.be.null; }); + + it('should register config set after instantiation', () => { + // start with userSync off + const userSync = newTestUserSync({syncEnabled: false}); + // turn it on with setConfig() + config.setConfig({userSync: {syncEnabled: true}}); + userSync.registerSync('image', 'testBidder', 'http://example.com'); + userSync.syncUsers(); + expect(triggerPixelStub.getCall(0)).to.not.be.null; + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.equal('http://example.com'); + }); }); diff --git a/webpack.conf.js b/webpack.conf.js index 59d03790a4b..68db3a389f2 100644 --- a/webpack.conf.js +++ b/webpack.conf.js @@ -51,6 +51,10 @@ module.exports = { test: /\.json$/, loader: 'json-loader' }, + { + test: /\.md$/, + loader: 'ignore-loader' + }, { test: /constants.json$/, include: /(src)/,