From 02e9f599754d553bbef934fe452ce01ba65cc9e1 Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Fri, 6 Oct 2017 17:00:55 -0700 Subject: [PATCH 1/3] Implement native click tracking * Fire based on postMessage in adserver creative * Fix tests, add comments --- modules/appnexusAstBidAdapter.js | 1 + src/bidmanager.js | 9 ++--- src/native.js | 60 ++++++++++++++++++++++++++++---- src/secureCreatives.js | 4 +-- test/spec/native_spec.js | 33 ++++++++++++++++++ 5 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 test/spec/native_spec.js diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index b0997992a0c..8d11c6420ac 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -201,6 +201,7 @@ function newBid(serverBid, rtbBid) { image: nativeAd.main_img && nativeAd.main_img.url, icon: nativeAd.icon && nativeAd.icon.url, clickUrl: nativeAd.link.url, + clickTrackers: nativeAd.link.click_trackers, impressionTrackers: nativeAd.impression_trackers, }; } else { diff --git a/src/bidmanager.js b/src/bidmanager.js index 800a0fe9579..4a217db2473 100644 --- a/src/bidmanager.js +++ b/src/bidmanager.js @@ -1,6 +1,6 @@ import { uniques, flatten, adUnitsFilter, getBidderRequest } from './utils'; import { getPriceBucketString } from './cpmBucketManager'; -import { NATIVE_KEYS, nativeBidIsValid } from './native'; +import { nativeBidIsValid, setNativeTargeting } from './native'; import { isValidVideoBid } from './video'; import { getCacheUrl, store } from './videoCache'; import { Renderer } from 'src/Renderer'; @@ -275,13 +275,8 @@ function getKeyValueTargetingPairs(bidderCode, custBidObj) { custBidObj.sendStandardTargeting = defaultBidderSettingsMap[bidderCode].sendStandardTargeting; } - // set native key value targeting if (custBidObj['native']) { - Object.keys(custBidObj['native']).forEach(asset => { - const key = NATIVE_KEYS[asset]; - const value = custBidObj['native'][asset]; - if (key) { keyValues[key] = value; } - }); + keyValues = Object.assign({}, keyValues, setNativeTargeting(custBidObj)); } return keyValues; diff --git a/src/native.js b/src/native.js index 544258818ec..496b827e8a3 100644 --- a/src/native.js +++ b/src/native.js @@ -94,14 +94,60 @@ export function nativeBidIsValid(bid) { } /* - * Native responses may have impression trackers. This retrieves the - * impression tracker urls for the given ad object and fires them. + * Native responses may have associated impression or click trackers. + * This retrieves the appropriate tracker urls for the given ad object and + * fires them. As a native creatives may be in a cross-origin frame, it may be + * necessary to invoke this function via postMessage. secureCreatives is + * configured to fire this function when it receives a `message` of 'Prebid Native' + * and an `adId` with the value of the `bid.adId`. When a message is posted with + * these parameters, impression trackers are fired. To fire click trackers, the + * message should contain an `action` set to 'click'. + * + * // Native creative template example usage + * + * %%PATTERN:hb_native_title%% + * + * + * */ -export function fireNativeImpressions(adObject) { - const impressionTrackers = - adObject['native'] && adObject['native'].impressionTrackers; +export function fireNativeTrackers(message, adObject) { + let trackers; - (impressionTrackers || []).forEach(tracker => { - triggerPixel(tracker); + if (message.action === 'click') { + trackers = adObject['native'] && adObject['native'].clickTrackers; + } else { + trackers = adObject['native'] && adObject['native'].impressionTrackers; + } + + (trackers || []).forEach(triggerPixel); + + return trackers; +} + +/** + * Sets native targeting key-value paris + * @param {Object} bid + * @return {Object} targeting + */ +export function setNativeTargeting(bid) { + let keyValues = {}; + + Object.keys(bid['native']).forEach(asset => { + const key = NATIVE_KEYS[asset]; + const value = bid['native'][asset]; + if (key) { + keyValues[key] = value; + } }); + + return keyValues; } diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 2402ba755f4..efc1386fde3 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -4,7 +4,7 @@ */ import events from './events'; -import { fireNativeImpressions } from './native'; +import { fireNativeTrackers } from './native'; import { EVENTS } from './constants'; const BID_WON = EVENTS.BID_WON; @@ -42,7 +42,7 @@ function receiveMessage(ev) { // adId: '%%PATTERN:hb_adid%%' // }), '*'); if (data.message === 'Prebid Native') { - fireNativeImpressions(adObject); + fireNativeTrackers(data, adObject); $$PREBID_GLOBAL$$._winningBids.push(adObject); events.emit(BID_WON, adObject); } diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js new file mode 100644 index 00000000000..c4a5d9a5a9d --- /dev/null +++ b/test/spec/native_spec.js @@ -0,0 +1,33 @@ +import { expect } from 'chai'; +import { fireNativeTrackers, setNativeTargeting } from 'src/native'; + +const bid = { + native: { + title: 'Native Creative', + body: 'Cool description great stuff', + cta: 'Do it', + sponsoredBy: 'AppNexus', + clickUrl: 'https://www.link.example', + clickTrackers: ['https://tracker.example'], + impressionTrackers: ['https://impression.example'], + } +}; + +describe('native.js', () => { + it('sets native targeting keys', () => { + const targeting = setNativeTargeting(bid); + expect(targeting.hb_native_title).to.equal(bid.native.title); + expect(targeting.hb_native_body).to.equal(bid.native.body); + expect(targeting.hb_native_linkurl).to.equal(bid.native.clickUrl); + }); + + it('fires impression trackers', () => { + const fired = fireNativeTrackers({}, bid); + expect(fired).to.deep.equal(bid.native.impressionTrackers); + }); + + it('fires click trackers', () => { + const fired = fireNativeTrackers({ action: 'click' }, bid); + expect(fired).to.deep.equal(bid.native.clickTrackers); + }); +}); From 8a815b0bdd4b791a6a7acade72c9bb680fa34ec6 Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Fri, 13 Oct 2017 12:48:47 -0700 Subject: [PATCH 2/3] Require landing page urls on native bid responses --- src/native.js | 5 +++++ test/spec/bidmanager_spec.js | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/native.js b/src/native.js index 496b827e8a3..f499194c71c 100644 --- a/src/native.js +++ b/src/native.js @@ -78,6 +78,11 @@ export function nativeBidIsValid(bid) { return false; } + // all native bid responses must defined a landing page url + if (!deepAccess(bid, 'native.clickUrl')) { + return false; + } + const requestedAssets = bidRequest.nativeParams; if (!requestedAssets) { return true; diff --git a/test/spec/bidmanager_spec.js b/test/spec/bidmanager_spec.js index 5263741885a..8434206c4bf 100644 --- a/test/spec/bidmanager_spec.js +++ b/test/spec/bidmanager_spec.js @@ -590,7 +590,10 @@ describe('bidmanager.js', function () { { bidderCode: 'appnexusAst', mediaType: 'native', - native: {title: 'foo'} + native: { + title: 'foo', + clickUrl: 'example.link' + } } ); From 5b0c078feb850572e8540efab8db151cba6fb185 Mon Sep 17 00:00:00 2001 From: Matt Lane Date: Tue, 17 Oct 2017 11:07:05 -0700 Subject: [PATCH 3/3] Address code review comments --- src/bidmanager.js | 4 ++-- src/native.js | 8 +++----- test/spec/native_spec.js | 27 ++++++++++++++++++++------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/bidmanager.js b/src/bidmanager.js index 4a217db2473..5d6a336b2a8 100644 --- a/src/bidmanager.js +++ b/src/bidmanager.js @@ -1,6 +1,6 @@ import { uniques, flatten, adUnitsFilter, getBidderRequest } from './utils'; import { getPriceBucketString } from './cpmBucketManager'; -import { nativeBidIsValid, setNativeTargeting } from './native'; +import { nativeBidIsValid, getNativeTargeting } from './native'; import { isValidVideoBid } from './video'; import { getCacheUrl, store } from './videoCache'; import { Renderer } from 'src/Renderer'; @@ -276,7 +276,7 @@ function getKeyValueTargetingPairs(bidderCode, custBidObj) { } if (custBidObj['native']) { - keyValues = Object.assign({}, keyValues, setNativeTargeting(custBidObj)); + keyValues = Object.assign({}, keyValues, getNativeTargeting(custBidObj)); } return keyValues; diff --git a/src/native.js b/src/native.js index f499194c71c..c992cf9ad61 100644 --- a/src/native.js +++ b/src/native.js @@ -78,7 +78,7 @@ export function nativeBidIsValid(bid) { return false; } - // all native bid responses must defined a landing page url + // all native bid responses must define a landing page url if (!deepAccess(bid, 'native.clickUrl')) { return false; } @@ -134,16 +134,14 @@ export function fireNativeTrackers(message, adObject) { } (trackers || []).forEach(triggerPixel); - - return trackers; } /** - * Sets native targeting key-value paris + * Gets native targeting key-value paris * @param {Object} bid * @return {Object} targeting */ -export function setNativeTargeting(bid) { +export function getNativeTargeting(bid) { let keyValues = {}; Object.keys(bid['native']).forEach(asset => { diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index c4a5d9a5a9d..977575a4d19 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; -import { fireNativeTrackers, setNativeTargeting } from 'src/native'; +import { fireNativeTrackers, getNativeTargeting } from 'src/native'; +const utils = require('src/utils'); const bid = { native: { @@ -14,20 +15,32 @@ const bid = { }; describe('native.js', () => { - it('sets native targeting keys', () => { - const targeting = setNativeTargeting(bid); + let triggerPixelStub; + + beforeEach(() => { + triggerPixelStub = sinon.stub(utils, 'triggerPixel'); + }); + + afterEach(() => { + utils.triggerPixel.restore(); + }); + + it('gets native targeting keys', () => { + const targeting = getNativeTargeting(bid); expect(targeting.hb_native_title).to.equal(bid.native.title); expect(targeting.hb_native_body).to.equal(bid.native.body); expect(targeting.hb_native_linkurl).to.equal(bid.native.clickUrl); }); it('fires impression trackers', () => { - const fired = fireNativeTrackers({}, bid); - expect(fired).to.deep.equal(bid.native.impressionTrackers); + fireNativeTrackers({}, bid); + sinon.assert.calledOnce(triggerPixelStub); + sinon.assert.calledWith(triggerPixelStub, bid.native.impressionTrackers[0]); }); it('fires click trackers', () => { - const fired = fireNativeTrackers({ action: 'click' }, bid); - expect(fired).to.deep.equal(bid.native.clickTrackers); + fireNativeTrackers({ action: 'click' }, bid); + sinon.assert.calledOnce(triggerPixelStub); + sinon.assert.calledWith(triggerPixelStub, bid.native.clickTrackers[0]); }); });