diff --git a/modules/appnexusAstBidAdapter.js b/modules/appnexusAstBidAdapter.js index 78b997b3ef45..c4e2686db15b 100644 --- a/modules/appnexusAstBidAdapter.js +++ b/modules/appnexusAstBidAdapter.js @@ -211,6 +211,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 800a0fe9579c..5d6a336b2a85 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, getNativeTargeting } 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, getNativeTargeting(custBidObj)); } return keyValues; diff --git a/src/native.js b/src/native.js index 544258818ecc..c992cf9ad611 100644 --- a/src/native.js +++ b/src/native.js @@ -78,6 +78,11 @@ export function nativeBidIsValid(bid) { return false; } + // all native bid responses must define a landing page url + if (!deepAccess(bid, 'native.clickUrl')) { + return false; + } + const requestedAssets = bidRequest.nativeParams; if (!requestedAssets) { return true; @@ -94,14 +99,58 @@ 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; + + if (message.action === 'click') { + trackers = adObject['native'] && adObject['native'].clickTrackers; + } else { + trackers = adObject['native'] && adObject['native'].impressionTrackers; + } - (impressionTrackers || []).forEach(tracker => { - triggerPixel(tracker); + (trackers || []).forEach(triggerPixel); +} + +/** + * Gets native targeting key-value paris + * @param {Object} bid + * @return {Object} targeting + */ +export function getNativeTargeting(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 2402ba755f4a..efc1386fde39 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/bidmanager_spec.js b/test/spec/bidmanager_spec.js index 5263741885a1..8434206c4bfd 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' + } } ); diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js new file mode 100644 index 000000000000..977575a4d19e --- /dev/null +++ b/test/spec/native_spec.js @@ -0,0 +1,46 @@ +import { expect } from 'chai'; +import { fireNativeTrackers, getNativeTargeting } from 'src/native'; +const utils = require('src/utils'); + +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', () => { + 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', () => { + fireNativeTrackers({}, bid); + sinon.assert.calledOnce(triggerPixelStub); + sinon.assert.calledWith(triggerPixelStub, bid.native.impressionTrackers[0]); + }); + + it('fires click trackers', () => { + fireNativeTrackers({ action: 'click' }, bid); + sinon.assert.calledOnce(triggerPixelStub); + sinon.assert.calledWith(triggerPixelStub, bid.native.clickTrackers[0]); + }); +});