Skip to content

Onetag Bid Adapter : added native legacy support #13084

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

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
77 changes: 50 additions & 27 deletions modules/onetagBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getStorageManager } from '../src/storageManager.js';
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { deepClone, logError, deepAccess, getWinDimensions } from '../src/utils.js';
import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js';
import { toOrtbNativeRequest } from '../src/native.js';

/**
* @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest
Expand Down Expand Up @@ -39,6 +40,10 @@ export function hasTypeVideo(bid) {
return typeof bid.mediaTypes !== 'undefined' && typeof bid.mediaTypes.video !== 'undefined';
}

export function hasTypeNative(bid) {
return typeof bid.mediaTypes !== 'undefined' && typeof bid.mediaTypes.native !== 'undefined';
}

export function isValid(type, bid) {
if (type === BANNER) {
return parseSizes(bid).length > 0;
Expand All @@ -49,13 +54,18 @@ export function isValid(type, bid) {
}
} else if (type === NATIVE) {
if (typeof bid.mediaTypes.native !== 'object' || bid.mediaTypes.native === null) return false;

const assets = bid.mediaTypes.native?.ortb?.assets;
const eventTrackers = bid.mediaTypes.native?.ortb?.eventtrackers;
if (!isNativeOrtbVersion(bid)) {
if (bid.nativeParams === undefined) return false;
const ortbConversion = toOrtbNativeRequest(bid.nativeParams);
return ortbConversion && ortbConversion.assets && Array.isArray(ortbConversion.assets) && ortbConversion.assets.length > 0 && ortbConversion.assets.every(asset => isValidAsset(asset));
}

let isValidAssets = false;
let isValidEventTrackers = false;

const assets = bid.mediaTypes.native?.ortb?.assets;
const eventTrackers = bid.mediaTypes.native?.ortb?.eventtrackers;

if (assets && Array.isArray(assets) && assets.length > 0 && assets.every(asset => isValidAsset(asset))) {
isValidAssets = true;
}
Expand All @@ -80,12 +90,11 @@ const isValidEventTracker = function(et) {
}

const isValidAsset = function(asset) {
if (!asset.id || !Number.isInteger(asset.id)) return false;
if (!asset.hasOwnProperty("id") || !Number.isInteger(asset.id)) return false;
const hasValidContent = asset.title || asset.img || asset.data || asset.video;
if (!hasValidContent) return false;
if (asset.title && (!asset.title.len || !Number.isInteger(asset.title.len))) return false;
if (asset.img && ((!asset.img.wmin || !Number.isInteger(asset.img.wmin)) || (!asset.img.hmin || !Number.isInteger(asset.img.hmin)))) return false;
if (asset.data && !asset.data.type) return false;
if (asset.data && (!asset.data.type || !Number.isInteger(asset.data.type))) return false;
if (asset.video && (!asset.video.mimes || !asset.video.minduration || !asset.video.maxduration || !asset.video.protocols)) return false;
return true;
}
Expand Down Expand Up @@ -294,7 +303,7 @@ function getPageInfo(bidderRequest) {
timing: getTiming(),
version: {
prebid: '$prebid.version$',
adapter: '1.1.2'
adapter: '1.1.3'
}
};
}
Expand Down Expand Up @@ -324,17 +333,27 @@ function requestsToBids(bidRequests) {
return bannerObj;
});
const nativeBidRequests = bidRequests.filter(bidRequest => isValid(NATIVE, bidRequest)).map(bidRequest => {
const bannerObj = {};
setGeneralInfo.call(bannerObj, bidRequest);
bannerObj['sizes'] = parseSizes(bidRequest);
bannerObj['type'] = NATIVE + NATIVE_SUFFIX;
bannerObj['mediaTypeInfo'] = deepClone(bidRequest.mediaTypes.native);
bannerObj['priceFloors'] = getBidFloor(bidRequest, NATIVE, bannerObj['sizes']);
return bannerObj;
const nativeObj = {};
setGeneralInfo.call(nativeObj, bidRequest);
nativeObj['sizes'] = parseSizes(bidRequest);
nativeObj['type'] = NATIVE + NATIVE_SUFFIX;
nativeObj['mediaTypeInfo'] = deepClone(bidRequest.mediaTypes.native);
if (!isNativeOrtbVersion(bidRequest)) {
const ortbConversion = toOrtbNativeRequest(bidRequest.nativeParams);
nativeObj['mediaTypeInfo'] = {};
nativeObj['mediaTypeInfo'].adTemplate = bidRequest.nativeParams.adTemplate;
nativeObj['mediaTypeInfo'].ortb = ortbConversion;
}
nativeObj['priceFloors'] = getBidFloor(bidRequest, NATIVE, nativeObj['sizes']);
return nativeObj;
});
return videoBidRequests.concat(bannerBidRequests).concat(nativeBidRequests);
}

function isNativeOrtbVersion(bidRequest) {
return bidRequest.mediaTypes.native.ortb && typeof bidRequest.mediaTypes.native.ortb === 'object';
}

function setGeneralInfo(bidRequest) {
const params = bidRequest.params;
this['adUnitCode'] = bidRequest.adUnitCode;
Expand Down Expand Up @@ -458,20 +477,24 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gpp
}

function getBidFloor(bidRequest, mediaType, sizes) {
const priceFloors = [];
if (typeof bidRequest.getFloor === 'function') {
sizes.forEach(size => {
const floor = bidRequest.getFloor({
currency: 'EUR',
mediaType: mediaType || '*',
size: [size.width, size.height]
}) || {};
floor.size = deepClone(size);
if (!floor.floor) { floor.floor = null; }
priceFloors.push(floor);
});
if (typeof bidRequest.getFloor !== 'function') return [];
const getFloorObject = (size) => {
const floorData = bidRequest.getFloor({
currency: 'EUR',
mediaType: mediaType || '*',
size: size || '*'
}) || {};

return {
...floorData,
size: size ? deepClone(size) : undefined,
floor: floorData.floor != null ? floorData.floor : null
};
};
if (Array.isArray(sizes) && sizes.length > 0) {
return sizes.map(size => getFloorObject([size.width, size.height]));
}
return priceFloors;
return [getFloorObject('*')];
}

export function isSchainValid(schain) {
Expand Down
153 changes: 115 additions & 38 deletions test/spec/modules/onetagBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { expect } from 'chai';
import { find } from 'src/polyfill.js';
import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js';
import { INSTREAM, OUTSTREAM } from 'src/video.js';
import { toOrtbNativeRequest } from 'src/native.js';
import { hasTypeNative } from '../../../modules/onetagBidAdapter';

const NATIVE_SUFFIX = 'Ad';

Expand Down Expand Up @@ -44,6 +46,57 @@ describe('onetag', function () {
};
}

function createNativeLegacyBid(bidRequest) {
let bid = bidRequest || createBid();
bid.mediaTypes = bid.mediaTypes || {};
bid.mediaTypes.native = {
adTemplate: "<div><figure><img decoding=\"async\" referrerpolicy=\"no-referrer\" loading=\"lazy\" src=\"##hb_native_image##\" alt=\"##hb_native_brand##\" width=\"0\" height=\"0\"></figure><div class=\"a-nativeframe__text\"><span class=\"a-nativeframe__label\">##hb_native_brand##</span><h3 class=\"a-nativeframe__title\">##hb_native_title##</h3><div class=\"a-nativeframe__details\"><span class=\"a-nativeframe__cta\">##hb_native_cta##</span><span class=\"a-nativeframe__info\">##hb_native_brand##</span></div></div><a class=\"o-faux-link\" href=\"##hb_native_linkurl##\" target=\"_blank\"></a>",
title: {
required: 1,
sendId: 1
},
body: {
required: 1,
sendId: 1
},
cta: {
required: 0,
sendId: 1
},
displayUrl: {
required: 0,
sendId: 1
},
icon: {
required: 0,
sendId: 1
},
image: {
required: 1,
sendId: 1
},
sponsoredBy: {
required: 1,
sendId: 1
}
}
bid = addNativeParams(bid);
const ortbConversion = toOrtbNativeRequest(bid.nativeParams);
bid.mediaTypes.native = {};
bid.mediaTypes.native.adTemplate = bid.nativeParams.adTemplate;
bid.mediaTypes.native.ortb = ortbConversion;
return bid;
}

function addNativeParams(bidRequest) {
let bidParams = bidRequest.nativeParams || {};
for (const property in bidRequest.mediaTypes.native) {
bidParams[property] = bidRequest.mediaTypes.native[property];
}
bidRequest.nativeParams = bidParams;
return bidRequest;
}

function createNativeBid(bidRequest) {
const bid = bidRequest || createBid();
bid.mediaTypes = bid.mediaTypes || {};
Expand All @@ -54,7 +107,7 @@ describe('onetag', function () {
assets: [{
id: 1,
required: 1,
title: {
title: {
len: 140
}
},
Expand All @@ -81,7 +134,7 @@ describe('onetag', function () {
minduration: 5,
maxduration: 30,
protocols: [2, 3]
}
}
}],
eventtrackers: [{
event: 1,
Expand Down Expand Up @@ -128,12 +181,13 @@ describe('onetag', function () {
return createInstreamVideoBid(createBannerBid());
}

let bannerBid, instreamVideoBid, outstreamVideoBid, nativeBid;
let bannerBid, instreamVideoBid, outstreamVideoBid, nativeBid, nativeLegacyBid;
beforeEach(() => {
bannerBid = createBannerBid();
instreamVideoBid = createInstreamVideoBid();
outstreamVideoBid = createOutstreamVideoBid();
nativeBid = createNativeBid();
nativeLegacyBid = createNativeLegacyBid();
})

describe('isBidRequestValid', function () {
Expand All @@ -150,86 +204,94 @@ describe('onetag', function () {
});
describe('banner bidRequest', function () {
it('Should return false when the sizes array is empty', function () {
// TODO (dgirardi): this test used to pass because `bannerBid` was global state
// and other test code made it invalid for reasons other than sizes.
// cleaning up the setup code, it now (correctly) fails.
bannerBid.sizes = [];
// expect(spec.isBidRequestValid(bannerBid)).to.be.false;
bannerBid.mediaTypes.banner.sizes = [];
expect(spec.isBidRequestValid(bannerBid)).to.be.false;
});
});
describe('native bidRequest', function () {
it('Should return true when correct native bid is passed', function () {
const nativeBid = createNativeBid();
expect(spec.isBidRequestValid(nativeBid)).to.be.true;
const nativeLegacyBid = createNativeLegacyBid();
expect(spec.isBidRequestValid(nativeBid)).to.be.true && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.true;
});
it('Should return false when native is not an object', function () {
const nativeBid = createNativeBid();
nativeBid.mediaTypes.native = 30;
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
const nativeLegacyBid = createNativeLegacyBid();
nativeBid.mediaTypes.native = nativeLegacyBid.mediaTypes.native = 30;
expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false;
});
it('Should return false when native.ortb is not an object', function () {
it('Should return false when native.ortb if defined but it isn\'t an object', function () {
const nativeBid = createNativeBid();
nativeBid.mediaTypes.native.ortb = 30 || 'string';
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
});
it('Should return false when native.ortb.assets is not an array', function () {
const nativeBid = createNativeBid();
nativeBid.mediaTypes.native.ortb.assets = 30;
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
const nativeLegacyBid = createNativeLegacyBid();
nativeBid.mediaTypes.native.ortb.assets = nativeLegacyBid.mediaTypes.native.ortb.assets = 30;
expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false;
});
it('Should return false when native.ortb.assets is an empty array', function () {
const nativeBid = createNativeBid();
nativeBid.mediaTypes.native.ortb.assets = [];
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
const nativeLegacyBid = createNativeLegacyBid();
nativeBid.mediaTypes.native.ortb.assets = nativeLegacyBid.mediaTypes.native.ortb.assets = [];
expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false;
});
it('Should return false when native.ortb.assets[i] doesnt have \'id\'', function () {
const nativeBid = createNativeBid();
const nativeLegacyBid = createNativeLegacyBid();
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[0], 'id');
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
Reflect.deleteProperty(nativeLegacyBid.mediaTypes.native.ortb.assets[0], 'id');
expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false;
});
it('Should return false when native.ortb.assets[i] doesnt have any of \'title\', \'img\', \'data\' and \'video\' properties', function () {
const nativeBid = createNativeBid();
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[0], 'title');
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
const nativeLegacyBid = createNativeLegacyBid();
const titleIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.title);
const legacyTitleIndex = nativeLegacyBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.title);
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[titleIndex], 'title');
Reflect.deleteProperty(nativeLegacyBid.mediaTypes.native.ortb.assets[legacyTitleIndex], 'title');
expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false;
});
it('Should return false when native.ortb.assets[i] have title, but doesnt have \'len\' property', function () {
const nativeBid = createNativeBid();
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[0].title, 'len');
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
});
it('Should return false when native.ortb.assets[i] is image but doesnt have \'wmin\' property', function () {
const nativeBid = createNativeBid();
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[1].img, 'wmin');
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
});
it('Should return false when native.ortb.assets[i] is image but doesnt have \'hmin\' property', function () {
const nativeBid = createNativeBid();
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[1].img, 'hmin');
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
const nativeLegacyBid = createNativeLegacyBid();
const titleIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.title);
const legacyTitleIndex = nativeLegacyBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.title);
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[titleIndex].title, 'len');
Reflect.deleteProperty(nativeLegacyBid.mediaTypes.native.ortb.assets[legacyTitleIndex].title, 'len');
expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false;
});
it('Should return false when native.ortb.assets[i] is data but doesnt have \'type\' property', function () {
const nativeBid = createNativeBid();
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[2].data, 'type');
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
const nativeLegacyBid = createNativeLegacyBid();
const dataIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.data);
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[dataIndex].data, 'type');
Reflect.deleteProperty(nativeLegacyBid.mediaTypes.native.ortb.assets[dataIndex].data, 'type');
expect(spec.isBidRequestValid(nativeBid)).to.be.false && expect(spec.isBidRequestValid(nativeLegacyBid)).to.be.false;
});
it('Should return false when native.ortb.assets[i] is video but doesnt have \'mimes\' property', function () {
const nativeBid = createNativeBid();
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'mimes');
const videoIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.video);
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[videoIndex].video, 'mimes');
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
});
it('Should return false when native.ortb.assets[i] is video but doesnt have \'minduration\' property', function () {
const nativeBid = createNativeBid();
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'minduration');
const videoIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.video);
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[videoIndex].video, 'minduration');
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
});
it('Should return false when native.ortb.assets[i] is video but doesnt have \'maxduration\' property', function () {
const nativeBid = createNativeBid();
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'maxduration');
const videoIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.video);
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[videoIndex].video, 'maxduration');
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
});
it('Should return false when native.ortb.assets[i] is video but doesnt have \'protocols\' property', function () {
const nativeBid = createNativeBid();
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[3].video, 'protocols');
const videoIndex = nativeBid.mediaTypes.native.ortb.assets.findIndex(asset => asset.video);
Reflect.deleteProperty(nativeBid.mediaTypes.native.ortb.assets[videoIndex].video, 'protocols');
expect(spec.isBidRequestValid(nativeBid)).to.be.false;
});
it('Should return false when native.ortb.eventtrackers is not an array', function () {
Expand Down Expand Up @@ -324,7 +386,7 @@ describe('onetag', function () {
describe('buildRequests', function () {
let serverRequest, data;
before(() => {
serverRequest = spec.buildRequests([bannerBid, instreamVideoBid, nativeBid]);
serverRequest = spec.buildRequests([bannerBid, instreamVideoBid, nativeBid, nativeLegacyBid]);
data = JSON.parse(serverRequest.data);
});

Expand Down Expand Up @@ -387,6 +449,21 @@ describe('onetag', function () {
'type',
'priceFloors'
);
} else if (hasTypeNative(bid)) {
expect(bid).to.have.all.keys(
'adUnitCode',
'auctionId',
'bidId',
'bidderRequestId',
'pubId',
'ortb2Imp',
'transactionId',
'mediaTypeInfo',
'sizes',
'type',
'priceFloors'
) &&
expect(bid.mediaTypeInfo).to.have.key('ortb');
} else if (isValid(BANNER, bid)) {
expect(bid).to.have.all.keys(
'adUnitCode',
Expand Down