From 79c5ed712104add6b4d62f02154a31c32a1850d3 Mon Sep 17 00:00:00 2001 From: Nick Jacob Date: Thu, 13 Aug 2020 08:27:16 -0400 Subject: [PATCH] update amx bid adapter * add support for RTI adapters/userID * add coppa support * add first party data support * more flexibility in sizes * enable reporting by ad unit ID * document ad unit ID * add bid request count data to the request --- modules/amxBidAdapter.js | 74 +++++++++++++++++++- modules/amxBidAdapter.md | 1 + test/spec/modules/amxBidAdapter_spec.js | 91 ++++++++++++++++++++++++- 3 files changed, 163 insertions(+), 3 deletions(-) diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index 90fcf878d62..2e9529b633c 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -1,14 +1,18 @@ 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'; +import { config } from '../src/config.js'; +import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'amx'; +const storage = getStorageManager(737, BIDDER_CODE); const SIMPLE_TLD_TEST = /\.co\.\w{2,4}$/; const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; -const VERSION = 'pba1.0'; +const VERSION = 'pba1.2'; const xmlDTDRxp = /^\s*<\?xml[^\?]+\?>/; const VAST_RXP = /^\s*<\??(?:vast|xml)/i; const TRACKING_ENDPOINT = 'https://1x1.a-mo.net/hbx/'; +const AMUID_KEY = '__amuidpb'; const getLocation = (request) => parseUrl(deepAccess(request, 'refererInfo.canonicalUrl', location.href)) @@ -47,6 +51,22 @@ function getID(loc) { const enc = encodeURIComponent; +function getUIDSafe() { + try { + return storage.getDataFromLocalStorage(AMUID_KEY) + } catch (e) { + return null + } +} + +function setUIDSafe(uid) { + try { + storage.setDataInLocalStorage(AMUID_KEY, uid) + } catch (e) { + // do nothing + } +} + function nestedQs (qsData) { const out = []; Object.keys(qsData || {}).forEach((key) => { @@ -77,9 +97,20 @@ function convertRequest(bid) { const av = isVideoBid || size[1] > 100; const tid = deepAccess(bid, 'params.tagId') + const au = bid.params != null && typeof bid.params.adUnitId === 'string' + ? bid.params.adUnitId : bid.adUnitCode; + + const multiSizes = [ + bid.sizes, + deepAccess(bid, `mediaTypes.${BANNER}.sizes`, []) || [], + deepAccess(bid, `mediaTypes.${VIDEO}.sizes`, []) || [], + ] + const params = { + au, av, vr: isVideoBid, + ms: multiSizes, aw: size[0], ah: size[1], tf: 0, @@ -159,6 +190,16 @@ function resolveSize(bid, request, bidId) { return [bidRequest.aw, bidRequest.ah]; } +function values(source) { + if (Object.values != null) { + return Object.values(source) + } + + return Object.keys(source).map((key) => { + return source[key] + }); +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], @@ -173,11 +214,19 @@ export const spec = { const loc = getLocation(bidderRequest); const tagId = deepAccess(bidRequests[0], 'params.tagId', null); const testMode = deepAccess(bidRequests[0], 'params.testMode', 0); + const fbid = bidRequests[0] != null ? bidRequests[0] : { + bidderRequestsCount: 0, + bidderWinsCount: 0, + bidRequestsCount: 0 + } const payload = { a: bidderRequest.auctionId, B: 0, b: loc.host, + brc: fbid.bidderRequestsCount || 0, + bwc: fbid.bidderWinsCount || 0, + trc: fbid.bidRequestsCount || 0, tm: testMode, V: '$prebid.version$', i: (testMode && tagId != null) ? tagId : getID(loc), @@ -187,15 +236,32 @@ export const spec = { st: 'prebid', h: screen.height, w: screen.width, - gs: deepAccess(bidderRequest, 'gdprConsent.gdprApplies', '0'), + gs: deepAccess(bidderRequest, 'gdprConsent.gdprApplies', ''), gc: deepAccess(bidderRequest, 'gdprConsent.consentString', ''), u: deepAccess(bidderRequest, 'refererInfo.canonicalUrl', loc.href), do: loc.host, re: deepAccess(bidderRequest, 'refererInfo.referer'), + am: getUIDSafe(), usp: bidderRequest.uspConsent || '1---', smt: 1, d: '', m: createBidMap(bidRequests), + cpp: config.getConfig('coppa') ? 1 : 0, + fpd: config.getConfig('fpd'), + eids: values(bidRequests.reduce((all, bid) => { + // we only want unique ones in here + if (bid == null || bid.userIdAsEids == null) { + return all + } + + _each(bid.userIdAsEids, (value) => { + if (value == null) { + return; + } + all[value.source] = value + }); + return all; + }, {})), }; return { @@ -234,6 +300,10 @@ export const spec = { return []; } + if (response.am && typeof response.am === 'string') { + setUIDSafe(response.am); + } + return flatMap(Object.keys(response.r), (bidID) => { return flatMap(response.r[bidID], (siteBid) => siteBid.b.map((bid) => { diff --git a/modules/amxBidAdapter.md b/modules/amxBidAdapter.md index 4577f5f4f7c..2c38955028f 100644 --- a/modules/amxBidAdapter.md +++ b/modules/amxBidAdapter.md @@ -18,6 +18,7 @@ This module connects web publishers to AMX RTB video and display demand. | --- | -------- | ------- | ----------- | | `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 | +| `adUnitId` | no | `"sticky_banner"` | optional. To override the bid.adUnitCode provided by prebid. For use in ad-unit level reporting | # Test Parameters diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js index 790f3bf2581..4b21244501d 100644 --- a/test/spec/modules/amxBidAdapter_spec.js +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -1,7 +1,9 @@ import * as utils from 'src/utils.js'; +import { createEidsArray } from 'modules/userId/eids.js'; import { expect } from 'chai'; import { spec } from 'modules/amxBidAdapter.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; +import { config } from 'src/config.js'; const sampleRequestId = '82c91e127a9b93e'; const sampleDisplayAd = (additionalImpressions) => `${additionalImpressions}`; @@ -14,6 +16,28 @@ const sampleVideoAd = (addlImpression) => ` const embeddedTrackingPixel = `https://1x1.a-mo.net/hbx/g_impression?A=sample&B=20903`; const sampleNurl = 'https://example.exchange/nurl'; +const sampleFPD = { + context: { + keywords: 'sample keywords', + data: { + pageType: 'article' + } + }, + user: { + gender: 'O', + yob: 1982, + } +}; + +const stubConfig = (withStub) => { + const stub = sinon.stub(config, 'getConfig').callsFake( + (arg) => arg === 'fpd' ? sampleFPD : null + ) + + withStub(); + stub.restore(); +}; + const sampleBidderRequest = { gdprConsent: { gdprApplies: true, @@ -165,12 +189,59 @@ describe('AmxBidAdapter', () => { expect(data.tm).to.equal(true); }); - it('handles referer data and GDPR, USP Consent', () => { + it('handles referer data and GDPR, USP Consent, COPPA', () => { const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); delete data.m; // don't deal with "m" in this test expect(data.gs).to.equal(sampleBidderRequest.gdprConsent.gdprApplies) expect(data.gc).to.equal(sampleBidderRequest.gdprConsent.consentString) expect(data.usp).to.equal(sampleBidderRequest.uspConsent) + expect(data.cpp).to.equal(0) + }); + + it('will forward bid request count & wins count data', () => { + const bidderRequestsCount = Math.floor(Math.random() * 100) + const bidderWinsCount = Math.floor(Math.random() * 100) + const { data } = spec.buildRequests([{ + ...sampleBidRequestBase, + bidderRequestsCount, + bidderWinsCount + }], sampleBidderRequest); + + expect(data.brc).to.equal(bidderRequestsCount) + expect(data.bwc).to.equal(bidderWinsCount) + expect(data.trc).to.equal(0) + }); + it('will forward first-party data', () => { + stubConfig(() => { + const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); + expect(data.fpd).to.deep.equal(sampleFPD) + }); + }); + + it('will collect & forward RTI user IDs', () => { + const randomRTI = `greatRTI${Math.floor(Math.random() * 100)}` + const userId = { + britepoolid: 'sample-britepool', + criteoId: 'sample-criteo', + digitrustid: {data: {id: 'sample-digitrust'}}, + id5id: 'sample-id5', + idl_env: 'sample-liveramp', + lipb: {lipbid: 'sample-liveintent'}, + netId: 'sample-netid', + parrableId: { eid: 'sample-parrable' }, + pubcid: 'sample-pubcid', + [randomRTI]: 'sample-unknown', + tdid: 'sample-ttd', + }; + + const eids = createEidsArray(userId); + const bid = { + ...sampleBidRequestBase, + userIdAsEids: eids + }; + + const { data } = spec.buildRequests([bid, bid], sampleBidderRequest); + expect(data.eids).to.deep.equal(eids) }); it('can build a banner request', () => { @@ -188,6 +259,12 @@ describe('AmxBidAdapter', () => { expect(Object.keys(data.m).length).to.equal(2); expect(data.m[sampleRequestId]).to.deep.equal({ av: true, + au: 'div-gpt-ad-example', + ms: [ + [[320, 50]], + [[300, 250]], + [] + ], aw: 300, ah: 250, tf: 0, @@ -196,6 +273,12 @@ describe('AmxBidAdapter', () => { expect(data.m[sampleRequestId + '_2']).to.deep.equal({ av: true, aw: 300, + au: 'div-gpt-ad-example', + ms: [ + [[320, 50]], + [[300, 250]], + [] + ], i: 'example', ah: 250, tf: 0, @@ -207,6 +290,12 @@ describe('AmxBidAdapter', () => { const { data } = spec.buildRequests([sampleBidRequestVideo], sampleBidderRequest); expect(Object.keys(data.m).length).to.equal(1); expect(data.m[sampleRequestId + '_video']).to.deep.equal({ + au: 'div-gpt-ad-example', + ms: [ + [[300, 150]], + [], + [[360, 250]] + ], av: true, aw: 360, ah: 250,