Skip to content

Commit

Permalink
IX Bid Adapter: Native OpenRTB Request Support (prebid#8853)
Browse files Browse the repository at this point in the history
* fix native click trackers to only fire on click

* fix unit tests for ix

* remove version for native requests

* remove unnecessary request conversion

Co-authored-by: Zicong Zhou <[email protected]>
  • Loading branch information
2 people authored and jorgeluisrocha committed May 18, 2023
1 parent 448173f commit 42c52a4
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 410 deletions.
319 changes: 13 additions & 306 deletions modules/ixBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {find} from '../src/polyfill.js';
import {registerBidder} from '../src/adapters/bidderFactory.js';
import {INSTREAM, OUTSTREAM} from '../src/video.js';
import {Renderer} from '../src/Renderer.js';
import { convertOrtbRequestToProprietaryNative } from '../src/native.js';

const BIDDER_CODE = 'ix';
const ALIAS_BIDDER_CODE = 'roundel';
Expand Down Expand Up @@ -100,93 +99,6 @@ const VIDEO_PARAMS_ALLOW_LIST = [
'delivery', 'pos', 'companionad', 'api', 'companiontype', 'ext',
'playerSize', 'w', 'h'
];
const NATIVE_ASSET_TYPES = {
TITLE: 100,
IMG: 200,
VIDEO: 300,
DATA: 400
};
const NATIVE_IMAGE_TYPES = {
ICON: 1,
MAIN: 3
};
const NATIVE_DATA_TYPES = {
SPONSORED: 1,
DESC: 2,
RATING: 3,
LIKES: 4,
DOWNLOADS: 5,
PRICE: 6,
SALEPRICE: 7,
PHONE: 8,
ADDRESS: 9,
DESC2: 10,
DISPLAYURL: 11,
CTATEXT: 12
};
const NATIVE_DATA_MAP = {
[NATIVE_DATA_TYPES.SPONSORED]: 'sponsoredBy',
[NATIVE_DATA_TYPES.DESC]: 'body',
[NATIVE_DATA_TYPES.RATING]: 'rating',
[NATIVE_DATA_TYPES.LIKES]: 'likes',
[NATIVE_DATA_TYPES.DOWNLOADS]: 'downloads',
[NATIVE_DATA_TYPES.PRICE]: 'price',
[NATIVE_DATA_TYPES.SALEPRICE]: 'salePrice',
[NATIVE_DATA_TYPES.PHONE]: 'phone',
[NATIVE_DATA_TYPES.ADDRESS]: 'address',
[NATIVE_DATA_TYPES.DESC2]: 'body2',
[NATIVE_DATA_TYPES.DISPLAYURL]: 'displayUrl',
[NATIVE_DATA_TYPES.CTATEXT]: 'cta'
};
const NATIVE_ASSETS_MAP = {
'title': { assetType: NATIVE_ASSET_TYPES.TITLE },
'icon': { assetType: NATIVE_ASSET_TYPES.IMG, subtype: NATIVE_IMAGE_TYPES.ICON },
'image': { assetType: NATIVE_ASSET_TYPES.IMG, subtype: NATIVE_IMAGE_TYPES.MAIN },
'sponsoredBy': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.SPONSORED },
'body': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DESC },
'rating': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.RATING },
'likes': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.LIKES },
'downloads': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DOWNLOADS },
'price': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.PRICE },
'salePrice': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.SALEPRICE },
'phone': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.PHONE },
'address': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.ADDRESS },
'body2': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DESC2 },
'displayUrl': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.DISPLAYURL },
'cta': { assetType: NATIVE_ASSET_TYPES.DATA, subtype: NATIVE_DATA_TYPES.CTATEXT },
'video': { assetType: NATIVE_ASSET_TYPES.VIDEO }
};
const NATIVE_ALLOWED_PROPERTIES = [
'rendererUrl',
'sendTargetingKeys',
'adTemplate',
'type',
'ext',
'privacyLink',
'clickUrl',
'privacyIcon'
];
const NATIVE_ASSET_DEFAULT = {
TITLE: {
LEN: 25
},
VIDEO: {
MIMES: [
'video/mp4',
'video/webm'
],
MINDURATION: 0,
MAXDURATION: 120,
PROTOCOLS: [2, 3, 5, 6],
}
};
const NATIVE_EVENT_TYPES = {
IMRESSION: 1
};
const NATIVE_EVENT_TRACKING_METHOD = {
IMG: 1,
JS: 2
};
const LOCAL_STORAGE_KEY = 'ixdiag';
let hasRegisteredHandler = false;
export const storage = getStorageManager({gvlid: GLOBAL_VENDOR_ID, bidderCode: BIDDER_CODE});
Expand Down Expand Up @@ -307,50 +219,13 @@ function bidToVideoImp(bid) {
*/
function bidToNativeImp(bid) {
const imp = bidToImp(bid);
const nativeAdUnitRef = deepAccess(bid, 'mediaTypes.native');

const assets = [];

// Convert all native assets to imp object
for (const [adUnitProperty, adUnitValues] of Object.entries(nativeAdUnitRef)) {
if (!NATIVE_ASSETS_MAP[adUnitProperty]) {
continue;
}

const { assetType, subtype } = NATIVE_ASSETS_MAP[adUnitProperty];
let asset;
switch (assetType) {
case NATIVE_ASSET_TYPES.TITLE:
asset = createNativeTitleRequest(adUnitValues);
break;
case NATIVE_ASSET_TYPES.IMG:
asset = createNativeImgRequest(adUnitValues, subtype);
break;
case NATIVE_ASSET_TYPES.VIDEO:
asset = createNativeVideoRequest(adUnitValues);
break;
case NATIVE_ASSET_TYPES.DATA:
asset = createNativeDataRequest(adUnitValues, subtype);
break;
}
asset.id = assetType + (subtype || 0);
assets.push(asset);
}

if (assets.length === 0) {
logWarn('IX Bid Adapter: Native bid does not contain recognised assets in [mediaTypes.native]');
return {};
}

const request = {
assets: assets,
ver: '1.2',
eventtrackers: [{
event: 1,
methods: [1, 2]
}],
privacy: 1
};
const request = bid.nativeOrtbRequest
request.eventtrackers = [{
event: 1,
methods: [1, 2]
}];
request.privacy = 1;

imp.native = {
request: JSON.stringify(request),
Expand All @@ -365,80 +240,6 @@ function bidToNativeImp(bid) {
return imp;
}

/**
* Converts native bid asset to a native impression asset
* @param {object} bidAsset PBJS bid asset object
* @returns {object} IX impression asset object
*/
function createNativeTitleRequest(bidAsset) {
return {
required: bidAsset.required ? 1 : 0,
title: {
len: bidAsset.len ? bidAsset.len : NATIVE_ASSET_DEFAULT.TITLE.LEN,
ext: bidAsset.ext
}
}
}

/**
* Converts native bid asset to a native impression asset
* @param {object} bidAsset PBJS bid asset object
* @param {int} type The image type
* @returns {object} IX impression asset object
*/
function createNativeImgRequest(bidAsset, type) {
let asset = {
required: bidAsset.required ? 1 : 0,
img: {
type: type,
mimes: bidAsset.mimes,
ext: bidAsset.ext
}
}

if (bidAsset.hasOwnProperty('sizes') && bidAsset.sizes.length === 2) {
asset.img.wmin = bidAsset.sizes[0];
asset.img.hmin = bidAsset.sizes[1];
}

return asset
}

/**
* Converts native bid asset to a native impression asset
* @param {object} bidAsset PBJS bid asset object
* @returns {object} IX impression asset object
*/
function createNativeVideoRequest(bidAsset) {
return {
required: bidAsset.required ? 1 : 0,
video: {
mimes: bidAsset.mimes ? bidAsset.mimes : NATIVE_ASSET_DEFAULT.VIDEO.MIMES,
minduration: bidAsset.minduration ? bidAsset.minduration : NATIVE_ASSET_DEFAULT.VIDEO.MINDURATION,
maxduration: bidAsset.maxduration ? bidAsset.maxduration : NATIVE_ASSET_DEFAULT.VIDEO.MAXDURATION,
protocols: bidAsset.protocols ? bidAsset.protocols : NATIVE_ASSET_DEFAULT.VIDEO.PROTOCOLS,
ext: bidAsset.ext
}
}
}

/**
* Converts native bid asset to a native impression asset
* @param {object} bidAsset PBJS bid asset object
* @param {int} type The image type
* @returns {object} IX impression asset object
*/
function createNativeDataRequest(bidAsset, type) {
return {
required: bidAsset.required ? 1 : 0,
data: {
type: type,
len: bidAsset.len,
ext: bidAsset.ext
}
}
}

/**
* Converts an incoming PBJS bid to an IX Impression
* @param {object} bid PBJS bid object
Expand Down Expand Up @@ -559,7 +360,7 @@ function parseBid(rawBid, currency, bidRequest) {
bid.mediaTypes = bidRequest.mediaTypes;
bid.ttl = isValidExpiry ? rawBid.exp : VIDEO_TIME_TO_LIVE;
} else if (parsedAdm && parsedAdm.native) {
bid.native = interpretNativeAdm(parsedAdm.native);
bid.native = {ortb: parsedAdm.native};
bid.width = rawBid.w ? rawBid.w : 1;
bid.height = rawBid.h ? rawBid.h : 1;
bid.mediaType = NATIVE;
Expand All @@ -583,84 +384,6 @@ function parseBid(rawBid, currency, bidRequest) {
return bid;
}

/**
* Parse native adm and set native asset key names recognized by Prebid.js
* @param {string} adm Native adm complience
*/
function interpretNativeAdm(nativeResponse) {
const native = {
clickUrl: nativeResponse.link.url,
privacyLink: nativeResponse.privacy
};

for (const asset of nativeResponse.assets) {
const subtype = asset.id % 100;
const assetType = asset.id - subtype;

switch (assetType) {
case NATIVE_ASSET_TYPES.TITLE:
native.title = asset.title && asset.title.text;
break;
case NATIVE_ASSET_TYPES.IMG:
const image = {
url: asset.img && asset.img.url,
height: asset.img && asset.img.h,
width: asset.img && asset.img.w
};
native[subtype === NATIVE_IMAGE_TYPES.ICON ? 'icon' : 'image'] = image;
break;
case NATIVE_ASSET_TYPES.VIDEO:
native.video = asset.video && asset.video.vasttag;
break;
case NATIVE_ASSET_TYPES.DATA:
setDataAsset(native, asset, subtype);
break;
default:
logWarn(`IX Bid Adapter: native asset ID ${asset.id} could not be recognized`);
}
}

setTrackers(native, nativeResponse);
return native;
}

function setDataAsset(native, asset, type) {
if (!(type in NATIVE_DATA_MAP)) {
logWarn(`IX Bid Adapter: native data asset type ${type} is not supported`);
return;
}
native[NATIVE_DATA_MAP[type]] = asset.data && asset.data.value;
}

function setTrackers(native, nativeResponse) {
native.impressionTrackers = []

if (Array.isArray(nativeResponse.imptrackers)) {
native.impressionTrackers.push(...nativeResponse.imptrackers)
}

if (Array.isArray(nativeResponse.link.clicktrackers)) {
native.impressionTrackers.push(...nativeResponse.link.clicktrackers)
}

if (Array.isArray(nativeResponse.eventtrackers)) {
nativeResponse.eventtrackers.forEach(tracker => {
if (tracker.event !== NATIVE_EVENT_TYPES.IMRESSION) {
return
}

switch (tracker.method) {
case NATIVE_EVENT_TRACKING_METHOD.IMG:
native.impressionTrackers.push(tracker.url);
break;
case NATIVE_EVENT_TRACKING_METHOD.JS:
native.javascriptTrackers = `<script src=\"${tracker.url}\"></script>`;
break;
}
})
}
}

/**
* Determines whether or not the given object is valid size format.
*
Expand Down Expand Up @@ -758,25 +481,13 @@ function isValidBidFloorParams(bidFloor, bidFloorCur) {
bidFloorCur.match(curRegex));
}

function nativeMediaTypeValid(nativeObj) {
if (nativeObj === undefined) {
return true;
}

let hasValidAsset = false;

for (const property in nativeObj) {
if (!(property in NATIVE_ASSETS_MAP) && !NATIVE_ALLOWED_PROPERTIES.includes(property)) {
logError('IX Bid Adapter: native', { bidder: BIDDER_CODE, code: ERROR_CODES.PROPERTY_NOT_INCLUDED });
return false;
}

if (property in NATIVE_ASSETS_MAP) {
hasValidAsset = true;
}
function nativeMediaTypeValid(bid) {
const nativeMediaTypes = deepAccess(bid, 'mediaTypes.native');
if (nativeMediaTypes === undefined) {
return true
}

return hasValidAsset;
return bid.nativeOrtbRequest && Array.isArray(bid.nativeOrtbRequest.assets) && bid.nativeOrtbRequest.assets.length > 0
}

/**
Expand Down Expand Up @@ -837,8 +548,6 @@ function getEidInfo(allEids) {
*
*/
function buildRequest(validBidRequests, bidderRequest, impressions, version) {
// convert Native ORTB definition to old-style prebid native definition
validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests);
// Always use secure HTTPS protocol.
let baseUrl = SECURE_BID_URL;
// Get ids from Prebid User ID Modules
Expand Down Expand Up @@ -966,7 +675,6 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) {
// Use the siteId in the first bid request as the main siteId.
siteID = validBidRequests[0].params.siteId;
payload.s = siteID;
payload.v = version;
if (version) {
payload.v = version;
}
Expand Down Expand Up @@ -1603,7 +1311,6 @@ export const spec = {
const paramsSize = deepAccess(bid, 'params.size');
const mediaTypeBannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes');
const mediaTypeVideoRef = deepAccess(bid, 'mediaTypes.video');
const mediaTypeNativeRef = deepAccess(bid, 'mediaTypes.native');
const mediaTypeVideoPlayerSize = deepAccess(bid, 'mediaTypes.video.playerSize');
const hasBidFloor = bid.params.hasOwnProperty('bidFloor');
const hasBidFloorCur = bid.params.hasOwnProperty('bidFloorCur');
Expand Down Expand Up @@ -1670,7 +1377,7 @@ export const spec = {
}
}

return nativeMediaTypeValid(mediaTypeNativeRef);
return nativeMediaTypeValid(bid);
},

/**
Expand Down
Loading

0 comments on commit 42c52a4

Please sign in to comment.