|
| 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); |
0 commit comments