Skip to content

Commit 613b99b

Browse files
authored
Audiencerun bid adapter (#4761)
* New adapter AudienceRun * Fix issues AudiencerunAdapter * Fix issues AudiencerunAdapter * Fix issues AudiencerunAdapter * Fix issues AudiencerunAdapter
1 parent 9c08696 commit 613b99b

File tree

3 files changed

+394
-0
lines changed

3 files changed

+394
-0
lines changed

modules/audiencerunBidAdapter.js

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import * as utils from '../src/utils';
2+
import { config } from '../src/config';
3+
import { registerBidder } from '../src/adapters/bidderFactory';
4+
import { BANNER } from '../src/mediaTypes';
5+
6+
const BIDDER_CODE = 'audiencerun';
7+
const ENDPOINT_URL = 'https://d.audiencerun.com/prebid';
8+
9+
export const spec = {
10+
version: '1.0.0',
11+
code: BIDDER_CODE,
12+
supportedMediaTypes: [BANNER],
13+
14+
/**
15+
* Determines whether or not the given bid request is valid.
16+
*
17+
* @param {object} bid The bid to validate.
18+
* @return boolean True if this is a valid bid, and false otherwise.
19+
*/
20+
isBidRequestValid: function (bid) {
21+
let isValid = true;
22+
if (!utils.deepAccess(bid, 'params.zoneId')) {
23+
utils.logError('AudienceRun zoneId parameter is required. Bid aborted.');
24+
isValid = false;
25+
}
26+
return isValid;
27+
},
28+
29+
/**
30+
* Make a server request from the list of BidRequests.
31+
*
32+
* @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server.
33+
* @param {*} bidderRequest
34+
* @return {ServerRequest} Info describing the request to the server.
35+
*/
36+
buildRequests: function(bidRequests, bidderRequest) {
37+
const bids = bidRequests.map(bid => {
38+
const sizes = utils.deepAccess(bid, 'mediaTypes.banner.sizes', []);
39+
return {
40+
zoneId: utils.getValue(bid.params, 'zoneId'),
41+
sizes: sizes.map(size => ({
42+
w: size[0],
43+
h: size[1]
44+
})),
45+
bidfloor: bid.params.bidfloor || 0.0,
46+
bidId: bid.bidId,
47+
bidderRequestId: utils.getBidIdParameter('bidderRequestId', bid),
48+
adUnitCode: utils.getBidIdParameter('adUnitCode', bid),
49+
auctionId: utils.getBidIdParameter('auctionId', bid),
50+
transactionId: utils.getBidIdParameter('transactionId', bid)
51+
};
52+
});
53+
54+
const payload = {
55+
libVersion: this.version,
56+
referer: bidderRequest.refererInfo ? bidderRequest.refererInfo.referer || null : null,
57+
currencyCode: config.getConfig('currency.adServerCurrency'),
58+
timeout: config.getConfig('bidderTimeout'),
59+
bids
60+
};
61+
62+
if (bidderRequest && bidderRequest.gdprConsent) {
63+
payload.gdpr = {
64+
consent: bidderRequest.gdprConsent.consentString,
65+
applies: bidderRequest.gdprConsent.gdprApplies
66+
};
67+
} else {
68+
payload.gdpr = {
69+
consent: ''
70+
}
71+
}
72+
73+
return {
74+
method: 'POST',
75+
url: ENDPOINT_URL,
76+
data: JSON.stringify(payload),
77+
options: {
78+
withCredentials: true
79+
}
80+
};
81+
},
82+
83+
/**
84+
* Unpack the response from the server into a list of bids.
85+
*
86+
* @param {*} serverResponse A successful response from the server.
87+
* @return {Bid[]} An array of bids which were nested inside the server.
88+
*/
89+
interpretResponse: function (serverResponse, bidRequest) {
90+
const bids = [];
91+
utils._each(serverResponse.body.bid, function (bidObject) {
92+
if (!bidObject.cpm || bidObject.cpm === null || !bidObject.adm) {
93+
return;
94+
}
95+
96+
const bid = {};
97+
98+
bid.ad = bidObject.adm;
99+
bid.mediaType = BANNER;
100+
101+
// Common properties
102+
bid.requestId = bidObject.bidId;
103+
bid.adId = bidObject.zoneId;
104+
bid.cpm = parseFloat(bidObject.cpm);
105+
bid.creativeId = bidObject.crid;
106+
bid.currency = bidObject.currency ? bidObject.currency.toUpperCase() : 'USD';
107+
108+
bid.height = bidObject.h;
109+
bid.width = bidObject.w;
110+
bid.netRevenue = bidObject.isNet ? bidObject.isNet : false;
111+
bid.ttl = 300;
112+
113+
bids.push(bid);
114+
});
115+
return bids;
116+
},
117+
118+
/**
119+
* Register the user sync pixels which should be dropped after the auction.
120+
*
121+
* @param {SyncOptions} syncOptions Which user syncs are allowed?
122+
* @param {ServerResponse[]} serverResponses List of server's responses.
123+
* @return {UserSync[]} The user syncs which should be dropped.
124+
*/
125+
getUserSyncs: function(syncOptions, serverResponses) {
126+
if (!serverResponses || !serverResponses.length) return [];
127+
128+
const syncs = [];
129+
serverResponses.forEach(response => {
130+
response.body.bid.forEach(bidObject => {
131+
syncs.push({
132+
type: 'iframe',
133+
url: bidObject.syncUrl
134+
});
135+
});
136+
});
137+
138+
return syncs;
139+
}
140+
};
141+
142+
registerBidder(spec);

modules/audiencerunBidAdapter.md

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Overview
2+
3+
**Module Name**: AudienceRun Bidder Adapter
4+
**Module Type**: Bidder Adapter
5+
**Maintainer**: [email protected]
6+
7+
# Description
8+
9+
Module that connects to AudienceRun demand sources
10+
11+
Use `audiencerun` as bidder.
12+
13+
`zoneId` is required and must be 10 alphanumeric characters.
14+
15+
## AdUnits configuration example
16+
```
17+
var adUnits = [{
18+
code: 'ad-slot-300x600',
19+
mediaTypes: {
20+
banner: {
21+
sizes: [
22+
[300, 600]
23+
],
24+
}
25+
},
26+
bids: [{
27+
bidder: 'audiencerun',
28+
params: {
29+
zoneId: 'xtov2mgij0'
30+
}
31+
}]
32+
},{
33+
code: 'ad-slot-728x90',
34+
mediaTypes: {
35+
banner: {
36+
sizes: [
37+
[728, 90]
38+
],
39+
}
40+
},
41+
bids: [{
42+
bidder: 'audiencerun',
43+
params: {
44+
zoneId: 'u4q6z6u97b'
45+
}
46+
}]
47+
}];
48+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { expect } from 'chai';
2+
import { spec } from 'modules/audiencerunBidAdapter';
3+
import { newBidder } from 'src/adapters/bidderFactory';
4+
5+
const ENDPOINT = 'https://d.audiencerun.com/prebid';
6+
7+
const BID_SERVER_RESPONSE = {
8+
body: {
9+
bid: [
10+
{
11+
'bidId': '51ef8751f9aead',
12+
'zoneId': '12345abcde',
13+
'adId': '1234',
14+
'crid': '5678',
15+
'cpm': 8.021951999999999999,
16+
'currency': 'USD',
17+
'w': 728,
18+
'h': 90,
19+
'isNet': false,
20+
'buying_type': 'rtb',
21+
'syncUrl': 'https://ac.audiencerun.com/f/sync.html',
22+
'adm': '<!-- test creative -->'
23+
}
24+
]
25+
}
26+
};
27+
28+
describe('AudienceRun bid adapter tests', function() {
29+
const adapter = newBidder(spec);
30+
31+
describe('inherited functions', function() {
32+
it('exists and is a function', function() {
33+
expect(adapter.callBids).to.exist.and.to.be.a('function');
34+
});
35+
});
36+
37+
describe('isBidRequestValid', function() {
38+
let bid = {
39+
'bidder': 'audiencerun',
40+
'params': {
41+
'zoneId': '12345abcde'
42+
},
43+
'adUnitCode': 'adunit-code',
44+
'mediaTypes': {
45+
'banner': {
46+
'sizes': [[300, 250], [300, 600]]
47+
}
48+
},
49+
'bidId': '30b31c1838de1e',
50+
'bidderRequestId': '22edbae2733bf6',
51+
'auctionId': '1d1a030790a475',
52+
'creativeId': 'er2ee'
53+
};
54+
55+
it('should return true when required params found', function() {
56+
expect(spec.isBidRequestValid(bid)).to.equal(true);
57+
});
58+
59+
it('should return true when zoneId is valid', function() {
60+
let bid = Object.assign({}, bid);
61+
delete bid.params;
62+
bid.params = {
63+
'zoneId': '12345abcde'
64+
};
65+
66+
expect(spec.isBidRequestValid(bid)).to.equal(true);
67+
});
68+
69+
it('should return false when required params are not passed', function() {
70+
let bid = Object.assign({}, bid);
71+
delete bid.params;
72+
73+
bid.params = {};
74+
75+
expect(spec.isBidRequestValid(bid)).to.equal(false);
76+
});
77+
});
78+
79+
describe('buildRequests', function() {
80+
let bidRequests = [
81+
{
82+
'bidder': 'audiencerun',
83+
'bidId': '51ef8751f9aead',
84+
'params': {
85+
'zoneId': '12345abcde'
86+
},
87+
'adUnitCode': 'div-gpt-ad-1460505748561-0',
88+
'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec',
89+
'mediaTypes': {
90+
'banner': {
91+
'sizes': [[320, 50], [300, 250], [300, 600]]
92+
}
93+
},
94+
'bidderRequestId': '418b37f85e772c',
95+
'auctionId': '18fd8b8b0bd757',
96+
'bidRequestsCount': 1
97+
}
98+
];
99+
100+
it('sends a valid bid request to ENDPOINT via POST', function() {
101+
const request = spec.buildRequests(bidRequests, {
102+
gdprConsent: {
103+
consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA',
104+
gdprApplies: true
105+
},
106+
refererInfo: {
107+
canonicalUrl: 'https://example.com/canonical',
108+
referer: 'https://example.com'
109+
}
110+
});
111+
112+
expect(request.url).to.equal(ENDPOINT);
113+
expect(request.method).to.equal('POST');
114+
115+
const payload = JSON.parse(request.data);
116+
expect(payload.gdpr).to.exist;
117+
118+
expect(payload.bids).to.exist.and.to.be.an('array').and.to.have.lengthOf(1);
119+
expect(payload.referer).to.exist;
120+
121+
const bid = payload.bids[0];
122+
expect(bid).to.exist;
123+
expect(bid).to.have.property('bidId');
124+
expect(bid).to.have.property('zoneId');
125+
expect(bid).to.have.property('sizes');
126+
expect(bid.sizes[0].w).to.be.a('number');
127+
expect(bid.sizes[0].h).to.be.a('number');
128+
});
129+
130+
it('should send GDPR to endpoint and honor gdprApplies value', function() {
131+
let consentString = 'bogusConsent';
132+
let bidderRequest = {
133+
'gdprConsent': {
134+
'consentString': consentString,
135+
'gdprApplies': true
136+
}
137+
};
138+
139+
const request = spec.buildRequests(bidRequests, bidderRequest);
140+
const payload = JSON.parse(request.data);
141+
expect(payload.gdpr).to.exist;
142+
expect(payload.gdpr.consent).to.equal(consentString);
143+
expect(payload.gdpr.applies).to.equal(true);
144+
145+
let bidderRequest2 = {
146+
'gdprConsent': {
147+
'consentString': consentString,
148+
'gdprApplies': false
149+
}
150+
};
151+
152+
const request2 = spec.buildRequests(bidRequests, bidderRequest2);
153+
const payload2 = JSON.parse(request2.data);
154+
155+
expect(payload2.gdpr).to.exist;
156+
expect(payload2.gdpr.consent).to.equal(consentString);
157+
expect(payload2.gdpr.applies).to.equal(false);
158+
});
159+
});
160+
161+
describe('interpretResponse', function () {
162+
const expectedResponse = [{
163+
'requestId': '51ef8751f9aead',
164+
'adId': '12345abcde',
165+
'cpm': 8.021951999999999999,
166+
'width': '728',
167+
'height': '90',
168+
'creativeId': '5678',
169+
'currency': 'USD',
170+
'netRevenue': false,
171+
'ttl': 300,
172+
'ad': '<!-- test creative -->',
173+
'mediaType': 'banner'
174+
}];
175+
176+
it('should get the correct bid response by display ad', function () {
177+
let result = spec.interpretResponse(BID_SERVER_RESPONSE);
178+
expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0]));
179+
});
180+
181+
it('handles empty bid response', function () {
182+
const response = {
183+
body: {}
184+
};
185+
let result = spec.interpretResponse(response);
186+
expect(result.length).to.equal(0);
187+
});
188+
});
189+
190+
describe('getUserSyncs', function () {
191+
const serverResponses = [ BID_SERVER_RESPONSE ];
192+
const syncOptions = { iframeEnabled: true };
193+
194+
it('should return empty if no server responses', function() {
195+
const syncs = spec.getUserSyncs(syncOptions, []);
196+
expect(syncs).to.deep.equal([])
197+
});
198+
199+
it('should return user syncs', function () {
200+
const syncs = spec.getUserSyncs(syncOptions, serverResponses);
201+
expect(syncs).to.deep.equal([{type: 'iframe', url: 'https://ac.audiencerun.com/f/sync.html'}])
202+
});
203+
});
204+
});

0 commit comments

Comments
 (0)