From f22d7476b35ec65a3f52e4c9cfea315b810a50ec Mon Sep 17 00:00:00 2001 From: Mathieu Pheulpin Date: Wed, 12 May 2021 14:43:01 -0700 Subject: [PATCH 01/22] [WIP] OpenRTB Adapter [#177858674] --- modules/sharethroughBidAdapter.js | 291 ++++++++++++++++++++++++++---- 1 file changed, 260 insertions(+), 31 deletions(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index eef18288b17..b0d1e4a76c7 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -21,6 +21,119 @@ export const sharethroughAdapterSpec = { isBidRequestValid: bid => !!bid.params.pkey && bid.bidder === BIDDER_CODE, buildRequests: (bidRequests, bidderRequest) => { + const timeout = config.getConfig('bidderTimeout'); + + const nonHttp = sharethroughInternal.getProtocol().indexOf('http') < 0; + const secure = nonHttp || (sharethroughInternal.getProtocol().indexOf('https') > -1); + + const req = { + id: utils.generateUUID(), + cur: ['USD'], + tmax: timeout, + user: { + ext: { + eids: handleUniversalIds(bidRequests[0], [ + { attr: 'userId.idl_env', source: 'liveramp.com' }, + { attr: 'userId.id5id.uid', source: 'id5-sync.com' }, + { attr: 'userId.pubcid', source: 'pubcid.org' }, + { attr: 'userId.tdid', source: 'adserver.org' }, + { attr: 'userId.criteoId', source: 'criteo.com' }, + { attr: 'userId.britepoolid', source: 'britepool.com' }, + { attr: 'userId.lipb.lipbid', source: 'liveintent.com' }, + { attr: 'userId.intentiqid', source: 'intentiq.com' }, + { attr: 'userId.lotamePanoramaId', source: 'lotame.com' }, + { attr: 'userId.parrableId', source: 'parrable.com' }, + { attr: 'userId.netId', source: 'netid.de' }, + { attr: 'userId.sharedid', source: 'sharedid.org' }, + ]) + } + }, + regs: { + coppa: config.getConfig('coppa') === true ? 1 : 0, + ext: {}, + }, + source: { + ext: { + schain: bidRequest[0].schain + } + }, + bcat: bidRequests[0].params.bcat || [], + badv: bidRequests[0].params.badv || [], + }; + + if (bidderRequest.gdprConsent) { + const gdprApplies = bidderRequest.gdprConsent.gdprApplies === true; + req.regs.ext.gdpr = gdprApplies ? 1 : 0; + if (gdprApplies) { + req.user.ext.consent = bidderRequest.gdprConsent.consentString; + } + } + + if (bidderRequest.uspConsent) { + req.regs.ext.us_privacy = bidderRequest.uspConsent; + } + + const imps = bidRequests.map(bidReq => { + let impression; + + if (bidReq.mediaTypes && bidReq.mediaTypes.video) { + impression = { + video: { + topframe: utils.inIframe() ? 0 : 1, + skip: bidReq.mediaTypes.video.skip || 0, + linearity: bidReq.mediaTypes.video.linearity || 1, + minduration: bidReq.mediaTypes.video.minduration || 5, + maxduration: bidReq.mediaTypes.video.maxduration || 60, + playbackmethod: bidReq.mediaTypes.video.playbackmethod || [2], + api: getVideoApi(bidReq.mediaTypes.video), + mimes: bidReq.mediaTypes.video.mimes || ['video/mp4'], + protocols: getVideoProtocols(bidReq.mediaTypes.video), + h: bidReq.mediaTypes.video.playerSize[0][1], + w: bidReq.mediaTypes.video.playerSize[0][0] + } + }; + } else { + impression = { + banner: { + topframe: utils.inIframe() ? 0 : 1, + format: cleanSizes(bidReq.sizes) + } + }; + } + + // obj.tagid = String(bidReq.params.dmxid || bidReq.adUnitCode); + return { + id: bidReq.bidId, + secure: secure ? 1 : 0, + bidfloor: getFloor(bidReq), + ...impression + }; + }); + + return { + method: 'POST', + url: STR_ENDPOINT, + data: { + ...req, + imp: imps + }, + bidderRequest + }; + + + + + + + + + + + // ---------------------------------- + // OLD VERSION + + + return bidRequests.map(bidRequest => { let query = { placement_key: bidRequest.params.pkey, @@ -87,29 +200,29 @@ export const sharethroughAdapterSpec = { }, interpretResponse: ({ body }, req) => { - if (!body || !body.creatives || !body.creatives.length) { + if (!body || !body.seatbid || body.seatbid.length === 0) { return []; } - const creative = body.creatives[0]; let size = DEFAULT_SIZE; - if (req.strData.iframeSize || req.strData.sizes.length) { - size = req.strData.iframeSize - ? req.strData.iframeSize - : getLargestSize(req.strData.sizes); + if (req.params.iframeSize || req.params.sizes.length) { + size = req.params.iframeSize + ? req.params.iframeSize + : getLargestSize(req.params.sizes); } + const seatbid = body.seatbid[0]; return [{ - requestId: req.data.bidId, + requestId: seatbid.impid, width: size[0], height: size[1], - cpm: creative.cpm, - creativeId: creative.creative.creative_key, - dealId: creative.creative.deal_id, + cpm: +seatbid.price, + creativeId: seatbid.crid, + dealId: seatbid.dealid, currency: 'USD', netRevenue: true, ttl: 360, - ad: generateAd(body, req) + ad: generateAd(body, req) // body here is not a butler response anymore, need to get a butler formatted response from the iab response }]; }, @@ -140,34 +253,150 @@ export const sharethroughAdapterSpec = { onSetTargeting: (bid) => {} }; -function handleUniversalIds(bidRequest) { - if (!bidRequest.userId) return {}; - - const universalIds = {}; +function getVideoApi({ api }) { + let defaultValue = [2]; + if (api && Array.isArray(api) && api.length > 0) { + return api + } else { + return defaultValue; + } +} - const ttd = utils.deepAccess(bidRequest, 'userId.tdid'); - if (ttd) universalIds.ttduid = ttd; +function getVideoProtocols({ protocols }) { + let defaultValue = [2, 3, 5, 6, 7, 8]; + if (protocols && Array.isArray(protocols) && protocols.length > 0) { + return protocols; + } else { + return defaultValue; + } +} - const pubc = utils.deepAccess(bidRequest, 'userId.pubcid') || utils.deepAccess(bidRequest, 'crumbs.pubcid'); - if (pubc) universalIds.pubcid = pubc; +function cleanSizes(sizes) { + const supported = { + 160: { + 600: true, + }, + 300: { + 50: true, + 250: true, + 340: true, + 600: true, + }, + 320: { + 50: true, + 100: true, + 480: true, + }, + 336: { + 280: true, + }, + 400: { + 225: true, + 300: true, + }, + 480: { + 320: true, + }, + 640: { + 360: true, + 390: true, + }, + 660: { + 371: true, + }, + 728: { + 90: true, + }, + 854: { + 480: true, + }, + 970: { + 250: true, + } + } - const idl = utils.deepAccess(bidRequest, 'userId.idl_env'); - if (idl) universalIds.idluid = idl; + return sizes + .map(size => ({ w: +size[0], h: +size[1] })) + .filter(size => supported[size.w] && supported[size.w][size.h]); +} - const id5 = utils.deepAccess(bidRequest, 'userId.id5id.uid'); - if (id5) { - universalIds.id5uid = { id: id5 }; - const id5link = utils.deepAccess(bidRequest, 'userId.id5id.ext.linkType'); - if (id5link) universalIds.id5uid.linkType = id5link; +function getFloor(bid) { + let floor = null; + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: bid.mediaTypes.video ? 'video' : 'banner', + size: bid.sizes.map(size => ({ w: size[0], h: size[1] })) + }); + if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { + floor = parseFloat(floorInfo.floor); + } } + return floor !== null ? floor : bid.params.floor; +} - const lipb = utils.deepAccess(bidRequest, 'userId.lipb.lipbid'); - if (lipb) universalIds.liuid = lipb; +function shuffle(sizes, list) { + let removeSizes = sizes.filter(size => { + return list.map(l => `${l.size[0]}x${l.size[1]}`).indexOf(`${size[0]}x${size[1]}`) === -1 + }) + let reOrder = sizes.reduce((results, current) => { + if (results.length === 0) { + results.push(current); + return results; + } + results.push(current); + results = list.filter(l => results.map(r => `${r[0]}x${r[1]}`).indexOf(`${l.size[0]}x${l.size[1]}`) !== -1); + results = results.sort(function (a, b) { + return b.s - a.s; + }) + return results.map(r => r.size); + }, []) + return removeDuplicate([...reOrder, ...removeSizes]); +} - const shd = utils.deepAccess(bidRequest, 'userId.sharedid'); - if (shd) universalIds.shduid = shd; // object with keys: id & third +function removeDuplicate(values) { + return values.filter((elem, index) => { + return values.map(e => `${e[0]}x${e[1]}`).indexOf(`${elem[0]}x${elem[1]}`) === index + }) +} - return universalIds; +function handleUniversalIds(bidRequest, uids) { + return uids.map((uid) => ({ + source: uid.source, + uids: [{ id: utils.deepAccess(bidRequest, uid.attr), atype: 1 }] + })) + + + + + + // if (!bidRequest.userId) return {}; + // + // const universalIds = {}; + // + // const ttd = utils.deepAccess(bidRequest, 'userId.tdid'); + // if (ttd) universalIds.ttduid = ttd; + // + // const pubc = utils.deepAccess(bidRequest, 'userId.pubcid') || utils.deepAccess(bidRequest, 'crumbs.pubcid'); + // if (pubc) universalIds.pubcid = pubc; + // + // const idl = utils.deepAccess(bidRequest, 'userId.idl_env'); + // if (idl) universalIds.idluid = idl; + // + // const id5 = utils.deepAccess(bidRequest, 'userId.id5id.uid'); + // if (id5) { + // universalIds.id5uid = { id: id5 }; + // const id5link = utils.deepAccess(bidRequest, 'userId.id5id.ext.linkType'); + // if (id5link) universalIds.id5uid.linkType = id5link; + // } + // + // const lipb = utils.deepAccess(bidRequest, 'userId.lipb.lipbid'); + // if (lipb) universalIds.liuid = lipb; + // + // const shd = utils.deepAccess(bidRequest, 'userId.sharedid'); + // if (shd) universalIds.shduid = shd; // object with keys: id & third + // + // return universalIds; } function getLargestSize(sizes) { From 53b7dd405c9cbbb99d6dff4d51c964cdd5dea16b Mon Sep 17 00:00:00 2001 From: Mathieu Pheulpin Date: Wed, 19 May 2021 16:03:18 -0700 Subject: [PATCH 02/22] Final draft version for leveraging open RTB endpoint --- modules/sharethroughBidAdapter.js | 336 ++---------------------------- 1 file changed, 15 insertions(+), 321 deletions(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index b0d1e4a76c7..b81dda8aa7d 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -2,16 +2,15 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; -const VERSION = '3.3.2'; +const VERSION = '4.0.0'; const BIDDER_CODE = 'sharethrough'; +const SUPPLY_ID = 'WYu2BXv1'; + +// Todo: Update URL to new open RTB endpoint const STR_ENDPOINT = 'https://btlr.sharethrough.com/WYu2BXv1/v1'; -const DEFAULT_SIZE = [1, 1]; // this allows stubbing of utility function that is used internally by the sharethrough adapter export const sharethroughInternal = { - b64EncodeUnicode, - handleIframe, - isLockedInFrame, getProtocol }; @@ -54,6 +53,9 @@ export const sharethroughAdapterSpec = { }, source: { ext: { + id: SUPPLY_ID, + version: '$prebid.version$', + str: VERSION, schain: bidRequest[0].schain } }, @@ -96,14 +98,14 @@ export const sharethroughAdapterSpec = { impression = { banner: { topframe: utils.inIframe() ? 0 : 1, - format: cleanSizes(bidReq.sizes) + format: bidReq.sizes.map(size => ({ w: +size[0], h: +size[1] })) } }; } - // obj.tagid = String(bidReq.params.dmxid || bidReq.adUnitCode); return { id: bidReq.bidId, + tagid: String(bidReq.params.pkey), secure: secure ? 1 : 0, bidfloor: getFloor(bidReq), ...impression @@ -119,84 +121,6 @@ export const sharethroughAdapterSpec = { }, bidderRequest }; - - - - - - - - - - - // ---------------------------------- - // OLD VERSION - - - - return bidRequests.map(bidRequest => { - let query = { - placement_key: bidRequest.params.pkey, - bidId: bidRequest.bidId, - consent_required: false, - instant_play_capable: canAutoPlayHTML5Video(), - hbSource: 'prebid', - hbVersion: '$prebid.version$', - strVersion: VERSION - }; - - Object.assign(query, handleUniversalIds(bidRequest)); - - const nonHttp = sharethroughInternal.getProtocol().indexOf('http') < 0; - query.secure = nonHttp || (sharethroughInternal.getProtocol().indexOf('https') > -1); - - if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString) { - query.consent_string = bidderRequest.gdprConsent.consentString; - } - - if (bidderRequest && bidderRequest.gdprConsent) { - query.consent_required = !!bidderRequest.gdprConsent.gdprApplies; - } - - if (bidderRequest && bidderRequest.uspConsent) { - query.us_privacy = bidderRequest.uspConsent - } - - if (config.getConfig('coppa') === true) { - query.coppa = true - } - - if (bidRequest.schain) { - query.schain = JSON.stringify(bidRequest.schain); - } - - if (bidRequest.bidfloor) { - query.bidfloor = parseFloat(bidRequest.bidfloor); - } - - if (bidRequest.params.badv) { - query.badv = bidRequest.params.badv; - } - - if (bidRequest.params.bcat) { - query.bcat = bidRequest.params.bcat; - } - - // Data that does not need to go to the server, - // but we need as part of interpretResponse() - const strData = { - skipIframeBusting: bidRequest.params.iframe, - iframeSize: bidRequest.params.iframeSize, - sizes: bidRequest.sizes - }; - - return { - method: 'POST', - url: STR_ENDPOINT, - data: query, - strData: strData - }; - }) }, interpretResponse: ({ body }, req) => { @@ -204,26 +128,18 @@ export const sharethroughAdapterSpec = { return []; } - let size = DEFAULT_SIZE; - if (req.params.iframeSize || req.params.sizes.length) { - size = req.params.iframeSize - ? req.params.iframeSize - : getLargestSize(req.params.sizes); - } - - const seatbid = body.seatbid[0]; - return [{ + return body.seatbid.map(seatbid => ({ requestId: seatbid.impid, - width: size[0], - height: size[1], + width: +seatbid.w, + height: +seatbid.h, cpm: +seatbid.price, creativeId: seatbid.crid, - dealId: seatbid.dealid, + dealId: seatbid.dealid || null, currency: 'USD', netRevenue: true, ttl: 360, - ad: generateAd(body, req) // body here is not a butler response anymore, need to get a butler formatted response from the iab response - }]; + ad: seatbid.adm, + })); }, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { @@ -271,55 +187,6 @@ function getVideoProtocols({ protocols }) { } } -function cleanSizes(sizes) { - const supported = { - 160: { - 600: true, - }, - 300: { - 50: true, - 250: true, - 340: true, - 600: true, - }, - 320: { - 50: true, - 100: true, - 480: true, - }, - 336: { - 280: true, - }, - 400: { - 225: true, - 300: true, - }, - 480: { - 320: true, - }, - 640: { - 360: true, - 390: true, - }, - 660: { - 371: true, - }, - 728: { - 90: true, - }, - 854: { - 480: true, - }, - 970: { - 250: true, - } - } - - return sizes - .map(size => ({ w: +size[0], h: +size[1] })) - .filter(size => supported[size.w] && supported[size.w][size.h]); -} - function getFloor(bid) { let floor = null; if (typeof bid.getFloor === 'function') { @@ -335,185 +202,12 @@ function getFloor(bid) { return floor !== null ? floor : bid.params.floor; } -function shuffle(sizes, list) { - let removeSizes = sizes.filter(size => { - return list.map(l => `${l.size[0]}x${l.size[1]}`).indexOf(`${size[0]}x${size[1]}`) === -1 - }) - let reOrder = sizes.reduce((results, current) => { - if (results.length === 0) { - results.push(current); - return results; - } - results.push(current); - results = list.filter(l => results.map(r => `${r[0]}x${r[1]}`).indexOf(`${l.size[0]}x${l.size[1]}`) !== -1); - results = results.sort(function (a, b) { - return b.s - a.s; - }) - return results.map(r => r.size); - }, []) - return removeDuplicate([...reOrder, ...removeSizes]); -} - -function removeDuplicate(values) { - return values.filter((elem, index) => { - return values.map(e => `${e[0]}x${e[1]}`).indexOf(`${elem[0]}x${elem[1]}`) === index - }) -} function handleUniversalIds(bidRequest, uids) { return uids.map((uid) => ({ source: uid.source, uids: [{ id: utils.deepAccess(bidRequest, uid.attr), atype: 1 }] })) - - - - - - // if (!bidRequest.userId) return {}; - // - // const universalIds = {}; - // - // const ttd = utils.deepAccess(bidRequest, 'userId.tdid'); - // if (ttd) universalIds.ttduid = ttd; - // - // const pubc = utils.deepAccess(bidRequest, 'userId.pubcid') || utils.deepAccess(bidRequest, 'crumbs.pubcid'); - // if (pubc) universalIds.pubcid = pubc; - // - // const idl = utils.deepAccess(bidRequest, 'userId.idl_env'); - // if (idl) universalIds.idluid = idl; - // - // const id5 = utils.deepAccess(bidRequest, 'userId.id5id.uid'); - // if (id5) { - // universalIds.id5uid = { id: id5 }; - // const id5link = utils.deepAccess(bidRequest, 'userId.id5id.ext.linkType'); - // if (id5link) universalIds.id5uid.linkType = id5link; - // } - // - // const lipb = utils.deepAccess(bidRequest, 'userId.lipb.lipbid'); - // if (lipb) universalIds.liuid = lipb; - // - // const shd = utils.deepAccess(bidRequest, 'userId.sharedid'); - // if (shd) universalIds.shduid = shd; // object with keys: id & third - // - // return universalIds; -} - -function getLargestSize(sizes) { - function area(size) { - return size[0] * size[1]; - } - - return sizes.reduce((prev, current) => { - if (area(current) > area(prev)) { - return current - } else { - return prev - } - }); -} - -function generateAd(body, req) { - const strRespId = `str_response_${req.data.bidId}`; - - let adMarkup = ` -
-
- - `; - - if (req.strData.skipIframeBusting) { - // Don't break out of iframe - adMarkup = adMarkup + ``; - } else { - // Add logic to the markup that detects whether or not in top level document is accessible - // this logic will deploy sfp.js and/or iframe buster script(s) as appropriate - adMarkup = adMarkup + ` - - `; - } - - return adMarkup; -} - -function handleIframe () { - // only load iframe buster JS if we can access the top level document - // if we are 'locked in' to this frame then no point trying to bust out: we may as well render in the frame instead - var iframeBusterLoaded = false; - if (!window.lockedInFrame) { - var sfpIframeBusterJs = document.createElement('script'); - sfpIframeBusterJs.src = 'https://native.sharethrough.com/assets/sfp-set-targeting.js'; - sfpIframeBusterJs.type = 'text/javascript'; - try { - window.document.getElementsByTagName('body')[0].appendChild(sfpIframeBusterJs); - iframeBusterLoaded = true; - } catch (e) { - utils.logError('Trouble writing frame buster script, error details:', e); - } - } - - var clientJsLoaded = (!iframeBusterLoaded) ? !!(window.STR && window.STR.Tag) : !!(window.top.STR && window.top.STR.Tag); - if (!clientJsLoaded) { - var sfpJs = document.createElement('script'); - sfpJs.src = 'https://native.sharethrough.com/assets/sfp.js'; - sfpJs.type = 'text/javascript'; - - // only add sfp js to window.top if iframe busting successfully loaded; otherwise, add to iframe - try { - if (iframeBusterLoaded) { - window.top.document.getElementsByTagName('body')[0].appendChild(sfpJs); - } else { - window.document.getElementsByTagName('body')[0].appendChild(sfpJs); - } - } catch (e) { - utils.logError('Trouble writing sfp script, error details:', e); - } - } -} - -// determines if we are capable of busting out of the iframe we are in -// if we catch a DOMException when trying to access top-level document, it means we're stuck in the frame we're in -function isLockedInFrame () { - window.lockedInFrame = false; - try { - window.lockedInFrame = !window.top.document; - } catch (e) { - window.lockedInFrame = (e instanceof DOMException); - } -} - -// See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem -function b64EncodeUnicode(str) { - return btoa( - encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, - function toSolidBytes(match, p1) { - return String.fromCharCode('0x' + p1); - })); -} - -function canAutoPlayHTML5Video() { - const userAgent = navigator.userAgent; - if (!userAgent) return false; - - const isAndroid = /Android/i.test(userAgent); - const isiOS = /iPhone|iPad|iPod/i.test(userAgent); - const chromeVersion = parseInt((/Chrome\/([0-9]+)/.exec(userAgent) || [0, 0])[1]); - const chromeiOSVersion = parseInt((/CriOS\/([0-9]+)/.exec(userAgent) || [0, 0])[1]); - const safariVersion = parseInt((/Version\/([0-9]+)/.exec(userAgent) || [0, 0])[1]); - - if ( - (isAndroid && chromeVersion >= 53) || - (isiOS && (safariVersion >= 10 || chromeiOSVersion >= 53)) || - !(isAndroid || isiOS) - ) { - return true; - } else { - return false; - } } function getProtocol() { From be2fa52d5940e8cc6ec08fad42e980287601ff47 Mon Sep 17 00:00:00 2001 From: Mathieu Pheulpin Date: Wed, 19 May 2021 16:13:00 -0700 Subject: [PATCH 03/22] Merged master --- modules/sharethroughBidAdapter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 576f73cd21a..e6a26034413 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -138,9 +138,11 @@ export const sharethroughAdapterSpec = { currency: 'USD', netRevenue: true, ttl: 360, - meta: { advertiserDomains: creative.creative && creative.creative.adomain ? creative.creative.adomain : [] }, ad: seatbid.adm, - }]; + meta: { + advertiserDomains: seatbid.adomain || [] + }, + })); }, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { From 63933f92058d35ffb3250bc50452249b98a84d12 Mon Sep 17 00:00:00 2001 From: Mathieu Pheulpin Date: Thu, 27 May 2021 15:12:19 -0700 Subject: [PATCH 04/22] Some fixes around seat and origin/referer --- modules/sharethroughBidAdapter.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index e6a26034413..f3dde118e0d 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -29,6 +29,11 @@ export const sharethroughAdapterSpec = { id: utils.generateUUID(), cur: ['USD'], tmax: timeout, + site: { + domain: window.location.hostname, + page: window.location.href, + ref: bidderRequest.refererInfo ? bidderRequest.refererInfo.referer || null : null, + }, user: { ext: { eids: handleUniversalIds(bidRequests[0], [ @@ -124,23 +129,23 @@ export const sharethroughAdapterSpec = { }, interpretResponse: ({ body }, req) => { - if (!body || !body.seatbid || body.seatbid.length === 0) { + if (!body || !body.seatbid || body.seatbid.length === 0 || !body.seatbid[0].bid || body.seatbid[0].bid.length === 0) { return []; } - return body.seatbid.map(seatbid => ({ - requestId: seatbid.impid, - width: +seatbid.w, - height: +seatbid.h, - cpm: +seatbid.price, - creativeId: seatbid.crid, - dealId: seatbid.dealid || null, + return body.seatbid[0].bid.map(bid => ({ + requestId: bid.impid, + width: +bid.w, + height: +bid.h, + cpm: +bid.price, + creativeId: bid.crid, + dealId: bid.dealid || null, currency: 'USD', netRevenue: true, ttl: 360, - ad: seatbid.adm, + ad: bid.adm, meta: { - advertiserDomains: seatbid.adomain || [] + advertiserDomains: bid.adomain || [] }, })); }, From 3e88ed58ec463615e3e3c9f052a50aed20096606 Mon Sep 17 00:00:00 2001 From: Mathieu Pheulpin Date: Thu, 27 May 2021 17:01:46 -0700 Subject: [PATCH 05/22] Fix schain and CORS --- modules/sharethroughBidAdapter.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index f3dde118e0d..9a16304699a 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -61,7 +61,7 @@ export const sharethroughAdapterSpec = { id: SUPPLY_ID, version: '$prebid.version$', str: VERSION, - schain: bidRequest[0].schain + schain: bidRequests[0].schain } }, bcat: bidRequests[0].params.bcat || [], @@ -120,6 +120,11 @@ export const sharethroughAdapterSpec = { return { method: 'POST', url: STR_ENDPOINT, + options: { + contentType: 'application/json', + withCredentials: false, + crossOrigin: true, + }, data: { ...req, imp: imps From 12acb1aa11b8adb2a9400aa679bdbf0b62d62345 Mon Sep 17 00:00:00 2001 From: Eddy Pechuzal <46331062+epechuzal@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:06:46 -0700 Subject: [PATCH 06/22] Handle GPID (openrtb endpoint) (#1110) * Handle GPID in openrtb * Update modules/sharethroughBidAdapter.js Co-authored-by: Mathieu Pheulpin Co-authored-by: Mathieu Pheulpin --- modules/sharethroughBidAdapter.js | 43 ++++++++++++++++--------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 9a16304699a..e1e04ed0472 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -81,30 +81,31 @@ export const sharethroughAdapterSpec = { } const imps = bidRequests.map(bidReq => { - let impression; + const impression = {}; + + const gpid = utils.deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { + impression.ext = { gpid: gpid }; + } if (bidReq.mediaTypes && bidReq.mediaTypes.video) { - impression = { - video: { - topframe: utils.inIframe() ? 0 : 1, - skip: bidReq.mediaTypes.video.skip || 0, - linearity: bidReq.mediaTypes.video.linearity || 1, - minduration: bidReq.mediaTypes.video.minduration || 5, - maxduration: bidReq.mediaTypes.video.maxduration || 60, - playbackmethod: bidReq.mediaTypes.video.playbackmethod || [2], - api: getVideoApi(bidReq.mediaTypes.video), - mimes: bidReq.mediaTypes.video.mimes || ['video/mp4'], - protocols: getVideoProtocols(bidReq.mediaTypes.video), - h: bidReq.mediaTypes.video.playerSize[0][1], - w: bidReq.mediaTypes.video.playerSize[0][0] - } - }; + impression.video = { + topframe: utils.inIframe() ? 0 : 1, + skip: bidReq.mediaTypes.video.skip || 0, + linearity: bidReq.mediaTypes.video.linearity || 1, + minduration: bidReq.mediaTypes.video.minduration || 5, + maxduration: bidReq.mediaTypes.video.maxduration || 60, + playbackmethod: bidReq.mediaTypes.video.playbackmethod || [2], + api: getVideoApi(bidReq.mediaTypes.video), + mimes: bidReq.mediaTypes.video.mimes || ['video/mp4'], + protocols: getVideoProtocols(bidReq.mediaTypes.video), + h: bidReq.mediaTypes.video.playerSize[0][1], + w: bidReq.mediaTypes.video.playerSize[0][0] + } } else { - impression = { - banner: { - topframe: utils.inIframe() ? 0 : 1, - format: bidReq.sizes.map(size => ({ w: +size[0], h: +size[1] })) - } + impression.banner = { + topframe: utils.inIframe() ? 0 : 1, + format: bidReq.sizes.map(size => ({ w: +size[0], h: +size[1] })) }; } From bbfa3a3440e7581cccfd0c766c9fbc44f89f79cf Mon Sep 17 00:00:00 2001 From: Mathieu Pheulpin Date: Tue, 29 Jun 2021 15:47:30 -0700 Subject: [PATCH 07/22] Handling video responses --- modules/sharethroughBidAdapter.js | 109 ++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 34 deletions(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index e1e04ed0472..e07de7ae7b3 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,21 +1,24 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import * as utils from '../src/utils.js'; import { config } from '../src/config.js'; +import { BANNER, VIDEO } from '../src/mediaTypes'; const VERSION = '4.0.0'; const BIDDER_CODE = 'sharethrough'; const SUPPLY_ID = 'WYu2BXv1'; // Todo: Update URL to new open RTB endpoint -const STR_ENDPOINT = 'https://btlr.sharethrough.com/WYu2BXv1/v1'; +const STR_ENDPOINT = `http://localhost:3030/universal/v1?supplyId=${SUPPLY_ID}`; // this allows stubbing of utility function that is used internally by the sharethrough adapter export const sharethroughInternal = { - getProtocol + getProtocol, }; export const sharethroughAdapterSpec = { code: BIDDER_CODE, + supportedFormat: [BANNER, VIDEO], + supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: bid => !!bid.params.pkey && bid.bidder === BIDDER_CODE, @@ -49,8 +52,8 @@ export const sharethroughAdapterSpec = { { attr: 'userId.parrableId', source: 'parrable.com' }, { attr: 'userId.netId', source: 'netid.de' }, { attr: 'userId.sharedid', source: 'sharedid.org' }, - ]) - } + ]), + }, }, regs: { coppa: config.getConfig('coppa') === true ? 1 : 0, @@ -61,8 +64,8 @@ export const sharethroughAdapterSpec = { id: SUPPLY_ID, version: '$prebid.version$', str: VERSION, - schain: bidRequests[0].schain - } + schain: bidRequests[0].schain, + }, }, bcat: bidRequests[0].params.bcat || [], badv: bidRequests[0].params.badv || [], @@ -100,12 +103,12 @@ export const sharethroughAdapterSpec = { mimes: bidReq.mediaTypes.video.mimes || ['video/mp4'], protocols: getVideoProtocols(bidReq.mediaTypes.video), h: bidReq.mediaTypes.video.playerSize[0][1], - w: bidReq.mediaTypes.video.playerSize[0][0] - } + w: bidReq.mediaTypes.video.playerSize[0][0], + }; } else { impression.banner = { topframe: utils.inIframe() ? 0 : 1, - format: bidReq.sizes.map(size => ({ w: +size[0], h: +size[1] })) + format: bidReq.sizes.map(size => ({ w: +size[0], h: +size[1] })), }; } @@ -114,7 +117,7 @@ export const sharethroughAdapterSpec = { tagid: String(bidReq.params.pkey), secure: secure ? 1 : 0, bidfloor: getFloor(bidReq), - ...impression + ...impression, }; }); @@ -128,9 +131,9 @@ export const sharethroughAdapterSpec = { }, data: { ...req, - imp: imps + imp: imps, }, - bidderRequest + bidderRequest, }; }, @@ -139,21 +142,33 @@ export const sharethroughAdapterSpec = { return []; } - return body.seatbid[0].bid.map(bid => ({ - requestId: bid.impid, - width: +bid.w, - height: +bid.h, - cpm: +bid.price, - creativeId: bid.crid, - dealId: bid.dealid || null, - currency: 'USD', - netRevenue: true, - ttl: 360, - ad: bid.adm, - meta: { - advertiserDomains: bid.adomain || [] - }, - })); + return body.seatbid[0].bid.map(bid => { + const request = matchRequest(bid.id, req); + + const response = { + requestId: bid.impid, + width: +bid.w, + height: +bid.h, + cpm: +bid.price, + creativeId: bid.crid, + dealId: bid.dealid || null, + mediaType: request.mediaTypes && request.mediaTypes.video ? 'video' : 'banner', + currency: 'USD', + netRevenue: true, + ttl: 360, + ad: bid.adm, + meta: { + advertiserDomains: bid.adomain || [], + }, + }; + + if (response.mediaType === 'video') { + response.vastXml = cleanVast(bid.adm, bid.nurl); + response.ttl = 3600; + } + + return response; + }); }, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { @@ -174,19 +189,22 @@ export const sharethroughAdapterSpec = { }, // Empty implementation for prebid core to be able to find it - onTimeout: (data) => {}, + onTimeout: (data) => { + }, // Empty implementation for prebid core to be able to find it - onBidWon: (bid) => {}, + onBidWon: (bid) => { + }, // Empty implementation for prebid core to be able to find it - onSetTargeting: (bid) => {} + onSetTargeting: (bid) => { + }, }; function getVideoApi({ api }) { let defaultValue = [2]; if (api && Array.isArray(api) && api.length > 0) { - return api + return api; } else { return defaultValue; } @@ -207,7 +225,7 @@ function getFloor(bid) { const floorInfo = bid.getFloor({ currency: 'USD', mediaType: bid.mediaTypes.video ? 'video' : 'banner', - size: bid.sizes.map(size => ({ w: size[0], h: size[1] })) + size: bid.sizes.map(size => ({ w: size[0], h: size[1] })), }); if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { floor = parseFloat(floorInfo.floor); @@ -220,12 +238,35 @@ function getFloor(bid) { function handleUniversalIds(bidRequest, uids) { return uids.map((uid) => ({ source: uid.source, - uids: [{ id: utils.deepAccess(bidRequest, uid.attr), atype: 1 }] - })) + uids: [{ id: utils.deepAccess(bidRequest, uid.attr), atype: 1 }], + })); } function getProtocol() { return document.location.protocol; } +function cleanVast(str, nurl) { + try { + const toBeRemoved = /]*?src\s*=\s*['\"]([^'\"]*?)['\"][^>]*?>/; + const [img, url] = str.match(toBeRemoved); + str = str.replace(toBeRemoved, ''); + + if (img && url) { + const insrt = ``; + str = str.replace('', `${insrt}`); + } + return str; + } catch (e) { + if (!nurl) return str; + + const insrt = ``; + return str.replace('', `${insrt}`); + } +} + +function matchRequest(id, bidRequest) { + return bidRequest.bidderRequest.bids.find(bid => bid.bidId === id); +} + registerBidder(sharethroughAdapterSpec); From 9f926c2888719dd767f339742eec037440f9021d Mon Sep 17 00:00:00 2001 From: Mathieu Pheulpin Date: Thu, 1 Jul 2021 16:24:27 -0700 Subject: [PATCH 08/22] Revamp specs --- modules/sharethroughBidAdapter.js | 31 +- .../modules/sharethroughBidAdapter_spec.js | 873 +++++++----------- 2 files changed, 377 insertions(+), 527 deletions(-) diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index e07de7ae7b3..6d66da76110 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -55,6 +55,9 @@ export const sharethroughAdapterSpec = { ]), }, }, + device: { + ua: navigator.userAgent, + }, regs: { coppa: config.getConfig('coppa') === true ? 1 : 0, ext: {}, @@ -94,16 +97,16 @@ export const sharethroughAdapterSpec = { if (bidReq.mediaTypes && bidReq.mediaTypes.video) { impression.video = { topframe: utils.inIframe() ? 0 : 1, - skip: bidReq.mediaTypes.video.skip || 0, - linearity: bidReq.mediaTypes.video.linearity || 1, - minduration: bidReq.mediaTypes.video.minduration || 5, - maxduration: bidReq.mediaTypes.video.maxduration || 60, + skip: bidReq.mediaTypes.video.skip ?? 0, + linearity: bidReq.mediaTypes.video.linearity ?? 1, + minduration: bidReq.mediaTypes.video.minduration ?? 5, + maxduration: bidReq.mediaTypes.video.maxduration ?? 60, playbackmethod: bidReq.mediaTypes.video.playbackmethod || [2], api: getVideoApi(bidReq.mediaTypes.video), mimes: bidReq.mediaTypes.video.mimes || ['video/mp4'], protocols: getVideoProtocols(bidReq.mediaTypes.video), - h: bidReq.mediaTypes.video.playerSize[0][1], w: bidReq.mediaTypes.video.playerSize[0][0], + h: bidReq.mediaTypes.video.playerSize[0][1], }; } else { impression.banner = { @@ -133,6 +136,7 @@ export const sharethroughAdapterSpec = { ...req, imp: imps, }, + bidRequests, bidderRequest, }; }, @@ -152,7 +156,7 @@ export const sharethroughAdapterSpec = { cpm: +bid.price, creativeId: bid.crid, dealId: bid.dealid || null, - mediaType: request.mediaTypes && request.mediaTypes.video ? 'video' : 'banner', + mediaType: request.mediaTypes && request.mediaTypes.video ? VIDEO : BANNER, currency: 'USD', netRevenue: true, ttl: 360, @@ -162,9 +166,12 @@ export const sharethroughAdapterSpec = { }, }; - if (response.mediaType === 'video') { - response.vastXml = cleanVast(bid.adm, bid.nurl); + if (response.mediaType === VIDEO) { response.ttl = 3600; + response.vastXml = cleanVast(bid.adm, bid.nurl); + + // const blob = new Blob([response.vastXml], { type: 'application/xml' }); + // response.vastUrl = window.URL.createObjectURL(blob); } return response; @@ -224,7 +231,7 @@ function getFloor(bid) { if (typeof bid.getFloor === 'function') { const floorInfo = bid.getFloor({ currency: 'USD', - mediaType: bid.mediaTypes.video ? 'video' : 'banner', + mediaType: bid.mediaTypes && bid.mediaTypes.video ? 'video' : 'banner', size: bid.sizes.map(size => ({ w: size[0], h: size[1] })), }); if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { @@ -243,7 +250,7 @@ function handleUniversalIds(bidRequest, uids) { } function getProtocol() { - return document.location.protocol; + return window.location.protocol; } function cleanVast(str, nurl) { @@ -265,8 +272,8 @@ function cleanVast(str, nurl) { } } -function matchRequest(id, bidRequest) { - return bidRequest.bidderRequest.bids.find(bid => bid.bidId === id); +function matchRequest(id, request) { + return request.bidRequests.find(bid => bid.bidId === id); } registerBidder(sharethroughAdapterSpec); diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 3e406e1af44..239db28b01d 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -1,150 +1,10 @@ import { expect } from 'chai'; import { sharethroughAdapterSpec, sharethroughInternal } from 'modules/sharethroughBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import * as utils from '../../../src/utils.js'; import { config } from 'src/config'; +import * as utils from 'src/utils'; const spec = newBidder(sharethroughAdapterSpec).getSpec(); -const bidRequests = [ - { - bidder: 'sharethrough', - bidId: 'bidId1', - sizes: [[600, 300]], - placementCode: 'foo', - params: { - pkey: 'aaaa1111' - }, - ortb2Imp: { - ext: { - data: { - pbadslot: 'adslot-id-1' - } - } - }, - userId: { - tdid: 'fake-tdid', - pubcid: 'fake-pubcid', - idl_env: 'fake-identity-link', - id5id: { - uid: 'fake-id5id', - ext: { - linkType: 2 - } - }, - lipb: { - lipbid: 'fake-lipbid' - } - }, - crumbs: { - pubcid: 'fake-pubcid-in-crumbs-obj' - } - }, - { - bidder: 'sharethrough', - bidId: 'bidId2', - sizes: [[700, 400]], - placementCode: 'bar', - params: { - pkey: 'bbbb2222', - iframe: true - } - }, - { - bidder: 'sharethrough', - bidId: 'bidId3', - sizes: [[700, 400]], - placementCode: 'coconut', - params: { - pkey: 'cccc3333', - iframe: true, - iframeSize: [500, 500] - } - }, - { - bidder: 'sharethrough', - bidId: 'bidId4', - sizes: [[700, 400]], - placementCode: 'bar', - params: { - pkey: 'dddd4444', - badv: ['domain1.com', 'domain2.com'] - } - }, - { - bidder: 'sharethrough', - bidId: 'bidId5', - sizes: [[700, 400]], - placementCode: 'bar', - params: { - pkey: 'eeee5555', - bcat: ['IAB1-1', 'IAB1-2'] - } - }, -]; - -const prebidRequests = [ - { - method: 'POST', - url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', - data: { - bidId: 'bidId', - placement_key: 'pKey' - }, - strData: { - skipIframeBusting: false, - sizes: [] - } - }, - { - method: 'POST', - url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', - data: { - bidId: 'bidId', - placement_key: 'pKey' - }, - strData: { - skipIframeBusting: true, - sizes: [[300, 250], [300, 300], [250, 250], [600, 50]] - } - }, - { - method: 'POST', - url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', - data: { - bidId: 'bidId', - placement_key: 'pKey' - }, - strData: { - skipIframeBusting: true, - iframeSize: [500, 500], - sizes: [[300, 250], [300, 300], [250, 250], [600, 50]] - } - }, - { - method: 'POST', - url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', - data: { - bidId: 'bidId', - placement_key: 'pKey' - }, - strData: { - skipIframeBusting: false, - sizes: [[0, 0]] - } - }, - { - method: 'POST', - url: 'https://btlr.sharethrough.com/WYu2BXv1/v1', - data: { - bidId: 'bidId', - placement_key: 'pKey' - }, - strData: { - skipIframeBusting: false, - sizes: [[300, 250], [300, 300], [250, 250], [600, 50]] - } - } -]; const bidderResponse = { body: { @@ -165,83 +25,33 @@ const bidderResponse = { header: { get: (header) => header } }; -const setUserAgent = (uaString) => { - window.navigator['__defineGetter__']('userAgent', function() { - return uaString; - }); -}; - -describe('sharethrough internal spec', function() { - let windowStub, windowTopStub; - let stubbedReturn = [{ - appendChild: () => undefined - }] - beforeEach(function() { - windowStub = sinon.stub(window.document, 'getElementsByTagName'); - windowTopStub = sinon.stub(window.top.document, 'getElementsByTagName'); - windowStub.withArgs('body').returns(stubbedReturn); - windowTopStub.withArgs('body').returns(stubbedReturn); - }); - - afterEach(function() { - windowStub.restore(); - windowTopStub.restore(); - window.STR = undefined; - window.top.STR = undefined; - }); - - describe('we cannot access top level document', function() { - beforeEach(function() { - window.lockedInFrame = true; - }); - - afterEach(function() { - window.lockedInFrame = false; - }); - - it('appends sfp.js to the safeframe', function() { - sharethroughInternal.handleIframe(); - expect(windowStub.calledOnce).to.be.true; - }); +describe('sharethrough adapter spec', function() { + let protocolStub; + let inIframeStub; - it('does not append anything if sfp.js is already loaded in the safeframe', function() { - window.STR = { Tag: true }; - sharethroughInternal.handleIframe(); - expect(windowStub.notCalled).to.be.true; - expect(windowTopStub.notCalled).to.be.true; - }); + beforeEach(() => { + protocolStub = sinon.stub(sharethroughInternal, 'getProtocol').returns('https'); + inIframeStub = sinon.stub(utils, 'inIframe').returns(false); }); - describe('we are able to bust out of the iframe', function() { - it('appends sfp.js to window.top', function() { - sharethroughInternal.handleIframe(); - expect(windowStub.calledOnce).to.be.true; - expect(windowTopStub.calledOnce).to.be.true; - }); - - it('only appends sfp-set-targeting.js if sfp.js is already loaded on the page', function() { - window.top.STR = { Tag: true }; - sharethroughInternal.handleIframe(); - expect(windowStub.calledOnce).to.be.true; - expect(windowTopStub.notCalled).to.be.true; - }); + afterEach(() => { + protocolStub.restore(); + inIframeStub.restore(); }); -}); -describe('sharethrough adapter spec', function() { - describe('.code', function() { + describe('code', function() { it('should return a bidder code of sharethrough', function() { expect(spec.code).to.eql('sharethrough'); }); }); - describe('.isBidRequestValid', function() { + describe('isBidRequestValid', function() { it('should return false if req has no pkey', function() { const invalidBidRequest = { bidder: 'sharethrough', params: { - notPKey: 'abc123' - } + notPKey: 'abc123', + }, }; expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); @@ -250,383 +60,416 @@ describe('sharethrough adapter spec', function() { const invalidBidRequest = { bidder: 'notSharethrough', params: { - notPKey: 'abc123' + pkey: 'abc123', } }; expect(spec.isBidRequestValid(invalidBidRequest)).to.eql(false); }); it('should return true if req is correct', function() { - expect(spec.isBidRequestValid(bidRequests[0])).to.eq(true); - expect(spec.isBidRequestValid(bidRequests[1])).to.eq(true); + const validBidRequest = { + bidder: 'sharethrough', + params: { + pkey: 'abc123', + }, + }; + expect(spec.isBidRequestValid(validBidRequest)).to.eq(true); }); }); - describe('.buildRequests', function() { - it('should return an array of requests', function() { - const builtBidRequests = spec.buildRequests(bidRequests); + describe('open rtb', () => { + let bidRequests, bidderRequest; - expect(builtBidRequests[0].url).to.eq('https://btlr.sharethrough.com/WYu2BXv1/v1'); - expect(builtBidRequests[1].url).to.eq('https://btlr.sharethrough.com/WYu2BXv1/v1'); - expect(builtBidRequests[0].method).to.eq('POST'); + beforeEach(() => { + config.setConfig({ + bidderTimeout: 242, + coppa: true, + }); + + bidRequests = [ + { + bidder: 'sharethrough', + bidId: 'bidId1', + sizes: [[300, 250], [300, 600]], + params: { + pkey: 'aaaa1111', + bcat: ['cat1', 'cat2'], + badv: ['adv1', 'adv2'], + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'universal-id', + }, + }, + }, + userId: { + tdid: 'fake-tdid', + pubcid: 'fake-pubcid', + idl_env: 'fake-identity-link', + id5id: { + uid: 'fake-id5id', + ext: { + linkType: 2, + }, + }, + lipb: { + lipbid: 'fake-lipbid', + }, + }, + crumbs: { + pubcid: 'fake-pubcid-in-crumbs-obj', + }, + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'directseller.com', + sid: '00001', + rid: 'BidRequest1', + hp: 1, + }, + ], + }, + getFloor: () => ({ currency: 'USD', floor: 42 }), + }, + { + bidder: 'sharethrough', + bidId: 'bidId2', + sizes: [[600, 300]], + params: { + pkey: 'bbbb2222', + }, + mediaTypes: { + video: { + skip: 1, + linearity: 0, + minduration: 10, + maxduration: 30, + playbackmethod: [1], + api: [3], + mimes: ['video/3gpp'], + protocols: [2, 3], + playerSize: [[640, 480]], + }, + }, + getFloor: () => ({ currency: 'USD', floor: 42 }), + }, + ]; + + bidderRequest = { + refererInfo: { + referer: 'https://referer.com', + }, + }; }); - it('should set the instant_play_capable parameter correctly based on browser userAgent string', function() { - setUserAgent('Android Chrome/60'); - let builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.true; + describe('buildRequests', function() { - setUserAgent('iPhone Version/11'); - builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.true; - setUserAgent('iPhone CriOS/60'); - builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.true; + describe('top level object', () => { + it('should build openRTB request', () => { + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); - setUserAgent('Android Chrome/50'); - builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.false; + expect(builtRequest.method).to.equal('POST'); + expect(builtRequest.url).not.to.be.undefined; + expect(builtRequest.options).to.deep.equal({ + contentType: 'application/json', + withCredentials: false, + crossOrigin: true, + }); + expect(builtRequest.bidderRequest).to.deep.equal(bidderRequest); - setUserAgent('Android Chrome'); - builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.false; + const openRtbReq = builtRequest.data; + expect(openRtbReq.id).not.to.be.undefined; + expect(openRtbReq.cur).to.deep.equal(['USD']); + expect(openRtbReq.tmax).to.equal(242); - setUserAgent(undefined); - builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0].data.instant_play_capable).to.be.false; - }); + expect(openRtbReq.site.domain).not.to.be.undefined; + expect(openRtbReq.site.page).not.to.be.undefined; + expect(openRtbReq.site.ref).to.equal('https://referer.com'); - it('should set the secure parameter to false when the protocol is http', function() { - const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('http:'); - const bidRequest = spec.buildRequests(bidRequests, null)[0]; - expect(bidRequest.data.secure).to.be.false; - stub.restore(); - }); + expect(openRtbReq.user.ext.eids).to.deep.include({ source: 'adserver.org', uids: [{ id: 'fake-tdid', atype: 1 }] }); + expect(openRtbReq.user.ext.eids).to.deep.include({ source: 'pubcid.org', uids: [{ id: 'fake-pubcid', atype: 1 }] }); + expect(openRtbReq.user.ext.eids).to.deep.include({ source: 'liveramp.com', uids: [{ id: 'fake-identity-link', atype: 1 }] }); + expect(openRtbReq.user.ext.eids).to.deep.include({ source: 'id5-sync.com', uids: [{ id: 'fake-id5id', atype: 1 }] }); // todo linkType + // todo: rest of user sync when format is confirmed - it('should set the secure parameter to true when the protocol is https', function() { - const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('https:'); - const bidRequest = spec.buildRequests(bidRequests, null)[0]; - expect(bidRequest.data.secure).to.be.true; - stub.restore(); - }); + expect(openRtbReq.device.ua).to.equal(navigator.userAgent); + expect(openRtbReq.regs.coppa).to.equal(1); - it('should set the secure parameter to true when the protocol is neither http or https', function() { - const stub = sinon.stub(sharethroughInternal, 'getProtocol').returns('about:'); - const bidRequest = spec.buildRequests(bidRequests, null)[0]; - expect(bidRequest.data.secure).to.be.true; - stub.restore(); - }); + expect(openRtbReq.source.ext.id).to.equal('WYu2BXv1'); + expect(openRtbReq.source.ext.version).not.to.be.undefined; + expect(openRtbReq.source.ext.str).not.to.be.undefined; + expect(openRtbReq.source.ext.schain).to.deep.equal(bidRequests[0].schain); - it('should add ccpa parameter if uspConsent is present', function() { - const uspConsent = '1YNN'; - const bidderRequest = { uspConsent: uspConsent }; - const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(bidRequest.data.us_privacy).to.eq(uspConsent); - }); + expect(openRtbReq.bcat).to.deep.equal(bidRequests[0].params.bcat); + expect(openRtbReq.badv).to.deep.equal(bidRequests[0].params.badv); - it('should add consent parameters if gdprConsent is present', function() { - const gdprConsent = { consentString: 'consent_string123', gdprApplies: true }; - const bidderRequest = { gdprConsent: gdprConsent }; - const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(bidRequest.data.consent_required).to.eq(true); - expect(bidRequest.data.consent_string).to.eq('consent_string123'); - }); + expect(openRtbReq.imp).to.have.length(2); - it('should handle gdprConsent is present but values are undefined case', function() { - const gdprConsent = { consent_string: undefined, gdprApplies: undefined }; - const bidderRequest = { gdprConsent: gdprConsent }; - const bidRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - expect(bidRequest.data).to.not.include.any.keys('consent_string'); - }); + expect(openRtbReq.imp[0].id).to.equal('bidId1'); + expect(openRtbReq.imp[0].tagid).to.equal('aaaa1111'); + expect(openRtbReq.imp[0].secure).to.equal(1); + expect(openRtbReq.imp[0].bidfloor).to.equal(42); - it('should add the ttduid parameter if a bid request contains a value for Unified ID from The Trade Desk', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.ttduid).to.eq('fake-tdid'); - }); + expect(openRtbReq.imp[1].id).to.equal('bidId2'); + expect(openRtbReq.imp[1].tagid).to.equal('bbbb2222'); + expect(openRtbReq.imp[1].secure).to.equal(1); + expect(openRtbReq.imp[1].bidfloor).to.equal(42); + }); + }); - it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + - ' userId object of the bidrequest', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); - }); + describe('regulation', () => { + describe('gdpr', () => { + it('should populate request accordingly when gdpr applies', () => { + bidderRequest.gdprConsent = { + gdprApplies: true, + consentString: 'consent', + }; - it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + - ' crumbs object of the bidrequest', function() { - const bidData = utils.deepClone(bidRequests); - delete bidData[0].userId.pubcid; + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - const bidRequest = spec.buildRequests(bidData)[0]; - expect(bidRequest.data.pubcid).to.eq('fake-pubcid-in-crumbs-obj'); - }); + expect(openRtbReq.regs.ext.gdpr).to.equal(1); + expect(openRtbReq.user.ext.consent).to.equal('consent'); + }); - it('should add the pubcid parameter if a bid request contains a value for the Publisher Common ID Module in the' + - ' crumbs object of the bidrequest', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - delete bidRequest.userId; - expect(bidRequest.data.pubcid).to.eq('fake-pubcid'); - }); + it('should populate request accordingly when gdpr explicitly does not apply', () => { + bidderRequest.gdprConsent = { + gdprApplies: false, + }; - it('should add the idluid parameter if a bid request contains a value for Identity Link from Live Ramp', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.idluid).to.eq('fake-identity-link'); - }); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should add the id5uid parameter if a bid request contains a value for ID5', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.id5uid.id).to.eq('fake-id5id'); - expect(bidRequest.data.id5uid.linkType).to.eq(2); - }); + expect(openRtbReq.regs.ext.gdpr).to.equal(0); + expect(openRtbReq.user.ext.consent).to.be.undefined; + }); + }); - it('should add the liuid parameter if a bid request contains a value for LiveIntent ID', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.liuid).to.eq('fake-lipbid'); - }); + describe('US privacy', () => { + it('should populate request accordingly when us privacy applies', () => { + bidderRequest.uspConsent = 'consent'; - it('should add Sharethrough specific parameters', function() { - const builtBidRequests = spec.buildRequests(bidRequests); - expect(builtBidRequests[0]).to.deep.include({ - strData: { - skipIframeBusting: undefined, - iframeSize: undefined, - sizes: [[600, 300]] - } - }); - }); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should add a supply chain parameter if schain is present', function() { - // shallow copy of the first bidRequest obj, so we don't mutate - const bidRequest = Object.assign({}, bidRequests[0]); - bidRequest['schain'] = { - ver: '1.0', - complete: 1, - nodes: [ - { - asi: 'directseller.com', - sid: '00001', - rid: 'BidRequest1', - hp: 1 - } - ] - }; + expect(openRtbReq.regs.ext.us_privacy).to.equal('consent'); + }); + }); - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data.schain).to.eq(JSON.stringify(bidRequest.schain)); - }); + describe('coppa', () => { + it('should populate request accordingly when coppa does not apply', () => { + config.setConfig({ coppa: false }); - describe('gpid', () => { - it('should include the gpid param if pbadslot is found in ortb2Imp in the bid request', () => { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data.gpid).to.eq('adslot-id-1') - }); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should not include the gpid param if pbadslot is not found in ortb2Imp in the bid request', () => { - const bidRequest = spec.buildRequests(bidRequests)[1]; - expect(bidRequest.data).to.not.include.any.keys('gpid'); + expect(openRtbReq.regs.coppa).to.equal(0); + }); + }); }); - }); - it('should add badv if provided', () => { - const builtBidRequest = spec.buildRequests([bidRequests[3]])[0]; + describe('universal id', () => { + it('should include gpid when universal id is provided', () => { + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - expect(builtBidRequest.data.badv).to.have.members(['domain1.com', 'domain2.com']) - }); + expect(openRtbReq.imp[0].ext.gpid).to.equal('universal-id'); + expect(openRtbReq.imp[1].ext?.gpid).to.be.undefined; + }); + }); - it('should add bcat if provided', () => { - const builtBidRequest = spec.buildRequests([bidRequests[4]])[0]; + describe('secure flag', () => { + it('should be positive when protocol is https', () => { + protocolStub.returns('https'); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - expect(builtBidRequest.data.bcat).to.have.members(['IAB1-1', 'IAB1-2']) - }); + expect(openRtbReq.imp[0].secure).to.equal(1); + expect(openRtbReq.imp[1].secure).to.equal(1); + }); - it('should not add a supply chain parameter if schain is missing', function() { - const bidRequest = spec.buildRequests(bidRequests)[0]; - expect(bidRequest.data).to.not.include.any.keys('schain'); - }); + it('should be negative when protocol is http', () => { + protocolStub.returns('http'); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - it('should include the bidfloor parameter if it is present in the bid request', function() { - const bidRequest = Object.assign({}, bidRequests[0]); - bidRequest['getFloor'] = () => ({ currency: 'USD', floor: 0.5 }); - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data.bidfloor).to.eq(0.5); - }); + expect(openRtbReq.imp[0].secure).to.equal(0); + expect(openRtbReq.imp[1].secure).to.equal(0); + }); - it('should not include the bidfloor parameter if it is missing in the bid request', function() { - const bidRequest = Object.assign({}, bidRequests[0]); - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data).to.not.include.any.keys('bidfloor'); - }); + it('should be positive when protocol is neither http nor https', () => { + protocolStub.returns('about'); + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + const openRtbReq = builtRequest.data; - describe('coppa', function() { - it('should add coppa to request if enabled', function() { - config.setConfig({coppa: true}); - const bidRequest = Object.assign({}, bidRequests[0]); - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data.coppa).to.eq(true); + expect(openRtbReq.imp[0].secure).to.equal(1); + expect(openRtbReq.imp[1].secure).to.equal(1); + }); }); - it('should not add coppa to request if disabled', function() { - config.setConfig({coppa: false}); - const bidRequest = Object.assign({}, bidRequests[0]); - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data.coppa).to.be.undefined; - }); + describe('banner imp', () => { + it('should generate open rtb banner imp', () => { + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); - it('should not add coppa to request if unknown value', function() { - config.setConfig({coppa: 'something'}); - const bidRequest = Object.assign({}, bidRequests[0]); - const builtBidRequest = spec.buildRequests([bidRequest])[0]; - expect(builtBidRequest.data.coppa).to.be.undefined; + const bannerImp = builtRequest.data.imp[0].banner; + expect(bannerImp.topframe).to.equal(1); + expect(bannerImp.format).to.deep.equal([{ w: 300, h: 250 }, { w: 300, h: 600 }]); + }); }); - }); - }); - describe('.interpretResponse', function() { - it('returns a correctly parsed out response', function() { - expect(spec.interpretResponse(bidderResponse, prebidRequests[0])[0]).to.deep.include( - { - width: 1, - height: 1, - cpm: 12.34, - creativeId: 'aCreativeId', - dealId: 'aDealId', - currency: 'USD', - netRevenue: true, - ttl: 360, - meta: { advertiserDomains: [] } + describe('video imp', () => { + it('should generate open rtb video imp', () => { + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + + const videoImp = builtRequest.data.imp[1].video; + expect(videoImp.topframe).to.equal(1); + expect(videoImp.skip).to.equal(1); + expect(videoImp.linearity).to.equal(0); + expect(videoImp.minduration).to.equal(10); + expect(videoImp.maxduration).to.equal(30); + expect(videoImp.playbackmethod).to.deep.equal([1]); + expect(videoImp.api).to.deep.equal([3]); + expect(videoImp.mimes).to.deep.equal(['video/3gpp']); + expect(videoImp.protocols).to.deep.equal([2, 3]); + expect(videoImp.w).to.equal(640); + expect(videoImp.h).to.equal(480); }); - }); - it('returns a correctly parsed out response with largest size when strData.skipIframeBusting is true', function() { - expect(spec.interpretResponse(bidderResponse, prebidRequests[1])[0]).to.include( - { - width: 300, - height: 300, - cpm: 12.34, - creativeId: 'aCreativeId', - dealId: 'aDealId', - currency: 'USD', - netRevenue: true, - ttl: 360 + it('should set defaults if no value provided', () => { + delete bidRequests[1].mediaTypes.video.skip; + delete bidRequests[1].mediaTypes.video.linearity; + delete bidRequests[1].mediaTypes.video.minduration; + delete bidRequests[1].mediaTypes.video.maxduration; + delete bidRequests[1].mediaTypes.video.playbackmethod; + delete bidRequests[1].mediaTypes.video.api; + delete bidRequests[1].mediaTypes.video.mimes; + delete bidRequests[1].mediaTypes.video.protocols; + + const builtRequest = spec.buildRequests(bidRequests, bidderRequest); + + const videoImp = builtRequest.data.imp[1].video; + expect(videoImp.skip).to.equal(0); + expect(videoImp.linearity).to.equal(1); + expect(videoImp.minduration).to.equal(5); + expect(videoImp.maxduration).to.equal(60); + expect(videoImp.playbackmethod).to.deep.equal([2]); + expect(videoImp.api).to.deep.equal([2]); + expect(videoImp.mimes).to.deep.equal(['video/mp4']); + expect(videoImp.protocols).to.deep.equal([2, 3, 5, 6, 7, 8]); }); + }); }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is true and strData.iframeSize is provided', function() { - expect(spec.interpretResponse(bidderResponse, prebidRequests[2])[0]).to.include( - { - width: 500, - height: 500, - cpm: 12.34, - creativeId: 'aCreativeId', - dealId: 'aDealId', - currency: 'USD', - netRevenue: true, - ttl: 360 - }); - }); + describe('interpretResponse', function() { + let request; + let response; + + beforeEach(() => { + request = spec.buildRequests(bidRequests, bidderRequest); + response = { + body: { + seatbid: [{ + bid: [{ + id: 'bidId1', + impid: '123', + w: 300, + h: 250, + price: 42, + crid: 'creative', + dealid: 'deal', + adomain: ['domain.com'], + adm: 'markup', + }, { + id: 'bidId2', + impid: '456', + w: 640, + h: 480, + price: 42, + adm: 'vastTag', + }], + }], + }, + }; + }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains [0, 0] only', function() { - expect(spec.interpretResponse(bidderResponse, prebidRequests[3])[0]).to.include( - { - width: 0, - height: 0, - cpm: 12.34, - creativeId: 'aCreativeId', - dealId: 'aDealId', - currency: 'USD', - netRevenue: true, - ttl: 360 + describe('banner', () => { + it('should return a banner bid', () => { + const resp = spec.interpretResponse(response, request); + + const bannerBid = resp[0]; + expect(bannerBid.requestId).to.equal('123'); + expect(bannerBid.width).to.equal(300); + expect(bannerBid.height).to.equal(250); + expect(bannerBid.cpm).to.equal(42); + expect(bannerBid.creativeId).to.equal('creative'); + expect(bannerBid.dealId).to.equal('deal'); + expect(bannerBid.mediaType).to.equal('banner'); + expect(bannerBid.currency).to.equal('USD'); + expect(bannerBid.netRevenue).to.equal(true); + expect(bannerBid.ttl).to.equal(360); + expect(bannerBid.ad).to.equal('markup'); + expect(bannerBid.meta.advertiserDomains).to.deep.equal(['domain.com']); }); - }); + }); - it('returns a correctly parsed out response with explicitly defined size when strData.skipIframeBusting is false and strData.sizes contains multiple sizes', function() { - expect(spec.interpretResponse(bidderResponse, prebidRequests[4])[0]).to.include( - { - width: 300, - height: 300, - cpm: 12.34, - creativeId: 'aCreativeId', - dealId: 'aDealId', - currency: 'USD', - netRevenue: true, - ttl: 360 + describe('video', () => { + it('should return a banner bid', () => { + const resp = spec.interpretResponse(response, request); + + const bannerBid = resp[1]; + expect(bannerBid.requestId).to.equal('456'); + expect(bannerBid.width).to.equal(640); + expect(bannerBid.height).to.equal(480); + expect(bannerBid.cpm).to.equal(42); + expect(bannerBid.creativeId).to.be.undefined; + expect(bannerBid.dealId).to.be.null; + expect(bannerBid.mediaType).to.equal('video'); + expect(bannerBid.currency).to.equal('USD'); + expect(bannerBid.netRevenue).to.equal(true); + expect(bannerBid.ttl).to.equal(3600); + expect(bannerBid.ad).to.equal('vastTag'); + expect(bannerBid.meta.advertiserDomains).to.deep.equal([]); + expect(bannerBid.vastXml).not.to.be.undefined; }); + }); }); - it('returns a blank array if there are no creatives', function() { - const bidResponse = { body: { creatives: [] } }; - expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; - }); - - it('returns a blank array if body object is empty', function() { - const bidResponse = { body: {} }; - expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; - }); - - it('returns a blank array if body is null', function() { - const bidResponse = { body: null }; - expect(spec.interpretResponse(bidResponse, prebidRequests[0])).to.be.an('array').that.is.empty; - }); - - it('correctly generates ad markup when skipIframeBusting is false', function() { - const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[0])[0].ad; - let resp = null; - - expect(() => btoa(JSON.stringify(bidderResponse))).to.throw(); - expect(() => resp = sharethroughInternal.b64EncodeUnicode(JSON.stringify(bidderResponse))).not.to.throw(); - expect(adMarkup).to.match( - /data-str-native-key="pKey" data-stx-response-name="str_response_bidId"/); - expect(!!adMarkup.indexOf(resp)).to.eql(true); - - // insert functionality to autodetect whether or not in safeframe, and handle JS insertion - expect(adMarkup).to.match(/isLockedInFrame/); - expect(adMarkup).to.match(/handleIframe/); - }); - - it('correctly generates ad markup when skipIframeBusting is true', function() { - const adMarkup = spec.interpretResponse(bidderResponse, prebidRequests[1])[0].ad; - let resp = null; - - expect(() => btoa(JSON.stringify(bidderResponse))).to.throw(); - expect(() => resp = sharethroughInternal.b64EncodeUnicode(JSON.stringify(bidderResponse))).not.to.throw(); - expect(adMarkup).to.match( - /data-str-native-key="pKey" data-stx-response-name="str_response_bidId"/); - expect(!!adMarkup.indexOf(resp)).to.eql(true); - expect(adMarkup).to.match( - /