diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js
index 4d9b95e5948..617ce49f171 100644
--- a/modules/insticatorBidAdapter.js
+++ b/modules/insticatorBidAdapter.js
@@ -1,7 +1,7 @@
import {config} from '../src/config.js';
import {BANNER, VIDEO} from '../src/mediaTypes.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
-import {deepAccess, generateUUID, logError, isArray, isInteger, isArrayOfNums} from '../src/utils.js';
+import {deepAccess, generateUUID, logError, isArray, isInteger, isArrayOfNums, deepSetValue} from '../src/utils.js';
import {getStorageManager} from '../src/storageManager.js';
import {find} from '../src/polyfill.js';
@@ -12,35 +12,37 @@ const USER_ID_COOKIE_EXP = 2592000000; // 30 days
const BID_TTL = 300; // 5 minutes
const GVLID = 910;
-const isSubarray = (arr, target) => {
- if (!isArrayOfNums(arr) || arr.length === 0) {
- return false;
- }
- const targetSet = new Set(target);
- return arr.every(el => targetSet.has(el));
-};
-
export const OPTIONAL_VIDEO_PARAMS = {
'minduration': (value) => isInteger(value),
'maxduration': (value) => isInteger(value),
- 'protocols': (value) => isSubarray(value, [2, 3, 5, 6, 7, 8]), // protocols values supported by Inticator, according to the OpenRTB spec
+ 'protocols': (value) => isArrayOfNums(value), // protocols values supported by Inticator, according to the OpenRTB spec
'startdelay': (value) => isInteger(value),
'linearity': (value) => isInteger(value) && [1].includes(value),
'skip': (value) => isInteger(value) && [1, 0].includes(value),
'skipmin': (value) => isInteger(value),
'skipafter': (value) => isInteger(value),
'sequence': (value) => isInteger(value),
- 'battr': (value) => isSubarray(value, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]),
+ 'battr': (value) => isArrayOfNums(value),
'maxextended': (value) => isInteger(value),
'minbitrate': (value) => isInteger(value),
'maxbitrate': (value) => isInteger(value),
- 'playbackmethod': (value) => isSubarray(value, [1, 2, 3, 4]),
+ 'playbackmethod': (value) => isArrayOfNums(value),
'playbackend': (value) => isInteger(value) && [1, 2, 3].includes(value),
- 'delivery': (value) => isSubarray(value, [1, 2, 3]),
+ 'delivery': (value) => isArrayOfNums(value),
'pos': (value) => isInteger(value) && [0, 1, 2, 3, 4, 5, 6, 7].includes(value),
- 'api': (value) => isSubarray(value, [1, 2, 3, 4, 5, 6, 7]),
+ 'api': (value) => isArrayOfNums(value),
};
+const ORTB_SITE_FIRST_PARTY_DATA = {
+ 'cat': v => Array.isArray(v) && v.every(c => typeof c === 'string'),
+ 'sectioncat': v => Array.isArray(v) && v.every(c => typeof c === 'string'),
+ 'pagecat': v => Array.isArray(v) && v.every(c => typeof c === 'string'),
+ 'search': v => typeof v === 'string',
+ 'mobile': v => isInteger(),
+ 'content': v => typeof v === 'object',
+ 'keywords': v => typeof v === 'string',
+}
+
export const storage = getStorageManager({bidderCode: BIDDER_CODE});
config.setDefaults({
@@ -103,6 +105,7 @@ function buildVideo(bidRequest) {
const placement = deepAccess(bidRequest, 'mediaTypes.video.placement') || 3;
const plcmt = deepAccess(bidRequest, 'mediaTypes.video.plcmt') || undefined;
const playerSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize');
+ const context = deepAccess(bidRequest, 'mediaTypes.video.context');
if (!w && playerSize) {
if (Array.isArray(playerSize[0])) {
@@ -121,17 +124,26 @@ function buildVideo(bidRequest) {
const bidRequestVideo = deepAccess(bidRequest, 'mediaTypes.video');
const videoBidderParams = deepAccess(bidRequest, 'params.video', {});
+
let optionalParams = {};
for (const param in OPTIONAL_VIDEO_PARAMS) {
- if (bidRequestVideo[param]) {
+ if (bidRequestVideo[param] && OPTIONAL_VIDEO_PARAMS[param](bidRequestVideo[param])) {
optionalParams[param] = bidRequestVideo[param];
}
+ // remove invalid optional params from bidder specific overrides
+ if (videoBidderParams[param] && !OPTIONAL_VIDEO_PARAMS[param](videoBidderParams[param])) {
+ delete videoBidderParams[param];
+ }
}
if (plcmt) {
optionalParams['plcmt'] = plcmt;
}
+ if (context !== undefined) {
+ optionalParams['context'] = context;
+ }
+
let videoObj = {
placement,
mimes,
@@ -190,31 +202,102 @@ function buildDevice(bidRequest) {
return device;
}
+function _getCoppa(bidderRequest) {
+ const coppa = deepAccess(bidderRequest, 'ortb2.regs.coppa');
+
+ // If coppa is defined in the request, use it
+ if (coppa !== undefined) {
+ return coppa;
+ }
+ return config.getConfig('coppa') === true ? 1 : 0;
+}
+
+function _getGppConsent(bidderRequest) {
+ let gpp = deepAccess(bidderRequest, 'gppConsent.gppString')
+ let gppSid = deepAccess(bidderRequest, 'gppConsent.applicableSections')
+
+ if (!gpp || !gppSid) {
+ gpp = deepAccess(bidderRequest, 'ortb2.regs.gpp', '')
+ gppSid = deepAccess(bidderRequest, 'ortb2.regs.gpp_sid', [])
+ }
+ return { gpp, gppSid }
+}
+
+function _getUspConsent(bidderRequest) {
+ return (deepAccess(bidderRequest, 'uspConsent')) ? { uspConsent: bidderRequest.uspConsent } : false;
+}
+
function buildRegs(bidderRequest) {
+ let regs = {
+ ext: {},
+ };
if (bidderRequest.gdprConsent) {
- return {
- ext: {
- gdpr: bidderRequest.gdprConsent.gdprApplies ? 1 : 0,
- gdprConsentString: bidderRequest.gdprConsent.consentString,
- },
- };
+ regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0;
+ regs.ext.gdprConsentString = bidderRequest.gdprConsent.consentString;
+ }
+
+ regs.coppa = _getCoppa(bidderRequest);
+
+ const { gpp, gppSid } = _getGppConsent(bidderRequest);
+
+ if (gpp) {
+ regs.ext.gpp = gpp;
+ }
+
+ if (gppSid) {
+ regs.ext.gppSid = gppSid;
+ }
+
+ const usp = _getUspConsent(bidderRequest);
+
+ if (usp) {
+ regs.ext.us_privacy = usp.uspConsent;
+ regs.ext.ccpa = usp.uspConsent
+ }
+
+ const dsa = deepAccess(bidderRequest, 'ortb2.regs.ext.dsa');
+ if (dsa) {
+ regs.ext.dsa = dsa;
}
- return {};
+ return regs;
}
function buildUser(bid) {
const userId = getUserId() || generateUUID();
const yob = deepAccess(bid, 'params.user.yob')
const gender = deepAccess(bid, 'params.user.gender')
+ const keywords = deepAccess(bid, 'params.user.keywords')
+ const data = deepAccess(bid, 'params.user.data')
+ const ext = deepAccess(bid, 'params.user.ext')
setUserId(userId);
- return {
+ const userData = {
id: userId,
- yob,
- gender,
- };
+ }
+
+ if (yob) {
+ userData.yob = yob;
+ }
+
+ if (gender) {
+ userData.gender = gender;
+ }
+
+ if (keywords) {
+ userData.keywords = keywords;
+ }
+
+ if (data) {
+ userData.data = data;
+ }
+
+ if (ext) {
+ userData.ext = ext;
+ }
+
+ return userData
}
function extractSchain(bids, requestId) {
@@ -283,6 +366,20 @@ function buildRequest(validBidRequests, bidderRequest) {
req.user.ext = { eids };
}
+ const ortb2SiteData = deepAccess(bidderRequest, 'ortb2.site');
+ if (ortb2SiteData) {
+ for (const key in ORTB_SITE_FIRST_PARTY_DATA) {
+ const value = ortb2SiteData[key];
+ if (value && ORTB_SITE_FIRST_PARTY_DATA[key](value)) {
+ req.site[key] = value;
+ }
+ }
+ }
+
+ if (bidderRequest.gdprConsent) {
+ deepSetValue(req, 'user.ext.consent', bidderRequest.gdprConsent.consentString);
+ }
+
return req;
}
@@ -326,6 +423,13 @@ function buildBid(bid, bidderRequest) {
bidResponse.vastUrl = 'data:text/xml;charset=utf-8;base64,' + window.btoa(bidResponse.vastXml.replace(/\\"/g, '"'));
}
+ if (bid.ext && bid.ext.dsa) {
+ bidResponse.ext = {
+ ...bidResponse.ext,
+ dsa: bid.ext.dsa,
+ }
+ }
+
return bidResponse;
}
@@ -453,7 +557,6 @@ function validateVideo(bid) {
if (video[param]) {
if (!OPTIONAL_VIDEO_PARAMS[param](video[param])) {
logError(`insticator: video ${param} is invalid or not supported by insticator`);
- return false
}
}
}
@@ -485,6 +588,13 @@ export const spec = {
let endpointUrl = config.getConfig('insticator.endpointUrl') || ENDPOINT;
endpointUrl = endpointUrl.replace(/^http:/, 'https:');
+ // Use the first bid request's bid_request_url if it exists ( for updating server url)
+ if (validBidRequests.length > 0) {
+ if (deepAccess(validBidRequests[0], 'params.bid_endpoint_request_url')) {
+ endpointUrl = deepAccess(validBidRequests[0], 'params.bid_endpoint_request_url').replace(/^http:/, 'https:');
+ }
+ }
+
if (validBidRequests.length > 0) {
requests.push({
method: 'POST',
diff --git a/test/spec/modules/insticatorBidAdapter_spec.js b/test/spec/modules/insticatorBidAdapter_spec.js
index 86f96834547..5e41cd6d7aa 100644
--- a/test/spec/modules/insticatorBidAdapter_spec.js
+++ b/test/spec/modules/insticatorBidAdapter_spec.js
@@ -75,9 +75,10 @@ describe('InsticatorBidAdapter', function () {
ortb2: {
source: {
tid: '74f78609-a92d-4cf1-869f-1b244bbfb5d2',
- }
+ },
},
timeout: 300,
+ gdprApplies: 1,
gdprConsent: {
consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==',
vendorData: {},
@@ -261,25 +262,6 @@ describe('InsticatorBidAdapter', function () {
})).to.be.true;
});
- it('should return false if optional video fields are not valid', () => {
- expect(spec.isBidRequestValid({
- ...bidRequest,
- ...{
- mediaTypes: {
- video: {
- mimes: [
- 'video/mp4',
- 'video/mpeg',
- ],
- playerSize: [250, 300],
- placement: 1,
- startdelay: 'NaN',
- },
- }
- }
- })).to.be.false;
- });
-
it('should return false if video min duration > max duration', () => {
expect(spec.isBidRequestValid({
...bidRequest,
@@ -497,11 +479,58 @@ describe('InsticatorBidAdapter', function () {
expect(data.user.id).to.equal(USER_ID_STUBBED);
});
- it('should return empty regs object if no gdprConsent is passed', function () {
+
+ it('should return with coppa regs object if no gdprConsent is passed', function () {
const requests = spec.buildRequests([bidRequest], { ...bidderRequest, ...{ gdprConsent: false } });
const data = JSON.parse(requests[0].data);
- expect(data.regs).to.be.an('object').that.is.empty;
+ expect(data.regs).to.be.an('object');
+ expect(data.regs.coppa).to.be.oneOf([0, 1]);
});
+
+ it('should return with us_privacy string if uspConsent is passed', function () {
+ const requests = spec.buildRequests([bidRequest], { ...bidderRequest, ...{ uspConsent: '1YNN' } });
+ const data = JSON.parse(requests[0].data);
+ expect(data.regs).to.be.an('object');
+ expect(data.regs.ext).to.be.an('object');
+ expect(data.regs.ext.us_privacy).to.equal('1YNN');
+ expect(data.regs.ext.ccpa).to.equal('1YNN');
+ });
+
+ it('should return with gpp if gppConsent is passed', function () {
+ const requests = spec.buildRequests([bidRequest], { ...bidderRequest, ...{ gppConsent: { gppString: '1YNN', applicableSections: ['1', '2'] } } });
+ const data = JSON.parse(requests[0].data);
+ expect(data.regs).to.be.an('object');
+ expect(data.regs.ext).to.be.an('object');
+ expect(data.regs.ext.gppSid).to.deep.equal(['1', '2']);
+ });
+
+ it('should create the request with dsa data and return with dsa object', function() {
+ const dsa = {
+ dsarequired: 2,
+ pubrender: 1,
+ datatopub: 2,
+ transparency: [{
+ domain: 'google.com',
+ dsaparams: [1, 2]
+ }]
+ }
+ const bidRequestWithDsa = {
+ ...bidderRequest,
+ ortb2: {
+ regs: {
+ ext: {
+ dsa: dsa
+ }
+ }
+ }
+ }
+ const requests = spec.buildRequests([bidRequest], {...bidRequestWithDsa});
+ const data = JSON.parse(requests[0].data);
+ expect(data.regs).to.be.an('object');
+ expect(data.regs.ext).to.be.an('object');
+ expect(data.regs.ext.dsa).to.deep.equal(dsa);
+ });
+
it('should return empty array if no valid requests are passed', function () {
expect(spec.buildRequests([], bidderRequest)).to.be.an('array').that.have.lengthOf(0);
});
@@ -539,6 +568,129 @@ describe('InsticatorBidAdapter', function () {
expect(data.imp[0].video.w).to.equal(640);
expect(data.imp[0].video.h).to.equal(480);
});
+
+ it('should have sites first party data if present in bidderRequest ortb2', function () {
+ bidderRequest = {
+ ...bidderRequest,
+ ortb2: {
+ ...bidderRequest.ortb2,
+ site: {
+ keywords: 'keyword1,keyword2',
+ search: 'search',
+ content: {
+ title: 'title',
+ keywords: 'keyword3,keyword4',
+ genre: 'rock'
+ },
+ cat: ['IAB1', 'IAB2']
+ }
+ }
+ }
+ const requests = spec.buildRequests([bidRequest], bidderRequest);
+ const data = JSON.parse(requests[0].data);
+ expect(data).to.have.property('site');
+ expect(data.site).to.have.property('keywords');
+ expect(data.site.keywords).to.equal('keyword1,keyword2');
+ expect(data.site).to.have.property('search');
+ expect(data.site.search).to.equal('search');
+ expect(data.site).to.have.property('content');
+ expect(data.site.content).to.have.property('title');
+ expect(data.site.content.title).to.equal('title');
+ expect(data.site.content).to.have.property('keywords');
+ expect(data.site.content.keywords).to.equal('keyword3,keyword4');
+ expect(data.site.content).to.have.property('genre');
+ expect(data.site.content.genre).to.equal('rock');
+ expect(data.site).to.have.property('cat');
+ expect(data.site.cat).to.deep.equal(['IAB1', 'IAB2']);
+ });
+
+ it('should have device.sua if present in bidderRequest ortb2', function () {
+ bidderRequest = {
+ ...bidderRequest,
+ ortb2: {
+ ...bidderRequest.ortb2,
+ device: {
+ ...bidderRequest.ortb2.device,
+ sua: {}
+ }
+ }
+ }
+ const requests = spec.buildRequests([bidRequest], bidderRequest);
+ const data = JSON.parse(requests[0].data);
+ expect(data).to.have.property('device');
+ expect(data.device).to.have.property('sua');
+ })
+
+ it('should use param bid_endpoint_request_url for request endpoint if present', function () {
+ const tempBiddRequest = {
+ ...bidRequest,
+ params: {
+ ...bidRequest.params,
+ bid_endpoint_request_url: 'https://example.com'
+ }
+ }
+ const requests = spec.buildRequests([tempBiddRequest], bidderRequest);
+ expect(requests[0].url).to.equal('https://example.com');
+ });
+
+ it('should have user keywords if present in bidrequest', function () {
+ const tempBiddRequest = {
+ ...bidRequest,
+ params: {
+ ...bidRequest.params,
+ user: {
+ keywords: 'keyword1,keyword2'
+ }
+ }
+ }
+ const requests = spec.buildRequests([tempBiddRequest], bidderRequest);
+ const data = JSON.parse(requests[0].data);
+ expect(data.user).to.have.property('keywords');
+ expect(data.user.keywords).to.equal('keyword1,keyword2');
+ });
+
+ it('should remove video params if they are invalid', function () {
+ const tempBiddRequest = {
+ ...bidRequest,
+ mediaTypes: {
+ ...bidRequest.mediaTypes,
+ video: {
+ mimes: [
+ 'video/mp4',
+ 'video/mpeg',
+ 'video/x-flv',
+ 'video/webm',
+ 'video/ogg',
+ ],
+ protocols: 'NaN',
+ w: '300',
+ h: '250',
+ }
+ }
+ }
+ const requests = spec.buildRequests([tempBiddRequest], bidderRequest);
+ const data = JSON.parse(requests[0].data);
+ expect(data.imp[0].video).to.not.have.property('plcmt');
+ });
+
+ it('should have user consent and gdpr string if gdprConsent is passed', function () {
+ const requests = spec.buildRequests([bidRequest], bidderRequest);
+ const data = JSON.parse(requests[0].data);
+ expect(data.regs).to.be.an('object');
+ expect(data.regs.ext).to.be.an('object');
+ expect(data.regs.ext.gdpr).to.equal(1);
+ expect(data.regs.ext.gdprConsentString).to.equal(bidderRequest.gdprConsent.consentString);
+ expect(data.user.ext).to.have.property('consent');
+ expect(data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString);
+ });
+
+ it('should have one or more privacy policies if present in bidrequest, like gpp, gdpr and us_privacy', function () {
+ const requests = spec.buildRequests([bidRequest], { ...bidderRequest, ...{ uspConsent: '1YNN' } });
+ const data = JSON.parse(requests[0].data);
+ expect(data.regs.ext).to.have.property('gdpr');
+ expect(data.regs.ext).to.have.property('us_privacy');
+ expect(data.regs.ext).to.have.property('gppSid');
+ });
});
describe('interpretResponse', function () {
@@ -837,4 +989,105 @@ describe('InsticatorBidAdapter', function () {
expect(bidResponse.vastUrl).to.match(/^data:text\/xml;charset=utf-8;base64,[\w+/=]+$/)
});
})
+
+ describe(`Response with DSA data`, function() {
+ const bidRequestDsa = {
+ method: 'POST',
+ url: 'https://ex.ingage.tech/v1/openrtb',
+ options: {
+ contentType: 'application/json',
+ withCredentials: true,
+ },
+ data: '',
+ bidderRequest: {
+ bidderRequestId: '22edbae2733bf6',
+ auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2',
+ timeout: 300,
+ bids: [
+ {
+ bidder: 'insticator',
+ params: {
+ adUnitId: '1a2b3c4d5e6f1a2b3c4d'
+ },
+ adUnitCode: 'adunit-code-1',
+ mediaTypes: {
+ video: {
+ mimes: [
+ 'video/mp4',
+ 'video/mpeg',
+ ],
+ playerSize: [[250, 300]],
+ placement: 2,
+ plcmt: 2,
+ }
+ },
+ bidId: 'bid1',
+ }
+ ],
+ ortb2: {
+ regs: {
+ ext: {
+ dsa: {
+ dsarequired: 2,
+ pubrender: 1,
+ datatopub: 2,
+ transparency: [{
+ domain: 'google.com',
+ dsaparams: [1, 2]
+ }]
+ }
+ }}
+ },
+ }
+ };
+
+ const bidResponseDsa = {
+ body: {
+ id: '22edbae2733bf6',
+ bidid: 'foo9876',
+ cur: 'USD',
+ seatbid: [
+ {
+ seat: 'some-dsp',
+ bid: [
+ {
+ ad: '',
+ impid: 'bid1',
+ crid: 'crid1',
+ price: 0.5,
+ w: 300,
+ h: 250,
+ adm: '',
+ exp: 60,
+ adomain: ['test1.com'],
+ ext: {
+ meta: {
+ test: 1,
+ },
+ dsa: {
+ behalf: 'Advertiser',
+ paid: 'Advertiser',
+ transparency: [{
+ domain: 'google.com',
+ dsaparams: [1, 2]
+ }],
+ adrender: 1
+ }
+ },
+ }
+ ],
+ },
+ ]
+ }
+ };
+ const bidRequestWithDsa = utils.deepClone(bidRequestDsa);
+ it('should have related properties for DSA data', function() {
+ const serverResponseWithDsa = utils.deepClone(bidResponseDsa);
+ const bidResponse = spec.interpretResponse(serverResponseWithDsa, bidRequestWithDsa)[0];
+ expect(bidResponse).to.have.any.keys('ext');
+ expect(bidResponse.ext.dsa).to.have.property('behalf', 'Advertiser');
+ expect(bidResponse.ext.dsa).to.have.property('paid', 'Advertiser');
+ expect(bidResponse.ext.dsa).to.have.property('adrender', 1);
+ });
+ });
});