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]);
+ });
+});