Skip to content

Commit a7ad5ef

Browse files
pm-harshad-maneIsaac A. Dettman
authored andcommitted
SupplyChain object support in Prebid (#4084)
* 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
1 parent 87e84b8 commit a7ad5ef

File tree

8 files changed

+575
-41
lines changed

8 files changed

+575
-41
lines changed

modules/prebidServerBidAdapter/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,14 @@ const OPEN_RTB_PROTOCOL = {
685685
utils.deepSetValue(request, 'user.ext.digitrust', digiTrust);
686686
}
687687

688+
// pass schain object if it is present
689+
const schain = utils.deepAccess(bidRequests, '0.bids.0.schain');
690+
if (schain) {
691+
request.source.ext = {
692+
schain: schain
693+
};
694+
}
695+
688696
if (!utils.isEmpty(aliases)) {
689697
request.ext.prebid.aliases = aliases;
690698
}

modules/pubmaticBidAdapter.js

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,38 @@ function _blockedIabCategoriesValidation(payload, blockedIabCategories) {
738738
}
739739
}
740740

741+
function _handleDealCustomTargetings(payload, dctrArr, validBidRequests) {
742+
var dctr = '';
743+
var dctrLen;
744+
// set dctr value in site.ext, if present in validBidRequests[0], else ignore
745+
if (dctrArr.length > 0) {
746+
if (validBidRequests[0].params.hasOwnProperty('dctr')) {
747+
dctr = validBidRequests[0].params.dctr;
748+
if (utils.isStr(dctr) && dctr.length > 0) {
749+
var arr = dctr.split('|');
750+
dctr = '';
751+
arr.forEach(val => {
752+
dctr += (val.length > 0) ? (val.trim() + '|') : '';
753+
});
754+
dctrLen = dctr.length;
755+
if (dctr.substring(dctrLen, dctrLen - 1) === '|') {
756+
dctr = dctr.substring(0, dctrLen - 1);
757+
}
758+
payload.site.ext = {
759+
key_val: dctr.trim()
760+
}
761+
} else {
762+
utils.logWarn(LOG_WARN_PREFIX + 'Ignoring param : dctr with value : ' + dctr + ', expects string-value, found empty or non-string value');
763+
}
764+
if (dctrArr.length > 1) {
765+
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');
766+
}
767+
} else {
768+
utils.logWarn(LOG_WARN_PREFIX + 'dctr value not found in 1st adunit, ignoring values from subsequent adunits');
769+
}
770+
}
771+
}
772+
741773
export const spec = {
742774
code: BIDDER_CODE,
743775
supportedMediaTypes: [BANNER, VIDEO, NATIVE],
@@ -779,11 +811,10 @@ export const spec = {
779811
var conf = _initConf(refererInfo);
780812
var payload = _createOrtbTemplate(conf);
781813
var bidCurrency = '';
782-
var dctr = '';
783-
var dctrLen;
784814
var dctrArr = [];
785815
var bid;
786816
var blockedIabCategories = [];
817+
787818
validBidRequests.forEach(originalBid => {
788819
bid = utils.deepClone(originalBid);
789820
bid.params.adSlot = bid.params.adSlot || '';
@@ -835,6 +866,21 @@ export const spec = {
835866
payload.ext.wrapper.wp = 'pbjs';
836867
payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED);
837868
payload.user.geo = {};
869+
payload.user.geo.lat = _parseSlotParam('lat', conf.lat);
870+
payload.user.geo.lon = _parseSlotParam('lon', conf.lon);
871+
payload.user.yob = _parseSlotParam('yob', conf.yob);
872+
payload.device.geo = payload.user.geo;
873+
payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim();
874+
payload.site.domain = _getDomainFromURL(payload.site.page);
875+
876+
// adding schain object
877+
if (validBidRequests[0].schain) {
878+
payload.source = {
879+
ext: {
880+
schain: validBidRequests[0].schain
881+
}
882+
};
883+
}
838884

839885
// Attaching GDPR Consent Params
840886
if (bidderRequest && bidderRequest.gdprConsent) {
@@ -849,43 +895,10 @@ export const spec = {
849895
};
850896
}
851897

852-
payload.user.geo.lat = _parseSlotParam('lat', conf.lat);
853-
payload.user.geo.lon = _parseSlotParam('lon', conf.lon);
854-
payload.user.yob = _parseSlotParam('yob', conf.yob);
855-
payload.device.geo = payload.user.geo;
856-
payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim();
857-
payload.site.domain = _getDomainFromURL(payload.site.page);
858-
859-
// set dctr value in site.ext, if present in validBidRequests[0], else ignore
860-
if (dctrArr.length > 0) {
861-
if (validBidRequests[0].params.hasOwnProperty('dctr')) {
862-
dctr = validBidRequests[0].params.dctr;
863-
if (utils.isStr(dctr) && dctr.length > 0) {
864-
var arr = dctr.split('|');
865-
dctr = '';
866-
arr.forEach(val => {
867-
dctr += (val.length > 0) ? (val.trim() + '|') : '';
868-
});
869-
dctrLen = dctr.length;
870-
if (dctr.substring(dctrLen, dctrLen - 1) === '|') {
871-
dctr = dctr.substring(0, dctrLen - 1);
872-
}
873-
payload.site.ext = {
874-
key_val: dctr.trim()
875-
}
876-
} else {
877-
utils.logWarn(LOG_WARN_PREFIX + 'Ignoring param : dctr with value : ' + dctr + ', expects string-value, found empty or non-string value');
878-
}
879-
if (dctrArr.length > 1) {
880-
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');
881-
}
882-
} else {
883-
utils.logWarn(LOG_WARN_PREFIX + 'dctr value not found in 1st adunit, ignoring values from subsequent adunits');
884-
}
885-
}
886-
898+
_handleDealCustomTargetings(payload, dctrArr, validBidRequests);
887899
_handleEids(payload, validBidRequests);
888900
_blockedIabCategoriesValidation(payload, blockedIabCategories);
901+
889902
return {
890903
method: 'POST',
891904
url: ENDPOINT,
@@ -902,6 +915,8 @@ export const spec = {
902915
interpretResponse: (response, request) => {
903916
const bidResponses = [];
904917
var respCur = DEFAULT_CURRENCY;
918+
let parsedRequest = JSON.parse(request.data);
919+
let parsedReferrer = parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : '';
905920
try {
906921
if (response.body && response.body.seatbid && utils.isArray(response.body.seatbid)) {
907922
// Supporting multiple bid responses for same adSize
@@ -910,7 +925,6 @@ export const spec = {
910925
seatbidder.bid &&
911926
utils.isArray(seatbidder.bid) &&
912927
seatbidder.bid.forEach(bid => {
913-
let parsedRequest = JSON.parse(request.data);
914928
let newBid = {
915929
requestId: bid.impid,
916930
cpm: (parseFloat(bid.price) || 0).toFixed(2),
@@ -921,7 +935,7 @@ export const spec = {
921935
currency: respCur,
922936
netRevenue: NET_REVENUE,
923937
ttl: 300,
924-
referrer: parsedRequest.site && parsedRequest.site.ref ? parsedRequest.site.ref : '',
938+
referrer: parsedReferrer,
925939
ad: bid.adm
926940
};
927941
if (parsedRequest.imp && parsedRequest.imp.length > 0) {

modules/schain.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import {config} from '../src/config';
2+
import {getGlobal} from '../src/prebidGlobal';
3+
import { isNumber, isStr, isArray, isPlainObject, hasOwn, logError, isInteger } from '../src/utils';
4+
5+
// https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/supplychainobject.md
6+
7+
const schainErrorPrefix = 'Invalid schain object found: ';
8+
const shouldBeAString = ' should be a string';
9+
const shouldBeAnInteger = ' should be an Integer';
10+
const shouldBeAnObject = ' should be an object';
11+
const shouldBeAnArray = ' should be an Array';
12+
const MODE = {
13+
STRICT: 'strict',
14+
RELAXED: 'relaxed',
15+
OFF: 'off'
16+
};
17+
18+
// validate the supply chain object
19+
export function isSchainObjectValid(schainObject, returnOnError) {
20+
if (!isPlainObject(schainObject)) {
21+
logError(schainErrorPrefix + `schain` + shouldBeAnObject);
22+
if (returnOnError) return false;
23+
}
24+
25+
// complete: Integer
26+
if (!isNumber(schainObject.complete) || !isInteger(schainObject.complete)) {
27+
logError(schainErrorPrefix + `schain.complete` + shouldBeAnInteger);
28+
if (returnOnError) return false;
29+
}
30+
31+
// ver: String
32+
if (!isStr(schainObject.ver)) {
33+
logError(schainErrorPrefix + `schain.ver` + shouldBeAString);
34+
if (returnOnError) return false;
35+
}
36+
37+
// ext: Object [optional]
38+
if (hasOwn(schainObject, 'ext')) {
39+
if (!isPlainObject(schainObject.ext)) {
40+
logError(schainErrorPrefix + `schain.ext` + shouldBeAnObject);
41+
if (returnOnError) return false;
42+
}
43+
}
44+
45+
// nodes: Array of objects
46+
if (!isArray(schainObject.nodes)) {
47+
logError(schainErrorPrefix + `schain.nodes` + shouldBeAnArray);
48+
if (returnOnError) return false;
49+
}
50+
51+
// now validate each node
52+
let isEachNodeIsValid = true;
53+
schainObject.nodes.forEach(node => {
54+
// asi: String
55+
if (!isStr(node.asi)) {
56+
isEachNodeIsValid = isEachNodeIsValid && false;
57+
logError(schainErrorPrefix + `schain.nodes[].asi` + shouldBeAString);
58+
}
59+
60+
// sid: String
61+
if (!isStr(node.sid)) {
62+
isEachNodeIsValid = isEachNodeIsValid && false;
63+
logError(schainErrorPrefix + `schain.nodes[].sid` + shouldBeAString);
64+
}
65+
66+
// hp: Integer
67+
if (!isNumber(node.hp) || !isInteger(node.hp)) {
68+
isEachNodeIsValid = isEachNodeIsValid && false;
69+
logError(schainErrorPrefix + `schain.nodes[].hp` + shouldBeAnInteger);
70+
}
71+
72+
// rid: String [Optional]
73+
if (hasOwn(node, 'rid')) {
74+
if (!isStr(node.rid)) {
75+
isEachNodeIsValid = isEachNodeIsValid && false;
76+
logError(schainErrorPrefix + `schain.nodes[].rid` + shouldBeAString);
77+
}
78+
}
79+
80+
// name: String [Optional]
81+
if (hasOwn(node, 'name')) {
82+
if (!isStr(node.name)) {
83+
isEachNodeIsValid = isEachNodeIsValid && false;
84+
logError(schainErrorPrefix + `schain.nodes[].name` + shouldBeAString);
85+
}
86+
}
87+
88+
// domain: String [Optional]
89+
if (hasOwn(node, 'domain')) {
90+
if (!isStr(node.domain)) {
91+
isEachNodeIsValid = isEachNodeIsValid && false;
92+
logError(schainErrorPrefix + `schain.nodes[].domain` + shouldBeAString);
93+
}
94+
}
95+
96+
// ext: Object [Optional]
97+
if (hasOwn(node, 'ext')) {
98+
if (!isPlainObject(node.ext)) {
99+
isEachNodeIsValid = isEachNodeIsValid && false;
100+
logError(schainErrorPrefix + `schain.nodes[].ext` + shouldBeAnObject);
101+
}
102+
}
103+
});
104+
105+
if (returnOnError && !isEachNodeIsValid) {
106+
return false;
107+
}
108+
109+
return true;
110+
}
111+
112+
export function copySchainObjectInAdunits(adUnits, schainObject) {
113+
// copy schain object in all adUnits as adUnits[].bid.schain
114+
adUnits.forEach(adUnit => {
115+
adUnit.bids.forEach(bid => {
116+
bid.schain = schainObject;
117+
});
118+
});
119+
}
120+
121+
export function init(config) {
122+
let mode = MODE.STRICT;
123+
getGlobal().requestBids.before(function(fn, reqBidsConfigObj) {
124+
let schainObject = config.getConfig('schain');
125+
if (!isPlainObject(schainObject)) {
126+
logError(schainErrorPrefix + 'schain config will not be passed to bidders as schain is not an object.');
127+
} else {
128+
if (isStr(schainObject.validation) && Object.values(MODE).indexOf(schainObject.validation) != -1) {
129+
mode = schainObject.validation;
130+
}
131+
if (mode === MODE.OFF) {
132+
// no need to validate
133+
copySchainObjectInAdunits(reqBidsConfigObj.adUnits || getGlobal().adUnits, schainObject.config);
134+
} else {
135+
if (isSchainObjectValid(schainObject.config, mode === MODE.STRICT)) {
136+
copySchainObjectInAdunits(reqBidsConfigObj.adUnits || getGlobal().adUnits, schainObject.config);
137+
} else {
138+
logError(schainErrorPrefix + 'schain config will not be passed to bidders as it is not valid.');
139+
}
140+
}
141+
}
142+
// calling fn allows prebid to continue processing
143+
return fn.call(this, reqBidsConfigObj);
144+
}, 40);
145+
}
146+
147+
init(config)

modules/schain.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# schain module
2+
3+
Aggregators who manage Prebid wrappers on behalf of multiple publishers need to declare their intermediary status in the Supply Chain Object.
4+
As the spec prohibits us from adding upstream intermediaries, Prebid requests in this case need to come with the schain information.
5+
In this use case, it's seems cumbersome to have every bidder in the wrapper separately configured the same schain information.
6+
7+
Refer:
8+
- https://iabtechlab.com/sellers-json/
9+
- https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/supplychainobject.md
10+
11+
## Sample code for passing the schain object
12+
```
13+
pbjs.setConfig( {
14+
"schain":
15+
"validation": "strict",
16+
"config": {
17+
"ver":"1.0",
18+
"complete": 1,
19+
"nodes": [
20+
{
21+
"asi":"indirectseller.com",
22+
"sid":"00001",
23+
"hp":1
24+
},
25+
26+
{
27+
"asi":"indirectseller-2.com",
28+
"sid":"00002",
29+
"hp":0
30+
},
31+
]
32+
}
33+
}
34+
});
35+
```
36+
37+
## Workflow
38+
The schain module is not enabled by default as it may not be neccessary for all publishers.
39+
If required, schain module can be included as following
40+
```
41+
$ gulp build --modules=schain,pubmaticBidAdapter,openxBidAdapter,rubiconBidAdapter,sovrnBidAdapter
42+
```
43+
The schain module will validate the schain object passed using pbjs.setConfig API.
44+
If the schain object is valid then it will be passed on to bidders/adapters in ```validBidRequests[].schain```
45+
You may refer pubmaticBidAdapter implementaion for the same.
46+
47+
## Validation modes
48+
- ```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.
49+
- ```relaxed```: In this mode, errors are thrown for an invalid schain object but the invalid schain object is still passed to adapters.
50+
- ```off```: In this mode, no validations are performed and schain object is passed as is to adapters.

src/utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,7 @@ export function _map(object, callback) {
571571
return output;
572572
}
573573

574-
var hasOwn = function (objectToCheck, propertyToCheckFor) {
574+
export function hasOwn(objectToCheck, propertyToCheckFor) {
575575
if (objectToCheck.hasOwnProperty) {
576576
return objectToCheck.hasOwnProperty(propertyToCheckFor);
577577
} else {

0 commit comments

Comments
 (0)