Skip to content

Commit 2b843d0

Browse files
committed
initial refactor of prebidServerBidAdapter working w/o tests
1 parent d7d1738 commit 2b843d0

File tree

4 files changed

+424
-90
lines changed

4 files changed

+424
-90
lines changed

modules/prebidServerBidAdapter.js

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
import Adapter from 'src/adapter';
2+
import bidfactory from 'src/bidfactory';
3+
import * as utils from 'src/utils';
4+
import { ajax } from 'src/ajax';
5+
import { STATUS, S2S } from 'src/constants';
6+
import { cookieSet } from 'src/cookie.js';
7+
import adaptermanager from 'src/adaptermanager';
8+
import { config } from 'src/config';
9+
import { VIDEO } from 'src/mediaTypes';
10+
import CONSTANTS from 'src/constants.json';
11+
12+
const getConfig = config.getConfig;
13+
14+
const TYPE = S2S.SRC;
15+
const cookieSetUrl = 'https://acdn.adnxs.com/cookieset/cs.js';
16+
let _synced = false;
17+
18+
let _s2sConfigDefaults = {
19+
enabled: false,
20+
endpoint: CONSTANTS.S2S.DEFAULT_ENDPOINT,
21+
timeout: 1000,
22+
maxBids: 1,
23+
adapter: CONSTANTS.S2S.ADAPTER,
24+
syncEndpoint: CONSTANTS.S2S.SYNC_ENDPOINT,
25+
cookieSet: true,
26+
bidders: []
27+
};
28+
let _s2sConfig = _s2sConfigDefaults;
29+
30+
/**
31+
* Set config for server to server header bidding
32+
* @typedef {Object} options - required
33+
* @property {boolean} enabled enables S2S bidding
34+
* @property {string[]} bidders bidders to request S2S
35+
* === optional params below ===
36+
* @property {string} [endpoint] endpoint to contact
37+
* @property {number} [timeout] timeout for S2S bidders - should be lower than `pbjs.requestBids({timeout})`
38+
* @property {string} [adapter] adapter code to use for S2S
39+
* @property {string} [syncEndpoint] endpoint URL for syncing cookies
40+
* @property {boolean} [cookieSet] enables cookieSet functionality
41+
*/
42+
export function setS2sConfig(options) {
43+
let keys = Object.keys(options);
44+
if (!keys.includes('accountId')) {
45+
utils.logError('accountId missing in Server to Server config');
46+
return;
47+
}
48+
49+
if (!keys.includes('bidders')) {
50+
utils.logError('bidders missing in Server to Server config');
51+
return;
52+
}
53+
_s2sConfig = Object.assign({}, _s2sConfigDefaults, options);
54+
if (options.syncEndpoint) {
55+
queueSync(options.bidders);
56+
}
57+
}
58+
getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig));
59+
60+
/**
61+
* @param {Array} bidderCodes list of bidders to request user syncs for.
62+
*/
63+
function queueSync(bidderCodes) {
64+
if (_synced) {
65+
return;
66+
}
67+
_synced = true;
68+
const payload = JSON.stringify({
69+
uuid: utils.generateUUID(),
70+
bidders: bidderCodes
71+
});
72+
ajax(_s2sConfig.syncEndpoint, (response) => {
73+
try {
74+
response = JSON.parse(response);
75+
response.bidder_status.forEach(bidder => doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder));
76+
} catch (e) {
77+
utils.logError(e);
78+
}
79+
},
80+
payload, {
81+
contentType: 'text/plain',
82+
withCredentials: true
83+
});
84+
}
85+
86+
/**
87+
* Try to convert a value to a type.
88+
* If it can't be done, the value will be returned.
89+
*
90+
* @param {string} typeToConvert The target type. e.g. "string", "number", etc.
91+
* @param {*} value The value to be converted into typeToConvert.
92+
*/
93+
function tryConvertType(typeToConvert, value) {
94+
if (typeToConvert === 'string') {
95+
return value && value.toString();
96+
} else if (typeToConvert === 'number') {
97+
return Number(value);
98+
} else {
99+
return value;
100+
}
101+
}
102+
103+
const tryConvertString = tryConvertType.bind(null, 'string');
104+
const tryConvertNumber = tryConvertType.bind(null, 'number');
105+
106+
const paramTypes = {
107+
'appnexus': {
108+
'member': tryConvertString,
109+
'invCode': tryConvertString,
110+
'placementId': tryConvertNumber
111+
},
112+
'rubicon': {
113+
'accountId': tryConvertNumber,
114+
'siteId': tryConvertNumber,
115+
'zoneId': tryConvertNumber
116+
},
117+
'indexExchange': {
118+
'siteID': tryConvertNumber
119+
},
120+
'audienceNetwork': {
121+
'placementId': tryConvertString
122+
},
123+
'pubmatic': {
124+
'publisherId': tryConvertString,
125+
'adSlot': tryConvertString
126+
},
127+
'districtm': {
128+
'member': tryConvertString,
129+
'invCode': tryConvertString,
130+
'placementId': tryConvertNumber
131+
},
132+
'pulsepoint': {
133+
'cf': tryConvertString,
134+
'cp': tryConvertNumber,
135+
'ct': tryConvertNumber
136+
},
137+
};
138+
139+
let _cookiesQueued = false;
140+
141+
/**
142+
* Bidder adapter for Prebid Server
143+
*/
144+
export function PrebidServer() {
145+
let baseAdapter = new Adapter('prebidServer');
146+
147+
function convertTypes(adUnits) {
148+
adUnits.forEach(adUnit => {
149+
adUnit.bids.forEach(bid => {
150+
const types = paramTypes[bid.bidder] || [];
151+
Object.keys(types).forEach(key => {
152+
if (bid.params[key]) {
153+
const converted = types[key](bid.params[key]);
154+
if (converted !== bid.params[key]) {
155+
utils.logMessage(`Mismatched type for Prebid Server : ${bid.bidder} : ${key}. Required Type:${types[key]}`);
156+
}
157+
bid.params[key] = converted;
158+
159+
// don't send invalid values
160+
if (isNaN(bid.params[key])) {
161+
delete bid.params.key;
162+
}
163+
}
164+
});
165+
});
166+
});
167+
}
168+
169+
/* Prebid executes this function when the page asks to send out bid requests */
170+
baseAdapter.callBids = function(bidRequest, bidRequests, addBidResponse, done, ajax) {
171+
const isDebug = !!getConfig('debug');
172+
const adUnits = utils.cloneJson(bidRequest.ad_units);
173+
adUnits.forEach(adUnit => {
174+
let videoMediaType = utils.deepAccess(adUnit, 'mediaTypes.video');
175+
if (videoMediaType) {
176+
// pbs expects a ad_unit.video attribute if the imp is video
177+
adUnit.video = Object.assign({}, videoMediaType);
178+
delete adUnit.mediaTypes;
179+
// default is assumed to be 'banner' so if there is a video type we assume video only until PBS can support multi format auction.
180+
adUnit.media_types = [VIDEO];
181+
}
182+
});
183+
convertTypes(adUnits);
184+
let requestJson = {
185+
account_id: _s2sConfig.accountId,
186+
tid: bidRequest.tid,
187+
max_bids: _s2sConfig.maxBids,
188+
timeout_millis: _s2sConfig.timeout,
189+
secure: _s2sConfig.secure,
190+
url: utils.getTopWindowUrl(),
191+
prebid_version: '$prebid.version$',
192+
ad_units: adUnits.filter(hasSizes),
193+
is_debug: isDebug
194+
};
195+
196+
// in case config.bidders contains invalid bidders, we only process those we sent requests for.
197+
const requestedBidders = requestJson.ad_units.map(adUnit => adUnit.bids.map(bid => bid.bidder).filter(utils.uniques)).reduce(utils.flatten).filter(utils.uniques);
198+
function processResponse(response) {
199+
handleResponse(response, requestedBidders, bidRequests, addBidResponse, done);
200+
}
201+
const payload = JSON.stringify(requestJson);
202+
ajax(_s2sConfig.endpoint, processResponse, payload, {
203+
contentType: 'text/plain',
204+
withCredentials: true
205+
});
206+
};
207+
208+
// at this point ad units should have a size array either directly or mapped so filter for that
209+
function hasSizes(unit) {
210+
return unit.sizes && unit.sizes.length;
211+
}
212+
213+
/**
214+
* Run a cookie sync for the given type, url, and bidder
215+
*
216+
* @param {string} type the type of sync, "image", "redirect", "iframe"
217+
* @param {string} url the url to sync
218+
* @param {string} bidder name of bidder doing sync for
219+
*/
220+
function doBidderSync(type, url, bidder) {
221+
if (!url) {
222+
utils.logError(`No sync url for bidder "${bidder}": ${url}`);
223+
} else if (type === 'image' || type === 'redirect') {
224+
utils.logMessage(`Invoking image pixel user sync for bidder: "${bidder}"`);
225+
utils.triggerPixel(url);
226+
} else if (type == 'iframe') {
227+
utils.logMessage(`Invoking iframe user sync for bidder: "${bidder}"`);
228+
utils.insertUserSyncIframe(url);
229+
} else {
230+
utils.logError(`User sync type "${type}" not supported for bidder: "${bidder}"`);
231+
}
232+
}
233+
234+
/* Notify Prebid of bid responses so bids can get in the auction */
235+
function handleResponse(response, requestedBidders, bidRequests, addBidResponse, done) {
236+
let result;
237+
try {
238+
result = JSON.parse(response);
239+
240+
if (result.status === 'OK' || result.status === 'no_cookie') {
241+
if (result.bidder_status) {
242+
result.bidder_status.forEach(bidder => {
243+
if (bidder.no_cookie && !_cookiesQueued) {
244+
doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder);
245+
}
246+
});
247+
}
248+
249+
// do client-side syncs if available
250+
requestedBidders.forEach(bidder => {
251+
let clientAdapter = adaptermanager.getBidAdapter(bidder);
252+
if (clientAdapter && clientAdapter.registerSyncs) {
253+
clientAdapter.registerSyncs();
254+
}
255+
});
256+
257+
if (result.bids) {
258+
result.bids.forEach(bidObj => {
259+
let bidRequest = utils.getBidRequest(bidObj.bid_id, bidRequests);
260+
let cpm = bidObj.price;
261+
let status;
262+
if (cpm !== 0) {
263+
status = STATUS.GOOD;
264+
} else {
265+
status = STATUS.NO_BID;
266+
}
267+
268+
let bidObject = bidfactory.createBid(status, bidRequest);
269+
bidObject.source = TYPE;
270+
bidObject.creative_id = bidObj.creative_id;
271+
bidObject.bidderCode = bidObj.bidder;
272+
bidObject.cpm = cpm;
273+
// From ORTB see section 4.2.3: adm Optional means of conveying ad markup in case the bid wins; supersedes the win notice if markup is included in both.
274+
if (bidObj.media_type === VIDEO) {
275+
bidObject.mediaType = VIDEO;
276+
if (bidObj.adm) {
277+
bidObject.vastXml = bidObj.adm;
278+
}
279+
if (bidObj.nurl) {
280+
bidObject.vastUrl = bidObj.nurl;
281+
}
282+
} else {
283+
if (bidObj.adm && bidObj.nurl) {
284+
bidObject.ad = bidObj.adm;
285+
bidObject.ad += utils.createTrackPixelHtml(decodeURIComponent(bidObj.nurl));
286+
} else if (bidObj.adm) {
287+
bidObject.ad = bidObj.adm;
288+
} else if (bidObj.nurl) {
289+
bidObject.adUrl = bidObj.nurl
290+
}
291+
}
292+
293+
bidObject.width = bidObj.width;
294+
bidObject.height = bidObj.height;
295+
bidObject.adserverTargeting = bidObj.ad_server_targeting;
296+
if (bidObj.deal_id) {
297+
bidObject.dealId = bidObj.deal_id;
298+
}
299+
300+
addBidResponse(bidObj.code, bidObject);
301+
});
302+
}
303+
304+
// const receivedBidIds = result.bids ? result.bids.map(bidObj => bidObj.bid_id) : [];
305+
306+
// issue a no-bid response for every bid request that can not be matched with received bids
307+
// requestedBidders.forEach(bidder => {
308+
// utils
309+
// .getBidderRequestAllAdUnits(bidder)
310+
// .bids.filter(bidRequest => !receivedBidIds.includes(bidRequest.bidId))
311+
// .forEach(bidRequest => {
312+
// let bidObject = bidfactory.createBid(STATUS.NO_BID, bidRequest);
313+
// bidObject.source = TYPE;
314+
// bidObject.adUnitCode = bidRequest.placementCode;
315+
// bidObject.bidderCode = bidRequest.bidder;
316+
// addBidResponse(bidObject.adUnitCode, bidObject);
317+
// });
318+
// });
319+
}
320+
if (result.status === 'no_cookie' && _s2sConfig.cookieSet) {
321+
// cookie sync
322+
cookieSet(cookieSetUrl);
323+
}
324+
} catch (error) {
325+
utils.logError(error);
326+
}
327+
328+
if (!result || (result.status && result.status.includes('Error'))) {
329+
utils.logError('error parsing response: ', result.status);
330+
}
331+
332+
done();
333+
}
334+
335+
return Object.assign(this, {
336+
callBids: baseAdapter.callBids,
337+
setBidderCode: baseAdapter.setBidderCode,
338+
type: TYPE
339+
});
340+
}
341+
342+
adaptermanager.registerBidAdapter(new PrebidServer(), 'prebidServer');
343+

0 commit comments

Comments
 (0)