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

GreenbidsAnalyticsAdapter and GreenbidsRtdProvider: Rework greenbids sampling and improve transparency #10792

Merged
merged 2 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
96 changes: 79 additions & 17 deletions modules/greenbidsAnalyticsAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ import {ajax} from '../src/ajax.js';
import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js';
import CONSTANTS from '../src/constants.json';
import adapterManager from '../src/adapterManager.js';
import {deepClone, logError, logInfo} from '../src/utils.js';
import {deepClone, generateUUID, logError, logInfo, logWarn} from '../src/utils.js';

const analyticsType = 'endpoint';

export const ANALYTICS_VERSION = '1.0.0';
export const ANALYTICS_VERSION = '2.0.0';

const ANALYTICS_SERVER = 'https://a.greenbids.ai';

const {
EVENTS: {
AUCTION_INIT,
AUCTION_END,
BID_TIMEOUT,
BILLABLE_EVENT,
}
} = CONSTANTS;

Expand All @@ -25,28 +27,60 @@ export const BIDDER_STATUS = {

const analyticsOptions = {};

export const parseBidderCode = function (bid) {
let bidderCode = bid.bidderCode || bid.bidder;
return bidderCode.toLowerCase();
};
export const isSampled = function(greenbidsId, samplingRate) {
if (samplingRate < 0 || samplingRate > 1) {
logWarn('Sampling rate must be between 0 and 1');
return true;
}
const hashInt = parseInt(greenbidsId.slice(-4), 16);

return hashInt < samplingRate * (0xFFFF + 1);
}

export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER, analyticsType}), {

cachedAuctions: {},
isSampled: true,
greenbidsId: null,
billingId: null,

initConfig(config) {
analyticsOptions.options = deepClone(config.options);
/**
* Required option: pbuid
* @type {boolean}
*/
analyticsOptions.options = deepClone(config.options);
if (typeof config.options.pbuid !== 'string' || config.options.pbuid.length < 1) {
if (typeof analyticsOptions.options.pbuid !== 'string' || analyticsOptions.options.pbuid.length < 1) {
logError('"options.pbuid" is required.');
return false;
}

/**
* Deprecate use of integerated 'sampling' config
* replace by greenbidsSampling
*/
if (typeof analyticsOptions.options.sampling === 'number') {
logWarn('"options.sampling" is deprecated, please use "greenbidsSampling" instead.');
analyticsOptions.options.greenbidsSampling = analyticsOptions.options.sampling;
// Set sampling to null to prevent prebid analytics integrated sampling to happen
analyticsOptions.options.sampling = null;
}

/**
* Discourage unsampled analytics
*/
if (typeof analyticsOptions.options.greenbidsSampling !== 'number' || analyticsOptions.options.greenbidsSampling >= 1) {
logWarn('"options.greenbidsSampling" is not set or >=1, using this analytics module unsampled is discouraged.');
analyticsOptions.options.greenbidsSampling = 1;
}

analyticsOptions.pbuid = config.options.pbuid
analyticsOptions.server = ANALYTICS_SERVER;
// resetting object default values on init
this.isSampled = true;
this.billingId = null;
this.greenbidsId = null;

return true;
},
sendEventMessage(endPoint, data) {
Expand All @@ -61,9 +95,11 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER
version: ANALYTICS_VERSION,
auctionId: auctionId,
referrer: window.location.href,
sampling: analyticsOptions.options.sampling,
sampling: analyticsOptions.options.greenbidsSampling,
prebid: '$prebid.version$',
greenbidsId: this.greenbidsId,
pbuid: analyticsOptions.pbuid,
billingId: this.billingId,
adUnits: [],
};
},
Expand Down Expand Up @@ -97,7 +133,6 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER
}
},
createBidMessage(auctionEndArgs, timeoutBids) {
logInfo(auctionEndArgs)
const {auctionId, timestamp, auctionEnd, adUnits, bidsReceived, noBids} = auctionEndArgs;
const message = this.createCommonMessage(auctionId);

Expand Down Expand Up @@ -125,6 +160,8 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER

timeoutBids.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.TIMEOUT));

message.billingId = this.billingId;

return message;
},
getCachedAuction(auctionId) {
Expand All @@ -133,6 +170,15 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER
};
return this.cachedAuctions[auctionId];
},
handleAuctionInit(auctionInitArgs) {
try {
this.greenbidsId = auctionInitArgs.adUnits[0].ortb2Imp.ext.greenbids.greenbidsId;
} catch (e) {
logInfo("Couldn't find Greenbids RTD info, assuming analytics only");
this.greenbidsId = generateUUID();
}
this.isSampled = isSampled(this.greenbidsId, analyticsOptions.options.greenbidsSampling);
},
handleAuctionEnd(auctionEndArgs) {
const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId);
this.sendEventMessage('/',
Expand All @@ -145,14 +191,30 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER
cachedAuction.timeoutBids.push(bid);
});
},
handleBillable(billableArgs) {
const vendor = billableArgs.vendor || 'unknown_vendor';
const billingId = billableArgs.billingId || 'unknown_billing_id';
/* Filter Greenbids Billable Events only */
if (vendor === 'greenbidsRtdProvider') {
this.billingId = billingId;
}
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IF you are only looking for greenbids vendor you prob do not need to set that local vendor var

  handleBillable(billableArgs) {
    /* Filter Greenbids Billable Events only */
    if (billableArgs .vendor === 'greenbidsRtdProvider') {
      this.billingId = billableArgs.billingId || 'unknown_billing_id';
    }
  }

Also, this seems to be setting a module wide billingId

Which is emitted by Greenbids at aucitonInit

But it seems you do not send the message to your server until auctionEnd

Prebid supports running multiple auctions at once, so it is possible this data will not match up together.

If it matters to your server and stuff, you may want to have this function handleBillable save to a map of auctionId => billingId

Then in your createBidMessage you grab the billingId used for the auction your are sending.

Something like:

  handleBillable(billableArgs) {
    /* Filter Greenbids Billable Events only */
    if (billableArgs .vendor === 'greenbidsRtdProvider') {
      this.billingIds[billableArgs.auctionId] = billableArgs.billingId || 'unknown_billing_id';
    }
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great feedback, it actually applied to more than billingId in the code, greenbidsId and isSampled were globally defined as well and not compatible with concurrent auctions. They have now been included in the cachedAuctions object which fixes this. Thanks for noticing !

track({eventType, args}) {
switch (eventType) {
case BID_TIMEOUT:
this.handleBidTimeout(args);
break;
case AUCTION_END:
this.handleAuctionEnd(args);
break;
if (eventType === AUCTION_INIT) {
this.handleAuctionInit(args);
}
if (this.isSampled) {
switch (eventType) {
case BID_TIMEOUT:
this.handleBidTimeout(args);
break;
case AUCTION_END:
this.handleAuctionEnd(args);
break;
case BILLABLE_EVENT:
this.handleBillable(args);
break;
}
}
},
getAnalyticsOptions() {
Expand Down
37 changes: 19 additions & 18 deletions modules/greenbidsAnalyticsAdapter.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
# Overview
#### Registration

```
Module Name: Greenbids Analytics Adapter
Module Type: Analytics Adapter
Maintainer: jb@greenbids.ai
```
The Greenbids Analytics adapter requires setup and approval from the
Greenbids team. Please reach out to our team for more information [greenbids.ai](https://greenbids.ai).

# Description
#### Analytics Options

Analytics adapter for Greenbids
{: .table .table-bordered .table-striped }
| Name | Scope | Description | Example | Type |
|-------------|---------|--------------------|-----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|------------------|
| pbuid | required | The Greenbids Publisher ID | greenbids-publisher-1 | string |
| greenbidsSampling | optional | sampling factor [0-1] (a value of 0.1 will filter 90% of the traffic) | 1.0 | float |

# Test Parameters
### Example Configuration

```
{
provider: 'greenbids',
options: {
pbuid: "PBUID_FROM_GREENBIDS"
sampling: 1.0
}
}
```
```javascript
pbjs.enableAnalytics({
provider: 'greenbids',
options: {
pbuid: "greenbids-publisher-1" // please contact Greenbids to get a pbuid for yourself
greenbidsSampling: 1.0
}
});
```
67 changes: 48 additions & 19 deletions modules/greenbidsRtdProvider.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { logError } from '../src/utils.js';
import { logError, deepClone, generateUUID, deepSetValue } from '../src/utils.js';
import { ajax } from '../src/ajax.js';
import { submodule } from '../src/hook.js';
import * as events from '../src/events.js';
import CONSTANTS from '../src/constants.json';

const MODULE_NAME = 'greenbidsRtdProvider';
const MODULE_VERSION = '1.0.0';
const MODULE_VERSION = '2.0.0';
const ENDPOINT = 'https://t.greenbids.ai';

const auctionInfo = {};
const rtdOptions = {};

function init(moduleConfig) {
Expand All @@ -16,22 +17,28 @@ function init(moduleConfig) {
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;
/* Emitting one billing event per auction */
events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, {
type: 'auction',
billingId: generateUUID(),
auctionId: auctionDetails.auctionId,
vendor: MODULE_NAME
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the plan is to emit a billing event once per auction no matter what right, even if the adapter did not do something?

I am not aware of if there is a scenario where that would happen, like if the greenbids endpoint returns empty or errors.

Just checking.

Copy link
Contributor Author

@jbogp jbogp Dec 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think billing for each auction makes the most sense because it correlates 1 to 1 with our infra cost, but I updated this to check that the greenbidsId is set in the ortb2Imp.ext this way we won't bill if the call to greenbids wasn't successful. Updated the tests to reflect this behavior.

}

function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) {
let promise = createPromise(reqBidsConfigObj);
let greenbidsId = generateUUID();
let promise = createPromise(reqBidsConfigObj, greenbidsId);
promise.then(callback);
}

function createPromise(reqBidsConfigObj) {
function createPromise(reqBidsConfigObj, greenbidsId) {
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
resolve(reqBidsConfigObj);
Expand All @@ -40,32 +47,43 @@ function createPromise(reqBidsConfigObj) {
ENDPOINT,
{
success: (response) => {
processSuccessResponse(response, timeoutId, reqBidsConfigObj);
processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId);
resolve(reqBidsConfigObj);
},
error: () => {
clearTimeout(timeoutId);
resolve(reqBidsConfigObj);
},
},
createPayload(reqBidsConfigObj),
{ contentType: 'application/json' }
createPayload(reqBidsConfigObj, greenbidsId),
{
contentType: 'application/json',
customHeaders: {
'Greenbids-Pbuid': rtdOptions.pbuid
}
}
);
});
}

function processSuccessResponse(response, timeoutId, reqBidsConfigObj) {
function processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId) {
clearTimeout(timeoutId);
const responseAdUnits = JSON.parse(response);

updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits);
updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits, greenbidsId);
}

function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits) {
function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits, greenbidsId) {
adUnits.forEach((adUnit) => {
const matchingAdUnit = findMatchingAdUnit(responseAdUnits, adUnit.code);
if (matchingAdUnit) {
removeFalseBidders(adUnit, matchingAdUnit);
deepSetValue(adUnit, 'ortb2Imp.ext.greenbids', {
greenbidsId: greenbidsId,
keptInAuction: matchingAdUnit.bidders,
isExploration: matchingAdUnit.isExploration
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, could end up being very valuable information for analytics

if (!matchingAdUnit.isExploration) {
removeFalseBidders(adUnit, matchingAdUnit);
}
}
});
}
Expand All @@ -85,14 +103,24 @@ function getFalseBidders(bidders) {
.map(([bidder]) => bidder);
}

function createPayload(reqBidsConfigObj) {
function stripAdUnits(adUnits) {
const stripedAdUnits = deepClone(adUnits);
return stripedAdUnits.map(adUnit => {
adUnit.bids = adUnit.bids.map(bid => {
return { bidder: bid.bidder };
});
return adUnit;
});
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this gets sent to your server, and you just only want bidder name.

Obviously I have no idea what your server is doing, but you could just return bids as array of strings instead of array of objects with one bidder key. Save some space.

Also, there may be quite a lot of other stuff on the adUnit, do you want all of that in your strippedAdUnits also? Like ORTB2Imp stuff, mediaTypes, etc?

Copy link
Contributor Author

@jbogp jbogp Dec 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting point for the array of strings structure I think we'll keep it that way for the moment with further optimizations coming down the line (like returning only the bidders to filter form the server for instance and not every bidder involved).

the ortb2Imp and mediaTypes are necessary though as we extract a lot of prediction features from that (bidder don't behave the same on a video only ad unit or a banner+video adunit), we might strip some useless parts in the futur but I'd rather keep everything for now.


function createPayload(reqBidsConfigObj, greenbidsId) {
return JSON.stringify({
auctionId: auctionInfo.auctionId,
version: MODULE_VERSION,
...rtdOptions,
referrer: window.location.href,
prebid: '$prebid.version$',
rtdOptions: rtdOptions,
adUnits: reqBidsConfigObj.adUnits,
greenbidsId: greenbidsId,
adUnits: stripAdUnits(reqBidsConfigObj.adUnits),
});
}

Expand All @@ -105,6 +133,7 @@ export const greenbidsSubmodule = {
findMatchingAdUnit: findMatchingAdUnit,
removeFalseBidders: removeFalseBidders,
getFalseBidders: getFalseBidders,
stripAdUnits: stripAdUnits,
};

submodule('realTimeData', greenbidsSubmodule);
2 changes: 1 addition & 1 deletion modules/greenbidsRtdProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

```
Module Name: Greenbids RTD Provider
Module Version: 2.0.0
Module Type: RTD Provider
Maintainer: jb@greenbids.ai
```
Expand All @@ -21,7 +22,6 @@ This module is configured as part of the `realTimeData.dataProviders` object.
| `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
Expand Down
Loading