Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Greenbids RTD Provider: create new RTD provider for Greenbids #9848

Merged
merged 1 commit into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
65 changes: 65 additions & 0 deletions modules/greenbidsRtdProvider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 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.targetTPR` | optional (default 0.95) | Target True positive rate for the throttling model | `0.99` | `[0-1]` |
| `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);
});
});
});