Skip to content

Commit

Permalink
add AMX adapter (#5383)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickjacob committed Jul 1, 2020
1 parent 4121e1a commit d8e5796
Show file tree
Hide file tree
Showing 3 changed files with 706 additions and 0 deletions.
279 changes: 279 additions & 0 deletions modules/amxBidAdapter.js
@@ -0,0 +1,279 @@
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO } from '../src/mediaTypes.js';
import { parseUrl, deepAccess, _each, formatQS, getUniqueIdentifierStr, triggerPixel } from '../src/utils.js';

const BIDDER_CODE = 'amx';
const SIMPLE_TLD_TEST = /\.co\.\w{2,4}$/;
const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c';
const VERSION = 'pba1.0';
const xmlDTDRxp = /^\s*<\?xml[^\?]+\?>/;
const VAST_RXP = /^\s*<\??(?:vast|xml)/i;
const TRACKING_ENDPOINT = 'https://1x1.a-mo.net/hbx/';

const getLocation = (request) =>
parseUrl(deepAccess(request, 'refererInfo.canonicalUrl', location.href))

const largestSize = (sizes, mediaTypes) => {
const allSizes = sizes
.concat(deepAccess(mediaTypes, `${BANNER}.sizes`, []) || [])
.concat(deepAccess(mediaTypes, `${VIDEO}.sizes`, []) || [])

return allSizes.sort((a, b) => (b[0] * b[1]) - (a[0] * a[1]))[0];
}

const generateDTD = (xmlDocument) =>
`<?xml version="${xmlDocument.xmlVersion}" encoding="${xmlDocument.xmlEncoding}" ?>`;

const isVideoADM = (html) => html != null && VAST_RXP.test(html);
const getMediaType = (bid) => isVideoADM(bid.adm) ? VIDEO : BANNER;
const nullOrType = (value, type) =>
value == null || (typeof value) === type // eslint-disable-line valid-typeof

function getID(loc) {
const host = loc.hostname.split('.');
const short = host.slice(
host.length - (SIMPLE_TLD_TEST.test(loc.host) ? 3 : 2)
).join('.');
return btoa(short).replace(/=+$/, '');
}

const enc = encodeURIComponent;

function nestedQs (qsData) {
const out = [];
Object.keys(qsData || {}).forEach((key) => {
out.push(enc(key) + '=' + enc(String(qsData[key])));
});

return enc(out.join('&'));
}

function createBidMap(bids) {
const out = {};
for (const bid of bids) {
out[bid.bidId] = convertRequest(bid)
}
return out;
}

const trackEvent = (eventName, data) =>
triggerPixel(`${TRACKING_ENDPOINT}g_${eventName}?${formatQS({
...data,
ts: Date.now(),
eid: getUniqueIdentifierStr(),
})}`);

function convertRequest(bid) {
const size = largestSize(bid.sizes, bid.mediaTypes) || [0, 0];
const av = bid.mediaType === VIDEO || VIDEO in bid.mediaTypes;
const tid = deepAccess(bid, 'params.tagId')

const params = {
av,
aw: size[0],
ah: size[1],
tf: 0,
};

if (typeof tid === 'string' && tid.length > 0) {
params.i = tid;
}
return params;
}

function decorateADM(bid) {
const impressions = deepAccess(bid, 'ext.himp', [])
.concat(bid.nurl != null ? [bid.nurl] : [])
.filter((imp) => imp != null && imp.length > 0)
.map((src) => `<img src="${src}" width="0" height="0"/>`)
.join('');
return bid.adm + impressions;
}

function decorateVideoADM(bid) {
const doc = new DOMParser().parseFromString(bid.adm, 'text/xml');
if (doc.querySelector('parsererror') != null) {
return null;
}

const root = doc.querySelector('InLine,Wrapper')
if (root == null) {
return null;
}

const pixels = [bid.nurl].concat(bid.ext.himp || [])
.filter((url) => url != null);

_each(pixels, (pxl) => {
const imagePixel = doc.createElement('Impression');
const cdata = doc.createCDATASection(pxl);
imagePixel.appendChild(cdata);
root.appendChild(imagePixel);
});

const dtdMatch = xmlDTDRxp.exec(bid.adm);
return (dtdMatch != null ? dtdMatch[0] : generateDTD(doc)) + doc.documentElement.outerHTML;
}

function resolveSize(bid, request, bidId) {
if (bid.w != null && bid.w > 1 && bid.h != null && bid.h > 1) {
return [bid.w, bid.h];
}

const bidRequest = request.m[bidId];
if (bidRequest == null) {
return [0, 0];
}

return [bidRequest.aw, bidRequest.ah];
}

export const spec = {
code: BIDDER_CODE,
supportedMediaTypes: [BANNER, VIDEO],

isBidRequestValid(bid) {
return nullOrType(deepAccess(bid, 'params.endpoint', null), 'string') &&
nullOrType(deepAccess(bid, 'params.tagId', null), 'string') &&
nullOrType(deepAccess(bid, 'params.testMode', null), 'boolean');
},

buildRequests(bidRequests, bidderRequest) {
const loc = getLocation(bidderRequest);
const tagId = deepAccess(bidRequests[0], 'params.tagId', null);
const testMode = deepAccess(bidRequests[0], 'params.testMode', 0);

const payload = {
a: bidderRequest.auctionId,
B: 0,
b: loc.host,
tm: testMode,
V: '$prebid.version$',
i: (testMode && tagId != null) ? tagId : getID(loc),
l: {},
f: 0.01,
cv: VERSION,
st: 'prebid',
h: screen.height,
w: screen.width,
gs: deepAccess(bidderRequest, 'gdprConsent.gdprApplies', '0'),
gc: deepAccess(bidderRequest, 'gdprConsent.consentString', ''),
u: deepAccess(bidderRequest, 'refererInfo.canonicalUrl', loc.href),
do: loc.host,
re: deepAccess(bidderRequest, 'refererInfo.referer'),
usp: bidderRequest.uspConsent || '1---',
smt: 9,
d: '',
m: createBidMap(bidRequests),
};

return {
data: payload,
method: 'POST',
url: deepAccess(bidRequests[0], 'params.endpoint', DEFAULT_ENDPOINT),
withCredentials: true,
};
},

getUserSyncs(syncOptions, serverResponses) {
return (serverResponses || [])
.flatMap(({ body: response }) =>
response != null && response.p != null ? (response.p.hreq || []) : [])
.map((syncPixel) =>
({
type: syncPixel.indexOf('__st=iframe') !== -1 ? 'iframe' : 'image',
url: syncPixel
})
).filter(({
type
}) => syncOptions.iframeEnabled || type === 'image')
},

interpretResponse(serverResponse, request) {
// validate the body/response
const response = serverResponse.body;
if (response == null || typeof response === 'string') {
return [];
}

return Object.keys(response.r).flatMap((bidID) => {
const biddata = response.r[bidID];
return biddata.flatMap((siteBid) =>
siteBid.b.map((bid) => {
const mediaType = getMediaType(bid);
const ad = mediaType === BANNER ? decorateADM(bid) : decorateVideoADM(bid);
if (ad == null) {
return null;
}

const size = resolveSize(bid, request.data, bidID);

return ({
requestId: bidID,
cpm: bid.price,
width: size[0],
height: size[1],
creativeId: bid.crid,
currency: 'USD',
netRevenue: true,
[mediaType === VIDEO ? 'vastXml' : 'ad']: ad,
meta: {
advertiserDomains: bid.adomain,
mediaType,
},
ttl: mediaType === VIDEO ? 90 : 70
});
})).filter((possibleBid) => possibleBid != null);
});
},

onSetTargeting(targetingData) {
if (targetingData == null) {
return;
}

trackEvent('pbst', {
A: targetingData.bidder,
w: targetingData.width,
h: targetingData.height,
bid: targetingData.adId,
c1: targetingData.mediaType,
np: targetingData.cpm,
aud: targetingData.requestId,
a: targetingData.adUnitCode,
c2: nestedQs(targetingData.adserverTargeting),
});
},

onTimeout(timeoutData) {
if (timeoutData == null) {
return;
}

trackEvent('pbto', {
A: timeoutData.bidder,
bid: timeoutData.bidId,
a: timeoutData.adUnitCode,
cn: timeoutData.timeout,
aud: timeoutData.auctionId,
});
},

onBidWon(bidWinData) {
if (bidWinData == null) {
return;
}

trackEvent('pbwin', {
A: bidWinData.bidder,
w: bidWinData.width,
h: bidWinData.height,
bid: bidWinData.adId,
C: bidWinData.mediaType === BANNER ? 0 : 1,
np: bidWinData.cpm,
a: bidWinData.adUnitCode,
});
},
};

registerBidder(spec);
37 changes: 37 additions & 0 deletions modules/amxBidAdapter.md
@@ -0,0 +1,37 @@
Overview
========

```
Module Name: AMX Adapter
Module Type: Bidder Adapter
Maintainer: prebid.support@amxrtb.com
```

Description
===========

This module connects web publishers to AMX RTB video and display demand.

# Bid Parameters

| Key | Required | Example | Description |
| --- | -------- | ------- | ----------- |
| `endpoint` | **yes** | `https://prebid.a-mo.net/a/c` | The url including https:// and any path |
| `testMode` | no | `true` | this will activate test mode / 100% fill with sample ads |
| `tagId` | no | `"eh3hffb"` | can be used for more specific targeting of inventory. Your account manager will provide this ID if needed |

# Test Parameters

```
var adUnits = [{
code: 'test-div',
sizes: [[300, 250]],
bids: [{
bidder: 'amx',
params: {
testMode: true,
endpoint: 'https://prebid.a-mo.net/a/c',
},
}]
}]
```

0 comments on commit d8e5796

Please sign in to comment.