diff --git a/.travis.yml b/.travis.yml index 73b5de5ae4a..364d47edf4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,5 @@ +sudo: required + dist: trusty language: node_js diff --git a/pr_review.md b/PR_REVIEW.md similarity index 52% rename from pr_review.md rename to PR_REVIEW.md index 1d8cf0c360c..80d11c35769 100644 --- a/pr_review.md +++ b/PR_REVIEW.md @@ -1,5 +1,9 @@ ## Summary -We take PR review seriously. Please read https://medium.com/@mrjoelkemp/giving-better-code-reviews-16109e0fdd36#.xa8lc4i23 to understand how a PR review should be conducted. Be rational and strict in your review, make sure you understand exactly what the submitter's intent is. Overall 1 person should take ownership of a particular PR. When they are satisfied it's in good condition to merge, they should request 1 additional team member to review as a sanity check. Only when the PR has 2 `LGTM` from the core team should it be merged. +We take PR review seriously. Please read https://medium.com/@mrjoelkemp/giving-better-code-reviews-16109e0fdd36#.xa8lc4i23 to understand how a PR review should be conducted. Be rational and strict in your review, make sure you understand exactly what the submitter's intent is. Anyone in the community can review a PR, but a Prebid Org member is also required. A Prebid Org member should take ownership of a PR and do the initial review. + +If the PR is for a standard bid adapter or a standard analytics adapter, just the one review from a core member is sufficient. The reviewer will check against [required conventions](http://prebid.org/dev-docs/bidder-adaptor.html#required-adapter-conventions) and may merge the PR after approving and confirming that the documentation PR against prebid.org is open and linked to the issue. + +For modules and core platform updates, the initial reviewer should request an additional team member to review as a sanity check. Merge should only happen when the PR has 2 `LGTM` from the core team and a documentation PR if required. ### General PR review Process - Checkout the branch (these instructions are available on the github PR page as well). @@ -13,7 +17,7 @@ We take PR review seriously. Please read https://medium.com/@mrjoelkemp/giving-b - If all above is good, add a `LGTM` comment and request 1 additional core member to review. - Once there is 2 `LGTM` on the PR, merge to master - Ask the submitter to add a PR for documentation if applicable. -- Add a line into the `draft release` notes for this submission. If no draft release is available, create one using this template https://gist.github.com/mkendall07/c3af6f4691bed8a46738b3675cb5a479 +- Add a line into the `draft release` notes for this submission. If no draft release is available, create one using [this template]( https://gist.github.com/mkendall07/c3af6f4691bed8a46738b3675cb5a479) ### New Adapter or updates to adapter process - Follow steps above for general review process. In addition, please verify the following: @@ -23,3 +27,20 @@ We take PR review seriously. Please read https://medium.com/@mrjoelkemp/giving-b - Verify that code re-use is being done properly and that changes introduced by a bidder don't impact other bidders. - If the adapter being submitted is an alias type, check with the bidder contact that is being aliased to make sure it's allowed. - If the adapter is triggering any user syncs make sure they are using the user sync module in the Prebid.js core. +- Requests to the bidder should support HTTPS +- Responses from the bidder should be compressed (such as gzip, compress, deflate) +- Bid responses may not use JSONP: All requests must be AJAX with JSON responses +- All user-sync (aka pixel) activity must be registered via the provided functions +- Adapters may not use the $$PREBID_GLOBAL$$ variable +- All adapters must support the creation of multiple concurrent instances. This means, for example, that adapters cannot rely on mutable global variables. + +## Ticket Coordinator + +Each week, Prebid Org assigns one person to keep an eye on incoming issues and PRs. That person should: +- Review issues and PRs at least once per weekday for new items. +- For PRs: assign PRs to individuals on the PR review list. Try to be equitable -- not all PRs are created equally. Use the "Assigned" field and add the "Needs Review" label. +- For Issues: try to address questions and troubleshooting requests on your own, assigning them to others as needed. +- Issues that are questions or troubleshooting requests may be closed if the originator doesn't respond within a week to requests for confirmation or details. +- Issues that are bug reports should be left open and assigned to someone in PR rotation to confirm or deny the bug status. +- It's polite to check with others before assigning them large tasks. +- If possible, check in on older items and see if they can be unstuck. diff --git a/integrationExamples/gpt/serverbidServer_example.html b/integrationExamples/gpt/serverbidServer_example.html new file mode 100644 index 00000000000..3d76e963663 --- /dev/null +++ b/integrationExamples/gpt/serverbidServer_example.html @@ -0,0 +1,103 @@ + +
+ + + + + + +`); +} + +function createAPVTag() { + const APVURL = 'https://cdn.apvdr.com/js/VideoAd.min.js'; + let apvScript = document.createElement('script'); + apvScript.type = 'text/javascript'; + apvScript.id = 'apv'; + apvScript.src = APVURL; + return apvScript.outerHTML; +} + +function insertVASTMethod(targetId, vastXml) { + let apvVideoAdParam = { + s: targetId + }; + let script = document.createElement(`script`); + script.type = 'text/javascript'; + script.innerHTML = `(function(){ new APV.VideoAd(${JSON.stringify(apvVideoAdParam)}).load('${vastXml.replace(/\r?\n/g, '')}'); })();`; + return script.outerHTML; +} + +/** + * + * @param ad + */ +function removeWrapper(ad) { + const bodyIndex = ad.indexOf('
'); + if (bodyIndex === -1 || lastBodyIndex === -1) return false; + return ad.substr(bodyIndex, lastBodyIndex).replace('
', ''); +} + +registerBidder(spec); diff --git a/modules/adgenerationBidAdapter.md b/modules/adgenerationBidAdapter.md new file mode 100644 index 00000000000..d694b376ff9 --- /dev/null +++ b/modules/adgenerationBidAdapter.md @@ -0,0 +1,68 @@ +# Overview + +``` +Module Name: Adgeneration Bid Adapter +Module Type: Bidder Adapter +Maintainer: ssp-ope@supership.jp +``` + +# Description + +Connects to Adgeneration exchange for bids. + +Adgeneration bid adapter supports Banner and Native. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', // banner + sizes: [[300, 250]], + bids: [ + { + bidder: 'adg', + params: { + id: '58278', // banner + } + }, + ] + }, + // Native adUnit + { + code: 'native-div', + sizes: [[1,1]], + mediaTypes: { + native: { + image: { + required: true + }, + title: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + body: { + required: true + }, + icon: { + required: true + } + }, + }, + bids: [ + { + bidder: 'adg', + params: { + id: '58279', //native + } + }, + ] + }, +]; +``` diff --git a/modules/adkernelAdnBidAdapter.js b/modules/adkernelAdnBidAdapter.js index 28fb564320d..c02a0cb138f 100644 --- a/modules/adkernelAdnBidAdapter.js +++ b/modules/adkernelAdnBidAdapter.js @@ -80,7 +80,7 @@ export const spec = { code: 'adkernelAdn', - supportedMediaTypes: [VIDEO], + supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bidRequest) { return 'params' in bidRequest && (typeof bidRequest.params.host === 'undefined' || typeof bidRequest.params.host === 'string') && diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 016cc5794fb..c3f34274692 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -16,7 +16,7 @@ export const spec = { code: 'adkernel', aliases: ['headbidding'], - supportedMediaTypes: [VIDEO], + supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bidRequest) { return 'params' in bidRequest && typeof bidRequest.params.host !== 'undefined' && 'zoneId' in bidRequest.params && !isNaN(Number(bidRequest.params.zoneId)); diff --git a/modules/adomikAnalyticsAdapter.js b/modules/adomikAnalyticsAdapter.js index 64e3ae14835..30ef9c7dd90 100644 --- a/modules/adomikAnalyticsAdapter.js +++ b/modules/adomikAnalyticsAdapter.js @@ -1,6 +1,7 @@ import adapter from 'src/AnalyticsAdapter'; import CONSTANTS from 'src/constants.json'; import adaptermanager from 'src/adaptermanager'; +import { logInfo } from 'src/utils'; import find from 'core-js/library/fn/array/find'; import findIndex from 'core-js/library/fn/array/find-index'; @@ -12,19 +13,14 @@ 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.initializeBucketEvents() adomikAdapter.currentContext.id = args.auctionId - adomikAdapter.currentContext.timeout = args.timeout - if (args.config.bidwonTimeout !== undefined && typeof args.config.bidwonTimeout === 'number') { - bidwonTimeout = args.config.bidwonTimeout; - } break; case bidTimeout: @@ -39,12 +35,9 @@ let adomikAdapter = Object.assign(adapter({}), break; case bidWon: - adomikAdapter.bucketEvents.push({ - type: 'winner', - event: { - id: args.adId, - placementCode: args.adUnitCode - } + adomikAdapter.sendWonEvent({ + id: args.adId, + placementCode: args.adUnitCode }); break; @@ -61,24 +54,25 @@ let adomikAdapter = Object.assign(adapter({}), break; case auctionEnd: - setTimeout(() => { - if (adomikAdapter.bucketEvents.length > 0) { - adomikAdapter.sendTypedEvent(); - } - }, bidwonTimeout); + if (adomikAdapter.bucketEvents.length > 0) { + adomikAdapter.sendTypedEvent(); + } break; } } } ); +adomikAdapter.initializeBucketEvents = function() { + adomikAdapter.bucketEvents = []; +} + 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 = []; @@ -108,8 +102,11 @@ adomikAdapter.sendTypedEvent = function() { }) }; + const stringBulkEvents = JSON.stringify(bulkEvents) + logInfo('Events sent to adomik prebid analytic ' + stringBulkEvents); + // Encode object in base64 - const encodedBuf = window.btoa(JSON.stringify(bulkEvents)); + const encodedBuf = window.btoa(stringBulkEvents); // Create final url and split it in 1600 characters max (+endpoint length) const encodedUri = encodeURIComponent(encodedBuf); @@ -122,6 +119,17 @@ adomikAdapter.sendTypedEvent = function() { }) }; +adomikAdapter.sendWonEvent = function (wonEvent) { + const stringWonEvent = JSON.stringify(wonEvent) + logInfo('Won event sent to adomik prebid analytic ' + wonEvent); + + // Encode object in base64 + const encodedBuf = window.btoa(stringWonEvent); + const encodedUri = encodeURIComponent(encodedBuf); + const img = new Image(1, 1); + img.src = `https://${adomikAdapter.currentContext.url}/?q=${encodedUri}&id=${adomikAdapter.currentContext.id}&won=true` +} + adomikAdapter.buildBidResponse = function (bid) { return { bidder: bid.bidderCode.toUpperCase(), @@ -181,23 +189,20 @@ adomikAdapter.buildTypedEvents = function () { return groupedTypedEvents; } -// Initialize adomik object -adomikAdapter.currentContext = {}; -adomikAdapter.bucketEvents = []; - adomikAdapter.adapterEnableAnalytics = adomikAdapter.enableAnalytics; adomikAdapter.enableAnalytics = function (config) { + adomikAdapter.currentContext = {}; + const initOptions = config.options; if (initOptions) { adomikAdapter.currentContext = { uid: initOptions.id, url: initOptions.url, - debug: initOptions.debug, id: '', timeouted: false, - timeout: 0, } + logInfo('Adomik Analytics enabled with config', initOptions); adomikAdapter.adapterEnableAnalytics(config); } }; diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index 44b1f381bbe..91bf0746afc 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -1,7 +1,7 @@ import * as utils from 'src/utils'; import * as url from 'src/url'; import {registerBidder} from 'src/adapters/bidderFactory'; -import {NATIVE, VIDEO} from 'src/mediaTypes'; +import {BANNER, NATIVE, VIDEO} from 'src/mediaTypes'; /** * Adapter for requesting bids from adxcg.net @@ -9,7 +9,7 @@ import {NATIVE, VIDEO} from 'src/mediaTypes'; */ const BIDDER_CODE = 'adxcg'; -const SUPPORTED_AD_TYPES = [VIDEO, NATIVE]; +const SUPPORTED_AD_TYPES = [BANNER, VIDEO, NATIVE]; const SOURCE = 'pbjs10'; export const spec = { code: BIDDER_CODE, diff --git a/modules/aolBidAdapter.js b/modules/aolBidAdapter.js index 9b4aa26e1a1..35acb95cdee 100644 --- a/modules/aolBidAdapter.js +++ b/modules/aolBidAdapter.js @@ -345,7 +345,7 @@ export const spec = { let bidResponse = bidResponses[0]; if (config.getConfig('aol.userSyncOn') === constants.EVENTS.BID_RESPONSE) { - if (!$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped && bidResponse.ext && bidResponse.ext.pixels) { + if (!$$PREBID_GLOBAL$$.aolGlobals.pixelsDropped && bidResponse && bidResponse.ext && bidResponse.ext.pixels) { $$PREBID_GLOBAL$$.aolGlobals.pixelsDropped = true; return parsePixelItems(bidResponse.ext.pixels); diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 7db2b9aab2f..96163518a3a 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -1,13 +1,12 @@ import { Renderer } from 'src/Renderer'; import * as utils from 'src/utils'; import { registerBidder } from 'src/adapters/bidderFactory'; -import { NATIVE, VIDEO } from 'src/mediaTypes'; +import { BANNER, NATIVE, VIDEO } from 'src/mediaTypes'; import find from 'core-js/library/fn/array/find'; import includes from 'core-js/library/fn/array/includes'; const BIDDER_CODE = 'appnexus'; const URL = '//ib.adnxs.com/ut/v3/prebid'; -const SUPPORTED_AD_TYPES = ['banner', 'video', 'native']; const VIDEO_TARGETING = ['id', 'mimes', 'minduration', 'maxduration', 'startdelay', 'skippable', 'playback_method', 'frameworks']; const USER_PARAMS = ['age', 'external_uid', 'segments', 'gender', 'dnt', 'language']; @@ -30,8 +29,8 @@ const SOURCE = 'pbjs'; export const spec = { code: BIDDER_CODE, - aliases: ['appnexusAst', 'brealtime', 'pagescience', 'defymedia', 'gourmetads', 'matomy', 'featureforward', 'oftmedia'], - supportedMediaTypes: [VIDEO, NATIVE], + aliases: ['appnexusAst', 'brealtime', 'pagescience', 'defymedia', 'gourmetads', 'matomy', 'featureforward', 'oftmedia', 'districtm'], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. @@ -103,8 +102,8 @@ export const spec = { serverResponse.tags.forEach(serverBid => { const rtbBid = getRtbBid(serverBid); if (rtbBid) { - if (rtbBid.cpm !== 0 && includes(SUPPORTED_AD_TYPES, rtbBid.ad_type)) { - const bid = newBid(serverBid, rtbBid); + if (rtbBid.cpm !== 0 && includes(this.supportedMediaTypes, rtbBid.ad_type)) { + const bid = newBid(serverBid, rtbBid, bidderRequest); bid.mediaType = parseMediaType(rtbBid); bids.push(bid); } @@ -124,11 +123,11 @@ export const spec = { } } -function newRenderer(adUnitCode, rtbBid) { +function newRenderer(adUnitCode, rtbBid, rendererOptions = {}) { const renderer = Renderer.install({ id: rtbBid.renderer_id, url: rtbBid.renderer_url, - config: { adText: `AppNexus Outstream Video Ad via Prebid.js` }, + config: rendererOptions, loaded: false, }); @@ -179,9 +178,10 @@ function getKeywords(keywords) { * Unpack the Server's Bid into a Prebid-compatible one. * @param serverBid * @param rtbBid + * @param bidderRequest * @return Bid */ -function newBid(serverBid, rtbBid) { +function newBid(serverBid, rtbBid, bidderRequest) { const bid = { requestId: serverBid.uuid, cpm: rtbBid.cpm, @@ -197,21 +197,25 @@ function newBid(serverBid, rtbBid) { width: rtbBid.rtb.video.player_width, height: rtbBid.rtb.video.player_height, vastUrl: rtbBid.rtb.video.asset_url, - descriptionUrl: rtbBid.rtb.video.asset_url, ttl: 3600 }); // This supports Outstream Video if (rtbBid.renderer_url) { + const rendererOptions = utils.deepAccess( + bidderRequest.bids[0], + 'renderer.options' + ); + Object.assign(bid, { adResponse: serverBid, - renderer: newRenderer(bid.adUnitCode, rtbBid) + renderer: newRenderer(bid.adUnitCode, rtbBid, rendererOptions) }); bid.adResponse.ad = bid.adResponse.ads[0]; bid.adResponse.ad.video = bid.adResponse.ad.rtb.video; } - } else if (rtbBid.rtb['native']) { - const nativeAd = rtbBid.rtb['native']; - bid['native'] = { + } else if (rtbBid.rtb[NATIVE]) { + const nativeAd = rtbBid.rtb[NATIVE]; + bid[NATIVE] = { title: nativeAd.title, body: nativeAd.desc, cta: nativeAd.ctatext, @@ -256,6 +260,7 @@ function bidToTag(bid) { const tag = {}; tag.sizes = transformSizes(bid.sizes); tag.primary_size = tag.sizes[0]; + tag.ad_types = []; tag.uuid = bid.bidId; if (bid.params.placementId) { tag.id = parseInt(bid.params.placementId, 10); @@ -294,19 +299,24 @@ function bidToTag(bid) { tag.keywords = getKeywords(bid.params.keywords); } - if (bid.mediaType === 'native' || utils.deepAccess(bid, 'mediaTypes.native')) { - tag.ad_types = ['native']; + if (bid.mediaType === NATIVE || utils.deepAccess(bid, `mediaTypes.${NATIVE}`)) { + tag.ad_types.push(NATIVE); if (bid.nativeParams) { const nativeRequest = buildNativeRequest(bid.nativeParams); - tag['native'] = {layouts: [nativeRequest]}; + tag[NATIVE] = {layouts: [nativeRequest]}; } } - const videoMediaType = utils.deepAccess(bid, 'mediaTypes.video'); + const videoMediaType = utils.deepAccess(bid, `mediaTypes.${VIDEO}`); const context = utils.deepAccess(bid, 'mediaTypes.video.context'); - if (bid.mediaType === 'video' || (videoMediaType && context !== 'outstream')) { + if (bid.mediaType === VIDEO || videoMediaType) { + tag.ad_types.push(VIDEO); + } + + // instream gets vastUrl, outstream gets vastXml + if (bid.mediaType === VIDEO || (videoMediaType && context !== 'outstream')) { tag.require_asset_url = true; } @@ -318,6 +328,13 @@ function bidToTag(bid) { .forEach(param => tag.video[param] = bid.params.video[param]); } + if ( + (utils.isEmpty(bid.mediaType) && utils.isEmpty(bid.mediaTypes)) || + (bid.mediaType === BANNER || (bid.mediaTypes && bid.mediaTypes[BANNER])) + ) { + tag.ad_types.push(BANNER); + } + return tag; } @@ -414,12 +431,12 @@ function handleOutstreamRendererEvents(bid, id, eventName) { function parseMediaType(rtbBid) { const adType = rtbBid.ad_type; - if (adType === 'video') { - return 'video'; - } else if (adType === 'native') { - return 'native'; + if (adType === VIDEO) { + return VIDEO; + } else if (adType === NATIVE) { + return NATIVE; } else { - return 'banner'; + return BANNER; } } diff --git a/modules/atomxBidAdapter.js b/modules/atomxBidAdapter.js new file mode 100644 index 00000000000..f946841dffc --- /dev/null +++ b/modules/atomxBidAdapter.js @@ -0,0 +1,107 @@ +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'atomx'; + +function getDomain() { + var domain = ''; + + try { + if ((domain === '') && (window.top == window)) { + domain = window.location.href; + } + + if ((domain === '') && (window.top == window.parent)) { + domain = document.referrer; + } + + if (domain == '') { + var atomxt = 'atomxtest'; + + // It should be impossible to change the window.location.ancestorOrigins. + window.location.ancestorOrigins[0] = atomxt; + if (window.location.ancestorOrigins[0] != atomxt) { + var ancestorOrigins = window.location.ancestorOrigins; + + // If the length is 0 we are a javascript tag running in the main domain. + // But window.top != window or window.location.hostname is empty. + if (ancestorOrigins.length == 0) { + // This browser is so fucked up, just return an empty string. + return ''; + } + + // ancestorOrigins is an array where [0] is our own window.location + // and [length-1] is the top window.location. + domain = ancestorOrigins[ancestorOrigins.length - 1]; + } + } + } catch (unused) { + } + + if (domain === '') { + domain = document.referrer; + } + + if (domain === '') { + domain = window.location.href; + } + + return domain.substr(0, 512); +} + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + return bid.params && (!!bid.params.id); + }, + + buildRequests: function(validBidRequests) { + return validBidRequests.map(bidRequest => { + return { + method: 'GET', + url: location.protocol + '//p.ato.mx/placement', + data: { + v: 12, + id: bidRequest.params.id, + size: utils.parseSizesInput(bidRequest.sizes)[0], + prebid: bidRequest.bidId, + b: 0, + h: '7t3y9', + type: 'javascript', + screen: window.screen.width + 'x' + window.screen.height + 'x' + window.screen.colorDepth, + timezone: new Date().getTimezoneOffset(), + domain: getDomain(), + r: document.referrer.substr(0, 512), + }, + }; + }); + }, + + interpretResponse: function (serverResponse, bidRequest) { + const body = serverResponse.body; + const res = { + requestId: body.code, + cpm: body.cpm * 1000, + width: body.width, + height: body.height, + creativeId: body.creative_id, + currency: 'USD', + netRevenue: true, + ttl: 60, + }; + + if (body.adm) { + res.ad = body.adm; + } else { + res.adUrl = body.url; + } + + return [res]; + }, + + getUserSyncs: function(syncOptions, serverResponses) { + return []; + }, +}; +registerBidder(spec); diff --git a/modules/atomxBidAdapter.md b/modules/atomxBidAdapter.md new file mode 100644 index 00000000000..7f32b12fdfe --- /dev/null +++ b/modules/atomxBidAdapter.md @@ -0,0 +1,25 @@ +# Overview +Module Name: Atomx Bidder Adapter Module +Type: Bidder Adapter +Maintainer: erik@atomx.com + +# Description +Atomx Bidder Adapter for Prebid.js. + +# Test Parameters +``` +var adUnits = [ +{ + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: 'atomx', + params: { + id: 4025860, + } + } + ] +} +]; +``` diff --git a/modules/audienceNetworkBidAdapter.js b/modules/audienceNetworkBidAdapter.js index 41ca539ccba..3153a64a48d 100644 --- a/modules/audienceNetworkBidAdapter.js +++ b/modules/audienceNetworkBidAdapter.js @@ -12,7 +12,7 @@ const code = 'audienceNetwork'; const currency = 'USD'; const method = 'GET'; const url = 'https://an.facebook.com/v2/placementbid.json'; -const supportedMediaTypes = ['video']; +const supportedMediaTypes = ['banner', 'video']; const netRevenue = true; const hb_bidder = 'fan'; @@ -143,7 +143,7 @@ const buildRequests = bids => { }; const video = findIndex(adformats, isVideo); if (video !== -1) { - [search.playerwidth, search.playerheight] = sizes[video].split('x').map(Number) + [search.playerwidth, search.playerheight] = expandSize(sizes[video]); } const data = formatQS(search); @@ -162,51 +162,50 @@ const buildRequests = bids => { const interpretResponse = ({ body }, { adformats, requestIds, sizes }) => { const ttl = Number(config.getConfig().bidderTimeout); - return body.errors && body.errors.length - ? [] - : Object.keys(body.bids) - // extract Array of bid responses - .map(placementId => body.bids[placementId]) - // flatten - .reduce((a, b) => a.concat(b), []) - // transform to bidResponse - .map((bid, i) => { - const { - bid_id: fb_bidid, - placement_id: creativeId, - bid_price_cents: cpm - } = bid; - - const format = adformats[i]; - const [width, height] = expandSize(flattenSize(sizes[i])); - const ad = createAdHtml(creativeId, format, fb_bidid); - const requestId = requestIds[i]; - - const bidResponse = { - // Prebid attributes - requestId, - cpm: cpm / 100, - width, - height, - ad, - ttl, - creativeId, - netRevenue, - currency, - // Audience Network attributes - hb_bidder, - fb_bidid, - fb_format: format, - fb_placementid: creativeId - }; - // Video attributes - if (isVideo(format)) { - const pageurl = getTopWindowUrlEncoded(); - bidResponse.mediaType = 'video'; - bidResponse.vastUrl = `https://an.facebook.com/v1/instream/vast.xml?placementid=${creativeId}&pageurl=${pageurl}&playerwidth=${width}&playerheight=${height}&bidid=${fb_bidid}`; - } - return bidResponse; - }); + const { bids = {} } = body; + return Object.keys(bids) + // extract Array of bid responses + .map(placementId => bids[placementId]) + // flatten + .reduce((a, b) => a.concat(b), []) + // transform to bidResponse + .map((bid, i) => { + const { + bid_id: fb_bidid, + placement_id: creativeId, + bid_price_cents: cpm + } = bid; + + const format = adformats[i]; + const [width, height] = expandSize(flattenSize(sizes[i])); + const ad = createAdHtml(creativeId, format, fb_bidid); + const requestId = requestIds[i]; + + const bidResponse = { + // Prebid attributes + requestId, + cpm: cpm / 100, + width, + height, + ad, + ttl, + creativeId, + netRevenue, + currency, + // Audience Network attributes + hb_bidder, + fb_bidid, + fb_format: format, + fb_placementid: creativeId + }; + // Video attributes + if (isVideo(format)) { + const pageurl = getTopWindowUrlEncoded(); + bidResponse.mediaType = 'video'; + bidResponse.vastUrl = `https://an.facebook.com/v1/instream/vast.xml?placementid=${creativeId}&pageurl=${pageurl}&playerwidth=${width}&playerheight=${height}&bidid=${fb_bidid}`; + } + return bidResponse; + }); }; export const spec = { diff --git a/modules/c1xBidAdapter.js b/modules/c1xBidAdapter.js new file mode 100644 index 00000000000..af1de373985 --- /dev/null +++ b/modules/c1xBidAdapter.js @@ -0,0 +1,167 @@ +import { registerBidder } from 'src/adapters/bidderFactory'; +import * as utils from 'src/utils'; +import { userSync } from 'src/userSync'; + +const BIDDER_CODE = 'c1x'; +const URL = 'https://ht.c1exchange.com/ht'; +const PIXEL_ENDPOINT = '//px.c1exchange.com/pubpixel/'; +const LOG_MSG = { + invalidBid: 'C1X: [ERROR] bidder returns an invalid bid', + noSite: 'C1X: [ERROR] no site id supplied', + noBid: 'C1X: [INFO] creating a NO bid for Adunit: ', + bidWin: 'C1X: [INFO] creating a bid for Adunit: ' +}; + +/** + * Adapter for requesting bids from C1X header tag server. + * v3.0 (c) C1X Inc., 2017 + */ + +export const c1xAdapter = { + code: BIDDER_CODE, + + // check the bids sent to c1x bidder + isBidRequestValid: function(bid) { + const siteId = bid.params.siteId || ''; + if (!siteId) { + utils.logError(LOG_MSG.noSite); + } + return !!(bid.adUnitCode && siteId); + }, + + buildRequests: function(bidRequests) { + let payload = {}; + let tagObj = {}; + const adunits = bidRequests.length; + const rnd = new Date().getTime(); + const c1xTags = bidRequests.map(bidToTag); + const bidIdTags = bidRequests.map(bidToShortTag); // include only adUnitCode and bidId from request obj + + // flattened tags in a tag object + tagObj = c1xTags.reduce((current, next) => Object.assign(current, next)); + const pixelId = tagObj.pixelId; + const useSSL = document.location.protocol; + + payload = { + adunits: adunits.toString(), + rnd: rnd.toString(), + response: 'json', + compress: 'gzip' + } + Object.assign(payload, tagObj); + + let payloadString = stringifyPayload(payload); + + if (pixelId) { + const pixelUrl = (useSSL ? 'https:' : 'http:') + PIXEL_ENDPOINT + pixelId; + userSync.registerSync('image', BIDDER_CODE, pixelUrl); + } + + // ServerRequest object + return { + method: 'GET', + url: URL, + data: payloadString, + bids: bidIdTags + }; + }, + + interpretResponse: function(serverResponse, requests) { + serverResponse = serverResponse.body; + requests = requests.bids || []; + const currency = 'USD'; + const bidResponses = []; + let netRevenue = false; + + if (!serverResponse || serverResponse.error) { + let errorMessage = serverResponse.error; + utils.logError(LOG_MSG.invalidBid + errorMessage); + return bidResponses; + } else { + serverResponse.forEach(bid => { + if (bid.bid) { + if (bid.bidType === 'NET_BID') { + netRevenue = !netRevenue; + } + const curBid = { + width: bid.width, + height: bid.height, + cpm: bid.cpm, + ad: bid.ad, + creativeId: bid.crid, + currency: currency, + ttl: 300, + netRevenue: netRevenue + }; + + for (let i = 0; i < requests.length; i++) { + if (bid.adId === requests[i].adUnitCode) { + curBid.requestId = requests[i].bidId; + } + } + utils.logInfo(LOG_MSG.bidWin + bid.adId + ' size: ' + curBid.width + 'x' + curBid.height); + bidResponses.push(curBid); + } else { + // no bid + utils.logInfo(LOG_MSG.noBid + bid.adId); + } + }); + } + + return bidResponses; + } +} + +function bidToTag(bid, index) { + const tag = {}; + const adIndex = 'a' + (index + 1).toString(); // ad unit id for c1x + const sizeKey = adIndex + 's'; + const priceKey = adIndex + 'p'; + // TODO: Multiple Floor Prices + + const sizesArr = bid.sizes; + const floorPriceMap = bid.params.floorPriceMap || ''; + tag['site'] = bid.params.siteId || ''; + + // prevent pixelId becoming undefined when publishers don't fill this param in ad units they have on the same page + if (bid.params.pixelId) { + tag['pixelId'] = bid.params.pixelId + } + + tag[adIndex] = bid.adUnitCode; + tag[sizeKey] = sizesArr.reduce((prev, current) => prev + (prev === '' ? '' : ',') + current.join('x'), ''); + + const newSizeArr = tag[sizeKey].split(','); + if (floorPriceMap) { + newSizeArr.forEach(size => { + if (size in floorPriceMap) { + tag[priceKey] = floorPriceMap[size].toString(); + } // we only accept one cpm price in floorPriceMap + }); + } + if (bid.params.pageurl) { + tag['pageurl'] = bid.params.pageurl; + } + + return tag; +} + +function bidToShortTag(bid) { + const tag = {}; + tag.adUnitCode = bid.adUnitCode; + tag.bidId = bid.bidId; + + return tag; +} + +function stringifyPayload(payload) { + let payloadString = ''; + payloadString = JSON.stringify(payload).replace(/":"|","|{"|"}/g, (foundChar) => { + if (foundChar == '":"') return '='; + else if (foundChar == '","') return '&'; + else return ''; + }); + return payloadString; +} + +registerBidder(c1xAdapter); diff --git a/modules/c1xBidAdapter.md b/modules/c1xBidAdapter.md new file mode 100644 index 00000000000..83a4ff1ea81 --- /dev/null +++ b/modules/c1xBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +Module Name: C1X Bidder Adapter +Module Type: Bidder Adapter +Maintainer: cathy@c1exchange.com + +# Description + +Module that connects to C1X's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 600], [300, 250]], + bids: [ + { + bidder: 'c1x', + params: { + siteId: '9999', + pixelId: '12345', + floorPriceMap: { + '300x250': 0.20, + '300x600': 0.30 + }, //optional + } + } + ] + }, + ]; +``` \ No newline at end of file diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js new file mode 100644 index 00000000000..df011bc102d --- /dev/null +++ b/modules/colossussspBidAdapter.js @@ -0,0 +1,132 @@ +import { registerBidder } from 'src/adapters/bidderFactory'; +import * as utils from 'src/utils'; + +const BIDDER_CODE = 'colossusssp'; +const URL = '//colossusssp.com/?c=o&m=multi'; +const URL_SYNC = '//colossusssp.com/?c=o&m=cookie'; + +let sizeObj = { + '468x60': 1, + '728x90': 2, + '300x600': 10, + '300x250': 15, + '300x100': 19, + '320x50': 43, + '300x50': 44, + '300x300': 48, + '300x1050': 54, + '970x90': 55, + '970x250': 57, + '1000x90': 58, + '320x80': 59, + '640x480': 65, + '320x480': 67, + '320x320': 72, + '320x160': 73, + '480x300': 83, + '970x310': 94, + '970x210': 96, + '480x320': 101, + '768x1024': 102, + '1000x300': 113, + '320x100': 117, + '800x250': 118, + '200x600': 119 +}; + +utils._each(sizeObj, (item, key) => sizeObj[item] = key); + +export const spec = { + code: BIDDER_CODE, + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: (bid) => { + return (!isNaN(bid.params.placement_id) && + ((bid.params.sizes !== undefined && bid.params.sizes.length > 0 && bid.params.sizes.some((sizeIndex) => sizeObj[sizeIndex] !== undefined)) || + (bid.sizes !== undefined && bid.sizes.length > 0 && bid.sizes.map((size) => `${size[0]}x${size[1]}`).some((size) => sizeObj[size] !== undefined)))); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: (validBidRequests) => { + let winTop = window; + try { + window.top.location.toString(); + winTop = window.top; + } catch (e) { + utils.logMessage(e); + }; + let location = utils.getTopWindowLocation(); + let placements = []; + let request = { + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language : '', + 'secure': location.protocol === 'https:' ? 1 : 0, + 'host': location.host, + 'page': location.pathname, + 'placements': placements + }; + for (let i = 0; i < validBidRequests.length; i++) { + let bid = validBidRequests[i]; + let placement = {}; + placement['placementId'] = bid.params.placement_id; + placement['bidId'] = bid.bidId; + placement['sizes'] = bid.sizes; + placements.push(placement); + } + return { + method: 'POST', + url: URL, + data: request + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: (serverResponse) => { + let response = []; + try { + serverResponse = serverResponse.body; + for (let i = 0; i < serverResponse.length; i++) { + let resItem = serverResponse[i]; + if (resItem.width && !isNaN(resItem.width) && + resItem.height && !isNaN(resItem.height) && + resItem.requestId && typeof resItem.requestId === 'string' && + resItem.cpm && !isNaN(resItem.cpm) && + resItem.ad && typeof resItem.ad === 'string' && + resItem.ttl && !isNaN(resItem.ttl) && + resItem.creativeId && typeof resItem.creativeId === 'string' && + resItem.netRevenue && typeof resItem.netRevenue === 'boolean' && + resItem.currency && typeof resItem.currency === 'string') { + response.push(resItem); + } + } + } catch (e) { + utils.logMessage(e); + }; + return response; + }, + + getUserSyncs: () => { + return [{ + type: 'image', + url: URL_SYNC + }]; + } +}; + +registerBidder(spec); diff --git a/modules/colossussspBidAdapter.md b/modules/colossussspBidAdapter.md new file mode 100644 index 00000000000..9a5b9a0fe39 --- /dev/null +++ b/modules/colossussspBidAdapter.md @@ -0,0 +1,26 @@ +# Overview + +``` +Module Name: Colossus SSP Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@colossusmediallc.com +``` + +# Description + +Module that connects to Colossus SSP demand sources + +# Test Parameters +``` + var adUnits = [{ + code: 'placementid_0', + sizes: [[300, 250]], + bids: [{ + bidder: 'colossusssp', + params: { + placement_id: 0 + } + }] + } + ]; +``` diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index 1f6c4f27b77..cae64983089 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -1,6 +1,6 @@ import * as utils from 'src/utils'; import {registerBidder} from 'src/adapters/bidderFactory'; -import { VIDEO } from 'src/mediaTypes'; +import { BANNER, VIDEO } from 'src/mediaTypes'; const BIDDER_CODE = 'conversant'; const URL = '//media.msg.dotomi.com/s2s/header/24'; @@ -10,7 +10,7 @@ const VERSION = '2.2.1'; export const spec = { code: BIDDER_CODE, aliases: ['cnvr'], // short code - supportedMediaTypes: [VIDEO], + supportedMediaTypes: [BANNER, VIDEO], /** * Determines whether or not the given bid request is valid. diff --git a/modules/danmarketplaceBidAdapter.js b/modules/danmarketplaceBidAdapter.js new file mode 100644 index 00000000000..24b3682042e --- /dev/null +++ b/modules/danmarketplaceBidAdapter.js @@ -0,0 +1,143 @@ +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; +const BIDDER_CODE = 'danmarketplace'; +const ENDPOINT_URL = '//ads.danmarketplace.com/hb'; +const TIME_TO_LIVE = 360; +const ADAPTER_SYNC_URL = '//ads.danmarketplace.com/push_sync'; +const LOG_ERROR_MESS = { + noAuid: 'Bid from response has no auid parameter - ', + noAdm: 'Bid from response has no adm parameter - ', + noBid: 'Array of bid objects is empty', + noPlacementCode: 'Can\'t find in requested bids the bid with auid - ', + emptyUids: 'Uids should be not empty', + emptySeatbid: 'Seatbid array from response has empty item', + emptyResponse: 'Response is empty', + hasEmptySeatbidArray: 'Response has empty seatbid array', + hasNoArrayOfBids: 'Seatbid from response has no array of bid objects - ' +}; + +/** + * Dentsu Aegis Network Marketplace Bid Adapter. + * Contact: niels@baarsma.net + * + */ +export const spec = { + code: BIDDER_CODE, + + aliases: ['DANMarketplace', 'DAN_Marketplace'], + + isBidRequestValid: function(bid) { + return !!bid.params.uid; + }, + + buildRequests: function(validBidRequests) { + const auids = []; + const bidsMap = {}; + const bids = validBidRequests || []; + let priceType = 'net'; + let reqId; + + bids.forEach(bid => { + if (bid.params.priceType === 'gross') { + priceType = 'gross'; + } + if (!bidsMap[bid.params.uid]) { + bidsMap[bid.params.uid] = [bid]; + auids.push(bid.params.uid); + } else { + bidsMap[bid.params.uid].push(bid); + } + reqId = bid.bidderRequestId; + }); + + const payload = { + u: utils.getTopWindowUrl(), + pt: priceType, + auids: auids.join(','), + r: reqId, + }; + + return { + method: 'GET', + url: ENDPOINT_URL, + data: payload, + bidsMap: bidsMap, + }; + }, + + interpretResponse: function(serverResponse, bidRequest) { + serverResponse = serverResponse && serverResponse.body + const bidResponses = []; + const bidsMap = bidRequest.bidsMap; + const priceType = bidRequest.data.pt; + + let errorMessage; + + if (!serverResponse) errorMessage = LOG_ERROR_MESS.emptyResponse; + else if (serverResponse.seatbid && !serverResponse.seatbid.length) { + errorMessage = LOG_ERROR_MESS.hasEmptySeatbidArray; + } + + if (!errorMessage && serverResponse.seatbid) { + serverResponse.seatbid.forEach(respItem => { + _addBidResponse(_getBidFromResponse(respItem), bidsMap, priceType, bidResponses); + }); + } + if (errorMessage) utils.logError(errorMessage); + return bidResponses; + }, + + getUserSyncs: function(syncOptions) { + if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: ADAPTER_SYNC_URL + }]; + } + } +} + +function _getBidFromResponse(respItem) { + if (!respItem) { + utils.logError(LOG_ERROR_MESS.emptySeatbid); + } else if (!respItem.bid) { + utils.logError(LOG_ERROR_MESS.hasNoArrayOfBids + JSON.stringify(respItem)); + } else if (!respItem.bid[0]) { + utils.logError(LOG_ERROR_MESS.noBid); + } + return respItem && respItem.bid && respItem.bid[0]; +} + +function _addBidResponse(serverBid, bidsMap, priceType, bidResponses) { + if (!serverBid) return; + let errorMessage; + if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid); + if (!serverBid.adm) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); + else { + const awaitingBids = bidsMap[serverBid.auid]; + if (awaitingBids) { + awaitingBids.forEach(bid => { + const bidResponse = { + requestId: bid.bidId, // bid.bidderRequestId, + cpm: serverBid.price, + width: serverBid.w, + height: serverBid.h, + creativeId: serverBid.auid, // bid.bidId, + currency: 'USD', + netRevenue: priceType !== 'gross', + ttl: TIME_TO_LIVE, + ad: serverBid.adm, + dealId: serverBid.dealid + }; + bidResponses.push(bidResponse); + }); + } else { + errorMessage = LOG_ERROR_MESS.noPlacementCode + serverBid.auid; + } + } + if (errorMessage) { + utils.logError(errorMessage); + } +} + +registerBidder(spec); diff --git a/modules/danmarketplaceBidAdapter.md b/modules/danmarketplaceBidAdapter.md new file mode 100755 index 00000000000..263385949bd --- /dev/null +++ b/modules/danmarketplaceBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +Module Name: Dentsu Aegis Network Marketplace Bidder Adapter +Module Type: Bidder Adapter +Maintainer: niels@baarsma.net + +# Description + +Module that connects to DAN Marketplace demand source to fetch bids. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "danmarketplace", + params: { + uid: '4', + priceType: 'gross' // by default is 'net' + } + } + ] + },{ + code: 'test-div', + sizes: [[728, 90]], + bids: [ + { + bidder: "danmarketplace", + params: { + uid: 5, + priceType: 'gross' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js new file mode 100644 index 00000000000..dfc5f514cf3 --- /dev/null +++ b/modules/eplanningBidAdapter.js @@ -0,0 +1,156 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'eplanning'; +const rnd = Math.random(); +const DEFAULT_SV = 'ads.us.e-planning.net'; +const DEFAULT_ISV = 'i.e-planning.net'; +const PARAMS = ['ci', 'sv', 't']; +const DOLLARS = 'USD'; +const NET_REVENUE = true; +const TTL = 120; +const NULL_SIZE = '1x1'; +const FILE = 'file'; + +export const spec = { + code: BIDDER_CODE, + isBidRequestValid: function(bid) { + return Boolean(bid.params.ci) || Boolean(bid.params.t); + }, + buildRequests: function(bidRequests) { + const method = 'GET'; + const dfpClientId = '1'; + const sec = 'ROS'; + let url; + let params; + const urlConfig = getUrlConfig(bidRequests); + + if (urlConfig.t) { + url = urlConfig.isv + '/layers/t_pbjs_2.json'; + params = {}; + } else { + url = '//' + (urlConfig.sv || DEFAULT_SV) + '/hb/1/' + urlConfig.ci + '/' + dfpClientId + '/' + (utils.getTopWindowLocation().hostname || FILE) + '/' + sec; + const referrerUrl = utils.getTopWindowReferrer(); + const spacesString = getSpacesString(bidRequests); + params = { + rnd: rnd, + e: spacesString, + ur: utils.getTopWindowUrl() || FILE, + r: 'pbjs', + pbv: '$prebid.version$', + ncb: '1' + }; + if (referrerUrl) { + params.fr = referrerUrl; + } + } + + return { + method: method, + url: url, + data: params, + adUnitToBidId: getBidIdMap(bidRequests), + }; + }, + interpretResponse: function(serverResponse, request) { + const response = serverResponse.body; + let bidResponses = []; + + if (response && !utils.isEmpty(response.sp)) { + response.sp.forEach(space => { + if (!utils.isEmpty(space.a)) { + space.a.forEach(ad => { + const bidResponse = { + requestId: request.adUnitToBidId[space.k], + cpm: ad.pr, + width: ad.w, + height: ad.h, + ad: ad.adm, + ttl: TTL, + creativeId: ad.crid, + netRevenue: NET_REVENUE, + currency: DOLLARS, + }; + bidResponses.push(bidResponse); + }); + } + }); + } + + return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + const response = !utils.isEmpty(serverResponses) && serverResponses[0].body; + + if (response && !utils.isEmpty(response.cs)) { + const responseSyncs = response.cs; + responseSyncs.forEach(sync => { + if (typeof sync === 'string' && syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: sync, + }); + } else if (typeof sync === 'object' && sync.ifr && syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: sync.u, + }) + } + }); + } + + return syncs; + }, +} + +function cleanName(name) { + return name.replace(/_|\.|-|\//g, '').replace(/\)\(|\(|\)/g, '_').replace(/^_+|_+$/g, ''); +} +function getUrlConfig(bidRequests) { + if (isTestRequest(bidRequests)) { + return getTestConfig(bidRequests.filter(br => br.params.t)); + } + + let config = {}; + bidRequests.forEach(bid => { + PARAMS.forEach(param => { + if (bid.params[param] && !config[param]) { + config[param] = bid.params[param]; + } + }); + }); + + if (config.sv) { + config.sv = '//' + config.sv; + } + + return config; +} +function isTestRequest(bidRequests) { + let isTest = false; + bidRequests.forEach(bid => isTest = bid.params.t); + return isTest; +} +function getTestConfig(bidRequests) { + let isv; + bidRequests.forEach(br => isv = isv || br.params.isv); + return { + t: true, + isv: '//' + (isv || DEFAULT_ISV) + }; +} +function getSpacesString(bids) { + const spacesString = bids.map(bid => + cleanName(bid.adUnitCode) + ':' + (bid.sizes && bid.sizes.length ? utils.parseSizesInput(bid.sizes).join(',') : NULL_SIZE) + ).join('+'); + + return spacesString; +} +function getBidIdMap(bidRequests) { + let map = {}; + bidRequests.forEach(bid => map[cleanName(bid.adUnitCode)] = bid.bidId); + return map; +} + +registerBidder(spec); diff --git a/modules/eplanningBidAdapter.md b/modules/eplanningBidAdapter.md new file mode 100644 index 00000000000..b6cfbb535b6 --- /dev/null +++ b/modules/eplanningBidAdapter.md @@ -0,0 +1,25 @@ +# Overview + +``` +Module Name: E-Planning Bid Adapter +Module Type: Bidder Adapter +Maintainer: ainsua@e-planning.net +``` + +# Description + +Connects to E-Planning exchange for bids. + +# Test Parameters +``` +var adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + sizes: [[300, 250]], + bids: [{ + bidder: 'eplanning', + params: { + t: 1 + } + }] +}]; +``` diff --git a/modules/freewheelSSPBidAdapter.js b/modules/freewheelSSPBidAdapter.js index 64ebb36478b..632df8fe93c 100644 --- a/modules/freewheelSSPBidAdapter.js +++ b/modules/freewheelSSPBidAdapter.js @@ -190,7 +190,7 @@ var getOutstreamScript = function(bid) { export const spec = { code: BIDDER_CODE, - supportedMediaTypes: ['video'], + supportedMediaTypes: ['banner', 'video'], aliases: ['stickyadstv'], // former name for freewheel-ssp /** * Determines whether or not the given bid request is valid. diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js new file mode 100644 index 00000000000..2627b7417f5 --- /dev/null +++ b/modules/gumgumBidAdapter.js @@ -0,0 +1,170 @@ +import * as utils from 'src/utils' + +import { config } from 'src/config' +import { registerBidder } from 'src/adapters/bidderFactory' + +const BIDDER_CODE = 'gumgum' +const ALIAS_BIDDER_CODE = ['gg'] +const BID_ENDPOINT = `https://g2.gumgum.com/hbid/imp` +const DT_CREDENTIALS = { member: 'YcXr87z2lpbB' } +const TIME_TO_LIVE = 60 +let browserParams = {}; + +// TODO: potential 0 values for browserParams sent to ad server +function _getBrowserParams() { + let topWindow + let topScreen + if (browserParams.vw) { + // we've already initialized browserParams, just return it. + return browserParams + } + + try { + topWindow = global.top; + topScreen = topWindow.screen; + } catch (error) { + utils.logError(error); + return browserParams + } + + browserParams = { + vw: topWindow.innerWidth, + vh: topWindow.innerHeight, + sw: topScreen.width, + sh: topScreen.height, + pu: utils.getTopWindowUrl(), + ce: utils.cookiesAreEnabled(), + dpr: topWindow.devicePixelRatio || 1 + } + return browserParams +} + +function getWrapperCode(wrapper, data) { + return wrapper.replace('AD_JSON', window.btoa(JSON.stringify(data))) +} + +// TODO: use getConfig() +function _getDigiTrustQueryParams() { + function getDigiTrustId () { + var digiTrustUser = (window.DigiTrust && window.DigiTrust.getUser) ? window.DigiTrust.getUser(DT_CREDENTIALS) : {}; + return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || ''; + }; + + 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': digiTrustId.id + }; +} + +/** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ +function isBidRequestValid (bid) { + const { + params, + adUnitCode + } = bid; + + switch (true) { + case !!(params.inScreen): break; + case !!(params.inSlot): break; + default: + utils.logWarn(`[GumGum] No product selected for the placement ${adUnitCode}, please check your implementation.`); + return false; + } + return true; +} + +/** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ +function buildRequests (validBidRequests) { + const bids = []; + utils._each(validBidRequests, bidRequest => { + const timeout = config.getConfig('bidderTimeout'); + const { + bidId, + params = {}, + transactionId + } = bidRequest; + const data = {} + + if (params.inScreen) { + data.t = params.inScreen; + data.pi = 2; + } + if (params.inSlot) { + data.si = parseInt(params.inSlot, 10); + data.pi = 3; + } + + bids.push({ + id: bidId, + tmax: timeout, + tId: transactionId, + pi: data.pi, + sizes: bidRequest.sizes, + url: BID_ENDPOINT, + method: 'GET', + data: Object.assign(data, _getBrowserParams(), _getDigiTrustQueryParams()) + }) + }); + return bids; +} + +/** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function interpretResponse (serverResponse, bidRequest) { + const bidResponses = [] + const serverResponseBody = serverResponse.body + const { + ad: { + price: cpm, + id: creativeId, + markup + }, + cw: wrapper + } = serverResponseBody + let isTestUnit = (bidRequest.data && bidRequest.data.pi === 3 && bidRequest.data.si === 9) + let [width, height] = utils.parseSizesInput(bidRequest.sizes)[0].split('x') + + if (creativeId) { + bidResponses.push({ + // dealId: DEAL_ID, + // referrer: REFERER, + ad: wrapper ? getWrapperCode(wrapper, Object.assign({}, serverResponseBody, { bidRequest })) : markup, + cpm: isTestUnit ? 0.1 : cpm, + creativeId, + currency: 'USD', + height, + netRevenue: true, + requestId: bidRequest.id, + ttl: TIME_TO_LIVE, + width + }) + } + return bidResponses +} + +export const spec = { + code: BIDDER_CODE, + aliases: ALIAS_BIDDER_CODE, + isBidRequestValid, + buildRequests, + interpretResponse +} +registerBidder(spec) diff --git a/modules/gumgumBidAdapter.md b/modules/gumgumBidAdapter.md new file mode 100644 index 00000000000..500c2a49e3b --- /dev/null +++ b/modules/gumgumBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +``` +Module Name: GumGum Bidder Adapter +Module Type: Bidder Adapter +Maintainer: engineering@gumgum.com +``` + +# Description + +GumGum adapter for Prebid.js 1.0 + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: 'gumgum', + params: { + inSlot: '9' // GumGum Slot ID given to the client + } + } + ] + },{ + code: 'test-div', + sizes: [[300, 50]], + bids: [ + { + bidder: 'gumgum', + params: { + inScreen: 'ggumtest' // GumGum Zone ID given to the client + } + } + ] + } +]; +``` diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 82c045b9db6..2dcdcb2a808 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -1,11 +1,12 @@ import * as utils from 'src/utils'; import { registerBidder } from 'src/adapters/bidderFactory'; +import { config } from 'src/config'; import { userSync } from 'src/userSync'; const BIDDER_CODE = 'improvedigital'; export const spec = { - version: '4.0.0', + version: '4.1.0', code: BIDDER_CODE, aliases: ['id'], @@ -113,6 +114,7 @@ function getNormalizedBidRequest(bid) { let localSize = utils.getBidIdParameter('size', bid.params) || null; let bidId = utils.getBidIdParameter('bidId', bid); let transactionId = utils.getBidIdParameter('transactionId', bid); + const currency = config.getConfig('currency.adServerCurrency'); let normalizedBidRequest = {}; if (placementId) { @@ -143,6 +145,9 @@ function getNormalizedBidRequest(bid) { if (transactionId) { normalizedBidRequest.transactionId = transactionId; } + if (currency) { + normalizedBidRequest.currency = currency; + } return normalizedBidRequest; } registerBidder(spec); @@ -160,7 +165,7 @@ function ImproveDigitalAdServerJSClient(endPoint) { AD_SERVER_BASE_URL: 'ad.360yield.com', END_POINT: endPoint || 'hb', AD_SERVER_URL_PARAM: 'jsonp=', - CLIENT_VERSION: 'JS-4.2.0', + CLIENT_VERSION: 'JS-4.3.3', MAX_URL_LENGTH: 2083, ERROR_CODES: { BAD_HTTP_REQUEST_TYPE_PARAM: 1, @@ -337,6 +342,9 @@ function ImproveDigitalAdServerJSClient(endPoint) { if (placementObject.adUnitId) { outputObject.adUnitId = placementObject.adUnitId; } + if (placementObject.currency) { + impressionObject.currency = placementObject.currency.toUpperCase(); + } if (placementObject.placementId) { impressionObject.pid = placementObject.placementId; } diff --git a/modules/improvedigitalBidAdapter.md b/modules/improvedigitalBidAdapter.md index 3d91d4f82f2..d70b624171f 100644 --- a/modules/improvedigitalBidAdapter.md +++ b/modules/improvedigitalBidAdapter.md @@ -12,6 +12,7 @@ Module that connects to Improve Digital's demand sources ``` var adUnits = [{ code: 'div-gpt-ad-1499748733608-0', + sizes: [[600, 290]], bids: [ { bidder: 'improvedigital', @@ -22,6 +23,7 @@ Module that connects to Improve Digital's demand sources ] }, { code: 'div-gpt-ad-1499748833901-0', + sizes: [[250, 250]], bids: [{ bidder: 'improvedigital', params: { @@ -33,6 +35,7 @@ Module that connects to Improve Digital's demand sources }] }, { code: 'div-gpt-ad-1499748913322-0', + sizes: [[300, 300]], bids: [{ bidder: 'improvedigital', params: { diff --git a/modules/inskinBidAdapter.js b/modules/inskinBidAdapter.js new file mode 100644 index 00000000000..5274b5b2ba8 --- /dev/null +++ b/modules/inskinBidAdapter.js @@ -0,0 +1,210 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'inskin'; + +const CONFIG = { + 'inskin': { + 'BASE_URI': 'https://mfad.inskinad.com/api/v2' + } +}; + +export const spec = { + code: BIDDER_CODE, + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!(bid.params.networkId && bid.params.siteId); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + + buildRequests: function(validBidRequests) { + // Do we need to group by bidder? i.e. to make multiple requests for + // different endpoints. + + let ret = { + method: 'POST', + url: '', + data: '', + bidRequest: [] + }; + + if (validBidRequests.length < 1) { + return ret; + } + + let ENDPOINT_URL; + + const data = Object.assign({ + placements: [], + time: Date.now(), + user: {}, + url: utils.getTopWindowUrl(), + referrer: document.referrer, + enableBotFiltering: true, + includePricingData: true, + parallel: true + }, validBidRequests[0].params); + + validBidRequests.map(bid => { + let config = CONFIG[bid.bidder]; + ENDPOINT_URL = config.BASE_URI; + + const placement = Object.assign({ + divName: bid.bidId, + adTypes: bid.adTypes || getSize(bid.sizes), + eventIds: [40, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295] + }, bid.params); + + placement.adTypes.push(5, 9, 163, 2163, 3006); + + if (placement.networkId && placement.siteId) { + data.placements.push(placement); + } + }); + + ret.data = JSON.stringify(data); + ret.bidRequest = validBidRequests; + ret.url = ENDPOINT_URL; + + return ret; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + let bid; + let bids; + let bidId; + let bidObj; + let bidResponses = []; + let bidsMap = {}; + + bids = bidRequest.bidRequest; + + serverResponse = (serverResponse || {}).body; + for (let i = 0; i < bids.length; i++) { + bid = {}; + bidObj = bids[i]; + bidId = bidObj.bidId; + + bidsMap[bidId] = bidObj; + + if (serverResponse) { + const decision = serverResponse.decisions && serverResponse.decisions[bidId]; + const price = decision && decision.pricing && decision.pricing.clearPrice; + + if (decision && price) { + bid.requestId = bidId; + bid.cpm = price; + bid.width = decision.width; + bid.height = decision.height; + bid.ad = retrieveAd(bidId, decision); + bid.currency = 'USD'; + bid.creativeId = decision.adId; + bid.ttl = 360; + bid.netRevenue = true; + bid.referrer = utils.getTopWindowUrl(); + + bidResponses.push(bid); + } + } + } + + if (bidResponses.length) { + window.addEventListener('message', function(e) { + if (!e.data || e.data.from !== 'ism-bid') { + return; + } + + const decision = serverResponse.decisions && serverResponse.decisions[e.data.bidId]; + if (!decision) { + return; + } + + const id = 'ism_tag_' + Math.floor((Math.random() * 10e16)); + window[id] = { + bidId: e.data.bidId, + serverResponse + }; + const script = document.createElement('script'); + script.src = 'https://cdn.inskinad.com/isfe/publishercode/' + bidsMap[e.data.bidId].params.siteId + '/default.js?autoload&id=' + id; + document.getElementsByTagName('head')[0].appendChild(script); + }); + } + + return bidResponses; + }, + + getUserSyncs: function(syncOptions) { + return []; + } +}; + +const sizeMap = [ + null, + '120x90', + '120x90', + '468x60', + '728x90', + '300x250', + '160x600', + '120x600', + '300x100', + '180x150', + '336x280', + '240x400', + '234x60', + '88x31', + '120x60', + '120x240', + '125x125', + '220x250', + '250x250', + '250x90', + '0x0', + '200x90', + '300x50', + '320x50', + '320x480', + '185x185', + '620x45', + '300x125', + '800x250' +]; + +sizeMap[77] = '970x90'; +sizeMap[123] = '970x250'; +sizeMap[43] = '300x600'; + +function getSize(sizes) { + const result = []; + sizes.forEach(function(size) { + const index = sizeMap.indexOf(size[0] + 'x' + size[1]); + if (index >= 0) { + result.push(index); + } + }); + return result; +} + +function retrieveAd(bidId, decision) { + return " + +``` \ No newline at end of file diff --git a/modules/nanointeractiveBidAdapter.js b/modules/nanointeractiveBidAdapter.js index c4cce6646d3..225859a4360 100644 --- a/modules/nanointeractiveBidAdapter.js +++ b/modules/nanointeractiveBidAdapter.js @@ -3,28 +3,21 @@ import { registerBidder } from '../src/adapters/bidderFactory'; import { BANNER } from '../src/mediaTypes'; export const BIDDER_CODE = 'nanointeractive'; -export const ENGINE_BASE_URL = 'http://tmp.audiencemanager.de/hb'; +export const ENGINE_BASE_URL = 'https://www.audiencemanager.de/hb'; -export const SECURITY = 'sec'; -export const DATA_PARTNER_ID = 'dpid'; export const DATA_PARTNER_PIXEL_ID = 'pid'; -export const ALG = 'alg'; export const NQ = 'nq'; export const NQ_NAME = 'name'; export const CATEGORY = 'category'; -const DEFAULT_ALG = 'ihr'; - export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], isBidRequestValid(bid) { - const sec = bid.params[SECURITY]; - const dpid = bid.params[DATA_PARTNER_ID]; const pid = bid.params[DATA_PARTNER_PIXEL_ID]; - return !!(sec && dpid && pid); + return !!(pid); }, buildRequests(bidRequests) { let payload = []; @@ -37,7 +30,7 @@ export const spec = { }, interpretResponse(serverResponse) { const bids = []; - serverResponse.forEach(serverBid => { + serverResponse.body.forEach(serverBid => { if (isEngineResponseValid(serverBid)) { bids.push(createSingleBidResponse(serverBid)); } @@ -48,10 +41,7 @@ export const spec = { function createSingleBidRequest(bid) { return { - [SECURITY]: bid.params[SECURITY], - [DATA_PARTNER_ID]: bid.params[DATA_PARTNER_ID], [DATA_PARTNER_PIXEL_ID]: bid.params[DATA_PARTNER_PIXEL_ID], - [ALG]: bid.params[ALG] || DEFAULT_ALG, [NQ]: [createNqParam(bid), createCategoryParam(bid)], sizes: bid.sizes.map(value => value[0] + 'x' + value[1]), bidId: bid.bidId, diff --git a/modules/nanointeractiveBidAdapter.md b/modules/nanointeractiveBidAdapter.md index 0159353750a..0df49999492 100644 --- a/modules/nanointeractiveBidAdapter.md +++ b/modules/nanointeractiveBidAdapter.md @@ -31,8 +31,6 @@ var adUnits = [ bids: [{ bidder: 'nanointeractive', params: { - sec: '04a0cb7fb9ac02840f7f33d68a883780', - dpid: '58bfec94eb0a1916fa380162', pid: '58bfec94eb0a1916fa380163' } }] @@ -44,8 +42,6 @@ var adUnits = [ bids: [{ bidder: 'nanointeractive', params: { - sec: '04a0cb7fb9ac02840f7f33d68a883780', - dpid: '58bfec94eb0a1916fa380162', pid: '58bfec94eb0a1916fa380163', nq: 'user search' } @@ -58,8 +54,6 @@ var adUnits = [ bids: [{ bidder: 'nanointeractive', params: { - sec: '04a0cb7fb9ac02840f7f33d68a883780', - dpid: '58bfec94eb0a1916fa380162', pid: '58bfec94eb0a1916fa380163', name: 'search' } diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 051f9ace2de..9ed4f1250f8 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -30,15 +30,21 @@ export const spec = { let requests = []; let bannerRequests = []; let videoRequests = []; - let bannerBids = []; - let videoBids = []; - bids.forEach(function (bid) { - if (bid.mediaType === VIDEO) { - videoBids.push(bid); - } else { - bannerBids.push(bid); + const {bannerBids, videoBids} = bids.reduce(function(acc, curBid) { + // Fallback to banner ads if nothing specified + if (!curBid.mediaTypes || utils.isEmpty(curBid.mediaTypes)) { + if (curBid.mediaType && curBid.mediaType == VIDEO) { + acc.videoBids.push(curBid); + } else { + acc.bannerBids.push(curBid); + } + } else if (curBid.mediaTypes.video) { + acc.videoBids.push(curBid); + } else if (curBid.mediaTypes.banner) { + acc.bannerBids.push(curBid); } - }); + return acc; + }, {bannerBids: [], videoBids: []}); // build banner requests if (bannerBids.length !== 0) { diff --git a/modules/openxBidAdapter.md b/modules/openxBidAdapter.md index 1f04c2fe466..9e9d3ebfa7a 100644 --- a/modules/openxBidAdapter.md +++ b/modules/openxBidAdapter.md @@ -16,13 +16,13 @@ Module that connects to OpenX's demand sources { code: 'test-div', sizes: [[728, 90]], // a display size - mediaType: 'banner', + mediaTypes: {'banner': {}}, bids: [ { - bidder: "openx", + bidder: 'openx', params: { - unit: "539439964", - delDomain: "se-demo-d.openx.net" + unit: '539439964', + delDomain: 'se-demo-d.openx.net' } } ] @@ -30,7 +30,7 @@ Module that connects to OpenX's demand sources { code: 'video1', sizes: [[640,480]], - mediaType: 'video', + mediaTypes: {'video': {}}, bids: [ { bidder: 'openx', diff --git a/modules/optimaticBidAdapter.js b/modules/optimaticBidAdapter.js index 2408eb8cefe..f406fe7828d 100644 --- a/modules/optimaticBidAdapter.js +++ b/modules/optimaticBidAdapter.js @@ -3,6 +3,8 @@ import { registerBidder } from 'src/adapters/bidderFactory'; export const ENDPOINT = '//mg-bid.optimatic.com/adrequest/'; export const spec = { + version: '1.0.4', + code: 'optimatic', supportedMediaTypes: ['video'], @@ -33,7 +35,7 @@ export const spec = { } catch (e) { response = null; } - if (!response || !bid || !bid.adm || !bid.price) { + if (!response || !bid || (!bid.adm && !bid.nurl) || !bid.price) { utils.logWarn(`No valid bids from ${spec.code} bidder`); return []; } @@ -43,7 +45,6 @@ export const spec = { bidderCode: spec.code, cpm: bid.price, creativeId: bid.id, - vastXml: bid.adm, width: size.width, height: size.height, mediaType: 'video', @@ -51,6 +52,11 @@ export const spec = { ttl: 300, netRevenue: true }; + if (bid.nurl) { + bidResponse.vastUrl = bid.nurl; + } else if (bid.adm) { + bidResponse.vastXml = bid.adm; + } return bidResponse; } }; diff --git a/modules/optimeraBidAdapter.js b/modules/optimeraBidAdapter.js new file mode 100644 index 00000000000..fc3c4c96f1b --- /dev/null +++ b/modules/optimeraBidAdapter.js @@ -0,0 +1,84 @@ +import {registerBidder} from 'src/adapters/bidderFactory'; +const BIDDER_CODE = 'optimera'; +const SCORES_BASE_URL = 'https://s3.amazonaws.com/elasticbeanstalk-us-east-1-397719490216/json/client/'; + +export const spec = { + code: BIDDER_CODE, + /** + * Determines whether or not the given bid request is valid. + * + * @param {bidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bidRequest) { + if (typeof bidRequest.params !== 'undefined' && typeof bidRequest.params.clientID !== 'undefined') { + return true; + } else { + return false; + } + }, + /** + * Make a server request from the list of BidRequests. + * + * We call the existing scores data file for ad slot placement scores. + * These scores will be added to the dealId to be pushed to DFP. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests) { + let optimeraHost = window.location.host; + let optimeraPathName = window.location.pathname; + let timestamp = Math.round(new Date().getTime() / 1000); + if (typeof validBidRequests[0].params.clientID !== 'undefined') { + let clientID = validBidRequests[0].params.clientID; + let scoresURL = SCORES_BASE_URL + clientID + '/' + optimeraHost + optimeraPathName + '.js'; + return { + method: 'GET', + url: scoresURL, + payload: validBidRequests, + data: {'t': timestamp} + }; + } + }, + /** + * Unpack the response from the server into a list of bids. + * + * Some required bid params are not needed for this so default + * values are used. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + let validBids = bidRequest.payload; + let bidResponses = []; + let dealId = ''; + if (typeof serverResponse.body !== 'undefined') { + let scores = serverResponse.body; + for (let i = 0; i < validBids.length; i++) { + if (typeof validBids[i].params.clientID !== 'undefined') { + if (validBids[i].adUnitCode in scores) { + dealId = scores[validBids[i].adUnitCode]; + } + let bidResponse = { + requestId: validBids[i].bidId, + ad: '
', + cpm: 0.01, + width: 0, + height: 0, + dealId: dealId, + ttl: 300, + creativeId: '1', + netRevenue: '0', + currency: 'USD' + }; + bidResponses.push(bidResponse); + } + } + } + return bidResponses; + } +} + +registerBidder(spec); diff --git a/modules/optimeraBidAdapter.md b/modules/optimeraBidAdapter.md new file mode 100644 index 00000000000..8fca42fdac0 --- /dev/null +++ b/modules/optimeraBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: Optimera Bidder Adapter +Module Type: Bidder Adapter +Maintainer: kcandiotti@optimera.nyc +``` + +# Description + +Module that adds ad placement visibility scores for DFP. + +# Test Parameters +``` + var adUnits = [{ + code: 'div-1', + sizes: [[300, 250], [300,600]], + bids: [ + { + bidder: 'optimera', + params: { + clientID: '0' + } + }] + },{ + code: 'div-0', + sizes: [[728, 90]], + bids: [ + { + bidder: 'optimera', + params: { + clientID: '0' + } + }] + }]; +``` diff --git a/modules/peak226BidAdapter.js b/modules/peak226BidAdapter.js new file mode 100644 index 00000000000..4f4ee2f97ff --- /dev/null +++ b/modules/peak226BidAdapter.js @@ -0,0 +1,97 @@ +import { registerBidder } from 'src/adapters/bidderFactory'; +import { BANNER } from 'src/mediaTypes'; +import { getTopWindowUrl, logWarn } from 'src/utils'; + +const BIDDER_CODE = 'peak226'; +const URL = '//a.ad216.com/header_bid'; + +export const spec = { + code: BIDDER_CODE, + + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + const { params } = bid; + + return !!params.uid; + }, + + buildRequests: function (validBidRequests) { + const bidsMap = validBidRequests.reduce((res, bid) => { + const { uid } = bid.params; + + res[uid] = res[uid] || []; + res[uid].push(bid); + + return res; + }, {}); + + return { + method: 'GET', + url: + URL + + toQueryString({ + u: getTopWindowUrl(), + auids: Object.keys(bidsMap).join(',') + }), + bidsMap + }; + }, + + interpretResponse: function (serverResponse, { bidsMap }) { + const response = serverResponse.body; + const bidResponses = []; + + if (!response) { + logWarn(`No response from ${spec.code} bidder`); + + return bidResponses; + } + + if (!response.seatbid || !response.seatbid.length) { + logWarn(`No seatbid in response from ${spec.code} bidder`); + + return bidResponses; + } + + response.seatbid.forEach((seatbid, i) => { + if (!seatbid.bid || !seatbid.bid.length) { + logWarn(`No bid in seatbid[${i}] response from ${spec.code} bidder`); + return; + } + seatbid.bid.forEach(responseBid => { + const requestBids = bidsMap[responseBid.auid]; + + requestBids.forEach(requestBid => { + bidResponses.push({ + requestId: requestBid.bidId, + bidderCode: spec.code, + width: responseBid.w, + height: responseBid.h, + mediaType: BANNER, + creativeId: responseBid.auid, + ad: responseBid.adm, + cpm: responseBid.price, + currency: 'USD', + netRevenue: true, + ttl: 360 + }); + }); + }); + }); + + return bidResponses; + } +}; + +function toQueryString(obj) { + return Object.keys(obj).reduce( + (str, key, i) => + typeof obj[key] === 'undefined' || obj[key] === '' + ? str + : `${str}${str ? '&' : '?'}${key}=${encodeURIComponent(obj[key])}`, + '' + ); +} + +registerBidder(spec); diff --git a/modules/peak226BidAdapter.md b/modules/peak226BidAdapter.md new file mode 100644 index 00000000000..bae15d6c99f --- /dev/null +++ b/modules/peak226BidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +``` +Module Name: Peak226 Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@edge226.com +``` + +# Description + +Module that connects to Peak226's demand sources + +# Test Parameters + +``` + var adUnits = [ + { + code: "test-div", + sizes: [[300, 250]], + mediaType: "banner", + bids: [ + { + bidder: "peak226", + params: { + uid: 76131369 + } + } + ] + } + ]; +``` diff --git a/modules/platformioBidAdapter.js b/modules/platformioBidAdapter.js index fa841dc6026..93907c9bb1a 100644 --- a/modules/platformioBidAdapter.js +++ b/modules/platformioBidAdapter.js @@ -1,10 +1,19 @@ -import {logError, getTopWindowLocation} from 'src/utils'; +import {logError, getTopWindowLocation, getTopWindowReferrer} from 'src/utils'; 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, +}; + export const spec = { code: 'platformio', + supportedMediaTypes: ['native'], isBidRequestValid: bid => ( !!(bid && bid.params && bid.params.pubId && bid.params.siteId) @@ -27,6 +36,7 @@ export const spec = { bidResponseAvailable(request, response.body) ), }; + function bidResponseAvailable(bidRequest, bidResponse) { const idToImpMap = {}; const idToBidMap = {}; @@ -44,19 +54,31 @@ function bidResponseAvailable(bidRequest, bidResponse) { if (idToBidMap[id]) { const bid = {}; bid.requestId = id; - bid.creativeId = idToBidMap[id].adid; + bid.adId = id; + bid.creativeId = id; bid.cpm = idToBidMap[id].price; bid.currency = bidResponse.cur; bid.ttl = 360; bid.netRevenue = true; - bid.ad = idToBidMap[id].adm; - bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); - bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_AD_ID(%7D|\})/gi, idToBidMap[id].adid); - bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); - bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); - bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); - bid.width = idToImpMap[id].banner.w; - bid.height = idToImpMap[id].banner.h; + if (idToImpMap[id]['native']) { + bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); + let nurl = idToBidMap[id].nurl; + nurl = nurl.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); + nurl = nurl.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); + bid['native']['impressionTrackers'] = [nurl]; + bid.mediaType = 'native'; + } else { + bid.ad = idToBidMap[id].adm; + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_IMP_ID(%7D|\})/gi, idToBidMap[id].impid); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_AD_ID(%7D|\})/gi, idToBidMap[id].adid); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_PRICE(%7D|\})/gi, idToBidMap[id].price); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_CURRENCY(%7D|\})/gi, bidResponse.cur); + bid.ad = bid.ad.replace(/\$(%7B|\{)AUCTION_BID_ID(%7D|\})/gi, bidResponse.bidid); + bid.width = idToImpMap[id].banner.w; + bid.height = idToImpMap[id].banner.h; + } bids.push(bid); } }); @@ -66,43 +88,95 @@ function impression(slot) { return { id: slot.bidId, banner: banner(slot), + 'native': nativeImpression(slot), bidfloor: slot.params.bidFloor || '0.000001', tagid: slot.params.placementId.toString(), }; } function banner(slot) { - const size = slot.params.size.toUpperCase().split('X'); - const width = parseInt(size[0]); - const height = parseInt(size[1]); - return { - w: width, - h: height, + if (!slot.nativeParams) { + const size = slot.params.size.toUpperCase().split('X'); + const width = parseInt(size[0]); + const height = parseInt(size[1]); + return { + w: width, + h: height, + }; }; } -function site(bidderRequest) { - const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.pubId : '0'; - const siteId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.siteId : '0'; - const appParams = bidderRequest[0].params.app; - if (!appParams) { + +function nativeImpression(slot) { + if (slot.nativeParams) { + const assets = []; + addAsset(assets, titleAsset(1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); + addAsset(assets, dataAsset(2, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); + addAsset(assets, dataAsset(3, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); + addAsset(assets, imageAsset(4, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN)); + addAsset(assets, imageAsset(5, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); return { - publisher: { - id: pubId.toString(), - domain: getTopWindowLocation().hostname, + request: JSON.stringify({ assets }), + ver: '1.1', + }; + } + return null; +} + +function addAsset(assets, asset) { + if (asset) { + assets.push(asset); + } +} + +function titleAsset(id, params, defaultLen) { + if (params) { + return { + id, + required: params.required ? 1 : 0, + title: { + len: params.len || defaultLen, }, - id: siteId.toString(), - ref: referrer(), - page: getTopWindowLocation().href, - } + }; } return null; } -function referrer() { - try { - return window.top.document.referrer; - } catch (e) { - return document.referrer; + +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; +} + +function dataAsset(id, params, type, defaultLen) { + return params ? { + id, + required: params.required ? 1 : 0, + data: { + type, + len: params.len || defaultLen, + } + } : null; +} + +function site(bidderRequest) { + const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.pubId : '0'; + const siteId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.siteId : '0'; + return { + publisher: { + id: pubId.toString(), + domain: getTopWindowLocation().hostname, + }, + id: siteId.toString(), + ref: getTopWindowReferrer(), + page: getTopWindowLocation().href, } } + function device() { return { ua: navigator.userAgent, @@ -122,4 +196,25 @@ function parse(rawResponse) { return null; } +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.id === 2 ? asset.data.value : keys.body; + keys.sponsoredBy = asset.data && asset.id === 3 ? asset.data.value : keys.sponsoredBy; + keys.icon = asset.img && asset.id === 4 ? asset.img.url : keys.icon; + keys.image = asset.img && asset.id === 5 ? asset.img.url : keys.image; + }); + if (nativeAd['native'].link) { + keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url); + } + return keys; + } + } + return null; +} + registerBidder(spec); diff --git a/modules/platformioBidAdapter.md b/modules/platformioBidAdapter.md index 0d019d1fe96..81ac81380b0 100644 --- a/modules/platformioBidAdapter.md +++ b/modules/platformioBidAdapter.md @@ -1,12 +1,13 @@ # Overview -**Module Name**: Platform.io Bidder Adapter -**Module Type**: Bidder Adapter -**Maintainer**: sk@ultralab.by +**Module Name**: Platform.io Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: siarhei.kasukhin@platform.io # Description -Connects to Platform.io demand source to fetch bids. +Connects to Platform.io demand source to fetch bids. +Banner and Native formats are supported. Please use ```platformio``` as the bidder code. # Test Parameters @@ -17,11 +18,30 @@ Please use ```platformio``` as the bidder code. bids: [{ bidder: 'platformio', params: { - pubId: '28082', // required - siteId: '26047', // required - size: '250X250', // required - placementId: '123', - bidFloor: '0.001' + pubId: '29521', // required + siteId: '26047', // required + size: '250X250', // required + placementId: '123', + bidFloor: '0.001' + } + }] + },{ + code: 'native-ad-div', + sizes: [[1, 1]], + nativeParams: { + title: { required: true, len: 75 }, + image: { required: true }, + body: { len: 200 }, + sponsoredBy: { len: 20 }, + icon: { required: false } + }, + bids: [{ + bidder: 'platformio', + params: { + pubId: '29521', // required + siteId: '26047', // required + placementId: '123', + bidFloor: '0.001' } }] }]; diff --git a/modules/pre1api.js b/modules/pre1api.js new file mode 100644 index 00000000000..707d10fbfd8 --- /dev/null +++ b/modules/pre1api.js @@ -0,0 +1,156 @@ + +/** + pre1api module + + This module supports backwards compatibility for those who need extra time to re-code their pages to work with the + Prebid 1.0 API. Use of this backwards compatibility module is recommended only as an interim solution. + + It provides equivalents for the following variables and functions that were deprecated in PBJS 1.0: + - pbjs._winningBids + - pbjs._bidsReceived + - pbjs._bidsRequested + - pbjs._adUnitCodes + - pbjs._adsReceived + - pbjs.cbTimeout + - pbjs.addCallback() + - pbjs.removeCallback() + - pbjs.allBidsAvailable() + - pbjs.bidderTimeout + - pbjs.logging + - pbjs.publisherDomain + - pbjs.setPriceGranularity() + - pbjs.enableSendAllBids() // and also defaults this value to `false` like pre-1.0 + - pbjs.setBidderSequence() + - pbjs.setS2SConfig() // and makes endpoints optional again (defaulting to the appnexus endpoints) + + This will not support the pre-1.0 sizeMapping feature. + + The drawback is that this module disables concurrency for requestBids(), queueing them as was done in pre-1.0. Anytime + an auction request is queued or one of these APIs is accessed it will display a deprecation warning in the console if + logging is enabled. So while this is useful for those that need more time to migrate, it eliminates one of the best + features of PBJS 1.0 as is required to emulate the old API. + */ + +import {config} from 'src/config'; +import {logWarn, logInfo} from 'src/utils'; + +const MODULE_NAME = 'pre-1.0 API'; + +let pbjs = window['$$PREBID_GLOBAL$$']; + +logInfo(`loading ${MODULE_NAME} module and patching prebid with deprecated APIs.`); + +let auctionQueue = []; + +let emptyFn = () => []; + +Object.defineProperty(pbjs, '_winningBids', { + get: () => pbjs.getAllWinningBids() +}); + +let auctionPropMap = { + _bidsReceived: auction => auction.getBidsReceived(), + _bidsRequested: auction => auction.getBidRequests(), + _adUnitCodes: auction => auction.getAdUnitCodes(), + allBidsAvailable: auction => auction.getBidRequests().every((bidRequest) => bidRequest.doneCbCallCount >= 1) +}; + +let configPropMap = { + cbTimeout: 'bidderTimeout', + bidderTimeout: 'bidderTimeout', + logging: 'debug', + publisherDomain: 'publisherDomain', + enableSendAllBids: 'enableSendAllBids', + setPriceGranularity: 'priceGranularity', + setBidderSequence: 'bidderSequence', + setS2SConfig: 's2sConfig' +}; + +pbjs.addCallback = pbjs.onEvent; +pbjs.removeCallback = pbjs.offEvent; + +// can't see anywhere that this was used, but it is listed in Prebid 1.0 transition guide... +// so just adding as empty array +pbjs._adsReceived = []; + +config.setDefaults({ + enableSendAllBids: false, + cache: { + url: 'https://prebid.adnxs.com/pbc/v1/cache' + } +}); + +let currAuction = { + getBidsReceived: emptyFn, + getBidsRequested: emptyFn, + getAdUnitCodes: emptyFn, + getTimeout: () => config.getConfig('bidderTimeout') +}; + +// we need to intercept s2sConfig rather than call setConfig or setDefaults directly, otherwise the code will fail when +// the server adapter attempts to validate the configuration passed in by the publisher +config.setConfig.addHook((config, next) => { + if (config.s2sConfig) { + config.s2sConfig = Object.assign({ + endpoint: 'https://prebid.adnxs.com/pbs/v1/auction', + syncEndpoint: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' + }, config.s2sConfig); + } + next(config); +}); + +/** + * Hook to queue and disallow concurrent auctions (as Prebid would function pre 1.0) + */ +pbjs.requestBids.addHook((config, next = config) => { + auctionQueue.push(() => { + let oldHandler = config.bidsBackHandler; + config.bidsBackHandler = (...args) => { + if (typeof oldHandler === 'function') { + oldHandler.apply(null, args); + } + + auctionQueue.shift(); + if (auctionQueue[0]) { + auctionQueue[0](); + } + }; + + currAuction = next(config); + }); + + if (auctionQueue.length === 1) { + auctionQueue[0](); + } else { + logWarn(`${MODULE_NAME} module: concurrency has been disabled and "$$PREBID_GLOBAL$$.requestBids" call was queued`); + } +}, 100); + +Object.keys(auctionPropMap).forEach(prop => { + if (prop === 'allBidsAvailable') { + pbjs[prop] = deprecated(prop, () => auctionPropMap[prop](currAuction)); + } + Object.defineProperty(pbjs, prop, { + get: deprecated(prop, () => auctionPropMap[prop](currAuction)) + }); +}); + +Object.keys(configPropMap).forEach(prop => { + if (prop === 'enableSendAllBids') { + pbjs[prop] = deprecated(prop, () => config.setConfig({[prop]: true})); + } else if (prop.lastIndexOf('set', 0) === 0) { + pbjs[prop] = deprecated(prop, value => config.setConfig({[configPropMap[prop]]: value})); + } else { + Object.defineProperty(pbjs, prop, { + get: deprecated(prop, () => config.getConfig(configPropMap[prop])), + set: deprecated(prop, value => config.setConfig({[configPropMap[prop]]: value})) + }); + } +}); + +function deprecated(name, fn) { + return (...args) => { + logWarn(`${MODULE_NAME} module: accessed deprecated API "$$PREBID_GLOBAL$$.${name}"`); + return fn.apply(null, args); + }; +} diff --git a/modules/prebidServerBidAdapter.js b/modules/prebidServerBidAdapter.js index 0540d325c79..e222ecb446b 100644 --- a/modules/prebidServerBidAdapter.js +++ b/modules/prebidServerBidAdapter.js @@ -166,6 +166,23 @@ const paramTypes = { }, }; +function _getDigiTrustQueryParams() { + function getDigiTrustId() { + let digiTrustUser = window.DigiTrust && (config.getConfig('digiTrustId') || window.DigiTrust.getUser({member: 'T9QSFKPDN9'})); + return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; + } + let digiTrustId = getDigiTrustId(); + // Verify there is an ID and this user has not opted out + if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) { + return null; + } + return { + id: digiTrustId.id, + keyv: digiTrustId.keyv, + pref: 0 + }; +} + /** * Bidder adapter for Prebid Server */ @@ -193,6 +210,8 @@ export function PrebidServer() { /* Prebid executes this function when the page asks to send out bid requests */ baseAdapter.callBids = function(s2sBidRequest, bidRequests, addBidResponse, done, ajax) { const isDebug = !!getConfig('debug'); + const app = !!getConfig('app'); + const device = !!getConfig('device'); const adUnits = utils.deepClone(s2sBidRequest.ad_units); adUnits.forEach(adUnit => { let videoMediaType = utils.deepAccess(adUnit, 'mediaTypes.video'); @@ -215,9 +234,17 @@ export function PrebidServer() { url: utils.getTopWindowUrl(), prebid_version: '$prebid.version$', ad_units: adUnits.filter(hasSizes), - is_debug: isDebug + is_debug: isDebug, + device: device, + app: app }; + let digiTrust = _getDigiTrustQueryParams(); + + if (digiTrust) { + requestJson.digiTrust = digiTrust; + } + // in case config.bidders contains invalid bidders, we only process those we sent requests for. const requestedBidders = requestJson.ad_units.map(adUnit => adUnit.bids.map(bid => bid.bidder).filter(utils.uniques)).reduce(utils.flatten).filter(utils.uniques); function processResponse(response) { @@ -254,7 +281,7 @@ export function PrebidServer() { requestedBidders.forEach(bidder => { let clientAdapter = adaptermanager.getBidAdapter(bidder); if (clientAdapter && clientAdapter.registerSyncs) { - clientAdapter.registerSyncs(); + clientAdapter.registerSyncs([]); } }); diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js new file mode 100644 index 00000000000..dfcde047580 --- /dev/null +++ b/modules/pubmaticBidAdapter.js @@ -0,0 +1,291 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +const constants = require('src/constants.json'); + +const BIDDER_CODE = 'pubmatic'; +const ENDPOINT = '//hbopenbid.pubmatic.com/translator?source=prebid-client'; +const USYNCURL = '//ads.pubmatic.com/AdServer/js/showad.js#PIX&kdntuid=1&p='; +const CURRENCY = 'USD'; +const AUCTION_TYPE = 1; +const UNDEFINED = undefined; +const CUSTOM_PARAMS = { + 'kadpageurl': '', // Custom page url + 'gender': '', // User gender + 'yob': '', // User year of birth + 'lat': '', // User location - Latitude + 'lon': '', // User Location - Longitude + 'wiid': '', // OpenWrap Wrapper Impression ID + 'profId': '', // OpenWrap Legacy: Profile ID + 'verId': '' // OpenWrap Legacy: version ID +}; +const NET_REVENUE = false; + +let publisherId = 0; + +function _getDomainFromURL(url) { + let anchor = document.createElement('a'); + anchor.href = url; + return anchor.hostname; +} + +function _parseSlotParam(paramName, paramValue) { + if (!utils.isStr(paramValue)) { + paramValue && utils.logWarn('PubMatic: Ignoring param key: ' + paramName + ', expects string-value, found ' + typeof paramValue); + return UNDEFINED; + } + + switch (paramName) { + case 'pmzoneid': + return paramValue.split(',').slice(0, 50).map(id => id.trim()).join(); + case 'kadfloor': + return parseFloat(paramValue) || UNDEFINED; + case 'lat': + return parseFloat(paramValue) || UNDEFINED; + case 'lon': + return parseFloat(paramValue) || UNDEFINED; + case 'yob': + return parseInt(paramValue) || UNDEFINED; + default: + return paramValue; + } +} + +function _cleanSlot(slotName) { + if (utils.isStr(slotName)) { + return slotName.replace(/^\s+/g, '').replace(/\s+$/g, ''); + } + return ''; +} + +function _parseAdSlot(bid) { + bid.params.adUnit = ''; + bid.params.adUnitIndex = '0'; + bid.params.width = 0; + bid.params.height = 0; + + bid.params.adSlot = _cleanSlot(bid.params.adSlot); + + var slot = bid.params.adSlot; + var splits = slot.split(':'); + + slot = splits[0]; + if (splits.length == 2) { + bid.params.adUnitIndex = splits[1]; + } + splits = slot.split('@'); + if (splits.length != 2) { + utils.logWarn('AdSlot Error: adSlot not in required format'); + return; + } + bid.params.adUnit = splits[0]; + splits = splits[1].split('x'); + if (splits.length != 2) { + utils.logWarn('AdSlot Error: adSlot not in required format'); + return; + } + bid.params.width = parseInt(splits[0]); + bid.params.height = parseInt(splits[1]); +} + +function _initConf() { + var conf = {}; + conf.pageURL = utils.getTopWindowUrl(); + conf.refURL = utils.getTopWindowReferrer(); + return conf; +} + +function _handleCustomParams(params, conf) { + if (!conf.kadpageurl) { + conf.kadpageurl = conf.pageURL; + } + + var key, value, entry; + for (key in CUSTOM_PARAMS) { + if (CUSTOM_PARAMS.hasOwnProperty(key)) { + value = params[key]; + if (value) { + entry = CUSTOM_PARAMS[key]; + + if (typeof entry === 'object') { + // will be used in future when we want to process a custom param before using + // 'keyname': {f: function() {}} + value = entry.f(value, conf); + } + + if (utils.isStr(value)) { + conf[key] = value; + } else { + utils.logWarn('PubMatic: Ignoring param : ' + key + ' with value : ' + CUSTOM_PARAMS[key] + ', expects string-value, found ' + typeof value); + } + } + } + } + return conf; +} + +function _createOrtbTemplate(conf) { + return { + id: '' + new Date().getTime(), + at: AUCTION_TYPE, + cur: [CURRENCY], + imp: [], + site: { + page: conf.pageURL, + ref: conf.refURL, + publisher: {} + }, + device: { + ua: navigator.userAgent, + js: 1, + dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, + h: screen.height, + w: screen.width, + language: navigator.language + }, + user: {}, + ext: {} + }; +} + +function _createImpressionObject(bid, conf) { + return { + id: bid.bidId, + tagid: bid.params.adUnit, + bidfloor: _parseSlotParam('kadfloor', bid.params.kadfloor), + secure: window.location.protocol === 'https:' ? 1 : 0, + banner: { + pos: 0, + w: bid.params.width, + h: bid.params.height, + topframe: utils.inIframe() ? 0 : 1, + }, + ext: { + pmZoneId: _parseSlotParam('pmzoneid', bid.params.pmzoneid) + } + }; +} + +export const spec = { + code: BIDDER_CODE, + + /** + * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: bid => { + if (bid && bid.params) { + if (!utils.isStr(bid.params.publisherId)) { + utils.logWarn('PubMatic Error: publisherId is mandatory and cannot be numeric. Call to OpenBid will not be sent.'); + return false; + } + if (!utils.isStr(bid.params.adSlot)) { + utils.logWarn('PubMatic: adSlotId is mandatory and cannot be numeric. Call to OpenBid will not be sent.'); + return false; + } + return true; + } + return false; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: validBidRequests => { + var conf = _initConf(); + var payload = _createOrtbTemplate(conf); + validBidRequests.forEach(bid => { + _parseAdSlot(bid); + if (!(bid.params.adSlot && bid.params.adUnit && bid.params.adUnitIndex && bid.params.width && bid.params.height)) { + utils.logWarn('PubMatic: Skipping the non-standard adslot:', bid.params.adSlot, bid); + return; + } + conf.pubId = conf.pubId || bid.params.publisherId; + conf = _handleCustomParams(bid.params, conf); + conf.transactionId = bid.transactionId; + payload.imp.push(_createImpressionObject(bid, conf)); + }); + + if (payload.imp.length == 0) { + return; + } + + payload.site.publisher.id = conf.pubId.trim(); + publisherId = conf.pubId.trim(); + payload.ext.wrapper = {}; + payload.ext.wrapper.profile = conf.profId || UNDEFINED; + payload.ext.wrapper.version = conf.verId || UNDEFINED; + payload.ext.wrapper.wiid = conf.wiid || UNDEFINED; + payload.ext.wrapper.wv = constants.REPO_AND_VERSION; + payload.ext.wrapper.transactionId = conf.transactionId; + payload.ext.wrapper.wp = 'pbjs'; + payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED); + payload.user.geo = {}; + payload.user.geo.lat = _parseSlotParam('lat', conf.lat); + payload.user.geo.lon = _parseSlotParam('lon', conf.lon); + payload.user.yob = _parseSlotParam('yob', conf.yob); + payload.device.geo = {}; + payload.device.geo.lat = _parseSlotParam('lat', conf.lat); + payload.device.geo.lon = _parseSlotParam('lon', conf.lon); + payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim(); + payload.site.domain = _getDomainFromURL(payload.site.page); + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(payload) + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} response A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: (response, request) => { + const bidResponses = []; + try { + if (response.body && response.body.seatbid && response.body.seatbid[0] && response.body.seatbid[0].bid) { + response.body.seatbid[0].bid.forEach(bid => { + let newBid = { + requestId: bid.impid, + cpm: (parseFloat(bid.price) || 0).toFixed(2), + width: bid.w, + height: bid.h, + creativeId: bid.crid || bid.id, + dealId: bid.dealid, + currency: CURRENCY, + netRevenue: NET_REVENUE, + ttl: 300, + referrer: utils.getTopWindowUrl(), + ad: bid.adm + }; + bidResponses.push(newBid); + }); + } + } catch (error) { + utils.logError(error); + } + return bidResponses; + }, + + /** + * Register User Sync. + */ + getUserSyncs: syncOptions => { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: USYNCURL + publisherId + }]; + } else { + utils.logWarn('PubMatic: Please enable iframe based user sync.'); + } + } +}; + +registerBidder(spec); diff --git a/modules/pubmaticBidAdapter.md b/modules/pubmaticBidAdapter.md new file mode 100644 index 00000000000..768b3c541f6 --- /dev/null +++ b/modules/pubmaticBidAdapter.md @@ -0,0 +1,53 @@ +# Overview + +``` +Module Name: PubMatic Bid Adapter +Module Type: Bidder Adapter +Maintainer: header-bidding@pubmatic.com +``` + +# Description + +Connects to PubMatic exchange for bids. + +PubMatic bid adapter supports Banner currently. + +# Sample Ad Unit: For Publishers +``` +var adUnits = [ +{ + code: 'test-div', + sizes: [ + [300, 250], + [728, 90] + ], + bids: [{ + bidder: 'pubmatic', + params: { + publisherId: '156209', // required + adSlot: 'pubmatic_test2@300x250', // required + pmzoneid: 'zone1, zone11', // optional + lat: '40.712775', // optional + lon: '-74.005973', // optional + yob: '1982', // optional + kadpageurl: 'www.test.com', // optional + gender: 'M', // optional + kadfloor: '0.50' // optional + } + }] +} +``` + +### Configuration + +PubMatic recommends the UserSync configuration below. Without it, the PubMatic adapter will not able to perform user syncs, which lowers match rate and reduces monetization. + +```javascript +pbjs.setConfig({ + userSync: { + iframeEnabled: true, + enabledBidders: ['pubmatic'], + syncDelay: 6000 + }}); +``` +Note: Combine the above the configuration with any other UserSync configuration. Multiple setConfig() calls overwrite each other and only last call for a given attribute will take effect. diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index 2c1f6f9174d..fc637cc9fff 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -28,7 +28,7 @@ export const spec = { aliases: ['pulseLite', 'pulsepointLite'], - supportedMediaTypes: ['native'], + supportedMediaTypes: ['banner', 'native'], isBidRequestValid: bid => ( !!(bid && bid.params && bid.params.cp && bid.params.ct) diff --git a/modules/rockyouBidAdapter.js b/modules/rockyouBidAdapter.js new file mode 100644 index 00000000000..975756d18bc --- /dev/null +++ b/modules/rockyouBidAdapter.js @@ -0,0 +1,363 @@ +import * as utils from 'src/utils'; +import { Renderer } from 'src/Renderer'; +import { BANNER, VIDEO } from 'src/mediaTypes'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'rockyou'; + +const BASE_REQUEST_PATH = 'https://tas.rockyou.net/servlet/rotator/'; +const IFRAME_SYNC_URL = 'https://prebid.tex-sync.rockyou.net/usersync2/tas'; +const VAST_PLAYER_LOCATION = 'https://rya-static.rockyou.com/rya/js/PreBidPlayer.js'; +export const ROTATION_ZONE = 'openrtbprod'; + +let isBidRequestValid = (bid) => { + return !!bid.params && !!bid.params.placementId; +}; + +/** +* The RockYou Ad Serving system currently only accepts one placementId +* per Ad request. For this reason, the first placementId indicated +* will be chosen as the predominant placementId for this request. +*/ +let determineOptimalPlacementId = (bidRequest) => { + return bidRequest.params.placementId; +} + +let determineOptimalRequestId = (bidRequest) => { + return bidRequest.bidId; +} + +let buildSiteComponent = (bidRequest) => { + let topLocation = utils.getTopWindowLocation(); + + let site = { + domain: topLocation.hostname, + page: topLocation.href, + ref: topLocation.origin + }; + + return site; +} + +let buildDeviceComponent = (bidRequest) => { + let device = { + js: 1, + language: ('language' in navigator) ? navigator.language : null + }; + + return device; +}; + +let extractValidSize = (bidRequest) => { + let width = null; + let height = null; + + if (!utils.isEmpty(bidRequest.sizes)) { + // Ensure the size array is normalized + let conformingSize = utils.parseSizesInput(bidRequest.sizes); + + if (!utils.isEmpty(conformingSize) && conformingSize[0] != null) { + // Currently only the first size is utilized + let splitSizes = conformingSize[0].split('x'); + + width = parseInt(splitSizes[0]); + height = parseInt(splitSizes[1]); + } + } + + return { + w: width, + h: height + }; +}; + +let generateVideoComponent = (bidRequest) => { + let impSize = extractValidSize(bidRequest); + + return { + w: impSize.w, + h: impSize.h + } +} + +let generateBannerComponent = (bidRequest) => { + let impSize = extractValidSize(bidRequest); + + return { + w: impSize.w, + h: impSize.h + } +} + +let generateImpBody = (bidRequest) => { + let mediaTypes = bidRequest.mediaTypes; + + let banner = null; + let video = null; + + // Assume banner if the mediatype is not included + if (mediaTypes && mediaTypes.video) { + video = generateVideoComponent(bidRequest); + } else { + banner = generateBannerComponent(bidRequest); + } + + return { + id: bidRequest.index, + banner: banner, + video: video + }; +} + +let generatePayload = (bidRequests) => { + // Generate the expected OpenRTB payload + + let rootBidRequest = bidRequests[0]; + + let index = 1; + bidRequests.forEach((bidRequest) => { + bidRequest.index = index; + index += 1; + }) + + let payload = { + id: determineOptimalRequestId(rootBidRequest), + site: buildSiteComponent(rootBidRequest), + device: buildDeviceComponent(rootBidRequest), + imp: bidRequests.map(generateImpBody) + }; + + return JSON.stringify(payload); +}; + +let overridableProperties = (request) => { + let rendererLocation = VAST_PLAYER_LOCATION; + let baseRequestPath = BASE_REQUEST_PATH; + let rotationZone = ROTATION_ZONE; + + if (!utils.isEmpty(request.rendererOverride)) { + rendererLocation = request.rendererOverride; + } + + if (request.params) { + if (!utils.isEmpty(request.params.baseRequestPath)) { + baseRequestPath = request.params.baseRequestPath; + } + + if (!utils.isEmpty(request.params.rotationZone)) { + rotationZone = request.params.rotationZone; + } + } + + return { + rendererLocation, + baseRequestPath, + rotationZone + } +} + +let buildRequests = (validBidRequests, requestRoot) => { + const requestType = 'POST'; + + let requestUrl = null; + let requestPayload = null; + let mediaTypes = null; + let adUnitCode = null; + let rendererOverride = null; + + // Due to the nature of how URLs are generated, there must + // be at least one bid request present for this to function + // correctly + if (!utils.isEmpty(validBidRequests)) { + let headBidRequest = validBidRequests[0]; + + let serverLocations = overridableProperties(headBidRequest); + + // requestUrl is the full endpoint w/ relevant adspot paramters + let placementId = determineOptimalPlacementId(headBidRequest); + requestUrl = `${serverLocations.baseRequestPath}${placementId}/0/vo?z=${serverLocations.rotationZone}`; + + // requestPayload is the POST body JSON for the OpenRtb request + requestPayload = generatePayload(validBidRequests); + + mediaTypes = headBidRequest.mediaTypes; + adUnitCode = headBidRequest.adUnitCode; + rendererOverride = headBidRequest.rendererOverride; + } + const result = { + method: requestType, + type: requestType, + url: requestUrl, + data: requestPayload, + mediaTypes, + requestId: requestRoot.bidderRequestId, + adUnitCode, + rendererOverride + }; + + return result; +}; + +let outstreamRender = (bid) => { + // push to render queue because player may not be loaded yet + bid.renderer.push(() => { + let adUnitCode = bid.renderer.config.adUnitCode; + + try { + RockYouVastPlayer.render(adUnitCode, bid, playerCallbacks(bid.renderer)); + } catch (pbe) { + utils.logError(pbe); + } + }); +} + +let rockYouEventTranslation = (rockYouEvent) => { + let translated; + switch (rockYouEvent) { + case 'LOAD': + translated = 'loaded'; + break; + case 'IMPRESSION': + translated = 'impression'; + break; + case 'COMPLETE': + case 'ERROR': + translated = 'ended' + break; + } + + return translated; +} + +let playerCallbacks = (renderer) => (id, eventName) => { + eventName = rockYouEventTranslation(eventName); + + if (eventName) { + renderer.handleVideoEvent({ id, eventName }); + } +}; + +let generateRenderer = (bid, adUnitCode, rendererLocation) => { + let renderer = Renderer.install({ + url: rendererLocation, + config: { + adText: `RockYou Outstream Video Ad`, + adUnitCode: adUnitCode + }, + id: bid.id + }); + + bid.renderer = renderer; + + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); + } + + renderer.setEventHandlers({ + impression: () => utils.logMessage('RockYou outstream video impression event'), + loaded: () => utils.logMessage('RockYou outstream video loaded event'), + ended: () => { + utils.logMessage('RockYou outstream renderer video event'); + document.querySelector(`#${adUnitCode}`).style.display = 'none'; + } + }); + + return renderer; +}; + +let interpretResponse = (serverResponse, request) => { + let responses = []; + + if (serverResponse.body) { + let responseBody = serverResponse.body; + + if (responseBody != null) { + let seatBids = responseBody.seatbid; + + if (!(utils.isEmpty(seatBids) || + utils.isEmpty(seatBids[0].bid))) { + let bid = seatBids[0].bid[0]; + + // handle any values that may end up undefined + let nullify = (value) => typeof value === 'undefined' ? null : value; + + let ttl = null; + if (bid.ext) { + ttl = nullify(bid.ext.ttl); + } + + let bidWidth = nullify(bid.w); + let bidHeight = nullify(bid.h); + + let bannerCreative = null; + let videoXml = null; + let mediaType = null; + let renderer = null; + + if (request.mediaTypes && request.mediaTypes.video) { + videoXml = bid.adm; + mediaType = VIDEO; + + let serversideLocations = overridableProperties(request); + + renderer = generateRenderer(bid, request.adUnitCode, serversideLocations.rendererLocation); + } else { + bannerCreative = bid.adm; + } + + let response = { + requestId: responseBody.id, + cpm: bid.price, + width: bidWidth, + height: bidHeight, + ad: bannerCreative, + ttl: ttl, + creativeId: bid.adid, + netRevenue: true, + currency: responseBody.cur, + vastUrl: null, + vastXml: videoXml, + dealId: null, + mediaType: mediaType, + renderer: renderer + }; + + responses.push(response); + } + } + } + + return responses; +}; + +let getUserSyncs = (syncOptions, serverResponses) => { + const syncs = [] + + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: IFRAME_SYNC_URL + }); + } + + return syncs; +} + +export const spec = { + code: BIDDER_CODE, + aliases: ['ry'], + isBidRequestValid: isBidRequestValid, + buildRequests: buildRequests, + interpretResponse: interpretResponse, + getUserSyncs: getUserSyncs, + supportedMediaTypes: [BANNER, VIDEO] +}; + +export const internals = { + playerCallbacks, + generateRenderer +} + +registerBidder(spec); diff --git a/modules/rockyouBidAdapter.md b/modules/rockyouBidAdapter.md new file mode 100644 index 00000000000..8cc9ba371d7 --- /dev/null +++ b/modules/rockyouBidAdapter.md @@ -0,0 +1,57 @@ +# Overview + +``` +Module Name: RockYou Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid.adapter@rockyou.com +``` + +# Description + +Connects to the RockYou exchange for bids. + +The RockYou bid adapter supports Banner and Video. + +For publishers who wish to be set up on the RockYou Ad Network, please contact +publishers@rockyou.com. + +RockYou user syncing requires the `userSync.iframeEnabled` property be set to `true`. + +# Test PARAMETERS +``` +var adUnits = [ + + // Banner adUnit + { + code: 'banner-div', + sizes: [[720, 480]], + + // Replace this object to test a new Adapter! + bids: [{ + bidder: 'rockyou', + params: { + placementId: '4322' + } + }] + }, + + // Video (outstream) + { + code: 'video-outstream', + sizes: [[720, 480]], + + mediaType: 'video', + mediaTypes: { + video: { + context: 'outstream' + } + }, + bids: [{ + bidder: 'rockyou', + params: { + placementId: '4307' + } + }] + } +] +``` diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 76a9095be72..ee490e55c15 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -73,7 +73,7 @@ utils._each(sizeMap, (item, key) => sizeMap[item] = key); export const spec = { code: 'rubicon', aliases: ['rubiconLite'], - supportedMediaTypes: ['video'], + supportedMediaTypes: ['banner', 'video'], /** * @param {object} bid * @return boolean @@ -112,9 +112,10 @@ export const spec = { if (bidRequest.mediaType === 'video') { let params = bidRequest.params; let size = parseSizes(bidRequest); + let page_rf = !params.referrer ? utils.getTopWindowUrl() : params.referrer; let data = { - page_url: !params.referrer ? utils.getTopWindowUrl() : params.referrer, + page_url: params.secure ? page_rf.replace(/^http:/i, 'https:') : page_rf, resolution: _getScreenResolution(), account_id: params.accountId, integration: INTEGRATION, @@ -265,6 +266,7 @@ export const spec = { requestId: bidRequest.bidId, currency: 'USD', creativeId: ad.creative_id, + mediaType: ad.creative_type, cpm: ad.cpm || 0, dealId: ad.deal, ttl: 300, // 5 minutes @@ -275,6 +277,7 @@ export const spec = { bid.height = bidRequest.params.video.playerHeight; bid.vastUrl = ad.creative_depot_url; bid.impression_id = ad.impression_id; + bid.videoCacheKey = ad.impression_id; } else { bid.ad = _renderCreative(ad.script, ad.impression_id); [bid.width, bid.height] = sizeMap[ad.size_id].split('x').map(num => Number(num)); diff --git a/modules/sekindoUMBidAdapter.js b/modules/sekindoUMBidAdapter.js index 6e4ae34bd76..09b94be967b 100644 --- a/modules/sekindoUMBidAdapter.js +++ b/modules/sekindoUMBidAdapter.js @@ -2,7 +2,7 @@ import * as utils from 'src/utils'; import {registerBidder} from 'src/adapters/bidderFactory'; export const spec = { code: 'sekindoUM', - supportedMediaTypes: ['video'], + supportedMediaTypes: ['banner', 'video'], /** * Determines whether or not the given bid request is valid. * diff --git a/modules/serverbidBidAdapter.js b/modules/serverbidBidAdapter.js index d120e5c4306..24bb5aa6255 100644 --- a/modules/serverbidBidAdapter.js +++ b/modules/serverbidBidAdapter.js @@ -18,12 +18,18 @@ const CONFIG = { }, 'adsparc': { 'BASE_URI': 'https://e.serverbid.com/api/v2' + }, + 'automatad': { + 'BASE_URI': 'https://e.serverbid.com/api/v2' } }; +let siteId = 0; +let bidder = 'serverbid'; + export const spec = { code: BIDDER_CODE, - aliases: ['connectad', 'onefiftytwo', 'insticator', 'adsparc'], + aliases: ['connectad', 'onefiftytwo', 'insticator', 'adsparc', 'automatad'], /** * Determines whether or not the given bid request is valid. @@ -59,6 +65,10 @@ export const spec = { let ENDPOINT_URL; + // These variables are used in creating the user sync URL. + siteId = validBidRequests[0].params.siteId; + bidder = validBidRequests[0].params.bidder; + const data = Object.assign({ placements: [], time: Date.now(), @@ -137,7 +147,21 @@ export const spec = { }, getUserSyncs: function(syncOptions) { - return []; + if (syncOptions.iframeEnabled) { + if (bidder === 'connectad') { + return [{ + type: 'iframe', + url: '//cdn.connectad.io/connectmyusers.php' + }]; + } else { + return [{ + type: 'iframe', + url: '//s.zkcdn.net/ss/' + siteId + '.html' + }]; + } + } else { + utils.logWarn(bidder + ': Please enable iframe based user syncing.'); + } } }; @@ -181,6 +205,10 @@ sizeMap[3230] = '970x280'; sizeMap[429] = '486x60'; sizeMap[374] = '700x500'; sizeMap[934] = '300x1050'; +sizeMap[1578] = '320x100'; +sizeMap[331] = '320x250'; +sizeMap[3301] = '320x267'; +sizeMap[2730] = '728x250'; function getSize(sizes) { const result = []; diff --git a/modules/serverbidBidAdapter.md b/modules/serverbidBidAdapter.md index 934362c69c4..87b51e665e2 100644 --- a/modules/serverbidBidAdapter.md +++ b/modules/serverbidBidAdapter.md @@ -35,7 +35,7 @@ Connects to Serverbid for receiving bids from configured demand sources. params: { networkId: '9969', siteId: '980639', - zoneId: '178503' + zoneIds: [178503] } } ] diff --git a/modules/serverbidServerBidAdapter.js b/modules/serverbidServerBidAdapter.js index 4be96a09bd6..1025d29a6c0 100644 --- a/modules/serverbidServerBidAdapter.js +++ b/modules/serverbidServerBidAdapter.js @@ -7,7 +7,7 @@ import { config } from 'src/config'; const TYPE = S2S.SRC; const getConfig = config.getConfig; -const REQUIRED_S2S_CONFIG_KEYS = ['siteId', 'networkId', 'bidders', 'endpoint']; +const REQUIRED_S2S_CONFIG_KEYS = ['siteId', 'networkId', 'bidders']; let _s2sConfig; @@ -100,30 +100,30 @@ ServerBidServerAdapter = function ServerBidServerAdapter() { function _callBids(s2sBidRequest, bidRequests, addBidResponse, done, ajax) { let bidRequest = s2sBidRequest; - // one request per ad unit + const data = { + placements: [], + time: Date.now(), + user: {}, + url: utils.getTopWindowUrl(), + referrer: document.referrer, + enableBotFiltering: true, + includePricingData: true, + parallel: true + }; + const allBids = []; + for (let i = 0; i < bidRequest.ad_units.length; i++) { let adunit = bidRequest.ad_units[i]; let siteId = _s2sConfig.siteId; let networkId = getLocalConfig().networkId; let sizes = adunit.sizes; - const data = { - placements: [], - time: Date.now(), - user: {}, - url: utils.getTopWindowUrl(), - referrer: document.referrer, - enableBotFiltering: true, - includePricingData: true, - parallel: true - }; - - const bids = adunit.bids || []; - + var bids = adunit.bids || []; // one placement for each of the bids for (let i = 0; i < bids.length; i++) { const bid = bids[i]; bid.code = adunit.code; + allBids.push(bid); const placement = Object.assign({}, { divName: bid.bid_id, @@ -138,9 +138,9 @@ ServerBidServerAdapter = function ServerBidServerAdapter() { data.placements.push(placement); } } - if (data.placements.length) { - ajax(BASE_URI, _responseCallback(addBidResponse, bids, done), JSON.stringify(data), { method: 'POST', withCredentials: true, contentType: 'application/json' }); - } + } + if (data.placements.length) { + ajax(BASE_URI, _responseCallback(addBidResponse, allBids, done), JSON.stringify(data), { method: 'POST', withCredentials: true, contentType: 'application/json' }); } } diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js new file mode 100644 index 00000000000..4828f3a36a8 --- /dev/null +++ b/modules/smartadserverBidAdapter.js @@ -0,0 +1,92 @@ +import * as utils from 'src/utils'; +import { + config +} from 'src/config'; +import { + registerBidder +} from 'src/adapters/bidderFactory'; +const BIDDER_CODE = 'smartadserver'; +export const spec = { + code: BIDDER_CODE, + aliases: ['smart'], // short code + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.siteId && bid.params.pageId && bid.params.formatId && bid.params.domain); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests) { + // use bidderRequest.bids[] to get bidder-dependent request info + + // if your bidder supports multiple currencies, use config.getConfig(currency) + // to find which one the ad server needs + + // pull requested transaction ID from bidderRequest.bids[].transactionId + var bid = validBidRequests[0]; + const payload = { + siteid: bid.params.siteId, + pageid: bid.params.pageId, + formatid: bid.params.formatId, + currencyCode: config.getConfig('currency.adServerCurrency'), + bidfloor: bid.params.bidfloor || 0.0, + targeting: bid.params.target && bid.params.target != '' ? bid.params.target : undefined, + tagId: bid.adUnitCode, + sizes: bid.sizes.map(size => ({ + w: size[0], + h: size[1] + })), + pageDomain: utils.getTopWindowUrl(), + transactionId: bid.transactionId, + timeout: config.getConfig('bidderTimeout'), + bidId: bid.bidId + }; + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: bid.params.domain + '/prebid/v1', + data: payloadString, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + var response = serverResponse.body; + try { + if (response) { + const bidResponse = { + requestId: JSON.parse(bidRequest.data).bidId, + cpm: response.cpm, + width: response.width, + height: response.height, + creativeId: response.creativeId, + dealId: response.dealId, + currency: response.currency, + netRevenue: response.isNetCpm, + ttl: response.ttl, + referrer: utils.getTopWindowUrl(), + adUrl: response.adUrl, + ad: response.ad + }; + bidResponses.push(bidResponse); + } + } catch (error) { + console.log('Error while parsing smart server response'); + } + return bidResponses; + } +} +registerBidder(spec); diff --git a/modules/smartadserverBidAdapter.md b/modules/smartadserverBidAdapter.md new file mode 100644 index 00000000000..f904aa40b3a --- /dev/null +++ b/modules/smartadserverBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: Smart Ad Server Bidder Adapter +Module Type: Bidder Adapter +Maintainer: gcarnec@smartadserver.com +``` + +# Description + +Connect to Smart for bids. + +The Smart adapter requires setup and approval from the Smart team. +Please reach out to your Technical account manager for more information. + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], // a display size + bids: [ + { + bidder: "smart", + params: { + domain: 'http://prg.smartadserver.com', + siteId: 207435, + pageId: 896536, + formatId: 62913 + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js new file mode 100644 index 00000000000..e453d84d586 --- /dev/null +++ b/modules/sonobiBidAdapter.js @@ -0,0 +1,143 @@ +import { registerBidder } from 'src/adapters/bidderFactory'; +import { getTopWindowLocation, parseSizesInput } from 'src/utils'; +import * as utils from '../src/utils'; + +const BIDDER_CODE = 'sonobi'; +const STR_ENDPOINT = 'https://apex.go.sonobi.com/trinity.json'; +const PAGEVIEW_ID = utils.generateUUID(); + +export const spec = { + code: BIDDER_CODE, + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid - The bid params to validate. + * @return {boolean} True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: bid => !!(bid.params && (bid.params.ad_unit || bid.params.placement_id) && (bid.params.sizes || bid.sizes)), + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests - an array of bids + * @return {object} ServerRequest - Info describing the request to the server. + */ + buildRequests: (validBidRequests) => { + const bids = validBidRequests.map(bid => { + let slotIdentifier = _validateSlot(bid) + if (/^[\/]?[\d]+[[\/].+[\/]?]?$/.test(slotIdentifier)) { + slotIdentifier = slotIdentifier.charAt(0) === '/' ? slotIdentifier : '/' + slotIdentifier + return { + [`${slotIdentifier}|${bid.bidId}`]: `${_validateSize(bid)}${_validateFloor(bid)}` + } + } else if (/^[0-9a-fA-F]{20}$/.test(slotIdentifier) && slotIdentifier.length === 20) { + return { + [bid.bidId]: `${slotIdentifier}|${_validateSize(bid)}${_validateFloor(bid)}` + } + } else { + utils.logError(`The ad unit code or Sonobi Placement id for slot ${bid.bidId} is invalid`); + } + }); + + const payload = { + 'key_maker': JSON.stringify(Object.assign({}, ...bids)), + 'ref': getTopWindowLocation().host, + 's': utils.generateUUID(), + 'pv': PAGEVIEW_ID, + }; + + if (validBidRequests[0].params.hfa) { + payload.hfa = validBidRequests[0].params.hfa; + } + + return { + method: 'GET', + url: STR_ENDPOINT, + withCredentials: true, + data: payload + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: (serverResponse) => { + const bidResponse = serverResponse.body; + const bidsReturned = []; + + if (Object.keys(bidResponse.slots).length === 0) { + return bidsReturned; + } + + Object.keys(bidResponse.slots).forEach(slot => { + const bid = bidResponse.slots[slot]; + + if (bid.sbi_aid && bid.sbi_mouse && bid.sbi_size) { + const bids = { + requestId: slot.split('|').slice(-1)[0], + cpm: Number(bid.sbi_mouse), + width: Number(bid.sbi_size.split('x')[0]) || 1, + height: Number(bid.sbi_size.split('x')[1]) || 1, + ad: _creative(bidResponse.sbi_dc, bid.sbi_aid), + ttl: 500, + creativeId: bid.sbi_aid, + netRevenue: true, + currency: 'USD', + }; + + if (bid.sbi_dozer) { + bids.dealId = bid.sbi_dozer; + } + + bidsReturned.push(bids); + } + }); + return bidsReturned; + }, + /** + * Register User Sync. + */ + getUserSyncs: (syncOptions, serverResponses) => { + const syncs = []; + if (syncOptions.pixelEnabled && serverResponses[0].body.sbi_px) { + serverResponses[0].body.sbi_px.forEach(pixel => { + syncs.push({ + type: pixel.type, + url: pixel.url + }); + }); + } + return syncs; + } +} + +function _validateSize (bid) { + if (bid.params.sizes) { + return parseSizesInput(bid.params.sizes).join(','); + } + return parseSizesInput(bid.sizes).join(','); +} + +function _validateSlot (bid) { + if (bid.params.ad_unit) { + return bid.params.ad_unit; + } + return bid.params.placement_id; +} + +function _validateFloor (bid) { + if (bid.params.floor) { + return `|f=${bid.params.floor}`; + } + return ''; +} + +function _creative (sbi_dc, sbi_aid) { + const src = 'https://' + sbi_dc + 'apex.go.sonobi.com/sbi.js?aid=' + sbi_aid + '&as=null'; + return ''; +} + +registerBidder(spec); diff --git a/modules/sonobiBidAdapter.md b/modules/sonobiBidAdapter.md new file mode 100644 index 00000000000..91e4a0e2b2e --- /dev/null +++ b/modules/sonobiBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +``` +Module Name: Sonobi Bidder Adapter +Module Type: Bidder Adapter +Maintainer: apex.prebid@sonobi.com +``` + +# Description + +Module that connects to Sonobi's demand sources. + +# Test Parameters +``` + var adUnits = [ + { + code: 'adUnit_af', + sizes: [[300, 250], [300, 600]], // a display size + bids: [ + { + bidder: 'sonobi', + params: { + ad_unit: '/7780971/sparks_prebid_MR', + placement_id: '1a2b3c4d5e6f1a2b3c4d', // ad_unit and placement_id are mutually exclusive + sizes: [[300, 250], [300, 600]], + floor: 1 // optional + } + } + ] + } + ]; +``` diff --git a/modules/ucfunnelBidAdapter.js b/modules/ucfunnelBidAdapter.js new file mode 100644 index 00000000000..7f652e38c3d --- /dev/null +++ b/modules/ucfunnelBidAdapter.js @@ -0,0 +1,95 @@ +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; +import { BANNER } from 'src/mediaTypes'; + +const VER = 'ADGENT_PREBID-2018011501'; +const BID_REQUEST_BASE_URL = '//hb.aralego.com/header'; +const UCFUNNEL_BIDDER_CODE = 'ucfunnel'; + +export const spec = { + code: UCFUNNEL_BIDDER_CODE, + supportedMediaTypes: [BANNER], + /** + * Check if the bid is a valid zone ID in either number or string form + * @param {object} bid the ucfunnel bid to validate + * @return boolean for whether or not a bid is valid + */ + isBidRequestValid: function(bid) { + return !!(bid && bid.params && bid.params.adid && typeof bid.params.adid === 'string'); + }, + + /** + * Format the bid request object for our endpoint + * @param {BidRequest[]} bidRequests Array of ucfunnel bidders + * @return object of parameters for Prebid AJAX request + */ + buildRequests: function(validBidRequests) { + var bidRequests = []; + for (var i = 0; i < validBidRequests.length; i++) { + var bid = validBidRequests[i]; + + var ucfunnelUrlParams = buildUrlParams(bid); + + bidRequests.push({ + method: 'GET', + url: BID_REQUEST_BASE_URL, + bidRequest: bid, + data: ucfunnelUrlParams + }); + } + return bidRequests; + }, + + /** + * Format ucfunnel responses as Prebid bid responses + * @param {ucfunnelResponseObj} ucfunnelResponse A successful response from ucfunnel. + * @return {Bid[]} An array of formatted bids. + */ + interpretResponse: function (ucfunnelResponseObj, request) { + var bidResponses = []; + var bidRequest = request.bidRequest; + var responseBody = ucfunnelResponseObj ? ucfunnelResponseObj.body : {}; + + bidResponses.push({ + requestId: bidRequest.bidId, + cpm: responseBody.cpm || 0, + width: responseBody.width, + height: responseBody.height, + creativeId: responseBody.ad_id, + dealId: responseBody.deal || null, + currency: 'USD', + netRevenue: true, + ttl: 1000, + mediaType: BANNER, + ad: responseBody.adm + }); + + return bidResponses; + } +}; +registerBidder(spec); + +function buildUrlParams(bid) { + const host = utils.getTopWindowLocation().host; + const page = utils.getTopWindowLocation().pathname; + const refer = document.referrer; + const language = navigator.language; + const dnt = (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0; + + let queryString = [ + 'ifr', '0', + 'bl', language, + 'je', '1', + 'dnt', dnt, + 'host', host, + 'u', page, + 'ru', refer, + 'adid', utils.getBidIdParameter('adid', bid.params), + 'ver', VER + ]; + + return queryString.reduce( + (memo, curr, index) => + index % 2 === 0 && queryString[index + 1] !== undefined ? memo + curr + '=' + encodeURIComponent(queryString[index + 1]) + '&' : memo, '' + ).slice(0, -1); +} diff --git a/modules/ucfunnelBidAdapter.md b/modules/ucfunnelBidAdapter.md new file mode 100644 index 00000000000..717d2a0089c --- /dev/null +++ b/modules/ucfunnelBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +``` +Module Name: ucfunnel Bid Adapter +Module Type: Bidder Adapter +Maintainer: ryan.chou@ucfunnel.com +``` + +# Description + +This module connects to ucfunnel's demand sources. It supports display, and rich media formats. +ucfunnel will provide ``adid`` that are specific to your ad type. +Please reach out to ``pr@ucfunnel.com`` to set up an ucfunnel account and above ids. +Use bidder code ```ucfunnel``` for all ucfunnel traffic. + +# Test Parameters + +``` + var adUnits = [ + { + code: 'test-LERC', + sizes: [[300, 250]], + bids: [{ + bidder: 'ucfunnel', + params: { + adid: "test-ad-83444226E44368D1E32E49EEBE6D29" //String - required + } + } + ]; +``` \ No newline at end of file diff --git a/modules/vertamediaBidAdapter.js b/modules/vertamediaBidAdapter.js index b314f6fd872..238495f3966 100644 --- a/modules/vertamediaBidAdapter.js +++ b/modules/vertamediaBidAdapter.js @@ -1,16 +1,19 @@ import * as utils from 'src/utils'; import {registerBidder} from 'src/adapters/bidderFactory'; -import {VIDEO} from 'src/mediaTypes'; +import {VIDEO, BANNER} from 'src/mediaTypes'; import {Renderer} from 'src/Renderer'; +import findIndex from 'core-js/library/fn/array/find-index'; -const URL = '//rtb.vertamedia.com/hb/'; +const URL = '//hb2.vertamedia.com/auction/'; const BIDDER_CODE = 'vertamedia'; +const OUTSTREAM = 'outstream'; +const DISPLAY = 'display'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [VIDEO], + supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { - return Boolean(bid && bid.params && bid.params.aid); + return bid && bid.params && bid.params.aid; }, /** @@ -25,7 +28,7 @@ export const spec = { bidderRequest, method: 'GET', url: URL - } + }; }); }, @@ -38,9 +41,6 @@ export const spec = { interpretResponse: function (serverResponse, {bidderRequest}) { serverResponse = serverResponse.body; const isInvalidValidResp = !serverResponse || !serverResponse.bids || !serverResponse.bids.length; - const videoMediaType = utils.deepAccess(bidderRequest.bids[0], 'mediaTypes.video'); - const context = utils.deepAccess(bidderRequest.bids[0], 'mediaTypes.video.context'); - const isMediaTypeOutstream = (videoMediaType && context === 'outstream'); let bids = []; @@ -54,76 +54,82 @@ export const spec = { } serverResponse.bids.forEach(serverBid => { - if (serverBid.cpm !== 0) { - const bid = createBid(isMediaTypeOutstream, serverBid); + const requestId = findIndex(bidderRequest.bids, (bidRequest) => { + return bidRequest.bidId === serverBid.requestId; + }); + + if (serverBid.cpm !== 0 && requestId !== -1) { + const bid = createBid(serverBid, getMediaType(bidderRequest.bids[requestId])); + bids.push(bid); } }); return bids; - }, + } }; /** - * Prepare all parameters for request + * Parse mediaType * @param bid {object} * @returns {object} */ function prepareRTBRequestParams(bid) { - let size = getSize(bid.sizes); + const mediaType = utils.deepAccess(bid, 'mediaTypes.video') ? VIDEO : DISPLAY; return { domain: utils.getTopWindowLocation().hostname, callbackId: bid.bidId, aid: bid.params.aid, - h: size.height, - w: size.width + ad_type: mediaType, + sizes: utils.parseSizesInput(bid.sizes).join() }; } /** - * Prepare size for request - * @param requestSizes {array} - * @returns {object} bid The bid to validate + * Prepare all parameters for request + * @param bidderRequest {object} + * @returns {object} */ -function getSize(requestSizes) { - const size = utils.parseSizesInput(requestSizes)[0]; - const parsed = {}; - - if (typeof size !== 'string') { - return parsed; - } - - let parsedSize = size.toUpperCase().split('X'); +function getMediaType(bidderRequest) { + const videoMediaType = utils.deepAccess(bidderRequest, 'mediaTypes.video'); + const context = utils.deepAccess(bidderRequest, 'mediaTypes.video.context'); - return { - height: parseInt(parsedSize[1], 10) || undefined, - width: parseInt(parsedSize[0], 10) || undefined - }; + return !videoMediaType ? DISPLAY : context === OUTSTREAM ? OUTSTREAM : VIDEO; } /** * Configure new bid by response - * @param isMediaTypeOutstream {boolean} * @param bidResponse {object} + * @param mediaType {Object} * @returns {object} */ -function createBid(isMediaTypeOutstream, bidResponse) { +function createBid(bidResponse, mediaType) { let bid = { requestId: bidResponse.requestId, creativeId: bidResponse.cmpId, - vastUrl: bidResponse.vastUrl, height: bidResponse.height, currency: bidResponse.cur, width: bidResponse.width, cpm: bidResponse.cpm, - mediaType: 'video', netRevenue: true, + mediaType, ttl: 3600 }; - if (isMediaTypeOutstream) { + if (mediaType === DISPLAY) { + return Object.assign(bid, { + ad: bidResponse.ad + }); + } + + Object.assign(bid, { + vastUrl: bidResponse.vastUrl + }); + + if (mediaType === OUTSTREAM) { Object.assign(bid, { + mediaType: 'video', adResponse: bidResponse, renderer: newRenderer(bidResponse.requestId) }); @@ -132,11 +138,16 @@ function createBid(isMediaTypeOutstream, bidResponse) { return bid; } +/** + * Create Vertamedia renderer + * @param requestId + * @returns {*} + */ function newRenderer(requestId) { const renderer = Renderer.install({ id: requestId, url: '//player.vertamedia.com/outstream-unit/2.01/outstream.min.js', - loaded: false, + loaded: false }); renderer.setRender(outstreamRender); @@ -144,6 +155,10 @@ function newRenderer(requestId) { return renderer; } +/** + * Initialise Vertamedia outstream + * @param bid + */ function outstreamRender(bid) { bid.renderer.push(() => { window.VOutstreamAPI.initOutstreams([{ diff --git a/modules/vertamediaBidAdapter.md b/modules/vertamediaBidAdapter.md index 0ce4f2cbd58..2f6faf5c840 100644 --- a/modules/vertamediaBidAdapter.md +++ b/modules/vertamediaBidAdapter.md @@ -9,33 +9,57 @@ Get access to multiple demand partners across VertaMedia AdExchange and maximize your yield with VertaMedia header bidding adapter. VertaMedia header bidding adapter connects with VertaMedia demand sources in order to fetch bids. -This adapter provides a solution for accessing Video demand +This adapter provides a solution for accessing Video demand and display demand # Test Parameters ``` - var adUnits = [{ + var adUnits = [ + + // Video instream adUnit + { code: 'div-test-div', - sizes: [[640, 480]], // ad size + sizes: [[640, 480]], + mediaTypes: { + video: { + context: 'instream' + } + }, bids: [{ - bidder: 'vertamedia', // adapter name - params: { - aid: 332842 - } + bidder: 'vertamedia', + params: { + aid: 332842 + } }] - }{ + }, + + // Video outstream adUnit + { code: 'outstream-test-div', - sizes: [[640, 480]], // ad size + sizes: [[640, 480]], mediaTypes: { - video: { - context: 'outstream' - } + video: { + context: 'outstream' + } }, bids: [{ - bidder: 'vertamedia', // adapter name - params: { - aid: 332842 - } + bidder: 'vertamedia', + params: { + aid: 332842 + } + }] + }, + + // Banner adUnit + { + code: 'div-test-div', + sizes: [[300, 250]], + bids: [{ + bidder: 'vertamedia', + params: { + aid: 324758 + } }] - }]; + } + ]; ``` diff --git a/modules/viBidAdapter.js b/modules/viBidAdapter.js new file mode 100644 index 00000000000..bcfc4e246ac --- /dev/null +++ b/modules/viBidAdapter.js @@ -0,0 +1,69 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { BANNER } from 'src/mediaTypes'; + +const BIDDER_CODE = 'vi'; +const SUPPORTED_MEDIA_TYPES = [BANNER]; + +function isBidRequestValid(bid) { + return !!(bid.params.pubId); +} + +function buildRequests(bidReqs) { + let imps = []; + utils._each(bidReqs, function (bid) { + imps.push({ + id: bid.bidId, + sizes: utils.parseSizesInput(bid.sizes).map(size => size.split('x')), + bidFloor: parseFloat(bid.params.bidFloor) > 0 ? bid.params.bidFloor : 0 + }); + }); + + const bidRequest = { + id: bidReqs[0].requestId, + imps: imps, + publisherId: utils.getBidIdParameter('pubId', bidReqs[0].params), + siteId: utils.getBidIdParameter('siteId', bidReqs[0].params), + cat: utils.getBidIdParameter('cat', bidReqs[0].params), + language: utils.getBidIdParameter('lang', bidReqs[0].params), + domain: utils.getTopWindowLocation().hostname, + page: utils.getTopWindowUrl(), + referrer: utils.getTopWindowReferrer() + }; + return { + method: 'POST', + url: `//pb.vi-serve.com/prebid/bid`, + data: JSON.stringify(bidRequest), + options: {contentType: 'application/json', withCredentials: false} + }; +} + +function interpretResponse(bids) { + let responses = []; + utils._each(bids.body, function(bid) { + responses.push({ + requestId: bid.id, + cpm: parseFloat(bid.price), + width: parseInt(bid.width, 10), + height: parseInt(bid.height, 10), + creativeId: bid.creativeId, + dealId: bid.dealId || null, + currency: 'USD', + netRevenue: true, + mediaType: BANNER, + ad: decodeURIComponent(`${bid.ad}`), + ttl: 60000 + }); + }); + return responses; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + isBidRequestValid, + buildRequests, + interpretResponse +} + +registerBidder(spec); diff --git a/modules/viBidAdapter.md b/modules/viBidAdapter.md new file mode 100644 index 00000000000..23288024fcc --- /dev/null +++ b/modules/viBidAdapter.md @@ -0,0 +1,41 @@ +# Overview + +``` +Module Name: vi bid adapter +Module Type: Bidder adapter +Maintainer: support@vi.ai +``` + +# Description + +The video intelligence (vi) adapter integration to the Prebid library. +Connects to vi’s demand sources. +There should be only one ad unit with vi bid adapter on each single page. + +# Test Parameters + +``` +var adUnits = [{ + code: 'div-0', + sizes: [[320, 480]], + bids: [{ + bidder: 'vi', + params: { + pubId: 'sb_test', + lang: 'en-US', + cat: 'IAB1', + bidFloor: 0.05 //optional + } + }] +}]; +``` + +# Parameters + +| Name | Scope | Description | Example | +| :------------ | :------- | :---------------------------------------------- | :--------------------------------- | +| `pubId` | required | Publisher ID, provided by vi | 'sb_test' | +| `lang` | required | Ad language, in ISO 639-1 language code format | 'en-US', 'es-ES', 'de' | +| `cat` | required | Ad IAB category (top-level or subcategory), single one supported | 'IAB1', 'IAB9-1' | +| `bidFloor` | optional | Lowest value of expected bid price | 0.001 | + diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js new file mode 100644 index 00000000000..981f5603a0a --- /dev/null +++ b/modules/vidazooBidAdapter.js @@ -0,0 +1,128 @@ +import * as utils from 'src/utils'; +import {registerBidder} from 'src/adapters/bidderFactory'; +import {BANNER} from 'src/mediaTypes'; +export const URL = '//prebid.cliipa.com'; +const BIDDER_CODE = 'vidazoo'; +const CURRENCY = 'USD'; +const TTL_SECONDS = 60 * 5; +const INTERNAL_SYNC_TYPE = { + IFRAME: 'iframe', + IMAGE: 'img' +}; +const EXTERNAL_SYNC_TYPE = { + IFRAME: 'iframe', + IMAGE: 'image' +}; + +function isBidRequestValid(bid) { + const params = bid.params || {}; + return !!(params.cId && params.pId); +} + +function buildRequest(bid, topWindowUrl, size) { + const {params, bidId} = bid; + const {bidFloor, cId, pId} = params; + // Prebid's util function returns AppNexus style sizes (i.e. 300x250) + const [width, height] = size.split('x'); + + return { + method: 'GET', + url: `${URL}/prebid/${cId}`, + data: { + url: topWindowUrl, + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + publisherId: pId, + width, + height + } + } +} + +function buildRequests(validBidRequests) { + const topWindowUrl = utils.getTopWindowUrl(); + const requests = []; + validBidRequests.forEach(validBidRequest => { + const sizes = utils.parseSizesInput(validBidRequest.sizes); + sizes.forEach(size => { + const request = buildRequest(validBidRequest, topWindowUrl, size); + requests.push(request); + }); + }); + return requests; +} + +function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + const {creativeId, ad, price, exp} = serverResponse.body; + if (!ad || !price) { + return []; + } + const {bidId, width, height} = request.data; + try { + return [{ + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + ad: ad + }]; + } catch (e) { + return []; + } +} + +function getUserSyncs(syncOptions, responses) { + const {iframeEnabled, pixelEnabled} = syncOptions; + + if (iframeEnabled) { + return [{ + type: 'iframe', + url: '//static.cliipa.com/basev/sync/user_sync.html' + }]; + } + + if (pixelEnabled) { + const lookup = {}; + const syncs = []; + responses.forEach(response => { + const {body} = response; + const cookies = body ? body.cookies || [] : []; + cookies.forEach(cookie => { + switch (cookie.type) { + case INTERNAL_SYNC_TYPE.IFRAME: + break; + case INTERNAL_SYNC_TYPE.IMAGE: + if (pixelEnabled && !lookup[cookie.src]) { + syncs.push({ + type: EXTERNAL_SYNC_TYPE.IMAGE, + url: cookie.src + }); + } + break; + } + }); + }); + return syncs; + } + + return []; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/vidazooBidAdapter.md b/modules/vidazooBidAdapter.md new file mode 100644 index 00000000000..972a99a1445 --- /dev/null +++ b/modules/vidazooBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +**Module Name:** Vidazoo Bidder Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** server-dev@getintent.com + +# Description + +Module that connects to Vidazoo's demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'vidazoo', + params: { + cId: '5a1c419d95fce900044c334e', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001 + } + } + ] + } +]; +``` diff --git a/modules/xendizBidAdapter.js b/modules/xendizBidAdapter.js new file mode 100644 index 00000000000..d40d3ada229 --- /dev/null +++ b/modules/xendizBidAdapter.js @@ -0,0 +1,102 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { BANNER } from 'src/mediaTypes'; + +const BIDDER_CODE = 'xendiz'; +const PREBID_ENDPOINT = 'prebid.xendiz.com'; +const SYNC_ENDPOINT = 'https://advsync.com/xendiz/ssp/?pixel=1'; + +const buildURI = () => { + return `//${PREBID_ENDPOINT}/request`; +} + +const getDevice = () => { + const lang = navigator.language; + const width = window.screen.width; + const height = window.screen.height; + + return [lang, width, height]; +}; + +const buildItem = (req) => { + return [ + req.bidId, + req.params, + req.adUnitCode, + req.sizes.map(s => `${s[0]}x${s[1]}`) + ] +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!bid.params.pid; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(bidRequests) { + const payload = { + id: bidRequests[0].auctionId, + items: bidRequests.map(buildItem), + device: getDevice(), + page: utils.getTopWindowUrl(), + dt: +new Date() + }; + const payloadString = JSON.stringify(payload); + + return { + method: 'POST', + url: buildURI(), + data: payloadString + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse) { + const bids = serverResponse.body.bids.map(bid => { + return { + requestId: bid.id, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.crid, + netRevenue: bid.netRevenue !== undefined ? bid.netRevenue : true, + dealId: bid.dealid, + currency: bid.cur || 'USD', + ttl: bid.exp || 900, + ad: bid.adm, + } + }); + + return bids; + }, + + getUserSyncs: function(syncOptions) { + if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: SYNC_ENDPOINT + }]; + } + } +} + +registerBidder(spec); diff --git a/modules/xendizBidAdapter.md b/modules/xendizBidAdapter.md new file mode 100644 index 00000000000..4ecabe7070f --- /dev/null +++ b/modules/xendizBidAdapter.md @@ -0,0 +1,41 @@ +# Overview + +Module Name: Xendiz Bidder Adapter +Module Type: Bidder Adapter +Maintainer: hello@xendiz.com + +# Description + +Module that connects to Xendiz demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + sizes: [[300, 250]], + bids: [ + { + bidder: "xendiz", + params: { + pid: '00000000-0000-0000-0000-000000000000' + } + } + ] + },{ + code: 'test-div', + sizes: [[300, 50]], + bids: [ + { + bidder: "xendiz", + params: { + pid: '00000000-0000-0000-0000-000000000000', + ext: { + uid: '550e8400-e29b-41d4-a716-446655440000' + } + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js new file mode 100644 index 00000000000..17c205359de --- /dev/null +++ b/modules/yieldlabBidAdapter.js @@ -0,0 +1,107 @@ +import * as utils from 'src/utils' +import { registerBidder } from 'src/adapters/bidderFactory' +import find from 'core-js/library/fn/array/find' +import { VIDEO, BANNER } from 'src/mediaTypes' + +const ENDPOINT = 'https://ad.yieldlab.net' +const BIDDER_CODE = 'yieldlab' +const BID_RESPONSE_TTL_SEC = 600 +const CURRENCY_CODE = 'EUR' + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO, BANNER], + + isBidRequestValid: function (bid) { + if (bid && bid.params && bid.params.placementId && bid.params.adSize) { + return true + } + return false + }, + + /** + * This method should build correct URL + * @param validBidRequests + * @returns {{method: string, url: string}} + */ + buildRequests: function (validBidRequests) { + const placementIds = [] + const timestamp = Date.now() + + utils._each(validBidRequests, function (bid) { + placementIds.push(bid.params.placementId) + }) + + const placements = placementIds.join(',') + + return { + method: 'GET', + url: `${ENDPOINT}/yp/${placements}?ts=${timestamp}&json=true`, + validBidRequests: validBidRequests + } + }, + + /** + * Map ad values and pricing and stuff + * @param serverResponse + * @param originalBidRequest + */ + interpretResponse: function (serverResponse, originalBidRequest) { + const bidResponses = [] + const timestamp = Date.now() + + originalBidRequest.validBidRequests.forEach(function (bidRequest) { + if (!serverResponse.body) { + return + } + + let matchedBid = find(serverResponse.body, function (bidResponse) { + return bidRequest.params.placementId == bidResponse.id + }) + + if (matchedBid) { + const sizes = parseSize(bidRequest.params.adSize) + const bidResponse = { + requestId: bidRequest.bidId, + cpm: matchedBid.price / 100, + width: sizes[0], + height: sizes[1], + creativeId: '' + matchedBid.id, + dealId: matchedBid.did, + currency: CURRENCY_CODE, + netRevenue: true, + ttl: BID_RESPONSE_TTL_SEC, + referrer: '', + ad: `` + } + if (isVideo(bidRequest)) { + bidResponse.mediaType = VIDEO + bidResponse.vastUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.accountId}/1x1?ts=${timestamp}` + } + + bidResponses.push(bidResponse) + } + }) + return bidResponses + } +}; + +/** + * Is this a video format? + * @param {String} format + * @returns {Boolean} + */ +function isVideo (format) { + return utils.deepAccess(format, 'mediaTypes.video') +} + +/** + * Expands a 'WxH' string as a 2-element [W, H] array + * @param {String} size + * @returns {Array} + */ +function parseSize (size) { + return size.split('x').map(Number) +} + +registerBidder(spec) diff --git a/modules/yieldlabBidAdapter.md b/modules/yieldlabBidAdapter.md new file mode 100644 index 00000000000..0c9183aa4cd --- /dev/null +++ b/modules/yieldlabBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: Yieldlab Bidder Adapter +Module Type: Bidder Adapter +Maintainer: api@platform-lunar.com +``` + +# Description + +Module that connects to Yieldlab's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: "test1", + sizes: [[800, 250]] + bids: [{ + bidder: "yieldlab", + params: { + placement: "4206978", + accountId: "2358365", + adSize: "800x250" + } + }] + }, { + code: "test2", + sizes: [[1, 1]], + mediaTypes: { + video: { + context: "instream" + } + }, + bids: [{ + bidder: "yieldlab", + params: { + placementId: "4207034", + accountId: "2358365", + adSize: "1x1" + } + }] + } + ]; +``` diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js new file mode 100644 index 00000000000..1047224a3b7 --- /dev/null +++ b/modules/yieldmoBidAdapter.js @@ -0,0 +1,304 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; + +const BIDDER_CODE = 'yieldmo'; +const CURRENCY = 'USD'; +const TIME_TO_LIVE = 300; +const NET_REVENUE = true; +const SYNC_ENDPOINT = 'https://static.yieldmo.com/blank.min.html?orig='; +const SERVER_ENDPOINT = 'https://ads.yieldmo.com/exchange/prebid'; +const localWindow = getTopWindow(); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: ['banner'], + /** + * Determines whether or not the given bid request is valid. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ + isBidRequestValid: function(bid) { + return !!(bid && bid.adUnitCode && bid.bidId); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(bidRequests) { + let serverRequest = { + p: [], + page_url: utils.getTopWindowUrl(), + bust: new Date().getTime().toString(), + pr: utils.getTopWindowReferrer(), + scrd: localWindow.devicePixelRatio || 0, + dnt: getDNT(), + e: getEnvironment(), + description: getPageDescription(), + title: localWindow.document.title || '', + w: localWindow.innerWidth, + h: localWindow.innerHeight + }; + for (var request of bidRequests) { + serverRequest.p.push(addPlacement(request)); + } + serverRequest.p = '[' + serverRequest.p.toString() + ']'; + return { + method: 'GET', + url: SERVER_ENDPOINT, + data: serverRequest + } + }, + /** + * Makes Yieldmo Ad Server response compatible to Prebid specs + * @param serverResponse successful response from Ad Server + * @param bidderRequest original bidRequest + * @return {Bid[]} an array of bids + */ + interpretResponse: function(serverResponse) { + let bids = []; + let data = serverResponse.body; + if (data.length > 0) { + for (var response of data) { + if (response.cpm && response.cpm > 0) { + bids.push(createNewBid(response)); + } + } + } + return bids; + }, + getUserSync: function(syncOptions) { + if (trackingEnabled(syncOptions)) { + return [{ + type: 'iframe', + url: SYNC_ENDPOINT + utils.getOrigin() + }]; + } else { + return []; + } + } +} +registerBidder(spec); + +/*************************************** + * Helper Functions + ***************************************/ + +/** + * Adds placement information to array + * @param request bid request + */ +function addPlacement(request) { + const placementInfo = { + placement_id: request.adUnitCode, + callback_id: request.bidId, + sizes: request.sizes + } + if (request.params && request.params.placementId) { + placementInfo.ym_placement_id = request.params.placementId + } + return JSON.stringify(placementInfo); +} + +/** + * creates a new bid with response information + * @param response server response + */ +function createNewBid(response) { + return { + requestId: response['callback_id'], + cpm: response.cpm, + width: response.width, + height: response.height, + creativeId: response.creativeId, + currency: CURRENCY, + netRevenue: NET_REVENUE, + ttl: TIME_TO_LIVE, + ad: response.ad + }; +} + +/** + * Detects if tracking is allowed + * @returns false if dnt or if not iframe/pixel enabled + */ +function trackingEnabled(options) { + return (isIOS() && !getDNT() && options.iframeEnabled); +} + +/** + * Detects whether we're in iOS + * @returns true if in iOS + */ +function isIOS() { + return /iPhone|iPad|iPod/i.test(window.navigator.userAgent); +} + +/** + * Detects whether dnt is true + * @returns true if user enabled dnt + */ +function getDNT() { + return window.doNotTrack === '1' || window.navigator.doNotTrack === '1' || false; +} + +/** + * get page description + */ +function getPageDescription() { + if (document.querySelector('meta[name="description"]')) { + return document.querySelector('meta[name="description"]').getAttribute('content'); // Value of the description metadata from the publisher's page. + } else { + return ''; + } +} + +function getTopWindow() { + try { + return window.top; + } catch (e) { + return window; + } +} + +/*************************************** + * Detect Environment Helper Functions + ***************************************/ + +/** + * Represents a method for loading Yieldmo ads. Environments affect + * which formats can be loaded into the page + * Environments: + * CodeOnPage: 0, // div directly on publisher's page + * Amp: 1, // google Accelerate Mobile Pages ampproject.org + * Mraid = 2, // native loaded through the MRAID spec, without Yieldmo's SDK https://www.iab.net/media/file/IAB_MRAID_v2_FINAL.pdf + * Dfp: 4, // google doubleclick for publishers https://www.doubleclickbygoogle.com/ + * DfpInAmp: 5, // AMP page containing a DFP iframe + * SafeFrame: 10, + * DfpSafeFrame: 11,Sandboxed: 16, // An iframe that can't get to the top window. + * SuperSandboxed: 89, // An iframe without allow-same-origin + * Unknown: 90, // A default sandboxed implementation delivered by EnvironmentDispatch when all positive environment checks fail + */ + +/** + * Detects what environment we're in + * @returns Environment kind + */ +function getEnvironment() { + if (isSuperSandboxedIframe()) { + return 89; + } else if (isDfpInAmp()) { + return 5; + } else if (isDfp()) { + return 4; + } else if (isAmp()) { + return 1; + } else if (isDFPSafeFrame()) { + return 11; + } else if (isSafeFrame()) { + return 10; + } else if (isMraid()) { + return 2; + } else if (isCodeOnPage()) { + return 0; + } else if (isSandboxedIframe()) { + return 16; + } else { + return 90; + } +} + +/** + * @returns true if we are running on the top window at dispatch time + */ +function isCodeOnPage() { + return window === window.parent; +} + +/** + * @returns true if the environment is both DFP and AMP + */ +function isDfpInAmp() { + return isDfp() && isAmp(); +} + +/** + * @returns true if the window is in an iframe whose id and parent element id match DFP + */ +function isDfp() { + try { + const frameElement = window.frameElement; + const parentElement = window.frameElement.parentNode; + if (frameElement && parentElement) { + return frameElement.id.indexOf('google_ads_iframe') > -1 && parentElement.id.indexOf('google_ads_iframe') > -1; + } + return false; + } catch (e) { + return false; + } +} + +/** +* @returns true if there is an AMP context object +*/ +function isAmp() { + try { + const ampContext = window.context || window.parent.context; + if (ampContext && ampContext.pageViewId) { + return ampContext; + } + return false; + } catch (e) { + return false; + } +} + +/** + * @returns true if the environment is a SafeFrame. + */ +function isSafeFrame() { + return window.$sf && window.$sf.ext; +} + +/** + * @returns true if the environment is a dfp safe frame. + */ +function isDFPSafeFrame() { + if (window.location && window.location.href) { + const href = window.location.href; + return isSafeFrame() && href.indexOf('google') !== -1 && href.indexOf('safeframe') !== -1; + } + return false; +} + +/** + * Return true if we are in an iframe and can't access the top window. + */ +function isSandboxedIframe() { + return window.top !== window && !window.frameElement; +} + +/** + * Return true if we cannot document.write to a child iframe (this implies no allow-same-origin) + */ +function isSuperSandboxedIframe() { + const sacrificialIframe = window.document.createElement('iframe'); + try { + sacrificialIframe.setAttribute('style', 'display:none'); + window.document.body.appendChild(sacrificialIframe); + sacrificialIframe.contentWindow._testVar = true; + window.document.body.removeChild(sacrificialIframe); + return false; + } catch (e) { + window.document.body.removeChild(sacrificialIframe); + return true; + } +} + +/** + * @returns true if the window has the attribute identifying MRAID + */ +function isMraid() { + return !!(window.mraid); +} diff --git a/modules/yieldmoBidAdapter.md b/modules/yieldmoBidAdapter.md new file mode 100644 index 00000000000..b93f72594bc --- /dev/null +++ b/modules/yieldmoBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +``` +Module Name: Yieldmo Bid Adapter +Module Type: Bidder Adapter +Maintainer: melody@yieldmo.com +Note: Our ads will only render in mobile +``` + +# Description + +Connects to Yieldmo Ad Server for bids. + +Yieldmo bid adapter supports Banner. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'div-gpt-ad-1460505748561-0', + sizes: [[300, 250], [300,600]], + bids: [{ + bidder: 'yieldmo', + params: { + placementId: '1779781193098233305' // string with at most 19 characters (may include numbers only) + } + }] + } +]; +``` \ No newline at end of file diff --git a/package.json b/package.json index b4ffaecda35..fdc8e5b7ea8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "1.1.1", + "version": "1.3.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { diff --git a/src/adaptermanager.js b/src/adaptermanager.js index 5577ffc6fca..b4a874a0736 100644 --- a/src/adaptermanager.js +++ b/src/adaptermanager.js @@ -234,7 +234,7 @@ exports.checkBidRequestSizes = (adUnits) => { if (mediaTypes && mediaTypes.video) { const video = mediaTypes.video; if (video.playerSize) { - if (Array.isArray(video.playerSize) && video.playerSize.length === 2 && Number.isInteger(video.playerSize[0]) && Number.isInteger(video.playerSize[1])) { + if (Array.isArray(video.playerSize) && video.playerSize.length === 2 && utils.isInteger(video.playerSize[0]) && utils.isInteger(video.playerSize[1])) { adUnit.sizes = video.playerSize; } else { utils.logError('Detected incorrect configuration of mediaTypes.video.playerSize. Please specify only one set of dimensions in a format like: [640, 480]. Removing invalid mediaTypes.video.playerSize property from request.'); diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index ed3cc28db32..10315241c40 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -172,8 +172,14 @@ export function newBidder(spec) { // After all the responses have come back, call done() and // register any required usersync pixels. const responses = []; - function afterAllResponses() { - done(); + function afterAllResponses(bids) { + const videoBid = bids && bids[0] && bids[0].mediaType && bids[0].mediaType === 'video'; + const cacheEnabled = config.getConfig('cache.url'); + + // video bids with cache enabled need to be cached first before they are considered done + if (!(videoBid && cacheEnabled)) { + done(); + } registerSyncs(responses); } @@ -281,7 +287,7 @@ export function newBidder(spec) { addBidUsingRequestMap(bids); } } - onResponse(); + onResponse(bids); function addBidUsingRequestMap(bid) { const bidRequest = bidRequestMap[bid.requestId]; diff --git a/src/auction.js b/src/auction.js index 2a4cc128c2a..0af6458ac09 100644 --- a/src/auction.js +++ b/src/auction.js @@ -56,7 +56,6 @@ import { Renderer } from 'src/Renderer'; import { config } from 'src/config'; import { userSync } from 'src/userSync'; import { createHook } from 'src/hook'; -import { videoAdUnit } from 'src/video'; import find from 'core-js/library/fn/array/find'; import includes from 'core-js/library/fn/array/includes'; @@ -153,20 +152,9 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels}) return innerBidRequestId === bidRequest.bidderRequestId; }); - const nonVideoBid = request.bids.filter(videoAdUnit).length === 0; - const videoBid = request.bids.filter(videoAdUnit).length > 0; - const videoBidNoCache = videoBid && !config.getConfig('cache.url'); - const videoBidWithCache = videoBid && config.getConfig('cache.url'); - - // video bids with cache enabled need to be cached first before saying they are done - if (!videoBidWithCache) { - request.doneCbCallCount += 1; - } - - // in case of mediaType video and prebidCache enabled, call bidsBackHandler after cache is stored. - if (nonVideoBid || videoBidNoCache) { - bidsBackAll() - } + // this is done for cache-enabled video bids in tryAddVideoBids, after the cache is stored + request.doneCbCallCount += 1; + bidsBackAll(); }, 1); } @@ -221,6 +209,44 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels}) } } +function doCallbacksIfTimedout(auctionInstance, bidResponse) { + if (bidResponse.timeToRespond > auctionInstance.getTimeout() + config.getConfig('timeoutBuffer')) { + auctionInstance.executeCallback(true); + } +} + +// Add a bid to the auction. +function addBidToAuction(auctionInstance, bidResponse) { + events.emit(CONSTANTS.EVENTS.BID_RESPONSE, bidResponse); + auctionInstance.addBidReceived(bidResponse); + + doCallbacksIfTimedout(auctionInstance, bidResponse); +} + +// Video bids may fail if the cache is down, or there's trouble on the network. +function tryAddVideoBid(auctionInstance, bidResponse, bidRequest, vastUrl) { + if (config.getConfig('cache.url')) { + store([bidResponse], function(error, cacheIds) { + if (error) { + utils.logWarn(`Failed to save to the video cache: ${error}. Video bid must be discarded.`); + + doCallbacksIfTimedout(auctionInstance, bidResponse); + } else { + bidResponse.videoCacheKey = cacheIds[0].uuid; + if (!vastUrl) { + bidResponse.vastUrl = getCacheUrl(bidResponse.videoCacheKey); + } + // only set this prop after the bid has been cached to avoid early ending auction early in bidsBackAll + bidRequest.doneCbCallCount += 1; + addBidToAuction(auctionInstance, bidResponse); + auctionInstance.bidsBackAll(); + } + }); + } else { + addBidToAuction(auctionInstance, bidResponse); + } +} + export const addBidResponse = createHook('asyncSeries', function(adUnitCode, bid) { let auctionInstance = this; let bidRequests = auctionInstance.getBidRequests(); @@ -230,46 +256,9 @@ export const addBidResponse = createHook('asyncSeries', function(adUnitCode, bid let bidResponse = getPreparedBidForAuction({adUnitCode, bid, bidRequest, auctionId}); if (bidResponse.mediaType === 'video') { - tryAddVideoBid(bidResponse); + tryAddVideoBid(auctionInstance, bidResponse, bidRequest, bid.vastUrl); } else { - doCallbacksIfNeeded(); - addBidToAuction(bidResponse); - } - - function doCallbacksIfNeeded() { - if (bidResponse.timeToRespond > auctionInstance.getTimeout() + config.getConfig('timeoutBuffer')) { - auctionInstance.executeCallback(true); - } - } - - // Add a bid to the auction. - function addBidToAuction() { - events.emit(CONSTANTS.EVENTS.BID_RESPONSE, bidResponse); - auctionInstance.addBidReceived(bidResponse); - } - - // Video bids may fail if the cache is down, or there's trouble on the network. - function tryAddVideoBid(bidResponse) { - if (config.getConfig('cache.url')) { - store([bidResponse], function(error, cacheIds) { - if (error) { - utils.logWarn(`Failed to save to the video cache: ${error}. Video bid must be discarded.`); - } else { - bidResponse.videoCacheKey = cacheIds[0].uuid; - if (!bid.vastUrl) { - bidResponse.vastUrl = getCacheUrl(bidResponse.videoCacheKey); - } - // only set this prop after the bid has been cached to avoid early ending auction early in bidsBackAll - bidRequest.doneCbCallCount += 1; - addBidToAuction(bidResponse); - auctionInstance.bidsBackAll(); - } - doCallbacksIfNeeded(); - }); - } else { - addBidToAuction(bidResponse); - doCallbacksIfNeeded(); - } + addBidToAuction(auctionInstance, bidResponse); } }, 'addBidResponse'); @@ -299,7 +288,7 @@ function getPreparedBidForAuction({adUnitCode, bid, bidRequest, auctionId}) { const adUnitRenderer = bidRequest.bids && bidRequest.bids[0] && bidRequest.bids[0].renderer; - if (adUnitRenderer) { + if (adUnitRenderer && adUnitRenderer.url) { bidObject.renderer = Renderer.install({ url: adUnitRenderer.url }); bidObject.renderer.setRender(adUnitRenderer.render); } @@ -379,7 +368,13 @@ export function getStandardBidderSettings() { val: function (bidResponse) { return bidResponse.source; } - } + }, + { + key: 'hb_format', + val: function (bidResponse) { + return bidResponse.mediaType; + } + }, ] } return bidder_settings[CONSTANTS.JSON_MAPPING.BD_SETTING_STANDARD]; @@ -403,7 +398,7 @@ export function getKeyValueTargetingPairs(bidderCode, custBidObj) { } // set native key value targeting - if (custBidObj.native) { + if (custBidObj['native']) { keyValues = Object.assign({}, keyValues, getNativeTargeting(custBidObj)); } diff --git a/src/config.js b/src/config.js index 146c0f17a65..ff68fc7bfff 100644 --- a/src/config.js +++ b/src/config.js @@ -10,6 +10,7 @@ import { isValidPriceConfig } from './cpmBucketManager'; import find from 'core-js/library/fn/array/find'; import includes from 'core-js/library/fn/array/includes'; +import { createHook } from 'src/hook'; const utils = require('./utils'); const DEFAULT_DEBUG = false; @@ -188,7 +189,7 @@ export function newConfig() { * Sets configuration given an object containing key-value pairs and calls * listeners that were added by the `subscribe` function */ - function setConfig(options) { + let setConfig = createHook('asyncSeries', function setConfig(options) { if (typeof options !== 'object') { utils.logError('setConfig options must be an object'); return; @@ -208,7 +209,7 @@ export function newConfig() { }); callSubscribers(topicalConfig); - } + }); /** * Sets configuration defaults which setConfig values can be applied on top of diff --git a/src/constants.json b/src/constants.json index e80c118ea83..9a2639ab83f 100644 --- a/src/constants.json +++ b/src/constants.json @@ -52,7 +52,8 @@ "hb_pb", "hb_size", "hb_deal", - "hb_source" + "hb_source", + "hb_format" ], "S2S" : { "SRC" : "s2s", diff --git a/src/prebid.js b/src/prebid.js index de74f2c4d7b..86fd678ee7e 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -2,14 +2,13 @@ import { getGlobal } from './prebidGlobal'; import { flatten, uniques, isGptPubadsDefined, adUnitsFilter, removeRequestId } from './utils'; -import { videoAdUnit, videoBidder, hasNonVideoBidder } from './video'; -import { nativeAdUnit, nativeBidder, hasNonNativeBidder } from './native'; import { listenMessagesFromCreative } from './secureCreatives'; import { userSync } from 'src/userSync.js'; import { loadScript } from './adloader'; import { config } from './config'; import { auctionManager } from './auctionManager'; import { targeting } from './targeting'; +import { createHook } from 'src/hook'; import includes from 'core-js/library/fn/array/includes'; var $$PREBID_GLOBAL$$ = getGlobal(); @@ -283,7 +282,7 @@ $$PREBID_GLOBAL$$.removeAdUnit = function (adUnitCode) { * @param {Array} requestOptions.labels * @alias module:pbjs.requestBids */ -$$PREBID_GLOBAL$$.requestBids = function ({ bidsBackHandler, timeout, adUnits, adUnitCodes, labels } = {}) { +$$PREBID_GLOBAL$$.requestBids = createHook('asyncSeries', function ({ bidsBackHandler, timeout, adUnits, adUnitCodes, labels } = {}) { events.emit('requestBids'); const cbTimeout = timeout || config.getConfig('bidderTimeout'); adUnits = adUnits || $$PREBID_GLOBAL$$.adUnits; @@ -298,24 +297,34 @@ $$PREBID_GLOBAL$$.requestBids = function ({ bidsBackHandler, timeout, adUnits, a adUnitCodes = adUnits && adUnits.map(unit => unit.code); } - // for video-enabled adUnits, only request bids for bidders that support video - adUnits.filter(videoAdUnit).filter(hasNonVideoBidder).forEach(adUnit => { - const nonVideoBidders = adUnit.bids - .filter(bid => !videoBidder(bid)) - .map(bid => bid.bidder); - - utils.logWarn(utils.unsupportedBidderMessage(adUnit, nonVideoBidders)); - adUnit.bids = adUnit.bids.filter(videoBidder); - }); - - // for native-enabled adUnits, only request bids for bidders that support native - adUnits.filter(nativeAdUnit).filter(hasNonNativeBidder).forEach(adUnit => { - const nonNativeBidders = adUnit.bids - .filter(bid => !nativeBidder(bid)) - .map(bid => bid.bidder); - - utils.logWarn(utils.unsupportedBidderMessage(adUnit, nonNativeBidders)); - adUnit.bids = adUnit.bids.filter(nativeBidder); + /* + * for a given adunit which supports a set of mediaTypes + * and a given bidder which supports a set of mediaTypes + * a bidder is eligible to participate on the adunit + * if it supports at least one of the mediaTypes on the adunit + */ + adUnits.forEach(adUnit => { + // get the adunit's mediaTypes, defaulting to banner if mediaTypes isn't present + const adUnitMediaTypes = Object.keys(adUnit.mediaTypes || {'banner': 'banner'}); + + // get the bidder's mediaTypes + const bidders = adUnit.bids.map(bid => bid.bidder); + const bidderRegistry = adaptermanager.bidderRegistry; + + bidders.forEach(bidder => { + const adapter = bidderRegistry[bidder]; + const spec = adapter && adapter.getSpec && adapter.getSpec() + // banner is default if not specified in spec + const bidderMediaTypes = (spec && spec.supportedMediaTypes) || ['banner']; + + // check if the bidder's mediaTypes are not in the adUnit's mediaTypes + const bidderEligible = adUnitMediaTypes.some(type => includes(bidderMediaTypes, type)); + if (!bidderEligible) { + // drop the bidder from the ad unit if it's not compatible + utils.logWarn(utils.unsupportedBidderMessage(adUnit, bidder)); + adUnit.bids = adUnit.bids.filter(bid => bid.bidder !== bidder); + } + }); }); if (!adUnits || adUnits.length === 0) { @@ -333,7 +342,8 @@ $$PREBID_GLOBAL$$.requestBids = function ({ bidsBackHandler, timeout, adUnits, a const auction = auctionManager.createAuction({adUnits, adUnitCodes, callback: bidsBackHandler, cbTimeout, labels}); auction.callBids(); -}; + return auction; +}); /** * diff --git a/src/utils.js b/src/utils.js index 2d185ebe4c6..0957b4e6c8f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -832,17 +832,16 @@ export function isSlotMatchingAdUnitCode(adUnitCode) { /** * Constructs warning message for when unsupported bidders are dropped from an adunit * @param {Object} adUnit ad unit from which the bidder is being dropped - * @param {Array} unSupportedBidders arrary of bidder codes that are not compatible with the adUnit + * @param {string} bidder bidder code that is not compatible with the adUnit * @return {string} warning message to display when condition is met */ -export function unsupportedBidderMessage(adUnit, unSupportedBidders) { - const mediaType = adUnit.mediaType || Object.keys(adUnit.mediaTypes).join(', '); - const plural = unSupportedBidders.length === 1 ? 'This bidder' : 'These bidders'; +export function unsupportedBidderMessage(adUnit, bidder) { + const mediaType = Object.keys(adUnit.mediaTypes || {'banner': 'banner'}).join(', '); return ` ${adUnit.code} is a ${mediaType} ad unit - containing bidders that don't support ${mediaType}: ${unSupportedBidders.join(', ')}. - ${plural} won't fetch demand. + containing bidders that don't support ${mediaType}: ${bidder}. + This bidder won't fetch demand. `; } diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index 3429087a24f..c73679c5b8c 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -37,6 +37,7 @@ describe('auctionmanager.js', function () { var size = '300x250'; var adId = '1adId'; var source = 'client'; + var mediatype = 'banner'; before(function () { bid.cpm = bidPriceCpm; @@ -54,6 +55,7 @@ describe('auctionmanager.js', function () { bid.bidderCode = bidderCode; bid.adId = adId; bid.source = source; + bid.mediaType = mediatype; }); it('No bidder level configuration defined - default', function () { @@ -62,7 +64,8 @@ describe('auctionmanager.js', function () { 'hb_adid': adId, 'hb_pb': bidPbMg, 'hb_size': size, - 'hb_source': source + 'hb_source': source, + 'hb_format': mediatype, }; var response = getKeyValueTargetingPairs(bidderCode, bid, CONSTANTS.GRANULARITY_OPTIONS.MEDIUM); assert.deepEqual(response, expected); @@ -100,7 +103,13 @@ describe('auctionmanager.js', function () { val: function (bidResponse) { return bidResponse.source; } - } + }, + { + key: 'hb_format', + val: function (bidResponse) { + return bidResponse.mediaType; + } + }, ] } @@ -111,7 +120,8 @@ describe('auctionmanager.js', function () { 'hb_adid': adId, 'hb_pb': bidPbHg, 'hb_size': size, - 'hb_source': source + 'hb_source': source, + 'hb_format': mediatype, }; var response = getKeyValueTargetingPairs(bidderCode, bid, CONSTANTS.GRANULARITY_OPTIONS.MEDIUM); assert.deepEqual(response, expected); @@ -154,7 +164,8 @@ describe('auctionmanager.js', function () { 'hb_adid': adId, 'hb_pb': bidPbHg, 'hb_size': size, - 'hb_source': source + 'hb_source': source, + 'hb_format': mediatype, }; var response = getKeyValueTargetingPairs(bidderCode, bid); assert.deepEqual(response, expected); @@ -197,7 +208,8 @@ describe('auctionmanager.js', function () { 'hb_adid': adId, 'hb_pb': bidPbMg, 'hb_size': size, - 'hb_source': source + 'hb_source': source, + 'hb_format': mediatype, }; var response = getKeyValueTargetingPairs(bidderCode, bid, CONSTANTS.GRANULARITY_OPTIONS.MEDIUM); assert.deepEqual(response, expected); @@ -362,7 +374,8 @@ describe('auctionmanager.js', function () { 'hb_adid': adId, 'hb_pb': 5.57, 'hb_size': '300x250', - 'hb_source': source + 'hb_source': source, + 'hb_format': mediatype, }; var response = getKeyValueTargetingPairs(bidderCode, bid); assert.deepEqual(response, expected); @@ -453,6 +466,7 @@ describe('auctionmanager.js', function () { let auction; let ajaxStub; const BIDDER_CODE = 'sampleBidder'; + const BIDDER_CODE1 = 'sampleBidder1'; let makeRequestsStub; let bids = [{ 'ad': 'creative', @@ -643,57 +657,6 @@ describe('auctionmanager.js', function () { assert.equal(addedBid.renderer.url, 'renderer.js'); }); }); - - describe('with auction timeout 20', () => { - let auction; - let adUnits; - let adUnitCodes; - let createAuctionStub; - let spec; - let getBidderRequestStub; - let eventsEmitSpy; - - beforeEach(() => { - adUnits = [{ - code: 'adUnit-code', - bids: [ - {bidder: BIDDER_CODE, params: {placementId: 'id'}}, - ] - }]; - adUnitCodes = ['adUnit-code']; - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: 20}); - createAuctionStub = sinon.stub(auctionModule, 'newAuction'); - createAuctionStub.returns(auction); - getBidderRequestStub = sinon.stub(utils, 'getBidderRequest'); - - let newBidRequest = Object.assign({}, bidRequests[0], {'start': 1000}); - getBidderRequestStub.returns(newBidRequest); - - spec = { - code: BIDDER_CODE, - isBidRequestValid: sinon.stub(), - buildRequests: sinon.stub(), - interpretResponse: sinon.stub(), - getUserSyncs: sinon.stub() - }; - eventsEmitSpy = sinon.spy(events, 'emit'); - }); - - afterEach(() => { - auctionModule.newAuction.restore(); - utils.getBidderRequest.restore(); - events.emit.restore(); - }); - - it('should emit BID_TIMEOUT for timed out bids', () => { - registerBidder(spec); - spec.buildRequests.returns([{'id': 123, 'method': 'POST'}]); - spec.isBidRequestValid.returns(true); - spec.interpretResponse.returns(bids); - auction.callBids(); - assert.ok(eventsEmitSpy.calledWith(CONSTANTS.EVENTS.BID_TIMEOUT), 'emitted events BID_TIMEOUT'); - }); - }); }); describe('addBidResponse', () => { diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index 0b2d65e7db9..9a322821b13 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -2,7 +2,6 @@ const { userSync } = require('../../../src/userSync'); const { config } = require('../../../src/config'); const { expect } = require('chai'); - const { isBidRequestValid, buildRequests, @@ -44,78 +43,71 @@ describe('33acrossBidAdapter:', function () { }); describe('isBidRequestValid:', function () { - context('valid bid request:', function () { - it('returns true when bidder, params.siteId, params.productId are set', function() { - const validBid = { - bidder: BIDDER_CODE, - params: { - siteId: SITE_ID, - productId: PRODUCT_ID - } + it('returns true when valid bid request is sent', function() { + const validBid = { + bidder: BIDDER_CODE, + params: { + siteId: SITE_ID, + productId: PRODUCT_ID } + } - expect(isBidRequestValid(validBid)).to.be.true; - }) + expect(isBidRequestValid(validBid)).to.be.true; }); - context('valid test bid request:', function () { - it('returns true when bidder, params.site.id, params.productId are set', function() { - const validBid = { - bidder: BIDDER_CODE, - params: { - site: { - id: SITE_ID - }, - productId: PRODUCT_ID - } + it('returns true when valid test bid request is sent', function() { + const validBid = { + bidder: BIDDER_CODE, + params: { + siteId: SITE_ID, + productId: PRODUCT_ID, + test: 1 } + } - expect(isBidRequestValid(validBid)).to.be.true; - }); + expect(isBidRequestValid(validBid)).to.be.true; }); - context('invalid bid request:', function () { - it('returns false when bidder not set to "33across"', function () { - const invalidBid = { - bidder: 'foo', - params: { - siteId: SITE_ID, - productId: PRODUCT_ID - } + it('returns false when bidder not set to "33across"', function () { + const invalidBid = { + bidder: 'foo', + params: { + siteId: SITE_ID, + productId: PRODUCT_ID } + } - expect(isBidRequestValid(invalidBid)).to.be.false; - }); + expect(isBidRequestValid(invalidBid)).to.be.false; + }); - it('returns false when params not set', function() { - const invalidBid = { - bidder: 'foo' - } + it('returns false when params not set', function() { + const invalidBid = { + bidder: 'foo' + } - expect(isBidRequestValid(invalidBid)).to.be.false; - }); + expect(isBidRequestValid(invalidBid)).to.be.false; + }); - it('returns false when params.siteId or params.site.id not set', function() { - const invalidBid = { - bidder: 'foo', - params: { - productId: PRODUCT_ID - } + it('returns false when site ID is not set in params', function() { + const invalidBid = { + bidder: 'foo', + params: { + productId: PRODUCT_ID } + } - expect(isBidRequestValid(invalidBid)).to.be.false; - }); + expect(isBidRequestValid(invalidBid)).to.be.false; + }); - it('returns false when params.productId not set', function() { - const invalidBid = { - bidder: 'foo', - params: { - siteId: SITE_ID - } + it('returns false when product ID not set in params', function() { + const invalidBid = { + bidder: 'foo', + params: { + siteId: SITE_ID } + } - expect(isBidRequestValid(invalidBid)).to.be.false; - }); + expect(isBidRequestValid(invalidBid)).to.be.false; }); }); @@ -148,27 +140,76 @@ describe('33acrossBidAdapter:', function () { }, id: 'b1' }; + const serverRequest = { + 'method': 'POST', + 'url': END_POINT, + 'data': JSON.stringify(ttxRequest), + 'options': { + 'contentType': 'application/json', + 'withCredentials': false + } + } + const builtServerRequests = buildRequests(this.bidRequests); + expect(builtServerRequests).to.deep.equal([ serverRequest ]); + expect(builtServerRequests.length).to.equal(1); + }); + + it('returns corresponding test server requests for each valid bidRequest', function() { + this.sandbox.stub(config, 'getConfig', () => { + return { + 'url': 'https://foo.com/hb/' + } + }); + + const ttxRequest = { + imp: [ { + banner: { + format: [ + { + w: 300, + h: 250, + ext: { } + }, + { + w: 728, + h: 90, + ext: { } + } + ] + }, + ext: { + ttx: { + prod: PRODUCT_ID + } + } + } ], + site: { + id: SITE_ID + }, + id: 'b1' + }; const serverRequest = { method: 'POST', - url: END_POINT, + url: 'https://foo.com/hb/', data: JSON.stringify(ttxRequest), options: { contentType: 'application/json', withCredentials: false } - } + }; + const builtServerRequests = buildRequests(this.bidRequests); expect(builtServerRequests).to.deep.equal([ serverRequest ]); expect(builtServerRequests.length).to.equal(1); }); - it('returns corresponding test server requests for each valid bidRequest', function() { + it('returns corresponding test server requests for each valid test bidRequest', function() { this.sandbox.stub(config, 'getConfig', () => { return { 'url': 'https://foo.com/hb/' } }); - + this.bidRequests[0].params.test = 1; const ttxRequest = { imp: [ { banner: { @@ -176,12 +217,12 @@ describe('33acrossBidAdapter:', function () { { w: 300, h: 250, - ext: {} + ext: { } }, { w: 728, h: 90, - ext: {} + ext: { } } ] }, @@ -194,7 +235,8 @@ describe('33acrossBidAdapter:', function () { site: { id: SITE_ID }, - id: 'b1' + id: 'b1', + test: 1 }; const serverRequest = { method: 'POST', @@ -268,11 +310,7 @@ describe('33acrossBidAdapter:', function () { bid: [ { id: '1', adm: '
', - ext: { - rp: { - advid: 1 - } - }, + crid: 1, h: 250, w: 300, price: 0.0938 @@ -322,11 +360,7 @@ describe('33acrossBidAdapter:', function () { bid: [ { id: '1', adm: '
', - ext: { - rp: { - advid: 1 - } - }, + crid: 1, h: 250, w: 300, price: 0.0940 @@ -334,11 +368,7 @@ describe('33acrossBidAdapter:', function () { { id: '2', adm: '
', - ext: { - rp: { - advid: 2 - } - }, + crid: 2, h: 250, w: 300, price: 0.0938 @@ -349,11 +379,7 @@ describe('33acrossBidAdapter:', function () { bid: [ { id: '3', adm: '
', - ext: { - rp: { - advid: 3 - } - }, + crid: 3, h: 250, w: 300, price: 0.0938 @@ -373,7 +399,7 @@ describe('33acrossBidAdapter:', function () { creativeId: 1, currency: 'USD', netRevenue: true - } + }; expect(interpretResponse({ body: serverResponse }, this.serverRequest)).to.deep.equal([ bidResponse ]); }); diff --git a/test/spec/modules/adformBidAdapter_spec.js b/test/spec/modules/adformBidAdapter_spec.js index 72f625d08c6..357e6e67f4d 100644 --- a/test/spec/modules/adformBidAdapter_spec.js +++ b/test/spec/modules/adformBidAdapter_spec.js @@ -22,7 +22,7 @@ describe('Adform adapter', () => { adxDomain: 'adx.adform.net' }; assert.isFalse(spec.isBidRequestValid(bid)); - }) + }); }); describe('buildRequests', () => { @@ -147,6 +147,7 @@ describe('Adform adapter', () => { bids = [ { adUnitCode: placementCode[0], + auctionId: '7aefb970-2045', bidId: '2a0cf4e', bidder: 'adform', bidderRequestId: '1ab8d9', @@ -154,29 +155,28 @@ describe('Adform adapter', () => { adxDomain: 'newDomain', tid: 45, placementCode: placementCode[0], - requestId: '7aefb970-2045', sizes: [[300, 250], [250, 300], [300, 600], [600, 300]], transactionId: '5f33781f-9552-4ca1' }, { adUnitCode: placementCode[1], + auctionId: '7aefb970-2045', bidId: '2a0cf5b', bidder: 'adform', bidderRequestId: '1ab8d9', params: params[1], placementCode: placementCode[1], - requestId: '7aefb970-2045', sizes: [[300, 250], [250, 300], [300, 600], [600, 300]], transactionId: '5f33781f-9552-4iuy' }, { adUnitCode: placementCode[2], + auctionId: '7aefb970-2045', bidId: '2a0cf6n', bidder: 'adform', bidderRequestId: '1ab8d9', params: params[2], placementCode: placementCode[2], - requestId: '7aefb970-2045', sizes: [[300, 250], [250, 300], [300, 600], [600, 300]], transactionId: '5f33781f-9552-7ev3' } diff --git a/test/spec/modules/adgenerationBidAdapter_spec.js b/test/spec/modules/adgenerationBidAdapter_spec.js new file mode 100644 index 00000000000..7718d6fabac --- /dev/null +++ b/test/spec/modules/adgenerationBidAdapter_spec.js @@ -0,0 +1,333 @@ +import {expect} from 'chai'; +import * as utils from 'src/utils'; +import {spec} from 'modules/adgenerationBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; +import {NATIVE} from 'src/mediaTypes'; + +describe('AdgenerationAdapter', () => { + const adapter = newBidder(spec); + const ENDPOINT = ['http://api-test.scaleout.jp/adsv/v1', 'https://d.socdm.com/adsv/v1']; + + describe('inherited functions', () => { + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', () => { + let bid = { + 'bidder': 'adg', + 'params': { + id: '58278', // banner + } + }; + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + const bidRequests = [ + { // banner + bidder: 'adg', + params: { + id: '58278', + width: '300', + height: '250' + }, + adUnitCode: 'adunit-code', + sizes: [[300, 250]], + bidId: '2f6ac468a9c15e', + bidderRequestId: '14a9f773e30243', + auctionId: '4aae9f05-18c6-4fcd-80cf-282708cd584a', + transactionTd: 'f76f6dfd-d64f-4645-a29f-682bac7f431a' + }, + { // native + bidder: 'adg', + params: { + id: '58278', + width: '300', + height: '250' + }, + mediaTypes: { + native: { + image: { + required: true + }, + title: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + body: { + required: true + }, + icon: { + required: true + } + }, + }, + adUnitCode: 'adunit-code', + sizes: [[1, 1]], + bidId: '2f6ac468a9c15e', + bidderRequestId: '14a9f773e30243', + auctionId: '4aae9f05-18c6-4fcd-80cf-282708cd584a', + transactionTd: 'f76f6dfd-d64f-4645-a29f-682bac7f431a' + } + ]; + const data = { + banner: 'posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&imark=1', + native: 'posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3' + }; + it('sends bid request to ENDPOINT via GET', () => { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.url).to.equal(ENDPOINT[1]); + expect(request.method).to.equal('GET'); + }); + + it('sends bid request to debug ENDPOINT via GET', () => { + bidRequests[0].params.debug = true; + const request = spec.buildRequests(bidRequests)[0]; + expect(request.url).to.equal(ENDPOINT[0]); + expect(request.method).to.equal('GET'); + }); + + it('should attache params to the banner request', () => { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.data).to.equal(data.banner); + }); + + it('should attache params to the native request', () => { + const request = spec.buildRequests(bidRequests)[1]; + expect(request.data).to.equal(data.native); + }); + }); + + describe('interpretResponse', () => { + const bidRequests = { + bidRequest: { + bidder: 'adg', + params: { + id: '58278', // banner + }, + mediaTypes: { + native: { + image: { + required: true + }, + title: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + body: { + required: true + }, + icon: { + required: true + } + } + }, + adUnitCode: 'adunit-code', + sizes: [[1, 1]], + bidId: '2f6ac468a9c15e', + bidderRequestId: '14a9f773e30243', + auctionId: '4aae9f05-18c6-4fcd-80cf-282708cd584a', + transactionTd: 'f76f6dfd-d64f-4645-a29f-682bac7f431a' + } + }; + + const serverResponse = { + ad: '↵
↵ ↵ ↵ ↵ ↵ ↵
', + beacon: '', + cpm: 36.0008, + displaytype: '1', + ids: {}, + location_params: null, + locationid: '58279', + native_ad: { + assets: [ + { + data: { + label: 'accompanying_text', + value: 'AD' + }, + id: 501 + }, + { + data: { + label: 'optout_url', + value: 'https://supership.jp/optout/' + }, + id: 502 + }, + { + data: { + ext: { + black_back: 'https://i.socdm.com/sdk/img/icon_adg_optout_26x26_white.png', + }, + label: 'information_icon_url', + value: 'https://i.socdm.com/sdk/img/icon_adg_optout_26x26_gray.png', + id: 503 + } + }, + { + id: 1, + required: 1, + title: {text: 'Title'} + }, + { + id: 2, + img: { + h: 250, + url: 'https://s3-ap-northeast-1.amazonaws.com/sdk-temp/adg-sample-ad/img/300x250.png', + w: 300 + }, + required: 1 + }, + { + id: 3, + img: { + h: 300, + url: 'https://placehold.jp/300x300.png', + w: 300 + }, + required: 1 + }, + { + data: {value: 'Description'}, + id: 5, + required: 0 + }, + { + data: {value: 'CTA'}, + id: 6, + required: 0 + }, + { + data: {value: 'Sponsored'}, + id: 4, + required: 0 + } + ], + imptrackers: ['https://s3-ap-northeast-1.amazonaws.com/adg-dummy-dsp/1x1.gif'], + link: { + clicktrackers: [ + 'https://s3-ap-northeast-1.amazonaws.com/adg-dummy-dsp/1x1_clicktracker_access.gif' + ], + url: 'https://supership.jp' + }, + }, + rotation: '0', + scheduleid: '512603', + sdktype: '0', + creativeid: '1k2kv35vsa5r', + dealid: 'fd5sa5fa7f', + ttl: 1000 + }; + + const bidResponses = [ + { + requestId: '2f6ac468a9c15e', + cpm: 36.0008, + width: 1, + height: 1, + creativeId: '1k2kv35vsa5r', + dealId: 'fd5sa5fa7f', + currency: 'JPY', + netRevenue: true, + ttl: 1000, + referrer: utils.getTopWindowUrl(), + ad: '↵
↵ ', + native: { + title: 'Title', + image: { + url: 'https://s3-ap-northeast-1.amazonaws.com/sdk-temp/adg-sample-ad/img/300x250.png', + height: 250, + width: 300 + }, + icon: { + url: 'https://placehold.jp/300x300.png', + height: 300, + width: 300 + }, + sponsoredBy: 'Sponsored', + body: 'Description', + cta: 'CTA', + clickUrl: 'https://supership.jp', + clickTrackers: ['https://s3-ap-northeast-1.amazonaws.com/adg-dummy-dsp/1x1_clicktracker_access.gif'], + impressionTrackers: ['https://s3-ap-northeast-1.amazonaws.com/adg-dummy-dsp/1x1.gif'] + }, + mediaType: NATIVE + } + ]; + + it('no bid responses', () => { + const result = spec.interpretResponse({body: serverResponse}, bidRequests); + expect(result.length).to.equal(0); + }); + + it('handles native responses', () => { + serverResponse.results = [{ad: '