Skip to content

Commit a79d5b2

Browse files
Relevant Digital Bid Adapter: initial release (prebid#9685)
* Relevant Digital Bid Adapter * More tests + comments * Use the recommended onBidWon callback + live-placements in .md file * Remove unused imports + adjust example-parameters in .md file * Renamed files + rewritten test-cases * Added documentation for 'pbsBufferMs' setting * Added 'useSourceBidderCode' setting to use S2S bidder's code instead of the client-side code in responses
1 parent 5b4390f commit a79d5b2

File tree

3 files changed

+610
-0
lines changed

3 files changed

+610
-0
lines changed

modules/relevantdigitalBidAdapter.js

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

modules/relevantdigitalBidAdapter.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Overview
2+
3+
```
4+
Module Name: Relevant Digital Bid Adapter
5+
Module Type: Bidder Adapter
6+
Maintainer: [email protected]
7+
```
8+
9+
# Description
10+
11+
This adapter is used for integration with providers using the **[Relevant Yield](https://www.relevant-digital.com/relevantyield)** platform. The provider will supply the necessary **pbsHost** and **accountId** settings along with the **placementId** bid parameters per ad unit.
12+
13+
# Example setup using pbjs.setConfig()
14+
This is the recommended method to set the global configuration parameters.
15+
```javascript
16+
pbjs.setConfig({
17+
relevantdigital: {
18+
pbsHost: 'pbs-example.relevant-digital.com',
19+
accountId: '6204e5fa70e3ad10821b84ff',
20+
},
21+
});
22+
23+
var adUnits = [
24+
{
25+
code: 'test-div',
26+
mediaTypes: { banner: { sizes: [[300, 250], [320, 320]] }},
27+
bids: [
28+
{
29+
bidder: 'relevantdigital',
30+
params: {
31+
placementId: '6204e83a077c5825441b8508_620f9e8e4fe67c1f87cd30ed',
32+
}
33+
}
34+
],
35+
}
36+
];
37+
```
38+
# Example setup using only bid params
39+
This method to set the global configuration parameters (like **pbsHost**) in **params** could simplify integration of a provider for some publishers. Setting different global config-parameters on different bids is not supported in general*, as the first settings found will be used and any subsequent global settings will be ignored.
40+
41+
 * _The exception is `useSourceBidderCode` which can be overriden individually per ad unit._
42+
```javascript
43+
var adUnits = [
44+
{
45+
code: 'test-div',
46+
mediaTypes: { banner: { sizes: [[300, 250], [320, 320]] }},
47+
bids: [
48+
{
49+
bidder: 'relevantdigital',
50+
params: {
51+
placementId: '6204e83a077c5825441b8508_620f9e8e4fe67c1f87cd30ed',
52+
pbsHost: 'pbs-example.relevant-digital.com',
53+
accountId: '6204e5fa70e3ad10821b84ff',
54+
}
55+
}
56+
],
57+
}
58+
];
59+
```
60+
61+
# Example setup with multiple providers
62+
**Notice:** Placements below are _not_ live test placements
63+
```javascript
64+
65+
pbjs.aliasBidder('relevantdigital', 'providerA');
66+
pbjs.aliasBidder('relevantdigital', 'providerB');
67+
68+
pbjs.setConfig({
69+
providerA: {
70+
pbsHost: 'pbs-example-a.relevant-digital.com',
71+
accountId: '620533ae7f5bbe1691bbb815',
72+
},
73+
providerB: {
74+
pbsHost: 'pbs-example-b.relevant-digital.com',
75+
accountId: '990533ae7f5bbe1691bbb815',
76+
},
77+
});
78+
79+
var adUnits = [
80+
{
81+
code: 'test-div',
82+
mediaTypes: { banner: { sizes: [[300, 250], [320, 320]] }},
83+
bids: [
84+
{
85+
bidder: 'providerA',
86+
params: {
87+
placementId: '610525862d7517bfd4bbb81e_620523b7d1dbed6b0fbbb817',
88+
}
89+
},
90+
{
91+
bidder: 'providerB',
92+
params: {
93+
placementId: '990525862d7517bfd4bbb81e_770523b7d1dbed6b0fbbb817',
94+
}
95+
},
96+
],
97+
}
98+
];
99+
```
100+
101+
# Bid Parameters
102+
103+
| Name | Scope | Description | Example | Type |
104+
|---------------|----------|---------------------------------------------------------|----------------------------|--------------|
105+
| `placementId` | required | The placement id. | `'6204e83a077c5825441b8508_620f9e8e4fe67c1f87cd30ed'` | `String` |
106+
| `pbsHost` | required if not set in config | Host name of the server. | `'pbs-example.relevant-digital.com'` | `String` |
107+
| `accountId` | required if not set in config | The account id. | `'6204e5fa70e3ad10821b84ff'` | `String` |
108+
| `useSourceBidderCode` | optional | Set to `true` in order to use the bidder code of the actual server-side bidder in bid responses. You **MUST** also use `allowAlternateBidderCodes: true` in `bidderSettings` if you enabled this - as otherwise the bids will be rejected.| `true` | `Boolean` |
109+
110+
# Config Parameters
111+
112+
| Name | Scope | Description | Example | Type |
113+
|---------------|----------|---------------------------------------------------------|----------------------------|--------------|
114+
| `pbsHost` | required if not set in bid parameters | Host name of the server. | `'pbs-example.relevant-digital.com'` | `String` |
115+
| `accountId` | required if not set in bid parameters | The account id. | `'6204e5fa70e3ad10821b84ff'` | `String` |
116+
| `pbsBufferMs` | optional | How much less in *milliseconds* the server's internal timeout should be compared to the normal Prebid timeout. Default is *250*. To be increased in cases of frequent timeouts. | `250` | `Integer` |
117+
| `useSourceBidderCode` | optional | Set to `true` in order to use the bidder code of the actual server-side bidder in bid responses. You **MUST** also use `allowAlternateBidderCodes: true` in `bidderSettings` if you enabled this - as otherwise the bids will be rejected.| `true` | `Boolean` |

0 commit comments

Comments
 (0)