Skip to content

Commit 03af06a

Browse files
committed
Greenbids RTD provider
1 parent d0bb0f5 commit 03af06a

File tree

3 files changed

+385
-0
lines changed

3 files changed

+385
-0
lines changed

modules/greenbidsRtdProvider.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { logError } from '../src/utils.js';
2+
import { ajax } from '../src/ajax.js';
3+
import { submodule } from '../src/hook.js';
4+
5+
const MODULE_NAME = 'greenbidsRtdProvider';
6+
const MODULE_VERSION = '1.0.0';
7+
const ENDPOINT = 'https://europe-west1-greenbids-357713.cloudfunctions.net/partner-selection';
8+
9+
const auctionInfo = {};
10+
const rtdOptions = {};
11+
12+
function init(moduleConfig) {
13+
let params = moduleConfig?.params;
14+
if (!params?.pbuid) {
15+
logError('Greenbids pbuid is not set!');
16+
return false;
17+
} else {
18+
rtdOptions.pbuid = params?.pbuid;
19+
rtdOptions.targetTPR = params?.targetTPR || 0.99;
20+
rtdOptions.timeout = params?.timeout || 200;
21+
return true;
22+
}
23+
}
24+
25+
function onAuctionInitEvent(auctionDetails) {
26+
auctionInfo.auctionId = auctionDetails.auctionId;
27+
}
28+
29+
function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) {
30+
let promise = createPromise(reqBidsConfigObj);
31+
promise.then(callback);
32+
}
33+
34+
function createPromise(reqBidsConfigObj) {
35+
return new Promise((resolve) => {
36+
const timeoutId = setTimeout(() => {
37+
resolve(reqBidsConfigObj);
38+
}, rtdOptions.timeout);
39+
ajax(
40+
ENDPOINT,
41+
{
42+
success: (response) => {
43+
processSuccessResponse(response, timeoutId, reqBidsConfigObj);
44+
resolve(reqBidsConfigObj);
45+
},
46+
error: () => {
47+
clearTimeout(timeoutId);
48+
resolve(reqBidsConfigObj);
49+
},
50+
},
51+
createPayload(reqBidsConfigObj),
52+
{ contentType: 'application/json' }
53+
);
54+
});
55+
}
56+
57+
function processSuccessResponse(response, timeoutId, reqBidsConfigObj) {
58+
clearTimeout(timeoutId);
59+
const responseAdUnits = JSON.parse(response);
60+
61+
updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits);
62+
}
63+
64+
function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits) {
65+
adUnits.forEach((adUnit) => {
66+
const matchingAdUnit = findMatchingAdUnit(responseAdUnits, adUnit.code);
67+
if (matchingAdUnit) {
68+
removeFalseBidders(adUnit, matchingAdUnit);
69+
}
70+
});
71+
}
72+
73+
function findMatchingAdUnit(responseAdUnits, adUnitCode) {
74+
return responseAdUnits.find((responseAdUnit) => responseAdUnit.code === adUnitCode);
75+
}
76+
77+
function removeFalseBidders(adUnit, matchingAdUnit) {
78+
const falseBidders = getFalseBidders(matchingAdUnit.bidders);
79+
adUnit.bids = adUnit.bids.filter((bidRequest) => !falseBidders.includes(bidRequest.bidder));
80+
}
81+
82+
function getFalseBidders(bidders) {
83+
return Object.entries(bidders)
84+
.filter(([bidder, shouldKeep]) => !shouldKeep)
85+
.map(([bidder]) => bidder);
86+
}
87+
88+
function createPayload(reqBidsConfigObj) {
89+
return JSON.stringify({
90+
auctionId: auctionInfo.auctionId,
91+
version: MODULE_VERSION,
92+
referrer: window.location.href,
93+
prebid: '$prebid.version$',
94+
rtdOptions: rtdOptions,
95+
adUnits: reqBidsConfigObj.adUnits,
96+
});
97+
}
98+
99+
export const greenbidsSubmodule = {
100+
name: MODULE_NAME,
101+
init: init,
102+
onAuctionInitEvent: onAuctionInitEvent,
103+
getBidRequestData: getBidRequestData,
104+
updateAdUnitsBasedOnResponse: updateAdUnitsBasedOnResponse,
105+
findMatchingAdUnit: findMatchingAdUnit,
106+
removeFalseBidders: removeFalseBidders,
107+
getFalseBidders: getFalseBidders,
108+
};
109+
110+
submodule('realTimeData', greenbidsSubmodule);

modules/greenbidsRtdProvider.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Overview
2+
3+
```
4+
Module Name: Greenbids RTD Provider
5+
Module Type: RTD Provider
6+
Maintainer: [email protected]
7+
```
8+
9+
# Description
10+
11+
The Greenbids RTD adapter allows to dynamically filter calls to SSP to reduce outgoing call to the programmatics chain, reducing ad serving carbon impact
12+
13+
## Configuration
14+
15+
This module is configured as part of the `realTimeData.dataProviders` object.
16+
17+
{: .table .table-bordered .table-striped }
18+
| Name | Scope | Description | Example | Type |
19+
|------------|----------|----------------------------------------|---------------|----------|
20+
| `name ` | required | Real time data module name | `'greenbidsRtdProvider'` | `string` |
21+
| `waitForIt ` | required (mandatory true value) | Tells prebid auction to wait for the result of this module | `'true'` | `boolean` |
22+
| `params` | required | | | `Object` |
23+
| `params.pbuid` | required | The client site id provided by Greenbids. | `'TEST_FROM_GREENBIDS'` | `string` |
24+
| `params.targetTPR` | optional (default 0.95) | Target True positive rate for the throttling model | `0.99` | `[0-1]` |
25+
| `params.timeout` | optional (default 200) | Maximum amount of milliseconds allowed for module to finish working (has to be <= to the realTimeData.auctionDelay property) | `200` | `number` |
26+
27+
#### Example
28+
29+
```javascript
30+
const greenbidsDataProvider = {
31+
name: 'greenbidsRtdProvider',
32+
waitForIt: true,
33+
params: {
34+
pbuid: 'TEST_FROM_GREENBIDS',
35+
timeout: 200
36+
}
37+
};
38+
39+
pbjs.setConfig({
40+
realTimeData: {
41+
auctionDelay: 200,
42+
dataProviders: [greenbidsDataProvider]
43+
}
44+
});
45+
```
46+
47+
## Integration
48+
To install the module, follow these instructions:
49+
50+
#### Step 1: Contact Greenbids to get a pbuid and account
51+
52+
#### Step 2: Integrate the Greenbids Analytics Adapter
53+
54+
Greenbids RTD module works hand in hand with Greenbids Analytics module
55+
See prebid Analytics modules -> Greenbids Analytics module
56+
57+
#### Step 3: Prepare the base Prebid file
58+
59+
- Option 1: Use Prebid [Download](/download.html) page to build the prebid package. Ensure that you do check *Greenbids RTD Provider* module
60+
61+
- Option 2: From the command line, run `gulp build --modules=greenbidsRtdProvider,...`
62+
63+
#### Step 4: Set configuration
64+
65+
Enable Greenbids Real Time Module using `pbjs.setConfig`. Example is provided in Configuration section.
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { expect } from 'chai';
2+
import sinon from 'sinon';
3+
import {
4+
deepClone,
5+
} from '../../../src/utils.js';
6+
import {
7+
greenbidsSubmodule
8+
} from 'modules/greenbidsRtdProvider.js';
9+
10+
describe('greenbidsRtdProvider', () => {
11+
let server;
12+
13+
beforeEach(() => {
14+
server = sinon.createFakeServer();
15+
});
16+
17+
afterEach(() => {
18+
server.restore();
19+
});
20+
21+
const endPoint = 'europe-west1-greenbids-357713.cloudfunctions.net';
22+
23+
const SAMPLE_MODULE_CONFIG = {
24+
params: {
25+
pbuid: '12345',
26+
timeout: 200,
27+
targetTPR: 0.95
28+
}
29+
};
30+
31+
const SAMPLE_REQUEST_BIDS_CONFIG_OBJ = {
32+
adUnits: [
33+
{
34+
code: 'adUnit1',
35+
bids: [
36+
{ bidder: 'appnexus', params: {} },
37+
{ bidder: 'rubicon', params: {} },
38+
{ bidder: 'ix', params: {} }
39+
]
40+
},
41+
{
42+
code: 'adUnit2',
43+
bids: [
44+
{ bidder: 'appnexus', params: {} },
45+
{ bidder: 'rubicon', params: {} },
46+
{ bidder: 'openx', params: {} }
47+
]
48+
}]
49+
};
50+
51+
const SAMPLE_RESPONSE_ADUNITS = [
52+
{
53+
code: 'adUnit1',
54+
bidders: {
55+
'appnexus': true,
56+
'rubicon': false,
57+
'ix': true
58+
}
59+
},
60+
{
61+
code: 'adUnit2',
62+
bidders: {
63+
'appnexus': false,
64+
'rubicon': true,
65+
'openx': true
66+
}
67+
}];
68+
69+
describe('init', () => {
70+
it('should return true and set rtdOptions if pbuid is present', () => {
71+
const result = greenbidsSubmodule.init(SAMPLE_MODULE_CONFIG);
72+
expect(result).to.be.true;
73+
});
74+
75+
it('should return false if pbuid is not present', () => {
76+
const result = greenbidsSubmodule.init({ params: {} });
77+
expect(result).to.be.false;
78+
});
79+
});
80+
81+
describe('updateAdUnitsBasedOnResponse', () => {
82+
it('should update ad units based on response', () => {
83+
const adUnits = JSON.parse(JSON.stringify(SAMPLE_REQUEST_BIDS_CONFIG_OBJ.adUnits));
84+
greenbidsSubmodule.updateAdUnitsBasedOnResponse(adUnits, SAMPLE_RESPONSE_ADUNITS);
85+
86+
expect(adUnits[0].bids).to.have.length(2);
87+
expect(adUnits[1].bids).to.have.length(2);
88+
});
89+
});
90+
91+
describe('findMatchingAdUnit', () => {
92+
it('should find matching ad unit by code', () => {
93+
const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS, 'adUnit1');
94+
expect(matchingAdUnit).to.deep.equal(SAMPLE_RESPONSE_ADUNITS[0]);
95+
});
96+
it('should return undefined if no matching ad unit is found', () => {
97+
const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS, 'nonexistent');
98+
expect(matchingAdUnit).to.be.undefined;
99+
});
100+
});
101+
102+
describe('removeFalseBidders', () => {
103+
it('should remove bidders with false value', () => {
104+
const adUnit = JSON.parse(JSON.stringify(SAMPLE_REQUEST_BIDS_CONFIG_OBJ.adUnits[0]));
105+
const matchingAdUnit = SAMPLE_RESPONSE_ADUNITS[0];
106+
greenbidsSubmodule.removeFalseBidders(adUnit, matchingAdUnit);
107+
expect(adUnit.bids).to.have.length(2);
108+
expect(adUnit.bids.map((bid) => bid.bidder)).to.not.include('rubicon');
109+
});
110+
});
111+
112+
describe('getFalseBidders', () => {
113+
it('should return an array of false bidders', () => {
114+
const bidders = {
115+
appnexus: true,
116+
rubicon: false,
117+
ix: true,
118+
openx: false
119+
};
120+
const falseBidders = greenbidsSubmodule.getFalseBidders(bidders);
121+
expect(falseBidders).to.have.length(2);
122+
expect(falseBidders).to.include('rubicon');
123+
expect(falseBidders).to.include('openx');
124+
});
125+
});
126+
127+
describe('getBidRequestData', () => {
128+
it('Callback is called if the server responds a 200 within the time limit', (done) => {
129+
let requestBids = deepClone(SAMPLE_REQUEST_BIDS_CONFIG_OBJ);
130+
let callback = sinon.stub();
131+
132+
greenbidsSubmodule.getBidRequestData(requestBids, callback, SAMPLE_MODULE_CONFIG);
133+
134+
setTimeout(() => {
135+
server.requests[0].respond(
136+
200,
137+
{'Content-Type': 'application/json'},
138+
JSON.stringify(SAMPLE_RESPONSE_ADUNITS)
139+
);
140+
done();
141+
}, 50);
142+
143+
setTimeout(() => {
144+
const requestUrl = new URL(server.requests[0].url);
145+
expect(requestUrl.host).to.be.eq(endPoint);
146+
expect(requestBids.adUnits[0].bids).to.have.length(2);
147+
expect(requestBids.adUnits[0].bids.map((bid) => bid.bidder)).to.not.include('rubicon');
148+
expect(requestBids.adUnits[0].bids.map((bid) => bid.bidder)).to.include('ix');
149+
expect(requestBids.adUnits[0].bids.map((bid) => bid.bidder)).to.include('appnexus');
150+
expect(requestBids.adUnits[1].bids).to.have.length(2);
151+
expect(requestBids.adUnits[1].bids.map((bid) => bid.bidder)).to.not.include('appnexus');
152+
expect(requestBids.adUnits[1].bids.map((bid) => bid.bidder)).to.include('rubicon');
153+
expect(requestBids.adUnits[1].bids.map((bid) => bid.bidder)).to.include('openx');
154+
expect(callback.calledOnce).to.be.true;
155+
}, 60);
156+
});
157+
});
158+
159+
describe('getBidRequestData', () => {
160+
it('Nothing changes if the server times out but still the callback is called', (done) => {
161+
let requestBids = deepClone(SAMPLE_REQUEST_BIDS_CONFIG_OBJ);
162+
let callback = sinon.stub();
163+
164+
greenbidsSubmodule.getBidRequestData(requestBids, callback, SAMPLE_MODULE_CONFIG);
165+
166+
setTimeout(() => {
167+
server.requests[0].respond(
168+
200,
169+
{'Content-Type': 'application/json'},
170+
JSON.stringify(SAMPLE_RESPONSE_ADUNITS)
171+
);
172+
done();
173+
}, 300);
174+
175+
setTimeout(() => {
176+
const requestUrl = new URL(server.requests[0].url);
177+
expect(requestUrl.host).to.be.eq(endPoint);
178+
expect(requestBids.adUnits[0].bids).to.have.length(3);
179+
expect(requestBids.adUnits[1].bids).to.have.length(3);
180+
expect(callback.calledOnce).to.be.true;
181+
}, 200);
182+
});
183+
});
184+
185+
describe('getBidRequestData', () => {
186+
it('callback is called if the server responds a 500 error within the time limit and no changes are made', (done) => {
187+
let requestBids = deepClone(SAMPLE_REQUEST_BIDS_CONFIG_OBJ);
188+
let callback = sinon.stub();
189+
190+
greenbidsSubmodule.getBidRequestData(requestBids, callback, SAMPLE_MODULE_CONFIG);
191+
192+
setTimeout(() => {
193+
server.requests[0].respond(
194+
500,
195+
{'Content-Type': 'application/json'},
196+
JSON.stringify({'failure': 'fail'})
197+
);
198+
done();
199+
}, 50);
200+
201+
setTimeout(() => {
202+
const requestUrl = new URL(server.requests[0].url);
203+
expect(requestUrl.host).to.be.eq(endPoint);
204+
expect(requestBids.adUnits[0].bids).to.have.length(3);
205+
expect(requestBids.adUnits[1].bids).to.have.length(3);
206+
expect(callback.calledOnce).to.be.true;
207+
}, 60);
208+
});
209+
});
210+
});

0 commit comments

Comments
 (0)