Skip to content

Commit

Permalink
add AMX adapter (after fixing ie11 test failure)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickjacob committed Jul 7, 2020
1 parent 83f19f4 commit 8f6f5ec
Show file tree
Hide file tree
Showing 4 changed files with 739 additions and 1 deletion.
2 changes: 1 addition & 1 deletion karma.conf.maker.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ module.exports = function(codeCoverage, browserstack, watchMode, file) {
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'test/test_index.js': ['webpack', 'sourcemap']
[file != null ? file : 'test/test_index.js']: ['webpack', 'sourcemap']
},

// web server port
Expand Down
317 changes: 317 additions & 0 deletions modules/amxBidAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
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];
}

function flatMap(input, mapFn) {
if (input == null) {
return []
}
return input.map(mapFn)
.reduce((acc, item) => item != null && acc.concat(item), [])
}

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 = {};
_each(bids, (bid) => {
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 isVideoBid = bid.mediaType === VIDEO || VIDEO in bid.mediaTypes
const av = isVideoBid || size[1] > 100;
const tid = deepAccess(bid, 'params.tagId')

const params = {
av,
vr: isVideoBid,
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 transformXmlSimple(bid) {
const pixels = []
_each([bid.nurl].concat(bid.ext != null && bid.ext.himp != null ? bid.ext.himp : []), (pixel) => {
if (pixel != null) {
pixels.push(`<Impression><![CDATA[${pixel}]]></Impression>`)
}
});
// find the current "Impression" here & slice ours in
const impressionIndex = bid.adm.indexOf('<Impression')
return bid.adm.slice(0, impressionIndex) + pixels.join('') + bid.adm.slice(impressionIndex)
}

function getOuterHTML(node) {
return 'outerHTML' in node && node.outerHTML != null
? node.outerHTML : (new XMLSerializer()).serializeToString(node)
}

function decorateVideoADM(bid) {
if (typeof DOMParser === 'undefined' || DOMParser.prototype.parseFromString == null) {
return transformXmlSimple(bid)
}

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

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

const pixels = [bid.nurl].concat(bid.ext != null && bid.ext.himp != null ? 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)) + getOuterHTML(doc.documentElement);
}

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: 1,
d: '',
m: createBidMap(bidRequests),
};

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

getUserSyncs(syncOptions, serverResponses) {
if (serverResponses == null || serverResponses.length === 0) {
return []
}
const output = []
_each(serverResponses, function ({ body: response }) {
if (response != null && response.p != null && response.p.hreq) {
_each(response.p.hreq, function (syncPixel) {
const pixelType = syncPixel.indexOf('__st=iframe') !== -1 ? 'iframe' : 'image';
if (syncOptions.iframeEnabled || pixelType === 'image') {
output.push({
url: syncPixel,
type: pixelType,
});
}
});
}
});
return output;
},

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

return flatMap(Object.keys(response.r), (bidID) => {
return flatMap(response.r[bidID], (siteBid) =>
siteBid.b.map((bid) => {
const mediaType = getMediaType(bid);
// let ad = null;
let 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
Original file line number Diff line number Diff line change
@@ -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 | `"cHJlYmlkLm9yZw"` | 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,
tagId: 'cHJlYmlkLm9yZw'
},
}]
}]
```
Loading

0 comments on commit 8f6f5ec

Please sign in to comment.