Skip to content

Commit

Permalink
Relay Bid Adapter : Initial Release (#10197)
Browse files Browse the repository at this point in the history
* Add Relay Bid Adapter.

* Fix imports and lint violations.

* Implement PR feedback.
  • Loading branch information
jcswart authored Aug 16, 2023
1 parent 1f6abf6 commit 62c9d29
Show file tree
Hide file tree
Showing 3 changed files with 309 additions and 0 deletions.
99 changes: 99 additions & 0 deletions modules/relayBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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';
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 isNumber((bid.params || {}).accountId);
};

function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) {
let syncs = []
for (const response of serverResponses) {
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 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 (missingGdprConsent) {
syncUrl.searchParams.set('gdpr_consent', gdprConsent.consentString);
sync.url = syncUrl.toString();
}
if (syncOptions.iframeEnabled && sync.type === 'iframe') {
syncs.push(sync);
} else if (syncOptions.pixelEnabled && sync.type === 'image') {
syncs.push(sync);
}
}
}

return syncs;
}

export const spec = {
code: BIDDER_CODE,
isBidRequestValid,
buildRequests,
interpretResponse,
getUserSyncs,
onTimeout: function (timeoutData) {
logMessage('Timeout: ', timeoutData);
},
onBidWon: function (bid) {
logMessage('Bid won: ', bid);
},
onBidderError: function ({ error, bidderRequest }) {
logMessage('Error: ', error, bidderRequest);
},
supportedMediaTypes: [BANNER, VIDEO, NATIVE]
}
registerBidder(spec);
79 changes: 79 additions & 0 deletions modules/relayBidAdapter.md
Original file line number Diff line number Diff line change
@@ -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'
}
}
}
}
}
}
]
}
];
```
131 changes: 131 additions & 0 deletions test/spec/modules/relayBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
});

0 comments on commit 62c9d29

Please sign in to comment.