Skip to content

Commit

Permalink
Support native click tracking (prebid#1691)
Browse files Browse the repository at this point in the history
* Implement native click tracking

* Fire based on postMessage in adserver creative
* Fix tests, add comments

* Require landing page urls on native bid responses

* Address code review comments
  • Loading branch information
matthewlane authored and mattpr committed Oct 31, 2017
1 parent 7f68db9 commit b454e2b
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 17 deletions.
1 change: 1 addition & 0 deletions modules/appnexusAstBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 2 additions & 7 deletions src/bidmanager.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down
63 changes: 56 additions & 7 deletions src/native.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
* <a href="%%CLICK_URL_UNESC%%%%PATTERN:hb_native_linkurl%%"
* target="_blank"
* onclick="fireTrackers('click')">
* %%PATTERN:hb_native_title%%
* </a>
*
* <script>
* function fireTrackers(action) {
* var message = {message: 'Prebid Native', adId: '%%PATTERN:hb_adid%%'};
* if (action === 'click') {message.action = 'click';} // fires click trackers
* window.parent.postMessage(JSON.stringify(message), '*');
* }
* fireTrackers(); // fires impressions when creative is loaded
* </script>
*/
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;
}
4 changes: 2 additions & 2 deletions src/secureCreatives.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 4 additions & 1 deletion test/spec/bidmanager_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,10 @@ describe('bidmanager.js', function () {
{
bidderCode: 'appnexusAst',
mediaType: 'native',
native: {title: 'foo'}
native: {
title: 'foo',
clickUrl: 'example.link'
}
}
);

Expand Down
46 changes: 46 additions & 0 deletions test/spec/native_spec.js
Original file line number Diff line number Diff line change
@@ -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]);
});
});

0 comments on commit b454e2b

Please sign in to comment.