Skip to content

Commit

Permalink
SupplyChain object support in Prebid (#4084)
Browse files Browse the repository at this point in the history
* moving dctr related code in a function

* moving parsedRequest variable out of the loop

and moving GDPR related block at bottom

* added a todo comment

* exporting hasOwn function

* added functionality to pass schain object

- adapter manager will validate schain object
- if it is valid then only it can be passed on to all bidders
- bidders do not need to validate again

* changed logMessage to logError

- also fixed isInteger check

* Moved schain related code from adapaterManager.js to schain.js

* indentation chnages

* logical bug fix

* added test cases for schain

* PubMatic: pass schain object in request

* indentation

* unit test for PubMatic schain support

* using isInteger from utils

* moved schain as a module

* indentation

* removed commented code

* added try-catch as the statement code was breaking CI for IE-11

* Revert "added try-catch as the statement code was breaking CI for IE-11"

This reverts commit 88f495f.

* added a try-catch for a staement as it was breaking CI sometimes

* added schain.md for schain module

* added a few links

* fixed typos

* simplified the approach in adpater code

* trying to restart CI

* Revert "trying to restart CI"

This reverts commit 25f877c.

* adding support in prebidServerBidAdpater as well

* bug fix

* minor changes

- moved consts out of function
- added a error log on finding an invalid schain object

* modified a comment

* added name to a test case

* Revert "added a try-catch for a staement as it was breaking CI sometimes"

This reverts commit e9606bf.

* moving schain validation logic inside PM adapter

* Revert "moving schain validation logic inside PM adapter"

This reverts commit 31d00d5.

* added validation mode: strict, relaxed, off

* updated documentation

* moved a comment

* changed value in example
  • Loading branch information
pm-harshad-mane authored and Isaac A. Dettman committed Aug 30, 2019
1 parent 87e84b8 commit a7ad5ef
Show file tree
Hide file tree
Showing 8 changed files with 575 additions and 41 deletions.
8 changes: 8 additions & 0 deletions modules/prebidServerBidAdapter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,14 @@ const OPEN_RTB_PROTOCOL = {
utils.deepSetValue(request, 'user.ext.digitrust', digiTrust);
}

// pass schain object if it is present
const schain = utils.deepAccess(bidRequests, '0.bids.0.schain');
if (schain) {
request.source.ext = {
schain: schain
};
}

if (!utils.isEmpty(aliases)) {
request.ext.prebid.aliases = aliases;
}
Expand Down
92 changes: 53 additions & 39 deletions modules/pubmaticBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,38 @@ function _blockedIabCategoriesValidation(payload, blockedIabCategories) {
}
}

function _handleDealCustomTargetings(payload, dctrArr, validBidRequests) {
var dctr = '';
var dctrLen;
// set dctr value in site.ext, if present in validBidRequests[0], else ignore
if (dctrArr.length > 0) {
if (validBidRequests[0].params.hasOwnProperty('dctr')) {
dctr = validBidRequests[0].params.dctr;
if (utils.isStr(dctr) && dctr.length > 0) {
var arr = dctr.split('|');
dctr = '';
arr.forEach(val => {
dctr += (val.length > 0) ? (val.trim() + '|') : '';
});
dctrLen = dctr.length;
if (dctr.substring(dctrLen, dctrLen - 1) === '|') {
dctr = dctr.substring(0, dctrLen - 1);
}
payload.site.ext = {
key_val: dctr.trim()
}
} else {
utils.logWarn(LOG_WARN_PREFIX + 'Ignoring param : dctr with value : ' + dctr + ', expects string-value, found empty or non-string value');
}
if (dctrArr.length > 1) {
utils.logWarn(LOG_WARN_PREFIX + 'dctr value found in more than 1 adunits. Value from 1st adunit will be picked. Ignoring values from subsequent adunits');
}
} else {
utils.logWarn(LOG_WARN_PREFIX + 'dctr value not found in 1st adunit, ignoring values from subsequent adunits');
}
}
}

export const spec = {
code: BIDDER_CODE,
supportedMediaTypes: [BANNER, VIDEO, NATIVE],
Expand Down Expand Up @@ -779,11 +811,10 @@ export const spec = {
var conf = _initConf(refererInfo);
var payload = _createOrtbTemplate(conf);
var bidCurrency = '';
var dctr = '';
var dctrLen;
var dctrArr = [];
var bid;
var blockedIabCategories = [];

validBidRequests.forEach(originalBid => {
bid = utils.deepClone(originalBid);
bid.params.adSlot = bid.params.adSlot || '';
Expand Down Expand Up @@ -835,6 +866,21 @@ export const spec = {
payload.ext.wrapper.wp = 'pbjs';
payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED);
payload.user.geo = {};
payload.user.geo.lat = _parseSlotParam('lat', conf.lat);
payload.user.geo.lon = _parseSlotParam('lon', conf.lon);
payload.user.yob = _parseSlotParam('yob', conf.yob);
payload.device.geo = payload.user.geo;
payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim();
payload.site.domain = _getDomainFromURL(payload.site.page);

// adding schain object
if (validBidRequests[0].schain) {
payload.source = {
ext: {
schain: validBidRequests[0].schain
}
};
}

// Attaching GDPR Consent Params
if (bidderRequest && bidderRequest.gdprConsent) {
Expand All @@ -849,43 +895,10 @@ export const spec = {
};
}

payload.user.geo.lat = _parseSlotParam('lat', conf.lat);
payload.user.geo.lon = _parseSlotParam('lon', conf.lon);
payload.user.yob = _parseSlotParam('yob', conf.yob);
payload.device.geo = payload.user.geo;
payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim();
payload.site.domain = _getDomainFromURL(payload.site.page);

// set dctr value in site.ext, if present in validBidRequests[0], else ignore
if (dctrArr.length > 0) {
if (validBidRequests[0].params.hasOwnProperty('dctr')) {
dctr = validBidRequests[0].params.dctr;
if (utils.isStr(dctr) && dctr.length > 0) {
var arr = dctr.split('|');
dctr = '';
arr.forEach(val => {
dctr += (val.length > 0) ? (val.trim() + '|') : '';
});
dctrLen = dctr.length;
if (dctr.substring(dctrLen, dctrLen - 1) === '|') {
dctr = dctr.substring(0, dctrLen - 1);
}
payload.site.ext = {
key_val: dctr.trim()
}
} else {
utils.logWarn(LOG_WARN_PREFIX + 'Ignoring param : dctr with value : ' + dctr + ', expects string-value, found empty or non-string value');
}
if (dctrArr.length > 1) {
utils.logWarn(LOG_WARN_PREFIX + 'dctr value found in more than 1 adunits. Value from 1st adunit will be picked. Ignoring values from subsequent adunits');
}
} else {
utils.logWarn(LOG_WARN_PREFIX + 'dctr value not found in 1st adunit, ignoring values from subsequent adunits');
}
}

_handleDealCustomTargetings(payload, dctrArr, validBidRequests);
_handleEids(payload, validBidRequests);
_blockedIabCategoriesValidation(payload, blockedIabCategories);

return {
method: 'POST',
url: ENDPOINT,
Expand All @@ -902,6 +915,8 @@ export const spec = {
interpretResponse: (response, request) => {
const bidResponses = [];
var respCur = DEFAULT_CURRENCY;
let parsedRequest = JSON.parse(request.data);
let parsedReferrer = parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : '';
try {
if (response.body && response.body.seatbid && utils.isArray(response.body.seatbid)) {
// Supporting multiple bid responses for same adSize
Expand All @@ -910,7 +925,6 @@ export const spec = {
seatbidder.bid &&
utils.isArray(seatbidder.bid) &&
seatbidder.bid.forEach(bid => {
let parsedRequest = JSON.parse(request.data);
let newBid = {
requestId: bid.impid,
cpm: (parseFloat(bid.price) || 0).toFixed(2),
Expand All @@ -921,7 +935,7 @@ export const spec = {
currency: respCur,
netRevenue: NET_REVENUE,
ttl: 300,
referrer: parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : '',
referrer: parsedReferrer,
ad: bid.adm
};
if (parsedRequest.imp && parsedRequest.imp.length > 0) {
Expand Down
147 changes: 147 additions & 0 deletions modules/schain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {config} from '../src/config';
import {getGlobal} from '../src/prebidGlobal';
import { isNumber, isStr, isArray, isPlainObject, hasOwn, logError, isInteger } from '../src/utils';

// https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/supplychainobject.md

const schainErrorPrefix = 'Invalid schain object found: ';
const shouldBeAString = ' should be a string';
const shouldBeAnInteger = ' should be an Integer';
const shouldBeAnObject = ' should be an object';
const shouldBeAnArray = ' should be an Array';
const MODE = {
STRICT: 'strict',
RELAXED: 'relaxed',
OFF: 'off'
};

// validate the supply chain object
export function isSchainObjectValid(schainObject, returnOnError) {
if (!isPlainObject(schainObject)) {
logError(schainErrorPrefix + `schain` + shouldBeAnObject);
if (returnOnError) return false;
}

// complete: Integer
if (!isNumber(schainObject.complete) || !isInteger(schainObject.complete)) {
logError(schainErrorPrefix + `schain.complete` + shouldBeAnInteger);
if (returnOnError) return false;
}

// ver: String
if (!isStr(schainObject.ver)) {
logError(schainErrorPrefix + `schain.ver` + shouldBeAString);
if (returnOnError) return false;
}

// ext: Object [optional]
if (hasOwn(schainObject, 'ext')) {
if (!isPlainObject(schainObject.ext)) {
logError(schainErrorPrefix + `schain.ext` + shouldBeAnObject);
if (returnOnError) return false;
}
}

// nodes: Array of objects
if (!isArray(schainObject.nodes)) {
logError(schainErrorPrefix + `schain.nodes` + shouldBeAnArray);
if (returnOnError) return false;
}

// now validate each node
let isEachNodeIsValid = true;
schainObject.nodes.forEach(node => {
// asi: String
if (!isStr(node.asi)) {
isEachNodeIsValid = isEachNodeIsValid && false;
logError(schainErrorPrefix + `schain.nodes[].asi` + shouldBeAString);
}

// sid: String
if (!isStr(node.sid)) {
isEachNodeIsValid = isEachNodeIsValid && false;
logError(schainErrorPrefix + `schain.nodes[].sid` + shouldBeAString);
}

// hp: Integer
if (!isNumber(node.hp) || !isInteger(node.hp)) {
isEachNodeIsValid = isEachNodeIsValid && false;
logError(schainErrorPrefix + `schain.nodes[].hp` + shouldBeAnInteger);
}

// rid: String [Optional]
if (hasOwn(node, 'rid')) {
if (!isStr(node.rid)) {
isEachNodeIsValid = isEachNodeIsValid && false;
logError(schainErrorPrefix + `schain.nodes[].rid` + shouldBeAString);
}
}

// name: String [Optional]
if (hasOwn(node, 'name')) {
if (!isStr(node.name)) {
isEachNodeIsValid = isEachNodeIsValid && false;
logError(schainErrorPrefix + `schain.nodes[].name` + shouldBeAString);
}
}

// domain: String [Optional]
if (hasOwn(node, 'domain')) {
if (!isStr(node.domain)) {
isEachNodeIsValid = isEachNodeIsValid && false;
logError(schainErrorPrefix + `schain.nodes[].domain` + shouldBeAString);
}
}

// ext: Object [Optional]
if (hasOwn(node, 'ext')) {
if (!isPlainObject(node.ext)) {
isEachNodeIsValid = isEachNodeIsValid && false;
logError(schainErrorPrefix + `schain.nodes[].ext` + shouldBeAnObject);
}
}
});

if (returnOnError && !isEachNodeIsValid) {
return false;
}

return true;
}

export function copySchainObjectInAdunits(adUnits, schainObject) {
// copy schain object in all adUnits as adUnits[].bid.schain
adUnits.forEach(adUnit => {
adUnit.bids.forEach(bid => {
bid.schain = schainObject;
});
});
}

export function init(config) {
let mode = MODE.STRICT;
getGlobal().requestBids.before(function(fn, reqBidsConfigObj) {
let schainObject = config.getConfig('schain');
if (!isPlainObject(schainObject)) {
logError(schainErrorPrefix + 'schain config will not be passed to bidders as schain is not an object.');
} else {
if (isStr(schainObject.validation) && Object.values(MODE).indexOf(schainObject.validation) != -1) {
mode = schainObject.validation;
}
if (mode === MODE.OFF) {
// no need to validate
copySchainObjectInAdunits(reqBidsConfigObj.adUnits || getGlobal().adUnits, schainObject.config);
} else {
if (isSchainObjectValid(schainObject.config, mode === MODE.STRICT)) {
copySchainObjectInAdunits(reqBidsConfigObj.adUnits || getGlobal().adUnits, schainObject.config);
} else {
logError(schainErrorPrefix + 'schain config will not be passed to bidders as it is not valid.');
}
}
}
// calling fn allows prebid to continue processing
return fn.call(this, reqBidsConfigObj);
}, 40);
}

init(config)
50 changes: 50 additions & 0 deletions modules/schain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# schain module

Aggregators who manage Prebid wrappers on behalf of multiple publishers need to declare their intermediary status in the Supply Chain Object.
As the spec prohibits us from adding upstream intermediaries, Prebid requests in this case need to come with the schain information.
In this use case, it's seems cumbersome to have every bidder in the wrapper separately configured the same schain information.

Refer:
- https://iabtechlab.com/sellers-json/
- https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/supplychainobject.md

## Sample code for passing the schain object
```
pbjs.setConfig( {
"schain":
"validation": "strict",
"config": {
"ver":"1.0",
"complete": 1,
"nodes": [
{
"asi":"indirectseller.com",
"sid":"00001",
"hp":1
},
{
"asi":"indirectseller-2.com",
"sid":"00002",
"hp":0
},
]
}
}
});
```

## Workflow
The schain module is not enabled by default as it may not be neccessary for all publishers.
If required, schain module can be included as following
```
$ gulp build --modules=schain,pubmaticBidAdapter,openxBidAdapter,rubiconBidAdapter,sovrnBidAdapter
```
The schain module will validate the schain object passed using pbjs.setConfig API.
If the schain object is valid then it will be passed on to bidders/adapters in ```validBidRequests[].schain```
You may refer pubmaticBidAdapter implementaion for the same.

## Validation modes
- ```strict```: It is the default validation mode. In this mode, schain object will not be passed to adapters if it is invalid. Errors are thrown for invalid schain object.
- ```relaxed```: In this mode, errors are thrown for an invalid schain object but the invalid schain object is still passed to adapters.
- ```off```: In this mode, no validations are performed and schain object is passed as is to adapters.
2 changes: 1 addition & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ export function _map(object, callback) {
return output;
}

var hasOwn = function (objectToCheck, propertyToCheckFor) {
export function hasOwn(objectToCheck, propertyToCheckFor) {
if (objectToCheck.hasOwnProperty) {
return objectToCheck.hasOwnProperty(propertyToCheckFor);
} else {
Expand Down
Loading

0 comments on commit a7ad5ef

Please sign in to comment.