-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { logError } from '../src/utils.js'; | ||
import { ajax } from '../src/ajax.js'; | ||
import { submodule } from '../src/hook.js'; | ||
|
||
const MODULE_NAME = 'greenbidsRtdProvider'; | ||
const MODULE_VERSION = '1.0.0'; | ||
const ENDPOINT = 'https://europe-west1-greenbids-357713.cloudfunctions.net/partner-selection'; | ||
|
||
const auctionInfo = {}; | ||
const rtdOptions = {}; | ||
|
||
function init(moduleConfig) { | ||
let params = moduleConfig?.params; | ||
if (!params?.pbuid) { | ||
logError('Greenbids pbuid is not set!'); | ||
return false; | ||
} else { | ||
rtdOptions.pbuid = params?.pbuid; | ||
rtdOptions.targetTPR = params?.targetTPR || 0.99; | ||
rtdOptions.timeout = params?.timeout || 200; | ||
return true; | ||
} | ||
} | ||
|
||
function onAuctionInitEvent(auctionDetails) { | ||
auctionInfo.auctionId = auctionDetails.auctionId; | ||
} | ||
|
||
function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { | ||
let promise = createPromise(reqBidsConfigObj); | ||
promise.then(callback); | ||
} | ||
|
||
function createPromise(reqBidsConfigObj) { | ||
return new Promise((resolve) => { | ||
const timeoutId = setTimeout(() => { | ||
resolve(reqBidsConfigObj); | ||
}, rtdOptions.timeout); | ||
ajax( | ||
ENDPOINT, | ||
{ | ||
success: (response) => { | ||
processSuccessResponse(response, timeoutId, reqBidsConfigObj); | ||
resolve(reqBidsConfigObj); | ||
}, | ||
error: () => { | ||
clearTimeout(timeoutId); | ||
resolve(reqBidsConfigObj); | ||
}, | ||
}, | ||
createPayload(reqBidsConfigObj), | ||
{ contentType: 'application/json' } | ||
); | ||
}); | ||
} | ||
|
||
function processSuccessResponse(response, timeoutId, reqBidsConfigObj) { | ||
clearTimeout(timeoutId); | ||
const responseAdUnits = JSON.parse(response); | ||
|
||
updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits); | ||
} | ||
|
||
function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits) { | ||
adUnits.forEach((adUnit) => { | ||
const matchingAdUnit = findMatchingAdUnit(responseAdUnits, adUnit.code); | ||
if (matchingAdUnit) { | ||
removeFalseBidders(adUnit, matchingAdUnit); | ||
} | ||
}); | ||
} | ||
|
||
function findMatchingAdUnit(responseAdUnits, adUnitCode) { | ||
return responseAdUnits.find((responseAdUnit) => responseAdUnit.code === adUnitCode); | ||
} | ||
|
||
function removeFalseBidders(adUnit, matchingAdUnit) { | ||
const falseBidders = getFalseBidders(matchingAdUnit.bidders); | ||
adUnit.bids = adUnit.bids.filter((bidRequest) => !falseBidders.includes(bidRequest.bidder)); | ||
} | ||
|
||
function getFalseBidders(bidders) { | ||
return Object.entries(bidders) | ||
.filter(([bidder, shouldKeep]) => !shouldKeep) | ||
.map(([bidder]) => bidder); | ||
} | ||
|
||
function createPayload(reqBidsConfigObj) { | ||
return JSON.stringify({ | ||
auctionId: auctionInfo.auctionId, | ||
version: MODULE_VERSION, | ||
referrer: window.location.href, | ||
prebid: '$prebid.version$', | ||
rtdOptions: rtdOptions, | ||
adUnits: reqBidsConfigObj.adUnits, | ||
}); | ||
} | ||
|
||
export const greenbidsSubmodule = { | ||
name: MODULE_NAME, | ||
init: init, | ||
onAuctionInitEvent: onAuctionInitEvent, | ||
getBidRequestData: getBidRequestData, | ||
updateAdUnitsBasedOnResponse: updateAdUnitsBasedOnResponse, | ||
findMatchingAdUnit: findMatchingAdUnit, | ||
removeFalseBidders: removeFalseBidders, | ||
getFalseBidders: getFalseBidders, | ||
}; | ||
|
||
submodule('realTimeData', greenbidsSubmodule); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# Overview | ||
|
||
``` | ||
Module Name: Greenbids RTD Provider | ||
Module Type: RTD Provider | ||
Maintainer: jb@greenbids.ai | ||
``` | ||
|
||
# Description | ||
|
||
The Greenbids RTD adapter allows to dynamically filter calls to SSP to reduce outgoing call to the programmatics chain, reducing ad serving carbon impact | ||
|
||
## Configuration | ||
|
||
This module is configured as part of the `realTimeData.dataProviders` object. | ||
|
||
{: .table .table-bordered .table-striped } | ||
| Name | Scope | Description | Example | Type | | ||
|------------|----------|----------------------------------------|---------------|----------| | ||
| `name ` | required | Real time data module name | `'greenbidsRtdProvider'` | `string` | | ||
| `waitForIt ` | required (mandatory true value) | Tells prebid auction to wait for the result of this module | `'true'` | `boolean` | | ||
| `params` | required | | | `Object` | | ||
| `params.pbuid` | required | The client site id provided by Greenbids. | `'TEST_FROM_GREENBIDS'` | `string` | | ||
| `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` | | ||
|
||
#### Example | ||
|
||
```javascript | ||
const greenbidsDataProvider = { | ||
name: 'greenbidsRtdProvider', | ||
waitForIt: true, | ||
params: { | ||
pbuid: 'TEST_FROM_GREENBIDS', | ||
timeout: 200 | ||
} | ||
}; | ||
pbjs.setConfig({ | ||
realTimeData: { | ||
auctionDelay: 200, | ||
dataProviders: [ greenbidsDataProvider ] | ||
} | ||
}); | ||
``` | ||
|
||
## Integration | ||
To install the module, follow these instructions: | ||
|
||
#### Step 1: Contact Greenbids to get a pbuid and account | ||
|
||
#### Step 2: Integrate the Greenbids Analytics Adapter | ||
|
||
Greenbids RTD module works hand in hand with Greenbids Analytics module | ||
See prebid Analytics modules -> Greenbids Analytics module | ||
|
||
#### Step 3: Prepare the base Prebid file | ||
|
||
- Option 1: Use Prebid [Download](/download.html) page to build the prebid package. Ensure that you do check *Greenbids RTD Provider* module | ||
|
||
- Option 2: From the command line, run `gulp build --modules=greenbidsRtdProvider,...` | ||
|
||
#### Step 4: Set configuration | ||
|
||
Enable Greenbids Real Time Module using `pbjs.setConfig`. Example is provided in Configuration section. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
import { expect } from 'chai'; | ||
import sinon from 'sinon'; | ||
import { | ||
deepClone, | ||
} from '../../../src/utils.js'; | ||
import { | ||
greenbidsSubmodule | ||
} from 'modules/greenbidsRtdProvider.js'; | ||
|
||
describe('greenbidsRtdProvider', () => { | ||
let server; | ||
|
||
beforeEach(() => { | ||
server = sinon.createFakeServer(); | ||
}); | ||
|
||
afterEach(() => { | ||
server.restore(); | ||
}); | ||
|
||
const endPoint = 'europe-west1-greenbids-357713.cloudfunctions.net'; | ||
|
||
const SAMPLE_MODULE_CONFIG = { | ||
params: { | ||
pbuid: '12345', | ||
timeout: 200, | ||
targetTPR: 0.95 | ||
} | ||
}; | ||
|
||
const SAMPLE_REQUEST_BIDS_CONFIG_OBJ = { | ||
adUnits: [ | ||
{ | ||
code: 'adUnit1', | ||
bids: [ | ||
{ bidder: 'appnexus', params: {} }, | ||
{ bidder: 'rubicon', params: {} }, | ||
{ bidder: 'ix', params: {} } | ||
] | ||
}, | ||
{ | ||
code: 'adUnit2', | ||
bids: [ | ||
{ bidder: 'appnexus', params: {} }, | ||
{ bidder: 'rubicon', params: {} }, | ||
{ bidder: 'openx', params: {} } | ||
] | ||
}] | ||
}; | ||
|
||
const SAMPLE_RESPONSE_ADUNITS = [ | ||
{ | ||
code: 'adUnit1', | ||
bidders: { | ||
'appnexus': true, | ||
'rubicon': false, | ||
'ix': true | ||
} | ||
}, | ||
{ | ||
code: 'adUnit2', | ||
bidders: { | ||
'appnexus': false, | ||
'rubicon': true, | ||
'openx': true | ||
} | ||
}]; | ||
|
||
describe('init', () => { | ||
it('should return true and set rtdOptions if pbuid is present', () => { | ||
const result = greenbidsSubmodule.init(SAMPLE_MODULE_CONFIG); | ||
expect(result).to.be.true; | ||
}); | ||
|
||
it('should return false if pbuid is not present', () => { | ||
const result = greenbidsSubmodule.init({ params: {} }); | ||
expect(result).to.be.false; | ||
}); | ||
}); | ||
|
||
describe('updateAdUnitsBasedOnResponse', () => { | ||
it('should update ad units based on response', () => { | ||
const adUnits = JSON.parse(JSON.stringify(SAMPLE_REQUEST_BIDS_CONFIG_OBJ.adUnits)); | ||
greenbidsSubmodule.updateAdUnitsBasedOnResponse(adUnits, SAMPLE_RESPONSE_ADUNITS); | ||
|
||
expect(adUnits[0].bids).to.have.length(2); | ||
expect(adUnits[1].bids).to.have.length(2); | ||
}); | ||
}); | ||
|
||
describe('findMatchingAdUnit', () => { | ||
it('should find matching ad unit by code', () => { | ||
const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS, 'adUnit1'); | ||
expect(matchingAdUnit).to.deep.equal(SAMPLE_RESPONSE_ADUNITS[0]); | ||
}); | ||
it('should return undefined if no matching ad unit is found', () => { | ||
const matchingAdUnit = greenbidsSubmodule.findMatchingAdUnit(SAMPLE_RESPONSE_ADUNITS, 'nonexistent'); | ||
expect(matchingAdUnit).to.be.undefined; | ||
}); | ||
}); | ||
|
||
describe('removeFalseBidders', () => { | ||
it('should remove bidders with false value', () => { | ||
const adUnit = JSON.parse(JSON.stringify(SAMPLE_REQUEST_BIDS_CONFIG_OBJ.adUnits[0])); | ||
const matchingAdUnit = SAMPLE_RESPONSE_ADUNITS[0]; | ||
greenbidsSubmodule.removeFalseBidders(adUnit, matchingAdUnit); | ||
expect(adUnit.bids).to.have.length(2); | ||
expect(adUnit.bids.map((bid) => bid.bidder)).to.not.include('rubicon'); | ||
}); | ||
}); | ||
|
||
describe('getFalseBidders', () => { | ||
it('should return an array of false bidders', () => { | ||
const bidders = { | ||
appnexus: true, | ||
rubicon: false, | ||
ix: true, | ||
openx: false | ||
}; | ||
const falseBidders = greenbidsSubmodule.getFalseBidders(bidders); | ||
expect(falseBidders).to.have.length(2); | ||
expect(falseBidders).to.include('rubicon'); | ||
expect(falseBidders).to.include('openx'); | ||
}); | ||
}); | ||
|
||
describe('getBidRequestData', () => { | ||
it('Callback is called if the server responds a 200 within the time limit', (done) => { | ||
let requestBids = deepClone(SAMPLE_REQUEST_BIDS_CONFIG_OBJ); | ||
let callback = sinon.stub(); | ||
|
||
greenbidsSubmodule.getBidRequestData(requestBids, callback, SAMPLE_MODULE_CONFIG); | ||
|
||
setTimeout(() => { | ||
server.requests[0].respond( | ||
200, | ||
{'Content-Type': 'application/json'}, | ||
JSON.stringify(SAMPLE_RESPONSE_ADUNITS) | ||
); | ||
done(); | ||
}, 50); | ||
|
||
setTimeout(() => { | ||
const requestUrl = new URL(server.requests[0].url); | ||
expect(requestUrl.host).to.be.eq(endPoint); | ||
expect(requestBids.adUnits[0].bids).to.have.length(2); | ||
expect(requestBids.adUnits[0].bids.map((bid) => bid.bidder)).to.not.include('rubicon'); | ||
expect(requestBids.adUnits[0].bids.map((bid) => bid.bidder)).to.include('ix'); | ||
expect(requestBids.adUnits[0].bids.map((bid) => bid.bidder)).to.include('appnexus'); | ||
expect(requestBids.adUnits[1].bids).to.have.length(2); | ||
expect(requestBids.adUnits[1].bids.map((bid) => bid.bidder)).to.not.include('appnexus'); | ||
expect(requestBids.adUnits[1].bids.map((bid) => bid.bidder)).to.include('rubicon'); | ||
expect(requestBids.adUnits[1].bids.map((bid) => bid.bidder)).to.include('openx'); | ||
expect(callback.calledOnce).to.be.true; | ||
}, 60); | ||
}); | ||
}); | ||
|
||
describe('getBidRequestData', () => { | ||
it('Nothing changes if the server times out but still the callback is called', (done) => { | ||
let requestBids = deepClone(SAMPLE_REQUEST_BIDS_CONFIG_OBJ); | ||
let callback = sinon.stub(); | ||
|
||
greenbidsSubmodule.getBidRequestData(requestBids, callback, SAMPLE_MODULE_CONFIG); | ||
|
||
setTimeout(() => { | ||
server.requests[0].respond( | ||
200, | ||
{'Content-Type': 'application/json'}, | ||
JSON.stringify(SAMPLE_RESPONSE_ADUNITS) | ||
); | ||
done(); | ||
}, 300); | ||
|
||
setTimeout(() => { | ||
const requestUrl = new URL(server.requests[0].url); | ||
expect(requestUrl.host).to.be.eq(endPoint); | ||
expect(requestBids.adUnits[0].bids).to.have.length(3); | ||
expect(requestBids.adUnits[1].bids).to.have.length(3); | ||
expect(callback.calledOnce).to.be.true; | ||
}, 200); | ||
}); | ||
}); | ||
|
||
describe('getBidRequestData', () => { | ||
it('callback is called if the server responds a 500 error within the time limit and no changes are made', (done) => { | ||
let requestBids = deepClone(SAMPLE_REQUEST_BIDS_CONFIG_OBJ); | ||
let callback = sinon.stub(); | ||
|
||
greenbidsSubmodule.getBidRequestData(requestBids, callback, SAMPLE_MODULE_CONFIG); | ||
|
||
setTimeout(() => { | ||
server.requests[0].respond( | ||
500, | ||
{'Content-Type': 'application/json'}, | ||
JSON.stringify({'failure': 'fail'}) | ||
); | ||
done(); | ||
}, 50); | ||
|
||
setTimeout(() => { | ||
const requestUrl = new URL(server.requests[0].url); | ||
expect(requestUrl.host).to.be.eq(endPoint); | ||
expect(requestBids.adUnits[0].bids).to.have.length(3); | ||
expect(requestBids.adUnits[1].bids).to.have.length(3); | ||
expect(callback.calledOnce).to.be.true; | ||
}, 60); | ||
}); | ||
}); | ||
}); |