diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js
index 2bdbdd6414b..ef14511f9f9 100644
--- a/modules/33acrossBidAdapter.js
+++ b/modules/33acrossBidAdapter.js
@@ -1,11 +1,20 @@
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { config } from '../src/config.js';
import {
- deepAccess, uniques, isArray, getWindowTop, isGptPubadsDefined, isSlotMatchingAdUnitCode, logInfo, logWarn,
- getWindowSelf
+ deepAccess,
+ uniques,
+ isArray,
+ getWindowTop,
+ isGptPubadsDefined,
+ isSlotMatchingAdUnitCode,
+ logInfo,
+ logWarn,
+ getWindowSelf,
+ mergeDeep,
} from '../src/utils.js';
-import {BANNER, VIDEO} from '../src/mediaTypes.js';
+import { BANNER, VIDEO } from '../src/mediaTypes.js';
+// **************************** UTILS *************************** //
const BIDDER_CODE = '33across';
const END_POINT = 'https://ssc.33across.com/api/v1/hb';
const SYNC_ENDPOINT = 'https://ssc-cms.33across.com/ps/?m=xch&rt=html&ru=deb';
@@ -42,6 +51,14 @@ const adapterState = {
const NON_MEASURABLE = 'nm';
+function getTTXConfig() {
+ const ttxSettings = Object.assign({},
+ config.getConfig('ttxSettings')
+ );
+
+ return ttxSettings;
+}
+
// **************************** VALIDATION *************************** //
function isBidRequestValid(bid) {
return (
@@ -74,6 +91,7 @@ function _validateGUID(bid) {
function _validateBanner(bid) {
const banner = deepAccess(bid, 'mediaTypes.banner');
+
// If there's no banner no need to validate against banner rules
if (banner === undefined) {
return true;
@@ -140,91 +158,125 @@ function _validateVideo(bid) {
// NOTE: With regards to gdrp consent data, the server will independently
// infer the gdpr applicability therefore, setting the default value to false
function buildRequests(bidRequests, bidderRequest) {
+ const {
+ ttxSettings,
+ gdprConsent,
+ uspConsent,
+ pageUrl
+ } = _buildRequestParams(bidRequests, bidderRequest);
+
+ const groupedRequests = _buildRequestGroups(ttxSettings, bidRequests);
+
+ const serverRequests = [];
+
+ for (const key in groupedRequests) {
+ serverRequests.push(
+ _createServerRequest({
+ bidRequests: groupedRequests[key],
+ gdprConsent,
+ uspConsent,
+ pageUrl,
+ ttxSettings
+ })
+ )
+ }
+
+ return serverRequests;
+}
+
+function _buildRequestParams(bidRequests, bidderRequest) {
+ const ttxSettings = getTTXConfig();
+
const gdprConsent = Object.assign({
consentString: undefined,
gdprApplies: false
}, bidderRequest && bidderRequest.gdprConsent);
const uspConsent = bidderRequest && bidderRequest.uspConsent;
+
const pageUrl = (bidderRequest && bidderRequest.refererInfo) ? (bidderRequest.refererInfo.referer) : (undefined);
adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(uniques);
- return bidRequests.map(bidRequest => _createServerRequest(
- {
- bidRequest,
- gdprConsent,
- uspConsent,
- pageUrl
- })
- );
+ return {
+ ttxSettings,
+ gdprConsent,
+ uspConsent,
+ pageUrl
+ }
+}
+
+function _buildRequestGroups(ttxSettings, bidRequests) {
+ const bidRequestsComplete = bidRequests.map(_inferProduct);
+ const enableSRAMode = ttxSettings && ttxSettings.enableSRAMode;
+ const keyFunc = (enableSRAMode === true) ? _getSRAKey : _getMRAKey;
+
+ return _groupBidRequests(bidRequestsComplete, keyFunc);
+}
+
+function _groupBidRequests(bidRequests, keyFunc) {
+ const groupedRequests = {};
+
+ bidRequests.forEach((req) => {
+ const key = keyFunc(req);
+
+ groupedRequests[key] = groupedRequests[key] || [];
+ groupedRequests[key].push(req);
+ });
+
+ return groupedRequests;
+}
+
+function _getSRAKey(bidRequest) {
+ return `${bidRequest.params.siteId}:${bidRequest.params.productId}`;
+}
+
+function _getMRAKey(bidRequest) {
+ return `${bidRequest.bidId}`;
}
// Infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request
-// NOTE: At this point, TTX only accepts request for a single impression
-function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl}) {
+function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, pageUrl, ttxSettings }) {
const ttxRequest = {};
- const params = bidRequest.params;
+ const { siteId, test } = bidRequests[0].params;
/*
* Infer data for the request payload
*/
- ttxRequest.imp = [{}];
+ ttxRequest.imp = [];
- if (deepAccess(bidRequest, 'mediaTypes.banner')) {
- ttxRequest.imp[0].banner = {
- ..._buildBannerORTB(bidRequest)
- }
- }
-
- if (deepAccess(bidRequest, 'mediaTypes.video')) {
- ttxRequest.imp[0].video = _buildVideoORTB(bidRequest);
- }
-
- ttxRequest.imp[0].ext = {
- ttx: {
- prod: _getProduct(bidRequest)
- }
- };
+ bidRequests.forEach((req) => {
+ ttxRequest.imp.push(_buildImpORTB(req));
+ });
- ttxRequest.site = { id: params.siteId };
+ ttxRequest.site = { id: siteId };
if (pageUrl) {
ttxRequest.site.page = pageUrl;
}
- // Go ahead send the bidId in request to 33exchange so it's kept track of in the bid response and
- // therefore in ad targetting process
- ttxRequest.id = bidRequest.bidId;
+ ttxRequest.id = bidRequests[0].auctionId;
if (gdprConsent.consentString) {
- ttxRequest.user = setExtension(
- ttxRequest.user,
- 'consent',
- gdprConsent.consentString
- )
+ ttxRequest.user = setExtensions(ttxRequest.user, {
+ 'consent': gdprConsent.consentString
+ });
}
- if (Array.isArray(bidRequest.userIdAsEids) && bidRequest.userIdAsEids.length > 0) {
- ttxRequest.user = setExtension(
- ttxRequest.user,
- 'eids',
- bidRequest.userIdAsEids
- )
+ if (Array.isArray(bidRequests[0].userIdAsEids) && bidRequests[0].userIdAsEids.length > 0) {
+ ttxRequest.user = setExtensions(ttxRequest.user, {
+ 'eids': bidRequests[0].userIdAsEids
+ });
}
- ttxRequest.regs = setExtension(
- ttxRequest.regs,
- 'gdpr',
- Number(gdprConsent.gdprApplies)
- );
+ ttxRequest.regs = setExtensions(ttxRequest.regs, {
+ 'gdpr': Number(gdprConsent.gdprApplies)
+ });
if (uspConsent) {
- ttxRequest.regs = setExtension(
- ttxRequest.regs,
- 'us_privacy',
- uspConsent
- )
+ ttxRequest.regs = setExtensions(ttxRequest.regs, {
+ 'us_privacy': uspConsent
+ });
}
ttxRequest.ext = {
@@ -237,16 +289,14 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl
}
};
- if (bidRequest.schain) {
- ttxRequest.source = setExtension(
- ttxRequest.source,
- 'schain',
- bidRequest.schain
- )
+ if (bidRequests[0].schain) {
+ ttxRequest.source = setExtensions(ttxRequest.source, {
+ 'schain': bidRequests[0].schain
+ });
}
// Finally, set the openRTB 'test' param if this is to be a test bid
- if (params.test === 1) {
+ if (test === 1) {
ttxRequest.test = 1;
}
@@ -259,8 +309,7 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl
};
// Allow the ability to configure the HB endpoint for testing purposes.
- const ttxSettings = config.getConfig('ttxSettings');
- const url = (ttxSettings && ttxSettings.url) || `${END_POINT}?guid=${params.siteId}`;
+ const url = (ttxSettings && ttxSettings.url) || `${END_POINT}?guid=${siteId}`;
// Return the server request
return {
@@ -272,14 +321,36 @@ function _createServerRequest({bidRequest, gdprConsent = {}, uspConsent, pageUrl
}
// BUILD REQUESTS: SET EXTENSIONS
-function setExtension(obj = {}, key, value) {
- return Object.assign({}, obj, {
- ext: Object.assign({}, obj.ext, {
- [key]: value
- })
+function setExtensions(obj = {}, extFields) {
+ return mergeDeep({}, obj, {
+ 'ext': extFields
});
}
+// BUILD REQUESTS: IMP
+function _buildImpORTB(bidRequest) {
+ const imp = {
+ id: bidRequest.bidId,
+ ext: {
+ ttx: {
+ prod: deepAccess(bidRequest, 'params.productId')
+ }
+ }
+ };
+
+ if (deepAccess(bidRequest, 'mediaTypes.banner')) {
+ imp.banner = {
+ ..._buildBannerORTB(bidRequest)
+ }
+ }
+
+ if (deepAccess(bidRequest, 'mediaTypes.video')) {
+ imp.video = _buildVideoORTB(bidRequest);
+ }
+
+ return imp;
+}
+
// BUILD REQUESTS: SIZE INFERENCE
function _transformSizes(sizes) {
if (isArray(sizes) && sizes.length === 2 && !isArray(sizes[0])) {
@@ -297,6 +368,14 @@ function _getSize(size) {
}
// BUILD REQUESTS: PRODUCT INFERENCE
+function _inferProduct(bidRequest) {
+ return mergeDeep({}, bidRequest, {
+ params: {
+ productId: _getProduct(bidRequest)
+ }
+ });
+}
+
function _getProduct(bidRequest) {
const { params, mediaTypes } = bidRequest;
@@ -367,7 +446,7 @@ function _buildVideoORTB(bidRequest) {
const video = {}
- const {w, h} = _getSize(videoParams.playerSize[0]);
+ const { w, h } = _getSize(videoParams.playerSize[0]);
video.w = w;
video.h = h;
@@ -388,11 +467,11 @@ function _buildVideoORTB(bidRequest) {
if (product === PRODUCT.INSTREAM) {
video.startdelay = video.startdelay || 0;
video.placement = 1;
- };
+ }
// bidfloors
if (typeof bidRequest.getFloor === 'function') {
- const bidfloors = _getBidFloors(bidRequest, {w: video.w, h: video.h}, VIDEO);
+ const bidfloors = _getBidFloors(bidRequest, { w: video.w, h: video.h }, VIDEO);
if (bidfloors) {
Object.assign(video, {
@@ -404,6 +483,7 @@ function _buildVideoORTB(bidRequest) {
});
}
}
+
return video;
}
@@ -556,54 +636,63 @@ function _isIframe() {
}
// **************************** INTERPRET RESPONSE ******************************** //
-// NOTE: At this point, the response from 33exchange will only ever contain one bid
-// i.e. the highest bid
function interpretResponse(serverResponse, bidRequest) {
- const bidResponses = [];
-
- // If there are bids, look at the first bid of the first seatbid (see NOTE above for assumption about ttx)
- if (serverResponse.body.seatbid.length > 0 && serverResponse.body.seatbid[0].bid.length > 0) {
- bidResponses.push(_createBidResponse(serverResponse.body));
- }
-
- return bidResponses;
+ const { seatbid, cur = 'USD' } = serverResponse.body;
+
+ if (!isArray(seatbid)) {
+ return [];
+ }
+
+ // Pick seats with valid bids and convert them into an Array of responses
+ // in format expected by Prebid Core
+ return seatbid
+ .filter((seat) => (
+ isArray(seat.bid) &&
+ seat.bid.length > 0
+ ))
+ .map((seat) => {
+ return (
+ seat.bid
+ .map((bid) => _createBidResponse(bid, cur))
+ );
+ })
+ .flat()
}
-// All this assumes that only one bid is ever returned by ttx
-function _createBidResponse(response) {
+function _createBidResponse(bid, cur) {
const isADomainPresent =
- response.seatbid[0].bid[0].adomain && response.seatbid[0].bid[0].adomain.length;
- const bid = {
- requestId: response.id,
+ bid.adomain && bid.adomain.length;
+ const bidResponse = {
+ requestId: bid.impid,
bidderCode: BIDDER_CODE,
- cpm: response.seatbid[0].bid[0].price,
- width: response.seatbid[0].bid[0].w,
- height: response.seatbid[0].bid[0].h,
- ad: response.seatbid[0].bid[0].adm,
- ttl: response.seatbid[0].bid[0].ttl || 60,
- creativeId: response.seatbid[0].bid[0].crid,
- mediaType: deepAccess(response.seatbid[0].bid[0], 'ext.ttx.mediaType', BANNER),
- currency: response.cur,
+ cpm: bid.price,
+ width: bid.w,
+ height: bid.h,
+ ad: bid.adm,
+ ttl: bid.ttl || 60,
+ creativeId: bid.crid,
+ mediaType: deepAccess(bid, 'ext.ttx.mediaType', BANNER),
+ currency: cur,
netRevenue: true
}
if (isADomainPresent) {
- bid.meta = {
- advertiserDomains: response.seatbid[0].bid[0].adomain
+ bidResponse.meta = {
+ advertiserDomains: bid.adomain
};
}
- if (bid.mediaType === VIDEO) {
- const vastType = deepAccess(response.seatbid[0].bid[0], 'ext.ttx.vastType', 'xml');
+ if (bidResponse.mediaType === VIDEO) {
+ const vastType = deepAccess(bid, 'ext.ttx.vastType', 'xml');
if (vastType === 'xml') {
- bid.vastXml = bid.ad;
+ bidResponse.vastXml = bidResponse.ad;
} else {
- bid.vastUrl = bid.ad;
+ bidResponse.vastUrl = bidResponse.ad;
}
}
- return bid;
+ return bidResponse;
}
// **************************** USER SYNC *************************** //
diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js
index b5443cdd5c2..141edc1e61c 100644
--- a/test/spec/modules/33acrossBidAdapter_spec.js
+++ b/test/spec/modules/33acrossBidAdapter_spec.js
@@ -16,20 +16,21 @@ function validateBuiltServerRequest(builtReq, expectedReq) {
describe('33acrossBidAdapter:', function () {
const BIDDER_CODE = '33across';
const SITE_ID = 'sample33xGUID123456789';
- const PRODUCT_ID = 'siab';
const END_POINT = 'https://ssc.33across.com/api/v1/hb';
let element, win;
let bidRequests;
let sandbox;
- function TtxRequestBuilder() {
+ function TtxRequestBuilder(siteId = SITE_ID) {
const ttxRequest = {
- imp: [{}],
+ imp: [{
+ id: 'b1'
+ }],
site: {
- id: SITE_ID
+ id: siteId
},
- id: 'b1',
+ id: 'r1',
regs: {
ext: {
gdpr: 0
@@ -46,66 +47,83 @@ describe('33acrossBidAdapter:', function () {
}
};
+ this.addImp = (id = 'b2') => {
+ ttxRequest.imp.push({ id });
+
+ return this;
+ }
+
this.withBanner = () => {
- Object.assign(ttxRequest.imp[0], {
- banner: {
- format: [
- {
- w: 300,
- h: 250
- },
- {
- w: 728,
- h: 90
- }
- ],
- ext: {
- ttx: {
- viewability: {
- amount: 100
+ ttxRequest.imp.forEach((imp) => {
+ Object.assign(imp, {
+ banner: {
+ format: [
+ {
+ w: 300,
+ h: 250
+ },
+ {
+ w: 728,
+ h: 90
+ }
+ ],
+ ext: {
+ ttx: {
+ viewability: {
+ amount: 100
+ }
}
}
}
- }
+ });
});
-
return this;
};
this.withBannerSizes = this.withSizes = sizes => {
- Object.assign(ttxRequest.imp[0].banner, { format: sizes });
+ ttxRequest.imp.forEach((imp) => {
+ Object.assign(imp.banner, { format: sizes });
+ });
+
return this;
};
this.withVideo = (params = {}) => {
- Object.assign(ttxRequest.imp[0], {
- video: {
- w: 300,
- h: 250,
- placement: 2,
- ...params
- }
+ ttxRequest.imp.forEach((imp) => {
+ Object.assign(imp, {
+ video: {
+ w: 300,
+ h: 250,
+ placement: 2,
+ ...params
+ }
+ });
});
return this;
};
this.withViewability = (viewability, format = 'banner') => {
- Object.assign(ttxRequest.imp[0][format], {
- ext: {
- ttx: { viewability }
- }
+ ttxRequest.imp.forEach((imp) => {
+ Object.assign(imp[format], {
+ ext: {
+ ttx: { viewability }
+ }
+ });
});
+
return this;
};
- this.withProduct = (prod = PRODUCT_ID) => {
- Object.assign(ttxRequest.imp[0], {
- ext: {
- ttx: {
- prod
+ this.withProduct = (prod = 'siab') => {
+ ttxRequest.imp.forEach((imp) => {
+ Object.assign(imp, {
+ ext: {
+ ttx: {
+ prod
+ }
}
- }
+ });
});
return this;
@@ -249,7 +267,7 @@ describe('33acrossBidAdapter:', function () {
bidderRequestId: 'b1a',
params: {
siteId: SITE_ID,
- productId: PRODUCT_ID
+ productId: 'siab'
},
adUnitCode: 'div-id',
auctionId: 'r1',
@@ -258,35 +276,61 @@ describe('33acrossBidAdapter:', function () {
}
];
+ this.addBid = (bidParams = {}) => {
+ bidRequests.push({
+ bidId: 'b2',
+ bidder: '33across',
+ bidderRequestId: 'b1b',
+ params: {
+ siteId: SITE_ID,
+ productId: 'siab'
+ },
+ adUnitCode: 'div-id',
+ auctionId: 'r1',
+ mediaTypes: {},
+ transactionId: 't2',
+ ...bidParams
+ });
+
+ return this;
+ };
+
this.withBanner = () => {
- bidRequests[0].mediaTypes.banner = {
- sizes: [
- [300, 250],
- [728, 90]
- ]
- };
+ bidRequests.forEach((bid) => {
+ bid.mediaTypes.banner = {
+ sizes: [
+ [300, 250],
+ [728, 90]
+ ]
+ };
+ });
return this;
};
this.withProduct = (prod) => {
- bidRequests[0].params.productId = prod;
-
+ bidRequests.forEach((bid) => {
+ bid.params.productId = prod;
+ });
return this;
};
this.withVideo = (params) => {
- bidRequests[0].mediaTypes.video = {
- playerSize: [[300, 250]],
- context: 'outstream',
- ...params
- };
+ bidRequests.forEach((bid) => {
+ bid.mediaTypes.video = {
+ playerSize: [[300, 250]],
+ context: 'outstream',
+ ...params
+ };
+ });
return this;
}
this.withUserIds = (eids) => {
- bidRequests[0].userIdAsEids = eids;
+ bidRequests.forEach((bid) => {
+ bid.userIdAsEids = eids;
+ });
return this;
};
@@ -315,6 +359,7 @@ describe('33acrossBidAdapter:', function () {
}
};
win = {
+ parent: null,
document: {
visibilityState: 'visible'
},
@@ -331,7 +376,7 @@ describe('33acrossBidAdapter:', function () {
sandbox = sinon.sandbox.create();
sandbox.stub(Date, 'now').returns(1);
- sandbox.stub(document, 'getElementById').withArgs('div-id').returns(element);
+ sandbox.stub(document, 'getElementById').returns(element);
sandbox.stub(utils, 'getWindowTop').returns(win);
sandbox.stub(utils, 'getWindowSelf').returns(win);
});
@@ -1376,10 +1421,146 @@ describe('33acrossBidAdapter:', function () {
});
});
});
+
+ context('when SRA mode is enabled', function() {
+ it('builds a single request with multiple imps corresponding to each group {siteId, productId}', function() {
+ sandbox.stub(config, 'getConfig').callsFake(() => {
+ return {
+ enableSRAMode: true
+ }
+ });
+
+ const bidRequests = new BidRequestsBuilder()
+ .addBid()
+ .addBid({
+ bidId: 'b3',
+ adUnitCode: 'div-id',
+ params: {
+ siteId: 'sample33xGUID123456780',
+ productId: 'siab'
+ }
+ })
+ .addBid({
+ bidId: 'b4',
+ adUnitCode: 'div-id',
+ params: {
+ siteId: 'sample33xGUID123456780',
+ productId: 'inview'
+ }
+ })
+ .withBanner()
+ .withVideo({context: 'outstream'})
+ .build();
+
+ const req1 = new TtxRequestBuilder()
+ .addImp()
+ .withProduct('siab')
+ .withBanner()
+ .withVideo()
+ .build();
+
+ const req2 = new TtxRequestBuilder('sample33xGUID123456780')
+ .withProduct('siab')
+ .withBanner()
+ .withVideo()
+ .build();
+
+ req2.imp[0].id = 'b3';
+
+ const req3 = new TtxRequestBuilder('sample33xGUID123456780')
+ .withProduct('inview')
+ .withBanner()
+ .withVideo()
+ .build();
+
+ req3.imp[0].id = 'b4';
+
+ const serverReq1 = new ServerRequestBuilder()
+ .withData(req1)
+ .build();
+
+ const serverReq2 = new ServerRequestBuilder()
+ .withData(req2)
+ .withUrl('https://ssc.33across.com/api/v1/hb?guid=sample33xGUID123456780')
+ .build();
+
+ const serverReq3 = new ServerRequestBuilder()
+ .withData(req3)
+ .withUrl('https://ssc.33across.com/api/v1/hb?guid=sample33xGUID123456780')
+ .build();
+
+ const builtServerRequests = spec.buildRequests(bidRequests, {});
+
+ expect(builtServerRequests).to.deep.equal([serverReq1, serverReq2, serverReq3]);
+ });
+ });
+
+ context('when SRA mode is not enabled', function() {
+ it('builds multiple requests, one corresponding to each Ad Unit', function() {
+ const bidRequests = new BidRequestsBuilder()
+ .addBid()
+ .addBid({
+ bidId: 'b3',
+ adUnitCode: 'div-id',
+ params: {
+ siteId: 'sample33xGUID123456780',
+ productId: 'siab'
+ }
+ })
+ .withBanner()
+ .withVideo({context: 'outstream'})
+ .build();
+
+ const req1 = new TtxRequestBuilder()
+ .withProduct('siab')
+ .withBanner()
+ .withVideo()
+ .build();
+
+ const req2 = new TtxRequestBuilder()
+ .withProduct('siab')
+ .withBanner()
+ .withVideo()
+ .build();
+
+ req2.imp[0].id = 'b2';
+
+ const req3 = new TtxRequestBuilder('sample33xGUID123456780')
+ .withProduct('siab')
+ .withBanner()
+ .withVideo()
+ .build();
+
+ req3.imp[0].id = 'b3';
+
+ const serverReq1 = new ServerRequestBuilder()
+ .withData(req1)
+ .build();
+
+ const serverReq2 = new ServerRequestBuilder()
+ .withData(req2)
+ .build();
+
+ const serverReq3 = new ServerRequestBuilder()
+ .withData(req3)
+ .withUrl('https://ssc.33across.com/api/v1/hb?guid=sample33xGUID123456780')
+ .build();
+
+ const builtServerRequests = spec.buildRequests(bidRequests, {});
+
+ expect(builtServerRequests)
+ .to.deep.equal([
+ serverReq1,
+ serverReq2,
+ serverReq3
+ ]);
+ });
+ });
});
describe('interpretResponse', function() {
let ttxRequest, serverRequest;
+ const videoBid = '