From 9f255f9416163f46b988e8578978cd2c65fcda56 Mon Sep 17 00:00:00 2001 From: Jarrod Swart Date: Mon, 10 Jul 2023 10:25:58 -0400 Subject: [PATCH 1/3] Add Relay Bid Adapter. --- modules/relayBidAdapter.js | 97 ++++++++++++++++ modules/relayBidAdapter.md | 79 +++++++++++++ test/spec/modules/relayBidAdapter_spec.js | 131 ++++++++++++++++++++++ 3 files changed, 307 insertions(+) create mode 100644 modules/relayBidAdapter.js create mode 100644 modules/relayBidAdapter.md create mode 100644 test/spec/modules/relayBidAdapter_spec.js diff --git a/modules/relayBidAdapter.js b/modules/relayBidAdapter.js new file mode 100644 index 00000000000..d0bb50ad211 --- /dev/null +++ b/modules/relayBidAdapter.js @@ -0,0 +1,97 @@ +import * as utils from 'src/utils'; +import { registerBidder } from 'src/adapters/bidderFactory'; +import { config } from 'src/config'; +import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' + +const BIDDER_CODE = 'relay'; +const METHOD = 'POST'; +const ENDPOINT_URL = 'https://e.relay.bid/p/openrtb2'; + +// The default impl from the prebid docs. +const CONVERTER = + ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + } + }); + +function buildRequests(bidRequests, bidderRequest) { + const prebidVersion = config.getConfig('prebid_version') || 'v8.1.0'; + // Group bids by accountId param + const groupedByAccountId = bidRequests.reduce((accu, item) => { + const accountId = ((item || {}).params || {}).accountId; + if (!accu[accountId]) { accu[accountId] = []; }; + accu[accountId].push(item); + return accu; + }, {}); + // Send one overall request with all grouped bids per accountId + let reqs = []; + for (const [accountId, accountBidRequests] of Object.entries(groupedByAccountId)) { + const url = `${ENDPOINT_URL}?a=${accountId}&pb=1&pbv=${prebidVersion}`; + const data = CONVERTER.toORTB({ bidRequests: accountBidRequests, bidderRequest }) + const req = { + method: METHOD, + url, + data + }; + reqs.push(req); + } + return reqs; +}; + +function interpretResponse(response, request) { + return CONVERTER.fromORTB({ response: response.body, request: request.data }).bids; +}; + +function isBidRequestValid(bid) { + return utils.isNumber((bid.params || {}).accountId); +}; + +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncs = [] + for (const response of serverResponses) { + const response_syncs = ((((response || {}).body || {}).ext || {}).user_syncs || []) + // Relay returns user_syncs in the format expected by prebid. If for any + // reason the request/response failed to properly capture the GDPR settings + // -- fallback to those identified by Prebid. + for (const sync of response_syncs) { + const sync_url = new URL(sync.url); + const missing_gdpr = !sync_url.searchParams.has('gdpr'); + const missing_gdpr_consent = !sync_url.searchParams.has('gdpr_consent'); + if (missing_gdpr) { + sync_url.searchParams.set('gdpr', Number(gdprConsent.gdprApplies)) + sync.url = sync_url.toString(); + } + if (missing_gdpr_consent) { + sync_url.searchParams.set('gdpr_consent', gdprConsent.consentString); + sync.url = sync_url.toString(); + } + if (syncOptions.iframeEnabled && sync.type === 'iframe') { syncs.push(sync); } + if (syncOptions.pixelEnabled && sync.type === 'image') { syncs.push(sync); } + } + } + + return syncs; +} + + +export const spec = { + code: BIDDER_CODE, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + onTimeout: function (timeoutData) { + utils.logMessage('Timeout: ', timeoutData); + }, + onBidWon: function (bid) { + utils.logMessage('Bid won: ', bid); + }, + onBidderError: function ({ error, bidderRequest }) { + utils.logMessage('Error: ', error, bidderRequest); + }, + supportedMediaTypes: [BANNER, VIDEO, NATIVE] +} +registerBidder(spec); diff --git a/modules/relayBidAdapter.md b/modules/relayBidAdapter.md new file mode 100644 index 00000000000..882e04b7b13 --- /dev/null +++ b/modules/relayBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Relay Bid Adapter +Module Type: Bid Adapter +Maintainer: relay@kevel.co +``` + +# Description + +Connects to Relay exchange API for bids. +Supports Banner, Video and Native. + +# Test Parameters + +``` +var adUnits = [ + // Banner with minimal bid configuration + { + code: 'minimal', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'relay', + params: { + accountId: 1234 + }, + ortb2imp: { + ext: { + relay: { + bidders: { + bidderA: { + param: 1234 + } + } + } + } + } + } + ] + }, + // Minimal video + { + code: 'video-minimal', + mediaTypes: { + video: { + maxduration: 30, + api: [1, 3], + mimes: ['video/mp4'], + placement: 3, + protocols: [2,3,5,6] + } + }, + bids: [ + { + bidder: 'relay', + params: { + accountId: 1234 + }, + ortb2imp: { + ext: { + relay: { + bidders: { + bidderA: { + param: 'example' + } + } + } + } + } + } + ] + } +]; +``` diff --git a/test/spec/modules/relayBidAdapter_spec.js b/test/spec/modules/relayBidAdapter_spec.js new file mode 100644 index 00000000000..38a3cfc9b97 --- /dev/null +++ b/test/spec/modules/relayBidAdapter_spec.js @@ -0,0 +1,131 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/relayBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'relay' +const endpoint = 'https://e.relay.bid/p/openrtb2'; + +describe('RelayBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder, + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: { + accountId: 15000, + }, + ortb2Imp: { + ext: { + relay: { + bidders: { + bidderA: { + theId: 'abc123' + }, + bidderB: { + theId: 'xyz789' + } + } + } + } + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder, + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: { + accountId: 30000, + }, + ortb2Imp: { + ext: { + relay: { + bidders: { + bidderA: { + theId: 'def456' + }, + bidderB: { + theId: 'uvw101112' + } + } + } + } + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: {} + } + + const bidderRequest = {}; + + describe('isBidRequestValid', function () { + it('Valid bids have a params.accountId.', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Invalid bids do not have a params.accountId.', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + const requests = spec.buildRequests(bids, bidderRequest); + const firstRequest = requests[0]; + const secondRequest = requests[1]; + + it('Creates two requests', function () { + expect(firstRequest).to.exist; + expect(firstRequest.data).to.exist; + expect(firstRequest.method).to.exist; + expect(firstRequest.method).to.equal('POST'); + expect(firstRequest.url).to.exist; + expect(firstRequest.url).to.equal(`${endpoint}?a=15000&pb=1&pbv=v8.1.0`); + + expect(secondRequest).to.exist; + expect(secondRequest.data).to.exist; + expect(secondRequest.method).to.exist; + expect(secondRequest.method).to.equal('POST'); + expect(secondRequest.url).to.exist; + expect(secondRequest.url).to.equal(`${endpoint}?a=30000&pb=1&pbv=v8.1.0`); + }); + + it('Does not generate requests when there are no bids', function () { + const request = spec.buildRequests([], bidderRequest); + expect(request).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function () { + it('Uses Prebid consent values if incoming sync URLs lack consent.', function () { + const syncOpts = { + iframeEnabled: true, + pixelEnabled: true + }; + const test_gdpr_applies = true; + const test_gdpr_consent_str = 'TEST_GDPR_CONSENT_STRING'; + const responses = [{ + body: { + ext: { + user_syncs: [ + { type: 'image', url: 'https://image-example.com' }, + { type: 'iframe', url: 'https://iframe-example.com' } + ] + } + } + }]; + + const sync_urls = spec.getUserSyncs(syncOpts, responses, { gdprApplies: test_gdpr_applies, consentString: test_gdpr_consent_str }); + expect(sync_urls).to.be.an('array'); + expect(sync_urls[0].url).to.equal('https://image-example.com/?gdpr=1&gdpr_consent=TEST_GDPR_CONSENT_STRING'); + expect(sync_urls[1].url).to.equal('https://iframe-example.com/?gdpr=1&gdpr_consent=TEST_GDPR_CONSENT_STRING'); + }); + }); +}); From cc1ae686fb3f5f6eb06c0d151bc7629d031c7251 Mon Sep 17 00:00:00 2001 From: Jarrod Swart Date: Wed, 12 Jul 2023 11:59:48 -0400 Subject: [PATCH 2/3] Fix imports and lint violations. --- modules/relayBidAdapter.js | 39 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/modules/relayBidAdapter.js b/modules/relayBidAdapter.js index d0bb50ad211..c95cb127d6d 100644 --- a/modules/relayBidAdapter.js +++ b/modules/relayBidAdapter.js @@ -1,7 +1,7 @@ -import * as utils from 'src/utils'; -import { registerBidder } from 'src/adapters/bidderFactory'; -import { config } from 'src/config'; -import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js'; +import { isNumber, logMessage } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js' const BIDDER_CODE = 'relay'; @@ -46,27 +46,27 @@ function interpretResponse(response, request) { }; function isBidRequestValid(bid) { - return utils.isNumber((bid.params || {}).accountId); + return isNumber((bid.params || {}).accountId); }; function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { let syncs = [] for (const response of serverResponses) { - const response_syncs = ((((response || {}).body || {}).ext || {}).user_syncs || []) + const responseSyncs = ((((response || {}).body || {}).ext || {}).user_syncs || []) // Relay returns user_syncs in the format expected by prebid. If for any // reason the request/response failed to properly capture the GDPR settings // -- fallback to those identified by Prebid. - for (const sync of response_syncs) { - const sync_url = new URL(sync.url); - const missing_gdpr = !sync_url.searchParams.has('gdpr'); - const missing_gdpr_consent = !sync_url.searchParams.has('gdpr_consent'); - if (missing_gdpr) { - sync_url.searchParams.set('gdpr', Number(gdprConsent.gdprApplies)) - sync.url = sync_url.toString(); + for (const sync of responseSyncs) { + const syncUrl = new URL(sync.url); + const missingGdpr = !syncUrl.searchParams.has('gdpr'); + const missingGdprConsent = !syncUrl.searchParams.has('gdpr_consent'); + if (missingGdpr) { + syncUrl.searchParams.set('gdpr', Number(gdprConsent.gdprApplies)) + sync.url = syncUrl.toString(); } - if (missing_gdpr_consent) { - sync_url.searchParams.set('gdpr_consent', gdprConsent.consentString); - sync.url = sync_url.toString(); + if (missingGdprConsent) { + syncUrl.searchParams.set('gdpr_consent', gdprConsent.consentString); + sync.url = syncUrl.toString(); } if (syncOptions.iframeEnabled && sync.type === 'iframe') { syncs.push(sync); } if (syncOptions.pixelEnabled && sync.type === 'image') { syncs.push(sync); } @@ -76,7 +76,6 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { return syncs; } - export const spec = { code: BIDDER_CODE, isBidRequestValid, @@ -84,13 +83,13 @@ export const spec = { interpretResponse, getUserSyncs, onTimeout: function (timeoutData) { - utils.logMessage('Timeout: ', timeoutData); + logMessage('Timeout: ', timeoutData); }, onBidWon: function (bid) { - utils.logMessage('Bid won: ', bid); + logMessage('Bid won: ', bid); }, onBidderError: function ({ error, bidderRequest }) { - utils.logMessage('Error: ', error, bidderRequest); + logMessage('Error: ', error, bidderRequest); }, supportedMediaTypes: [BANNER, VIDEO, NATIVE] } From 703e2c560931483d8abc4349943c8b1da77d3cc3 Mon Sep 17 00:00:00 2001 From: Jarrod Swart Date: Tue, 15 Aug 2023 14:21:04 -0400 Subject: [PATCH 3/3] Implement PR feedback. --- modules/relayBidAdapter.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/relayBidAdapter.js b/modules/relayBidAdapter.js index c95cb127d6d..af145a5e163 100644 --- a/modules/relayBidAdapter.js +++ b/modules/relayBidAdapter.js @@ -68,8 +68,11 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { syncUrl.searchParams.set('gdpr_consent', gdprConsent.consentString); sync.url = syncUrl.toString(); } - if (syncOptions.iframeEnabled && sync.type === 'iframe') { syncs.push(sync); } - if (syncOptions.pixelEnabled && sync.type === 'image') { syncs.push(sync); } + if (syncOptions.iframeEnabled && sync.type === 'iframe') { + syncs.push(sync); + } else if (syncOptions.pixelEnabled && sync.type === 'image') { + syncs.push(sync); + } } }