Skip to content

Commit

Permalink
Greenbids RTD provider
Browse files Browse the repository at this point in the history
  • Loading branch information
jbogp committed Apr 26, 2023
1 parent d0bb0f5 commit b7f67b8
Show file tree
Hide file tree
Showing 3 changed files with 383 additions and 0 deletions.
110 changes: 110 additions & 0 deletions modules/greenbidsRtdProvider.js
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);
63 changes: 63 additions & 0 deletions modules/greenbidsRtdProvider.md
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.
210 changes: 210 additions & 0 deletions test/spec/modules/greenbidsRtdProvider_spec.js
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);
});
});
});

0 comments on commit b7f67b8

Please sign in to comment.