From 1ae44aa5a1dbe767361c64f9841d8e73752428ad Mon Sep 17 00:00:00 2001 From: Klaas-Jan Boon Date: Tue, 14 Apr 2020 14:16:53 +0200 Subject: [PATCH 1/5] add Blue Billywig adapter --- modules/bluebillywigBidAdapter.js | 490 ++++++++++ modules/bluebillywigBidAdapter.md | 38 + .../modules/bluebillywigBidAdapter_spec.js | 886 ++++++++++++++++++ 3 files changed, 1414 insertions(+) create mode 100644 modules/bluebillywigBidAdapter.js create mode 100644 modules/bluebillywigBidAdapter.md create mode 100644 test/spec/modules/bluebillywigBidAdapter_spec.js diff --git a/modules/bluebillywigBidAdapter.js b/modules/bluebillywigBidAdapter.js new file mode 100644 index 00000000000..5bec6679732 --- /dev/null +++ b/modules/bluebillywigBidAdapter.js @@ -0,0 +1,490 @@ +import * as utils from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import adapterManager from '../src/adapterManager.js'; +import { VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; +import { Renderer } from '../src/Renderer.js'; + +const DEV_MODE = window.location.search.match(/bbpbs_debug=true/); + +// Blue Billywig Constants +export const BB_CONSTANTS = { + BIDDER_CODE: 'bluebillywig', + AUCTION_URL: '$$URL_STARTpbs.bluebillywig.com/openrtb2/auction?pub=$$PUBLICATION', + SYNC_URL: '$$URL_STARTpbs.bluebillywig.com/static/cookie-sync.html?pub=$$PUBLICATION', + RENDERER_URL: 'https://$$PUBLICATION.bbvms.com/r/$$RENDERER.js', + DEFAULT_TIMEOUT: 5000, + DEFAULT_TTL: 300, + DEFAULT_WIDTH: 768, + DEFAULT_HEIGHT: 432, + DEFAULT_NET_REVENUE: true +}; + +// Aliasing +const getConfig = config.getConfig; + +// Helper Functions +export const BB_HELPERS = { + addSiteAppDevice: (request, pageUrl) => { + if (!request) return; + + if (typeof getConfig('app') === 'object') request.app = getConfig('app'); + else if (pageUrl) request.site = { page: pageUrl }; + + if (typeof getConfig('device') === 'object') request.device = getConfig('device'); + if (!request.device) request.device = {}; + if (!request.device.w) request.device.w = window.innerWidth; + if (!request.device.h) request.device.h = window.innerHeight; + }, + addSchain: (request, validBidRequests) => { + if (!request) return; + + const schain = utils.deepAccess(validBidRequests, '0.schain'); + if (schain) request.source.ext = { schain: schain }; + }, + addAliases: (request, aliases) => { + if (!request) return; + + if (!utils.isEmpty(aliases)) request.ext.prebid.aliases = aliases; + }, + addCurrency: (request) => { + if (!request) return; + + const adServerCur = getConfig('currency.adServerCurrency'); + if (adServerCur && typeof adServerCur === 'string') request.cur = [adServerCur]; + else if (Array.isArray(adServerCur) && adServerCur.length) request.cur = [adServerCur[0]]; + }, + addUserIds: (request, validBidRequests) => { + if (!request) return; + + // NB straight rip from prebidServerAdapter, keep track for updates + const bidUserId = utils.deepAccess(validBidRequests, '0.userId'); + + if (bidUserId && typeof bidUserId === 'object' && (bidUserId.tdid || bidUserId.pubcid || bidUserId.parrableid || bidUserId.lipb || bidUserId.id5id || bidUserId.criteoId || bidUserId.britepoolid || bidUserId.idl_env)) { + utils.deepSetValue(request, 'user.ext.eids', []); + + if (bidUserId.tdid) { + request.user.ext.eids.push({ + source: 'adserver.org', + uids: [{ + id: bidUserId.tdid, + ext: { + rtiPartner: 'TDID' + } + }] + }); + } + + if (bidUserId.pubcid) { + request.user.ext.eids.push({ + source: 'pubcid.org', + uids: [{ + id: bidUserId.pubcid, + }] + }); + } + + if (bidUserId.parrableid) { + request.user.ext.eids.push({ + source: 'parrable.com', + uids: [{ + id: bidUserId.parrableid + }] + }); + } + + if (bidUserId.lipb && bidUserId.lipb.lipbid) { + const liveIntent = { + source: 'liveintent.com', + uids: [{ + id: bidUserId.lipb.lipbid + }] + }; + + if (Array.isArray(bidUserId.lipb.segments) && bidUserId.lipb.segments.length) { + liveIntent.ext = { + segments: bidUserId.lipb.segments + }; + } + request.user.ext.eids.push(liveIntent); + } + + if (bidUserId.id5id) { + request.user.ext.eids.push({ + source: 'id5-sync.com', + uids: [{ + id: bidUserId.id5id, + }] + }); + } + + if (bidUserId.criteoId) { + request.user.ext.eids.push({ + source: 'criteo.com', + uids: [{ + id: bidUserId.criteoId + }] + }); + } + + if (bidUserId.britepoolid) { + request.user.ext.eids.push({ + source: 'britepool.com', + uids: [{ + id: bidUserId.britepoolid + }] + }); + } + + if (bidUserId.idl_env) { + request.user.ext.eids.push({ + source: 'liveramp.com', + uids: [{ + id: bidUserId.idl_env + }] + }); + } + + if (bidUserId.netId) { + request.user.ext.eids.push({ + source: 'netid.de', + uids: [{ + id: bidUserId.netId + }] + }); + } + } + }, + addDigiTrust: (request, bidRequests) => { + const digiTrust = BB_HELPERS.getDigiTrustParams(bidRequests && bidRequests[0]); + if (digiTrust) utils.deepSetValue(request, 'user.ext.digitrust', digiTrust); + }, + substituteUrl: (url, publication, renderer) => { + return url.replace('$$URL_START', (DEV_MODE) ? 'https://dev.' : 'https://').replace('$$PUBLICATION', publication).replace('$$RENDERER', renderer); + }, + getAuctionUrl: (publication) => { + return BB_HELPERS.substituteUrl(BB_CONSTANTS.AUCTION_URL, publication); + }, + getSyncUrl: (publication) => { + return BB_HELPERS.substituteUrl(BB_CONSTANTS.SYNC_URL, publication); + }, + getRendererUrl: (publication, renderer) => { + return BB_HELPERS.substituteUrl(BB_CONSTANTS.RENDERER_URL, publication, renderer); + }, + getAliasesFromRegistry: (name) => { + if (!name) return null; + + let aliases = adapterManager.aliasRegistry[name]; + + if (aliases) return aliases; + else return null; + }, + getBidAdapterFromManager: (name) => { + if (!name) return null; + + const adapter = adapterManager.getBidAdapter && adapterManager.getBidAdapter(name); + return adapter || null; + }, + getDigiTrustParams: (bidRequest) => { + const digiTrustId = BB_HELPERS.getDigiTrustId(bidRequest); + + if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) return null; + return { + id: digiTrustId.id, + keyv: digiTrustId.keyv + } + }, + getDigiTrustId: (bidRequest) => { + const bidRequestDigiTrust = utils.deepAccess(bidRequest, 'userId.digitrustid.data'); + if (bidRequestDigiTrust) return bidRequestDigiTrust; + + const digiTrustUser = getConfig('digiTrustId'); + return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; + }, + transformBidParams: (adapter, bidRequest, name) => { + if (adapter && typeof adapter.getSpec().transformBidParams === 'function') { + if (bidRequest.params && bidRequest.params[name]) { + bidRequest.params[name] = adapter.getSpec().transformBidParams(bidRequest.params[name], true); + } + } + }, + transformRTBToPrebidProps: (bid, serverResponse) => { + bid.cpm = bid.price; delete bid.price; + bid.bidId = bid.impid; + bid.requestId = bid.impid; delete bid.impid; + bid.width = bid.w || BB_CONSTANTS.DEFAULT_WIDTH; + bid.height = bid.h || BB_CONSTANTS.DEFAULT_HEIGHT; + if (bid.adm) { + bid.ad = bid.adm; + bid.vastXml = bid.adm; + delete bid.adm; + } + if (bid.nurl && !bid.adm) { // ad markup is on win notice url, and adm is ommited according to OpenRTB 2.5 + bid.vastUrl = bid.nurl; + delete bid.nurl; + } + bid.netRevenue = BB_CONSTANTS.DEFAULT_NET_REVENUE; + bid.creativeId = bid.crid; delete bid.crid; + bid.currency = serverResponse.cur; + bid.ttl = BB_CONSTANTS.DEFAULT_TTL; + }, +}; + +// Renderer Functions +const BB_RENDERER = { + bootstrapPlayer: (bid) => { + const config = { + code: bid.adUnitCode, + }; + + if (bid.vastXml) config.vastXml = bid.vastXml; + else if (bid.vastUrl) config.vastUrl = bid.vastUrl; + + if (!bid.vastXml && !bid.vastUrl) { + utils.logWarn(`${BB_CONSTANTS.BIDDER_CODE}: No vastXml or vastUrl on bid, bailing...`); + return; + } + + const rendererId = BB_RENDERER.getRendererId(bid.publicationName, bid.rendererCode); + + const ele = document.getElementById(bid.adUnitCode); // NB convention + const renderer = window.bluebillywig.renderers.find((renderer) => renderer._id === rendererId); + + if (renderer) renderer.bootstrap(config, ele); + else utils.logWarn(`${BB_CONSTANTS.BIDDER_CODE}: Couldn't find a renderer with ${rendererId}`); + }, + newRenderer: (rendererUrl, adUnitCode) => { + const renderer = Renderer.install({ + url: rendererUrl, + loaded: false, + adUnitCode + }); + + try { + renderer.setRender(BB_RENDERER.outstreamRender); + } catch (err) { + utils.logWarn(`${BB_CONSTANTS.BIDDER_CODE}: Error tying to setRender on renderer`, err); + } + + return renderer; + }, + outstreamRender: (bid) => { + bid.renderer.push(() => BB_RENDERER.bootstrapPlayer(bid)); + }, + getRendererId: (pub, renderer) => { + return `${pub}-${renderer}`; // NB convention! + } +}; + +// Spec Functions +// These functions are used to construct the core spec for the adapter +export const spec = { + code: BB_CONSTANTS.BIDDER_CODE, + supportedMediaTypes: [VIDEO], + syncStore: { bidders: [], }, + isBidRequestValid(bid) { + const publicationNameRegex = /^\w+\.?\w+$/; + const rendererRegex = /^[\w+_]+$/; + + if (!bid.params) { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: no params set on bid. Rejecting bid: `, bid); + return false; + } + + if (!bid.params.hasOwnProperty('publicationName') || typeof bid.params.publicationName !== 'string') { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: no publicationName specified in bid params, or it's not a string. Rejecting bid: `, bid); + return false; + } else if (!publicationNameRegex.test(bid.params.publicationName)) { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: publicationName must be in format 'publication' or 'publication.environment'. Rejecting bid: `, bid); + return false; + } + + if ((!bid.params.hasOwnProperty('rendererCode') || typeof bid.params.rendererCode !== 'string')) { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: no rendererCode was specified in bid params. Rejecting bid: `, bid); + return false; + } else if (!rendererRegex.test(bid.params.rendererCode)) { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: rendererCode must be alphanumeric, including underscores. Rejecting bid: `, bid); + return false; + } + + if (!bid.params.accountId) { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: no accountId specified in bid params. Rejecting bid: `, bid); + return false; + } + + if (bid.params.hasOwnProperty('connections')) { + if (!Array.isArray(bid.params.connections)) { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: connections is not of type array. Rejecting bid: `, bid); + return false; + } else { + for (const connection of bid.params.connections) { + if (!bid.params.hasOwnProperty(connection)) { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: connection specified in params.connections, but not configured in params. Rejecting bid: `, bid); + return false; + } + } + } + } else { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: no connections specified in bid. Rejecting bid: `, bid); + return false; + } + + if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { + if (!bid.mediaTypes[VIDEO].hasOwnProperty('context')) { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: no context specified in bid. Rejecting bid: `, bid); + return false; + } + + if (bid.mediaTypes[VIDEO].context !== 'outstream') { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: video.context is invalid, must be "outstream". Rejecting bid: `, bid); + return false; + } + } else { + utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: mediaTypes or mediaTypes.video is not specified. Rejecting bid: `, bid); + return false; + } + + return true; + }, + buildRequests(validBidRequests, bidderRequest) { + const imps = []; + const aliases = {}; + + for (const validBidRequest of validBidRequests) { + const _this = this; + + const ext = validBidRequest.params.connections.reduce((extBuilder, connection) => { + const adapter = BB_HELPERS.getBidAdapterFromManager(connection); + BB_HELPERS.transformBidParams(adapter, validBidRequest, connection); + extBuilder[connection] = validBidRequest.params[connection]; + + // check for and store valid aliases to add to the request + let connectionAliases = BB_HELPERS.getAliasesFromRegistry(connection); + if (connectionAliases) aliases[connection] = connectionAliases; + + if (_this.syncStore.bidders.indexOf(connection) === -1) _this.syncStore.bidders.push(connection); + + return extBuilder; + }, {}); + + imps.push({ id: validBidRequest.bidId, ext, secure: window.location.protocol === 'https' ? 1 : 0, video: utils.deepAccess(validBidRequest, 'mediaTypes.video') }); + } + + const request = { + id: bidderRequest.auctionId, + source: {tid: bidderRequest.auctionId}, + tmax: BB_CONSTANTS.DEFAULT_TIMEOUT, + imp: imps, + test: DEV_MODE ? 1 : 0, + ext: { + prebid: { + targeting: { includewinners: true, includebidderkeys: false } + } + } + }; + + // handle privacy settings for GDPR/CCPA/COPPA + if (bidderRequest.gdprConsent) { + let gdprApplies = 0; + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') gdprApplies = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + utils.deepSetValue(request, 'regs.ext.gdpr', gdprApplies); + utils.deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + } + + if (bidderRequest.uspConsent) { + utils.deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); + this.syncStore.uspConsent = bidderRequest.uspConsent; + } + + if (getConfig('coppa') == true) utils.deepSetValue(request, 'regs.coppa', 1); + + // Enrich the request with any external data we may have + BB_HELPERS.addSiteAppDevice(request, bidderRequest.refererInfo && bidderRequest.refererInfo.referer); + BB_HELPERS.addSchain(request, validBidRequests); + BB_HELPERS.addAliases(request, aliases); + BB_HELPERS.addCurrency(request); + BB_HELPERS.addUserIds(request, validBidRequests); + BB_HELPERS.addDigiTrust(request, validBidRequests); + + return { + method: 'POST', + url: BB_HELPERS.getAuctionUrl(validBidRequests[0].params.publicationName), + data: JSON.stringify(request), + bidderRequest: bidderRequest + }; + }, + interpretResponse(serverResponse, request) { + serverResponse = serverResponse.body || {}; + + if (!serverResponse.hasOwnProperty('seatbid') || !Array.isArray(serverResponse.seatbid)) { + return []; + } + + const bids = []; + + for (const seatbid of serverResponse.seatbid) { + if (!seatbid.bid || !Array.isArray(seatbid.bid)) continue; + for (const bid of seatbid.bid) { + BB_HELPERS.transformRTBToPrebidProps(bid, serverResponse); + + const bidParams = request.bidderRequest.bids.find(_bid => _bid.bidId === bid.bidId).params; + + bid.publicationName = bidParams.publicationName; + bid.rendererCode = bidParams.rendererCode; + bid.accountId = bidParams.accountId; + + const rendererUrl = BB_HELPERS.getRendererUrl(bid.publicationName, bid.rendererCode); + + bid.renderer = BB_RENDERER.newRenderer(rendererUrl, bid.adUnitCode); + + bids.push(bid); + } + } + + return bids; + }, + getUserSyncs(syncOptions, serverResponses, gdpr) { + if (!serverResponses || !serverResponses.length) return []; + if (!syncOptions.iframeEnabled) return []; + + const queryString = []; + let accountId; + let publication; + + const serverResponse = serverResponses[0]; + if (!serverResponse.body || !serverResponse.body.seatbid) return []; + + for (const seatbid of serverResponse.body.seatbid) { + for (const bid of seatbid.bid) { + accountId = bid.accountId || null; + publication = bid.publicationName || null; + + if (publication && accountId) break; + } + if (publication && accountId) break; + } + + if (!publication || !accountId) return []; + + if (gdpr.gdprApplies) queryString.push(`gdpr=${gdpr.gdprApplies ? 1 : 0}`); + if (gdpr.gdprApplies && gdpr.consentString) queryString.push(`gdpr_consent=${gdpr.consentString}`); + + if (this.syncStore.uspConsent) queryString.push(`usp_consent=${this.syncStore.uspConsent}`); + + queryString.push(`accountId=${accountId}`); + queryString.push(`bidders=${btoa(JSON.stringify(this.syncStore.bidders))}`); + queryString.push(`cb=${Date.now()}-${Math.random().toString().replace('.', '')}`); + + if (DEV_MODE) queryString.push('bbpbs_debug=true'); + + // NB syncUrl by default starts with ?pub=$$PUBLICATION + const syncUrl = `${BB_HELPERS.getSyncUrl(publication)}&${queryString.join('&')}`; + + return [{ + type: 'iframe', + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/bluebillywigBidAdapter.md b/modules/bluebillywigBidAdapter.md new file mode 100644 index 00000000000..c600e343769 --- /dev/null +++ b/modules/bluebillywigBidAdapter.md @@ -0,0 +1,38 @@ +# Overview + +``` +Module Name: Blue Billywig Adapter +Module Type: Bidder Adapter +Maintainer: dev+prebid@bluebillywig.com +``` + +# Description + +Prebid Blue Billywig Bidder Adapter + +# Test Parameters + +``` + const adUnits = [{ + code: 'ad-unit', + sizes: [[[768,432],[640,480],[640,360]]], + mediaTypes: { + video: { + playerSize: [768, 432], + context: 'outstream', + mimes: ['video/mp4'], + protocols: [ 2,3,5,6] + } + }, + bids: [{ + bidder: 'bluebillywig', + params: { + publicationName: "bbprebid", + renderer: "renderer", + accountId: 642, + connections: [ 'bluebillywig' ], + bluebillywig: {} + } + }] + }]; +``` diff --git a/test/spec/modules/bluebillywigBidAdapter_spec.js b/test/spec/modules/bluebillywigBidAdapter_spec.js new file mode 100644 index 00000000000..799f39ccda3 --- /dev/null +++ b/test/spec/modules/bluebillywigBidAdapter_spec.js @@ -0,0 +1,886 @@ +import { expect } from 'chai'; +import { spec, BB_CONSTANTS } from 'modules/bluebillywigBidAdapter.js'; +import * as bidderFactory from 'src/adapters/bidderFactory.js'; +import { auctionManager } from 'src/auctionManager.js'; +import { deepClone, deepAccess } from 'src/utils.js'; +import { config } from 'src/config.js'; +import { VIDEO } from 'src/mediaTypes.js'; + +describe('BlueBillywigAdapter', () => { + describe('isBidRequestValid', () => { + const baseValidBid = { + bidder: BB_CONSTANTS.BIDDER_CODE, + params: { + accountId: 123, + publicationName: 'bbprebid.dev', + rendererCode: 'glorious_renderer', + connections: [ BB_CONSTANTS.BIDDER_CODE ], + bluebillywig: {} + }, + mediaTypes: { + video: { + context: 'outstream' + } + } + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(baseValidBid)).to.equal(true); + }); + + it('should return false when publicationName is missing', () => { + const bid = deepClone(baseValidBid); + delete bid.params.publicationName; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when publicationName is not a string', () => { + const bid = deepClone(baseValidBid); + + bid.params.publicationName = 123; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.publicationName = false; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.publicationName = void (0); + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.publicationName = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when publicationName is formatted poorly', () => { + const bid = deepClone(baseValidBid); + + bid.params.publicationName = 'bb.'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.publicationName = 'bb-test'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.publicationName = '?'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when renderer is not specified', () => { + const bid = deepClone(baseValidBid); + + delete bid.params.rendererCode; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when renderer is not a string', () => { + const bid = deepClone(baseValidBid); + + bid.params.rendererCode = 123; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.rendererCode = false; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.rendererCode = void (0); + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.rendererCode = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when renderer is formatted poorly', () => { + const bid = deepClone(baseValidBid); + + bid.params.rendererCode = 'bb.'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.rendererCode = 'bb-test'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.rendererCode = '?'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when accountId is not specified', () => { + const bid = deepClone(baseValidBid); + + delete bid.params.accountId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when connections is not specified', () => { + const bid = deepClone(baseValidBid); + + delete bid.params.connections; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when connections is not an array', () => { + const bid = deepClone(baseValidBid); + + bid.params.connections = 123; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.connections = false; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.connections = void (0); + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.connections = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.connections = 'string'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when a connection is missing', () => { + const bid = deepClone(baseValidBid); + + bid.params.connections.push('potatoes'); + expect(spec.isBidRequestValid(bid)).to.equal(false); + + bid.params.connections.pop(); + + delete bid.params[BB_CONSTANTS.BIDDER_CODE]; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail if bid has no mediaTypes', () => { + const bid = deepClone(baseValidBid); + + delete bid.mediaTypes; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail if bid has no mediaTypes.video', () => { + const bid = deepClone(baseValidBid); + + delete bid.mediaTypes[VIDEO]; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail if bid has no mediaTypes.video.context', () => { + const bid = deepClone(baseValidBid); + + delete bid.mediaTypes[VIDEO].context; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should fail if mediaTypes.video.context is not "outstream"', () => { + const bid = deepClone(baseValidBid); + + bid.mediaTypes[VIDEO].context = 'instream'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + const publicationName = 'bbprebid.dev'; + const rendererCode = 'glorious_renderer'; + + const baseValidBid = { + bidder: BB_CONSTANTS.BIDDER_CODE, + params: { + accountId: 123, + publicationName: publicationName, + rendererCode: rendererCode, + connections: [ BB_CONSTANTS.BIDDER_CODE ], + bluebillywig: {} + }, + mediaTypes: { + video: { + context: 'outstream' + } + } + }; + + const baseValidBidRequests = [baseValidBid]; + + const validBidderRequest = { + auctionId: '12abc345-67d8-9012-e345-6f78901a2b34', + auctionStart: 1585918458868, + bidderCode: BB_CONSTANTS.BIDDER_CODE, + bidderRequestId: '1a2345b67c8d9e0', + bids: [{ + adUnitCode: 'ad-unit-test', + auctionId: '12abc345-67d8-9012-e345-6f78901a2b34', + bidId: '1234ab567c89de0', + bidRequestsCount: 1, + bidder: BB_CONSTANTS.BIDDER_CODE, + bidderRequestId: '1a2345b67c8d9e0', + params: baseValidBid.params, + sizes: [[768, 432], [640, 480], [630, 360]], + transactionId: '2b34c5de-f67a-8901-bcd2-34567efabc89' + }], + start: 11585918458869, + timeout: 3000 + }; + + it('sends bid request to AUCTION_URL via POST', () => { + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + expect(request.url).to.equal(`https://pbs.bluebillywig.com/openrtb2/auction?pub=${publicationName}`); + expect(request.method).to.equal('POST'); + }); + + it('sends data as a string', () => { + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + expect(request.data).to.be.a('string'); + }); + + it('sends all bid parameters', () => { + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + expect(request).to.have.all.keys(['bidderRequest', 'data', 'method', 'url']); + }); + + it('builds the base request properly', () => { + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.id).to.equal(validBidderRequest.auctionId); + expect(payload.source).to.be.an('object'); + expect(payload.source.tid).to.equal(validBidderRequest.auctionId); + expect(payload.tmax).to.equal(BB_CONSTANTS.DEFAULT_TIMEOUT); + expect(payload.imp).to.be.an('array'); + expect(payload.test).to.be.a('number'); + expect(payload).to.have.nested.property('ext.prebid.targeting'); + expect(payload.ext.prebid.targeting).to.be.an('object'); + expect(payload.ext.prebid.targeting.includewinners).to.equal(true); + expect(payload.ext.prebid.targeting.includebidderkeys).to.equal(false); + }); + + it('adds an impression to the payload', () => { + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.imp.length).to.equal(1); + }); + + it('adds connections to ext', () => { + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.imp[0].ext).to.have.all.keys(['bluebillywig']); + }); + + it('adds gdpr when present', () => { + const newValidBidderRequest = deepClone(validBidderRequest); + newValidBidderRequest.gdprConsent = { + consentString: 'BOh7mtYOh7mtYAcABBENCU-AAAAncgPIXJiiAoao0PxBFkgCAC8ACIAAQAQQAAIAAAIAAAhBGAAAQAQAEQgAAAAAAABAAAAAAAAAAAAAAACAAAAAAAACgAAAAABAAAAQAAAAAAA', + gdprApplies: true + }; + + const request = spec.buildRequests(baseValidBidRequests, newValidBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.nested.property('regs.ext.gdpr'); + expect(payload.regs.ext.gdpr).to.be.a('number'); + expect(payload.regs.ext.gdpr).to.equal(1); + expect(payload).to.have.nested.property('user.ext.consent'); + expect(payload.user.ext.consent).to.equal(newValidBidderRequest.gdprConsent.consentString); + }); + + it('sets gdpr to 0 when explicitly gdprApplies: false', () => { + const newValidBidderRequest = deepClone(validBidderRequest); + newValidBidderRequest.gdprConsent = { + gdprApplies: false + }; + + const request = spec.buildRequests(baseValidBidRequests, newValidBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.nested.property('regs.ext.gdpr'); + expect(payload.regs.ext.gdpr).to.be.a('number'); + expect(payload.regs.ext.gdpr).to.equal(0); + }); + + it('adds usp_consent when present', () => { + const newValidBidderRequest = deepClone(validBidderRequest); + newValidBidderRequest.uspConsent = '1YYY'; + + const request = spec.buildRequests(baseValidBidRequests, newValidBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.nested.property('regs.ext.us_privacy'); + expect(payload.regs.ext.us_privacy).to.equal(newValidBidderRequest.uspConsent); + }); + + it('sets coppa to 1 when specified in config', () => { + config.setConfig({'coppa': true}); + + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.nested.property('regs.coppa'); + expect(payload.regs.coppa).to.equal(1); + + config.resetConfig(); + }); + + it('does not set coppa when disabled in the config', () => { + config.setConfig({'coppa': false}); + + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(deepAccess(payload, 'regs.coppa')).to.be.undefined; + + config.resetConfig(); + }); + + it('does not set coppa when not specified in config', () => { + config.resetConfig(); + + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(deepAccess(payload, 'regs.coppa')).to.be.undefined; + }); + + it('should add window size to request by default', () => { + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.nested.property('device.w'); + expect(payload).to.have.nested.property('device.h'); + expect(payload.device.w).to.be.a('number'); + expect(payload.device.h).to.be.a('number'); + }); + + it('should add app when specified in config', () => { + config.setConfig({ app: { bundle: 'org.prebid.mobile.demoapp', domain: 'prebid.org' } }); + + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.property('app'); + expect(payload).to.have.nested.property('app.bundle'); + expect(payload).to.have.nested.property('app.domain'); + expect(payload.app.bundle).to.equal('org.prebid.mobile.demoapp'); + expect(payload.app.domain).to.equal('prebid.org'); + + config.resetConfig(); + }); + + it('should add referrerInfo as site when no app is set', () => { + const newValidBidderRequest = deepClone(validBidderRequest); + + newValidBidderRequest.refererInfo = { referer: 'https://www.bluebillywig.com' }; + + const request = spec.buildRequests(baseValidBidRequests, newValidBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.nested.property('site.page'); + expect(payload.site.page).to.equal('https://www.bluebillywig.com'); + }); + + it('should not add referrerInfo as site when app is set', () => { + config.setConfig({ app: { bundle: 'org.prebid.mobile.demoapp', domain: 'prebid.org' } }); + + const newValidBidderRequest = deepClone(validBidderRequest); + newValidBidderRequest.refererInfo = { referer: 'https://www.bluebillywig.com' }; + + const request = spec.buildRequests(baseValidBidRequests, newValidBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.site).to.be.undefined; + config.resetConfig(); + }); + + it('should add device size to request when specified in config', () => { + config.setConfig({ device: { w: 1, h: 1 } }); + + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.nested.property('device.w'); + expect(payload).to.have.nested.property('device.h'); + expect(payload.device.w).to.be.a('number'); + expect(payload.device.h).to.be.a('number'); + expect(payload.device.w).to.equal(1); + expect(payload.device.h).to.equal(1); + + config.resetConfig(); + }); + + it('should set schain on the request when set on config', () => { + const schain = { + validation: 'lax', + config: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'indirectseller.com', + sid: '00001', + hp: 1 + } + ] + } + }; + + const newBaseValidBidRequests = deepClone(baseValidBidRequests); + newBaseValidBidRequests[0].schain = schain; + + const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.nested.property('source.ext.schain'); + expect(payload.source.ext.schain).to.deep.equal(schain); + }); + + it('should add currency when specified on the config', () => { + config.setConfig({ currency: { adServerCurrency: 'USD' } }); + + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.property('cur'); + expect(payload.cur).to.eql(['USD']); // NB not equal, eql to check for same array because [1] === [1] fails normally + + config.resetConfig(); + }); + + it('should also take in array for currency on the config', () => { + config.setConfig({ currency: { adServerCurrency: ['USD', 'PHP'] } }); + + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.property('cur'); + expect(payload.cur).to.eql(['USD']); // NB not equal, eql to check for same array because [1] === [1] fails normally + + config.resetConfig(); + }); + + it('should not set cur when currency is not specified on the config', () => { + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.cur).to.be.undefined; + }); + + it('should set user ids when present', () => { + const userId = { tdid: 123 }; + + const newBaseValidBidRequests = deepClone(baseValidBidRequests); + newBaseValidBidRequests[0].userId = userId; + + const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.nested.property('user.ext.eids'); + expect(payload.user.ext.eids).to.be.an('array'); + expect(payload.user.ext.eids.length).to.equal(1); + }); + + it('should not set user ids when none present', () => { + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(deepAccess(payload, 'user.ext.eids')).to.be.undefined; + }); + + it('should set digitrust when present on bid', () => { + const digiTrust = {data: {id: 'DTID', keyv: 4, privacy: {optout: false}, producer: 'ABC', version: 2}}; + + const newBaseValidBidRequests = deepClone(baseValidBidRequests); + newBaseValidBidRequests[0].userId = { digitrustid: digiTrust }; + + const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload).to.have.nested.property('user.ext.digitrust'); + expect(payload.user.ext.digitrust.id).to.equal(digiTrust.data.id); + expect(payload.user.ext.digitrust.keyv).to.equal(digiTrust.data.keyv); + }); + + it('should not set digitrust when opted out', () => { + const digiTrust = {data: {id: 'DTID', keyv: 4, privacy: {optout: true}, producer: 'ABC', version: 2}}; + + const newBaseValidBidRequests = deepClone(baseValidBidRequests); + newBaseValidBidRequests[0].userId = { digitrustid: digiTrust }; + + const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); + const payload = JSON.parse(request.data); + + expect(deepAccess(payload, 'user.ext.digitrust')).to.be.undefined; + }); + }); + describe('interpretResponse', () => { + const publicationName = 'bbprebid.dev'; + const rendererCode = 'glorious_renderer'; + + const baseValidBid = { + bidder: BB_CONSTANTS.BIDDER_CODE, + params: { + accountId: 123, + publicationName: publicationName, + rendererCode: rendererCode, + connections: [ BB_CONSTANTS.BIDDER_CODE ], + bluebillywig: {} + }, + mediaTypes: { + video: { + context: 'outstream' + } + } + }; + + const baseValidBidRequests = [baseValidBid]; + + const validBidderRequest = { + auctionId: '12abc345-67d8-9012-e345-6f78901a2b34', + auctionStart: 1585918458868, + bidderCode: BB_CONSTANTS.BIDDER_CODE, + bidderRequestId: '1a2345b67c8d9e0', + bids: [{ + adUnitCode: 'ad-unit-test', + auctionId: '12abc345-67d8-9012-e345-6f78901a2b34', + bidId: '1234ab567c89de0', + bidRequestsCount: 1, + bidder: BB_CONSTANTS.BIDDER_CODE, + bidderRequestId: '1a2345b67c8d9e0', + params: baseValidBid.params, + sizes: [[768, 432], [640, 480], [630, 360]], + transactionId: '2b34c5de-f67a-8901-bcd2-34567efabc89' + }], + start: 11585918458869, + timeout: 3000 + }; + + const validResponse = { + id: 'a12abc345-67d8-9012-e345-6f78901a2b34', + seatbid: [ + { + bid: [ + { + id: '1', + impid: '1234ab567c89de0', + price: 1, + adm: '\r\nBB Adserver00:00:51', + adid: '67069817', + adomain: [ + 'bluebillywig.com' + ], + cid: '3535', + crid: '67069817', + w: 1, + h: 1, + publicationName: 'bbprebid', + accountId: 123, + ext: { + prebid: { + targeting: { + hb_bidder: 'bluebillywig', + hb_pb: '1.00', + hb_size: '1x1' + }, + type: 'video' + }, + bidder: { + prebid: { + targeting: { + hb_bidder: 'bluebillywig', + hb_pb: '10.00', + hb_size: '1x1' + }, + type: 'video', + video: { + duration: 51, + primary_category: '' + } + }, + bidder: { + bluebillywig: { + brand_id: 1, + auction_id: 1, + bid_ad_type: 1, + creative_info: { + video: { + duration: 51, + mimes: [ + 'video/x-flv', + 'video/mp4', + 'video/webm' + ] + } + } + } + } + } + } + } + ], + seat: 'bluebillywig' + } + ], + cur: 'USD', + ext: { + responsetimemillis: { + bluebillywig: 0 + }, + tmaxrequest: 5000 + } + }; + + const serverResponse = { body: validResponse }; + + it('should build bid array', () => { + const response = deepClone(serverResponse); + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(result.length).to.equal(1); + }); + + it('should have all relevant fields', () => { + const response = deepClone(serverResponse); + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + const bid = result[0]; + + // BB_HELPERS.transformRTBToPrebidProps + expect(bid.cpm).to.equal(serverResponse.body.seatbid[0].bid[0].price); + expect(bid.bidId).to.equal(serverResponse.body.seatbid[0].bid[0].impid); + expect(bid.requestId).to.equal(serverResponse.body.seatbid[0].bid[0].impid); + expect(bid.width).to.equal(serverResponse.body.seatbid[0].bid[0].w || BB_CONSTANTS.DEFAULT_WIDTH); + expect(bid.height).to.equal(serverResponse.body.seatbid[0].bid[0].h || BB_CONSTANTS.DEFAULT_HEIGHT); + expect(bid.ad).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(bid.netRevenue).to.equal(BB_CONSTANTS.DEFAULT_NET_REVENUE); + expect(bid.creativeId).to.equal(serverResponse.body.seatbid[0].bid[0].crid); + expect(bid.currency).to.equal(serverResponse.body.cur); + expect(bid.ttl).to.equal(BB_CONSTANTS.DEFAULT_TTL); + + expect(bid.publicationName).to.equal(validBidderRequest.bids[0].params.publicationName); + expect(bid.rendererCode).to.equal(validBidderRequest.bids[0].params.rendererCode); + expect(bid.accountId).to.equal(validBidderRequest.bids[0].params.accountId); + }); + + it('should not give anything when seatbid is an empty array', () => { + const seatbidEmptyArray = deepClone(serverResponse); + seatbidEmptyArray.body.seatbid = []; + + const response = seatbidEmptyArray; + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(result.length).to.equal(0); + }); + + it('should not give anything when seatbid is missing', () => { + const seatbidMissing = deepClone(serverResponse); + delete seatbidMissing.body.seatbid; + + const response = seatbidMissing; + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(result.length).to.equal(0); + }); + + const seatbidNotArrayResponse = deepClone(serverResponse); + it('should not give anything when seatbid is not an array', () => { + const invalidValues = [ false, null, {}, void (0), 123, 'string' ]; + + for (const invalidValue of invalidValues) { + seatbidNotArrayResponse.body.seatbid = invalidValue + const response = deepClone(seatbidNotArrayResponse); // interpretResponse is destructive + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(result.length).to.equal(0); + } + }); + + it('should not give anything when seatbid.bid is an empty array', () => { + const seatbidBidEmpty = deepClone(serverResponse); + seatbidBidEmpty.body.seatbid[0].bid = []; + + const response = seatbidBidEmpty; + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(result.length).to.equal(0); + }); + + it('should not give anything when seatbid.bid is missing', () => { + const seatbidBidMissing = deepClone(serverResponse); + delete seatbidBidMissing.body.seatbid[0].bid; + + const response = seatbidBidMissing; + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(result.length).to.equal(0); + }); + + it('should not give anything when seatbid.bid is not an array', () => { + const seatbidBidNotArray = deepClone(serverResponse); + + const invalidValues = [ false, null, {}, void (0), 123, 'string' ]; + + for (const invalidValue of invalidValues) { + seatbidBidNotArray.body.seatbid[0].bid = invalidValue; + + const response = deepClone(seatbidBidNotArray); // interpretResponse is destructive + const request = spec.buildRequests(baseValidBidRequests, validBidderRequest); + const result = spec.interpretResponse(response, request); + + expect(result.length).to.equal(0); + } + }); + }); + describe('getUserSyncs', () => { + const publicationName = 'bbprebid.dev'; + const rendererCode = 'glorious_renderer'; + + const baseValidBid = { + bidder: BB_CONSTANTS.BIDDER_CODE, + params: { + accountId: 123, + publicationName: publicationName, + rendererCode: rendererCode, + connections: [ BB_CONSTANTS.BIDDER_CODE ], + bluebillywig: {} + }, + mediaTypes: { + video: { + context: 'outstream' + } + } + }; + + const validBidRequests = [baseValidBid]; + + const validBidderRequest = { + auctionId: '12abc345-67d8-9012-e345-6f78901a2b34', + auctionStart: 1585918458868, + bidderCode: BB_CONSTANTS.BIDDER_CODE, + bidderRequestId: '1a2345b67c8d9e0', + bids: [{ + adUnitCode: 'ad-unit-test', + auctionId: '12abc345-67d8-9012-e345-6f78901a2b34', + bidId: '1234ab567c89de0', + bidRequestsCount: 1, + bidder: BB_CONSTANTS.BIDDER_CODE, + bidderRequestId: '1a2345b67c8d9e0', + params: baseValidBid.params, + sizes: [[768, 432], [640, 480], [630, 360]], + transactionId: '2b34c5de-f67a-8901-bcd2-34567efabc89' + }], + start: 11585918458869, + timeout: 3000 + }; + const validResponse = { + id: 'a12abc345-67d8-9012-e345-6f78901a2b34', + seatbid: [ + { + bid: [ + { + id: '1', + impid: '1234ab567c89de0', + price: 1, + adm: '\r\nBB Adserver00:00:51', + adid: '67069817', + adomain: [ + 'bluebillywig.com' + ], + cid: '3535', + crid: '67069817', + w: 1, + h: 1, + publicationName: 'bbprebid', + accountId: 123, + ext: { + prebid: { + targeting: { + hb_bidder: 'bluebillywig', + hb_pb: '1.00', + hb_size: '1x1' + }, + type: 'video' + }, + bidder: { + prebid: { + targeting: { + hb_bidder: 'bluebillywig', + hb_pb: '10.00', + hb_size: '1x1' + }, + type: 'video', + video: { + duration: 51, + primary_category: '' + } + }, + bidder: { + bluebillywig: { + brand_id: 1, + auction_id: 1, + bid_ad_type: 1, + creative_info: { + video: { + duration: 51, + mimes: [ + 'video/x-flv', + 'video/mp4', + 'video/webm' + ] + } + } + } + } + } + } + } + ], + seat: 'bluebillywig' + } + ], + cur: 'USD', + ext: { + responsetimemillis: { + bluebillywig: 0 + }, + tmaxrequest: 5000 + } + }; + + const serverResponse = { body: validResponse }; + + const gdpr = { + consentString: 'BOh7mtYOh7mtYAcABBENCU-AAAAncgPIXJiiAoao0PxBFkgCAC8ACIAAQAQQAAIAAAIAAAhBGAAAQAQAEQgAAAAAAABAAAAAAAAA AAAAAACAAAAAAAACgAAAAABAAAAQAAAAAAA', + gdprApplies: true + }; + + it('should return empty if no server response', function () { + const result = spec.getUserSyncs({}, false, gdpr); + expect(result).to.be.empty; + }); + + it('should return empty if server response is empty', function () { + const result = spec.getUserSyncs({}, [], gdpr); + expect(result).to.be.empty; + }); + + it('should return empty if iframeEnabled is not true', () => { + const result = spec.getUserSyncs({iframeEnabled: false}, [serverResponse], gdpr); + expect(result).to.be.empty; + }); + + it('should append the various values if they exist', function() { + // push data to syncStore + spec.buildRequests(validBidRequests, validBidderRequest); + + const result = spec.getUserSyncs({iframeEnabled: true}, [serverResponse], gdpr); + + expect(result).to.not.be.empty; + + expect(result[0].url).to.include('gdpr=1'); + expect(result[0].url).to.include(gdpr.consentString); + expect(result[0].url).to.include('accountId=123'); + expect(result[0].url).to.include(`bidders=${btoa(JSON.stringify(validBidRequests[0].params.connections))}`); + expect(result[0].url).to.include('cb='); + }); + }); +}); From 263aeaecaf3ccecf5939fdffd7fa4c531e277f7b Mon Sep 17 00:00:00 2001 From: Klaas-Jan Boon Date: Tue, 21 Apr 2020 15:35:57 +0200 Subject: [PATCH 2/5] Blue Billywig Adapter - update according to review feedback --- modules/bluebillywigBidAdapter.js | 141 ++---------------- .../modules/bluebillywigBidAdapter_spec.js | 16 +- 2 files changed, 23 insertions(+), 134 deletions(-) diff --git a/modules/bluebillywigBidAdapter.js b/modules/bluebillywigBidAdapter.js index 5bec6679732..5c007f08cb8 100644 --- a/modules/bluebillywigBidAdapter.js +++ b/modules/bluebillywigBidAdapter.js @@ -1,14 +1,14 @@ import * as utils from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import adapterManager from '../src/adapterManager.js'; import { VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; +import { createEidsArray } from './userId/eids.js'; const DEV_MODE = window.location.search.match(/bbpbs_debug=true/); // Blue Billywig Constants -export const BB_CONSTANTS = { +const BB_CONSTANTS = { BIDDER_CODE: 'bluebillywig', AUCTION_URL: '$$URL_STARTpbs.bluebillywig.com/openrtb2/auction?pub=$$PUBLICATION', SYNC_URL: '$$URL_STARTpbs.bluebillywig.com/static/cookie-sync.html?pub=$$PUBLICATION', @@ -42,11 +42,6 @@ export const BB_HELPERS = { const schain = utils.deepAccess(validBidRequests, '0.schain'); if (schain) request.source.ext = { schain: schain }; }, - addAliases: (request, aliases) => { - if (!request) return; - - if (!utils.isEmpty(aliases)) request.ext.prebid.aliases = aliases; - }, addCurrency: (request) => { if (!request) return; @@ -57,102 +52,11 @@ export const BB_HELPERS = { addUserIds: (request, validBidRequests) => { if (!request) return; - // NB straight rip from prebidServerAdapter, keep track for updates const bidUserId = utils.deepAccess(validBidRequests, '0.userId'); + const eids = createEidsArray(bidUserId); - if (bidUserId && typeof bidUserId === 'object' && (bidUserId.tdid || bidUserId.pubcid || bidUserId.parrableid || bidUserId.lipb || bidUserId.id5id || bidUserId.criteoId || bidUserId.britepoolid || bidUserId.idl_env)) { - utils.deepSetValue(request, 'user.ext.eids', []); - - if (bidUserId.tdid) { - request.user.ext.eids.push({ - source: 'adserver.org', - uids: [{ - id: bidUserId.tdid, - ext: { - rtiPartner: 'TDID' - } - }] - }); - } - - if (bidUserId.pubcid) { - request.user.ext.eids.push({ - source: 'pubcid.org', - uids: [{ - id: bidUserId.pubcid, - }] - }); - } - - if (bidUserId.parrableid) { - request.user.ext.eids.push({ - source: 'parrable.com', - uids: [{ - id: bidUserId.parrableid - }] - }); - } - - if (bidUserId.lipb && bidUserId.lipb.lipbid) { - const liveIntent = { - source: 'liveintent.com', - uids: [{ - id: bidUserId.lipb.lipbid - }] - }; - - if (Array.isArray(bidUserId.lipb.segments) && bidUserId.lipb.segments.length) { - liveIntent.ext = { - segments: bidUserId.lipb.segments - }; - } - request.user.ext.eids.push(liveIntent); - } - - if (bidUserId.id5id) { - request.user.ext.eids.push({ - source: 'id5-sync.com', - uids: [{ - id: bidUserId.id5id, - }] - }); - } - - if (bidUserId.criteoId) { - request.user.ext.eids.push({ - source: 'criteo.com', - uids: [{ - id: bidUserId.criteoId - }] - }); - } - - if (bidUserId.britepoolid) { - request.user.ext.eids.push({ - source: 'britepool.com', - uids: [{ - id: bidUserId.britepoolid - }] - }); - } - - if (bidUserId.idl_env) { - request.user.ext.eids.push({ - source: 'liveramp.com', - uids: [{ - id: bidUserId.idl_env - }] - }); - } - - if (bidUserId.netId) { - request.user.ext.eids.push({ - source: 'netid.de', - uids: [{ - id: bidUserId.netId - }] - }); - } + if (eids && eids.length) { + utils.deepSetValue(request, 'user.ext.eids', eids); } }, addDigiTrust: (request, bidRequests) => { @@ -171,20 +75,6 @@ export const BB_HELPERS = { getRendererUrl: (publication, renderer) => { return BB_HELPERS.substituteUrl(BB_CONSTANTS.RENDERER_URL, publication, renderer); }, - getAliasesFromRegistry: (name) => { - if (!name) return null; - - let aliases = adapterManager.aliasRegistry[name]; - - if (aliases) return aliases; - else return null; - }, - getBidAdapterFromManager: (name) => { - if (!name) return null; - - const adapter = adapterManager.getBidAdapter && adapterManager.getBidAdapter(name); - return adapter || null; - }, getDigiTrustParams: (bidRequest) => { const digiTrustId = BB_HELPERS.getDigiTrustId(bidRequest); @@ -201,13 +91,6 @@ export const BB_HELPERS = { const digiTrustUser = getConfig('digiTrustId'); return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; }, - transformBidParams: (adapter, bidRequest, name) => { - if (adapter && typeof adapter.getSpec().transformBidParams === 'function') { - if (bidRequest.params && bidRequest.params[name]) { - bidRequest.params[name] = adapter.getSpec().transformBidParams(bidRequest.params[name], true); - } - } - }, transformRTBToPrebidProps: (bid, serverResponse) => { bid.cpm = bid.price; delete bid.price; bid.bidId = bid.impid; @@ -317,7 +200,8 @@ export const spec = { utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: connections is not of type array. Rejecting bid: `, bid); return false; } else { - for (const connection of bid.params.connections) { + for (let connectionIndex = 0; connectionIndex < bid.params.connections.length; connectionIndex++) { + const connection = bid.params.connections[connectionIndex]; if (!bid.params.hasOwnProperty(connection)) { utils.logError(`${BB_CONSTANTS.BIDDER_CODE}: connection specified in params.connections, but not configured in params. Rejecting bid: `, bid); return false; @@ -348,20 +232,14 @@ export const spec = { }, buildRequests(validBidRequests, bidderRequest) { const imps = []; - const aliases = {}; - for (const validBidRequest of validBidRequests) { + for (let validBidRequestIndex = 0; validBidRequestIndex < validBidRequests.length; validBidRequestIndex++) { + const validBidRequest = validBidRequests[validBidRequestIndex]; const _this = this; const ext = validBidRequest.params.connections.reduce((extBuilder, connection) => { - const adapter = BB_HELPERS.getBidAdapterFromManager(connection); - BB_HELPERS.transformBidParams(adapter, validBidRequest, connection); extBuilder[connection] = validBidRequest.params[connection]; - // check for and store valid aliases to add to the request - let connectionAliases = BB_HELPERS.getAliasesFromRegistry(connection); - if (connectionAliases) aliases[connection] = connectionAliases; - if (_this.syncStore.bidders.indexOf(connection) === -1) _this.syncStore.bidders.push(connection); return extBuilder; @@ -401,7 +279,6 @@ export const spec = { // Enrich the request with any external data we may have BB_HELPERS.addSiteAppDevice(request, bidderRequest.refererInfo && bidderRequest.refererInfo.referer); BB_HELPERS.addSchain(request, validBidRequests); - BB_HELPERS.addAliases(request, aliases); BB_HELPERS.addCurrency(request); BB_HELPERS.addUserIds(request, validBidRequests); BB_HELPERS.addDigiTrust(request, validBidRequests); diff --git a/test/spec/modules/bluebillywigBidAdapter_spec.js b/test/spec/modules/bluebillywigBidAdapter_spec.js index 799f39ccda3..fcf3e16ad8b 100644 --- a/test/spec/modules/bluebillywigBidAdapter_spec.js +++ b/test/spec/modules/bluebillywigBidAdapter_spec.js @@ -1,11 +1,23 @@ import { expect } from 'chai'; -import { spec, BB_CONSTANTS } from 'modules/bluebillywigBidAdapter.js'; +import { spec } from 'modules/bluebillywigBidAdapter.js'; import * as bidderFactory from 'src/adapters/bidderFactory.js'; import { auctionManager } from 'src/auctionManager.js'; import { deepClone, deepAccess } from 'src/utils.js'; import { config } from 'src/config.js'; import { VIDEO } from 'src/mediaTypes.js'; +const BB_CONSTANTS = { + BIDDER_CODE: 'bluebillywig', + AUCTION_URL: '$$URL_STARTpbs.bluebillywig.com/openrtb2/auction?pub=$$PUBLICATION', + SYNC_URL: '$$URL_STARTpbs.bluebillywig.com/static/cookie-sync.html?pub=$$PUBLICATION', + RENDERER_URL: 'https://$$PUBLICATION.bbvms.com/r/$$RENDERER.js', + DEFAULT_TIMEOUT: 5000, + DEFAULT_TTL: 300, + DEFAULT_WIDTH: 768, + DEFAULT_HEIGHT: 432, + DEFAULT_NET_REVENUE: true +}; + describe('BlueBillywigAdapter', () => { describe('isBidRequestValid', () => { const baseValidBid = { @@ -463,7 +475,7 @@ describe('BlueBillywigAdapter', () => { const userId = { tdid: 123 }; const newBaseValidBidRequests = deepClone(baseValidBidRequests); - newBaseValidBidRequests[0].userId = userId; + newBaseValidBidRequests[0].userId = { criteoId: 'sample-userid' }; const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); const payload = JSON.parse(request.data); From b52c832da2ce5dc657ec05331d8d07a824194eca Mon Sep 17 00:00:00 2001 From: Klaas-Jan Boon Date: Wed, 29 Apr 2020 14:52:06 +0200 Subject: [PATCH 3/5] Blue Billywig Adapter - update to try and pass CircleCI --- modules/bluebillywigBidAdapter.js | 69 +++++++++++++++++++------------ 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/modules/bluebillywigBidAdapter.js b/modules/bluebillywigBidAdapter.js index 5c007f08cb8..7be86068ebc 100644 --- a/modules/bluebillywigBidAdapter.js +++ b/modules/bluebillywigBidAdapter.js @@ -25,7 +25,7 @@ const getConfig = config.getConfig; // Helper Functions export const BB_HELPERS = { - addSiteAppDevice: (request, pageUrl) => { + addSiteAppDevice: function(request, pageUrl) { if (!request) return; if (typeof getConfig('app') === 'object') request.app = getConfig('app'); @@ -36,46 +36,46 @@ export const BB_HELPERS = { if (!request.device.w) request.device.w = window.innerWidth; if (!request.device.h) request.device.h = window.innerHeight; }, - addSchain: (request, validBidRequests) => { + addSchain: function(request, validBidRequests) { if (!request) return; const schain = utils.deepAccess(validBidRequests, '0.schain'); if (schain) request.source.ext = { schain: schain }; }, - addCurrency: (request) => { + addCurrency: function(request) { if (!request) return; const adServerCur = getConfig('currency.adServerCurrency'); if (adServerCur && typeof adServerCur === 'string') request.cur = [adServerCur]; else if (Array.isArray(adServerCur) && adServerCur.length) request.cur = [adServerCur[0]]; }, - addUserIds: (request, validBidRequests) => { + addUserIds: function(request, validBidRequests) { if (!request) return; const bidUserId = utils.deepAccess(validBidRequests, '0.userId'); const eids = createEidsArray(bidUserId); - if (eids && eids.length) { + if (eids.length) { utils.deepSetValue(request, 'user.ext.eids', eids); } }, - addDigiTrust: (request, bidRequests) => { + addDigiTrust: function(request, bidRequests) { const digiTrust = BB_HELPERS.getDigiTrustParams(bidRequests && bidRequests[0]); if (digiTrust) utils.deepSetValue(request, 'user.ext.digitrust', digiTrust); }, - substituteUrl: (url, publication, renderer) => { + substituteUrl: function (url, publication, renderer) { return url.replace('$$URL_START', (DEV_MODE) ? 'https://dev.' : 'https://').replace('$$PUBLICATION', publication).replace('$$RENDERER', renderer); }, - getAuctionUrl: (publication) => { + getAuctionUrl: function(publication) { return BB_HELPERS.substituteUrl(BB_CONSTANTS.AUCTION_URL, publication); }, - getSyncUrl: (publication) => { + getSyncUrl: function(publication) { return BB_HELPERS.substituteUrl(BB_CONSTANTS.SYNC_URL, publication); }, - getRendererUrl: (publication, renderer) => { + getRendererUrl: function(publication, renderer) { return BB_HELPERS.substituteUrl(BB_CONSTANTS.RENDERER_URL, publication, renderer); }, - getDigiTrustParams: (bidRequest) => { + getDigiTrustParams: function(bidRequest) { const digiTrustId = BB_HELPERS.getDigiTrustId(bidRequest); if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) return null; @@ -84,14 +84,14 @@ export const BB_HELPERS = { keyv: digiTrustId.keyv } }, - getDigiTrustId: (bidRequest) => { + getDigiTrustId: function(bidRequest) { const bidRequestDigiTrust = utils.deepAccess(bidRequest, 'userId.digitrustid.data'); if (bidRequestDigiTrust) return bidRequestDigiTrust; const digiTrustUser = getConfig('digiTrustId'); return (digiTrustUser && digiTrustUser.success && digiTrustUser.identity) || null; }, - transformRTBToPrebidProps: (bid, serverResponse) => { + transformRTBToPrebidProps: function(bid, serverResponse) { bid.cpm = bid.price; delete bid.price; bid.bidId = bid.impid; bid.requestId = bid.impid; delete bid.impid; @@ -115,7 +115,7 @@ export const BB_HELPERS = { // Renderer Functions const BB_RENDERER = { - bootstrapPlayer: (bid) => { + bootstrapPlayer: function(bid) { const config = { code: bid.adUnitCode, }; @@ -131,12 +131,20 @@ const BB_RENDERER = { const rendererId = BB_RENDERER.getRendererId(bid.publicationName, bid.rendererCode); const ele = document.getElementById(bid.adUnitCode); // NB convention - const renderer = window.bluebillywig.renderers.find((renderer) => renderer._id === rendererId); + + let renderer; + + for (let rendererIndex = 0; rendererIndex < window.bluebillywig.renderers.length; rendererIndex++) { + if (window.bluebillywig.renderers[rendererIndex]._id === rendererId) { + renderer = window.bluebillywig.renderers[rendererIndex]; + break; + } + } if (renderer) renderer.bootstrap(config, ele); else utils.logWarn(`${BB_CONSTANTS.BIDDER_CODE}: Couldn't find a renderer with ${rendererId}`); }, - newRenderer: (rendererUrl, adUnitCode) => { + newRenderer: function(rendererUrl, adUnitCode) { const renderer = Renderer.install({ url: rendererUrl, loaded: false, @@ -151,10 +159,10 @@ const BB_RENDERER = { return renderer; }, - outstreamRender: (bid) => { - bid.renderer.push(() => BB_RENDERER.bootstrapPlayer(bid)); + outstreamRender: function(bid) { + bid.renderer.push(function() { BB_RENDERER.bootstrapPlayer(bid) }); }, - getRendererId: (pub, renderer) => { + getRendererId: function(pub, renderer) { return `${pub}-${renderer}`; // NB convention! } }; @@ -237,7 +245,7 @@ export const spec = { const validBidRequest = validBidRequests[validBidRequestIndex]; const _this = this; - const ext = validBidRequest.params.connections.reduce((extBuilder, connection) => { + const ext = validBidRequest.params.connections.reduce(function(extBuilder, connection) { extBuilder[connection] = validBidRequest.params[connection]; if (_this.syncStore.bidders.indexOf(connection) === -1) _this.syncStore.bidders.push(connection); @@ -299,16 +307,25 @@ export const spec = { const bids = []; - for (const seatbid of serverResponse.seatbid) { + for (let seatbidIndex = 0; seatbidIndex < serverResponse.seatbid.length; seatbidIndex++) { + const seatbid = serverResponse.seatbid[seatbidIndex]; if (!seatbid.bid || !Array.isArray(seatbid.bid)) continue; - for (const bid of seatbid.bid) { + for (let bidIndex = 0; bidIndex < seatbid.bid.length; bidIndex++) { + const bid = seatbid.bid[bidIndex]; BB_HELPERS.transformRTBToPrebidProps(bid, serverResponse); - const bidParams = request.bidderRequest.bids.find(_bid => _bid.bidId === bid.bidId).params; + let bidParams; + for (let bidderRequestBidsIndex = 0; bidderRequestBidsIndex < request.bidderRequest.bids.length; bidderRequestBidsIndex++) { + if (request.bidderRequest.bids[bidderRequestBidsIndex].bidId === bid.bidId) { + bidParams = request.bidderRequest.bids[bidderRequestBidsIndex].params; + } + } - bid.publicationName = bidParams.publicationName; - bid.rendererCode = bidParams.rendererCode; - bid.accountId = bidParams.accountId; + if (bidParams) { + bid.publicationName = bidParams.publicationName; + bid.rendererCode = bidParams.rendererCode; + bid.accountId = bidParams.accountId; + } const rendererUrl = BB_HELPERS.getRendererUrl(bid.publicationName, bid.rendererCode); From 4772e9bc927b3a78f04d00aa1433d7551a1b445f Mon Sep 17 00:00:00 2001 From: Klaas-Jan Boon Date: Tue, 5 May 2020 12:07:27 +0200 Subject: [PATCH 4/5] Remove the last for .. of in bluebillywigBidAdapter.js, hopefully... --- modules/bluebillywigBidAdapter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/bluebillywigBidAdapter.js b/modules/bluebillywigBidAdapter.js index 7be86068ebc..9e4f5b62e48 100644 --- a/modules/bluebillywigBidAdapter.js +++ b/modules/bluebillywigBidAdapter.js @@ -348,8 +348,10 @@ export const spec = { const serverResponse = serverResponses[0]; if (!serverResponse.body || !serverResponse.body.seatbid) return []; - for (const seatbid of serverResponse.body.seatbid) { - for (const bid of seatbid.bid) { + for (let seatbidIndex = 0; seatbidIndex < serverResponse.body.seatbid.length; seatbidIndex++) { + const seatbid = serverResponse.body.seatbid[seatbidIndex]; + for (let bidIndex = 0; bidIndex < seatbid.bid.length; bidIndex++) { + const bid = seatbid.bid[bidIndex]; accountId = bid.accountId || null; publication = bid.publicationName || null; From 024f53024ebcd047a1b10803fdb4c438f6769171 Mon Sep 17 00:00:00 2001 From: Klaas-Jan Boon Date: Thu, 7 May 2020 11:20:34 +0200 Subject: [PATCH 5/5] Update bluebillywigBidAdapter test parameters to match renderer to rendererCode rename --- modules/bluebillywigBidAdapter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bluebillywigBidAdapter.md b/modules/bluebillywigBidAdapter.md index c600e343769..7879697baf5 100644 --- a/modules/bluebillywigBidAdapter.md +++ b/modules/bluebillywigBidAdapter.md @@ -28,7 +28,7 @@ Prebid Blue Billywig Bidder Adapter bidder: 'bluebillywig', params: { publicationName: "bbprebid", - renderer: "renderer", + rendererCode: "renderer", accountId: 642, connections: [ 'bluebillywig' ], bluebillywig: {}