forked from prebid/Prebid.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrelevantdigitalBidAdapter.js
205 lines (186 loc) · 7.47 KB
/
relevantdigitalBidAdapter.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {ortbConverter} from '../libraries/ortbConverter/converter.js'
import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js';
import {config} from '../src/config.js';
import {pbsExtensions} from '../libraries/pbsExtensions/pbsExtensions.js'
import {deepSetValue, isEmpty, deepClone, shuffle, triggerPixel, deepAccess} from '../src/utils.js';
const BIDDER_CODE = 'relevantdigital';
/** Global settings per bidder-code for this adapter (which might be > 1 if using aliasing) */
let configByBidder = {};
/** Used by the tests */
export const resetBidderConfigs = () => {
configByBidder = {};
};
/** Settings ber bidder-code. checkParams === true means that it can optionally be set in bid-params */
const FIELDS = [
{ name: 'pbsHost', checkParams: true, required: true },
{ name: 'accountId', checkParams: true, required: true },
{ name: 'pbsBufferMs', checkParams: false, required: false, default: 250 },
{ name: 'useSourceBidderCode', checkParams: false, required: false, default: false },
];
const SYNC_HTML = 'https://cdn.relevant-digital.com/resources/load-cookie.html';
const MAX_SYNC_COUNT = 10; // Max server-side bidder to sync at once via the iframe
/** Get settings for a bidder-code via config and, if needed, bid parameters */
const getBidderConfig = (bids) => {
const { bidder } = bids[0];
const cfg = configByBidder[bidder] || {
...Object.fromEntries(FIELDS.filter((f) => 'default' in f).map((f) => [f.name, f.default])),
syncedBidders: {}, // To keep track of S2S-bidders we already (started to) synced
};
if (cfg.complete) {
return cfg; // Most common case, we already have the settings we need (and we won't re-read them)
}
configByBidder[bidder] = cfg;
const bidderConfiguration = config.getConfig(bidder) || {};
// Read settings set by setConfig({ [bidder]: { ... }}) and if not available - from bid params
FIELDS.forEach(({ name, checkParams }) => {
cfg[name] = bidderConfiguration[name] || cfg[name];
if (!cfg[name] && checkParams) {
bids.forEach((bid) => {
cfg[name] = cfg[name] || bid.params?.[name];
});
}
});
cfg.complete = FIELDS.every((field) => !field.required || cfg[field.name]);
if (cfg.complete) {
cfg.pbsHost = cfg.pbsHost.trim().replace('http://', 'https://');
if (cfg.pbsHost.indexOf('https://') < 0) {
cfg.pbsHost = `https://${cfg.pbsHost}`;
}
}
return cfg;
}
const converter = ortbConverter({
context: {
netRevenue: true,
ttl: 300
},
processors: pbsExtensions,
imp(buildImp, bidRequest, context) {
// Set stored request id from placementId
const imp = buildImp(bidRequest, context);
const { placementId } = bidRequest.params;
deepSetValue(imp, 'ext.prebid.storedrequest.id', placementId);
delete imp.ext.prebid.bidder;
return imp;
},
overrides: {
bidResponse: {
bidderCode(orig, bidResponse, bid, { bidRequest }) {
const { bidder, params = {} } = bidRequest || {};
let useSourceBidderCode = configByBidder[bidder]?.useSourceBidderCode;
if ('useSourceBidderCode' in params) {
useSourceBidderCode = params.useSourceBidderCode;
}
// Only use the orignal function when useSourceBidderCode is true, else our own bidder code will be used
if (useSourceBidderCode) {
orig.apply(this, [...arguments].slice(1));
}
},
},
}
});
export const spec = {
code: BIDDER_CODE,
gvlid: 1100,
supportedMediaTypes: [BANNER, VIDEO, NATIVE],
/** We need both params.placementId + a complete configuration (pbsHost + accountId) to continue */
isBidRequestValid: (bid) => bid.params?.placementId && getBidderConfig([bid]).complete,
/** Trigger impression-pixel */
onBidWon(bid) {
if (bid.pbsWurl) {
triggerPixel(bid.pbsWurl)
}
if (bid.burl) {
triggerPixel(bid.burl)
}
},
/** Build BidRequest for PBS */
buildRequests(bidRequests, bidderRequest) {
const { bidder } = bidRequests[0];
const cfg = getBidderConfig(bidRequests);
const data = converter.toORTB({bidRequests, bidderRequest});
/** Set tmax, in general this will be timeout - pbsBufferMs */
const pbjsTimeout = bidderRequest.timeout || 1000;
data.tmax = Math.min(Math.max(pbjsTimeout - cfg.pbsBufferMs, cfg.pbsBufferMs), pbjsTimeout);
delete data.ext?.prebid?.aliases; // We don't need/want to send aliases to PBS
deepSetValue(data, 'ext.relevant', {
...data.ext?.relevant,
adapter: true, // For internal analytics
});
deepSetValue(data, 'ext.prebid.storedrequest.id', cfg.accountId);
data.ext.prebid.passthrough = {
...data.ext.prebid.passthrough,
relevant: { bidder }, // to find config for the right bidder-code in interpretResponse / getUserSyncs
};
return [{
method: 'POST',
url: `${cfg.pbsHost}/openrtb2/auction`,
data
}];
},
/** Read BidResponse from PBS and make necessary adjustments to not make it appear to come from unknown bidders */
interpretResponse(response, request) {
const resp = deepClone(response.body);
const { bidder } = request.data.ext.prebid.passthrough.relevant;
// Modify response times / errors for actual PBS bidders into a single value
const MODIFIERS = {
responsetimemillis: (values) => Math.max(...values),
errors: (values) => [].concat(...values),
};
Object.entries(MODIFIERS).forEach(([field, combineFn]) => {
const obj = resp.ext?.[field];
if (!isEmpty(obj)) {
resp.ext[field] = {[bidder]: combineFn(Object.values(obj))};
}
});
const bids = converter.fromORTB({response: resp, request: request.data}).bids;
return bids;
},
/** Do syncing, but avoid running the sync > 1 time for S2S bidders */
getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) {
if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) {
return [];
}
const syncs = [];
serverResponses.forEach(({ body }) => {
const { pbsHost, syncedBidders } = configByBidder[body.ext.prebid.passthrough.relevant.bidder] || {};
if (!pbsHost) {
return;
}
const { gdprApplies, consentString } = gdprConsent || {};
let bidders = Object.keys(body.ext?.responsetimemillis || {});
bidders = bidders.reduce((acc, curr) => {
if (!syncedBidders[curr]) {
acc.push(curr);
syncedBidders[curr] = true;
}
return acc;
}, []);
bidders = shuffle(bidders).slice(0, MAX_SYNC_COUNT); // Shuffle to not always leave out the same bidders
if (!bidders.length) {
return; // All bidders already synced
}
if (syncOptions.iframeEnabled) {
const params = {
endpoint: `${pbsHost}/cookie_sync`,
max_sync_count: bidders.length,
gdpr: gdprApplies ? 1 : 0,
gdpr_consent: consentString,
us_privacy: uspConsent,
bidders: bidders.join(','),
};
const qs = Object.entries(params)
.filter(([k, v]) => ![null, undefined, ''].includes(v))
.map(([k, v]) => `${k}=${encodeURIComponent(v.toString())}`)
.join('&');
syncs.push({ type: 'iframe', url: `${SYNC_HTML}?${qs}` });
} else { // Else, try to pixel-sync (for future-compatibility)
const pixels = deepAccess(body, `ext.relevant.sync`, []).filter(({ type }) => type === 'redirect');
syncs.push(...pixels.map(({ url }) => ({ type: 'image', url })));
}
});
return syncs;
},
};
registerBidder(spec);