Skip to content

Commit

Permalink
Prebid Analytics API (prebid#297)
Browse files Browse the repository at this point in the history
* Prebid Analytics API

Provides two new Prebid API methods to register analytics adapters and enable analytics tracking for any analytics module.
* `pbjs.enableAnalytics()` is called once on the page after all analytics libraries have been loaded
* `pbjs.registerAnalyticsAdapter` is available to register an adapter dynamically (optional)

An analytics provider can be configured with these steps:

1. Add the analytics library to your page as described in the provider's documentation (see `integrationExamples/gpt/examples...` for Google Analytics.)
1. Add a call to `pbjs.enableAnalytics(analyticsAdapters)`. `analyticsAdapters` could look like:
```
pbjs.que.push(function () {
      pbjs.enableAnalytics([{
          provider: 'ga',
          options: {
              global: 'ga',
              enableDistribution: false

          }
      }, {
              provider: 'example',
              options: {
                      foo: 1234
              }
      }
      ]);
  });
```
1. Create an analytics adapter to listen for Prebid events and call the analytics library (see `src/adapters/analytics/ga.js`)
1. Add the analytics adapter file name to the "analytics" array in `package.json`.

* review notes: restore `enableAnalytics` method name, prevent duplicate calls to adapter.enableAnalytics
* set analytics example libraries url to fqdn localhost:9999
* call _enqueue from containing scope
* fix bug `this` undefined
* Add `analyticsType` as 'library' or 'endpoint' and provide an `ajax` module to use when sending event payloads to an endpoint
Configure cross-repo analytics builds
* Consolidate logic
* Use in devbuild task
* Revert default param to work in node v5

* Read analytic sources before prebid source

* Update to address code notes

* Load adapters only if defined in  array
  • Loading branch information
Nate Guisinger authored and matthewlane committed Jul 1, 2016
1 parent 8fee8d7 commit e122486
Show file tree
Hide file tree
Showing 23 changed files with 763 additions and 117 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ gpt-each-bidder3.html

integrationExamples/gpt/gpt.html
integrationExamples/implementations/
src/adapters/analytics/libraries

# Coverage reports
build/coverage/
Expand Down
35 changes: 35 additions & 0 deletions gulpHelpers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
const fs = require('fs');
const path = require('path');
const argv = require('yargs').argv;
const MANIFEST = 'package.json';

module.exports = {
parseBrowserArgs: function (argv) {
return (argv.browsers) ? argv.browsers.split(',') : [];
Expand All @@ -12,5 +17,35 @@ module.exports = {
return str.replace(/\n/g, '')
.replace(/<\//g, '<\\/')
.replace(/\/>/g, '\\/>');
},

/*
* Get source files for analytics subdirectories in top-level `analytics`
* directory adjacent to Prebid.js.
* Invoke with gulp <task> --analytics
* Returns an array of source files for inclusion in build process
*/
getAnalyticsSources: function(directory) {
if (!argv.analytics) {return [];} // empty arrays won't affect a standard build

const directoryContents = fs.readdirSync(directory);
return directoryContents
.filter(file => isModuleDirectory(path.join(directory, file)))
.map(moduleDirectory => {
const module = require(path.join(directory, moduleDirectory, MANIFEST));
return path.join(directory, moduleDirectory, module.main);
});

// get only subdirectories that contain package.json with 'main' property
function isModuleDirectory(filePath) {
try {
const manifestPath = path.join(filePath, MANIFEST);
if (fs.statSync(manifestPath).isFile()) {
const module = require(manifestPath);
return module && module.main;
}
}
catch (error) {}
}
}
};
6 changes: 3 additions & 3 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ var prebid = require('./package.json');
var dateString = 'Updated : ' + (new Date()).toISOString().substring(0, 10);
var packageNameVersion = prebid.name + '_' + prebid.version;
var banner = '/* <%= prebid.name %> v<%= prebid.version %>\n' + dateString + ' */\n';
var analyticsDirectory = '../analytics';

// Tasks
gulp.task('default', ['clean', 'quality', 'webpack']);
Expand All @@ -42,7 +43,7 @@ gulp.task('clean', function () {

gulp.task('devpack', function () {
webpackConfig.devtool = 'source-map';
return gulp.src(['src/prebid.js'])
return gulp.src([...helpers.getAnalyticsSources(analyticsDirectory), 'src/prebid.js'])
.pipe(webpack(webpackConfig))
.pipe(replace('$prebid.version$', prebid.version))
.pipe(gulp.dest('build/dev'))
Expand All @@ -58,7 +59,7 @@ gulp.task('webpack', function () {

webpackConfig.devtool = null;

return gulp.src(['src/prebid.js'])
return gulp.src([...helpers.getAnalyticsSources(analyticsDirectory), 'src/prebid.js'])
.pipe(webpack(webpackConfig))
.pipe(replace('$prebid.version$', prebid.version))
.pipe(uglify())
Expand Down Expand Up @@ -175,4 +176,3 @@ gulp.task('docs', ['clean-docs'], function () {
})
.pipe(gulp.dest('docs'));
});

1 change: 0 additions & 1 deletion integrationExamples/gpt/gpt_aliasingBidder.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@
timeout: 1000
*/
});

});

</script>
Expand Down
40 changes: 40 additions & 0 deletions loaders/analyticsLoader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict';

const fs = require('fs');
const blockLoader = require('block-loader');
let analyticsAdapters = require('../package.json').analytics;

var options = {
start: '/** INSERT ANALYTICS - DO NOT EDIT OR REMOVE */',
end: '/** END INSERT ANALYTICS */',
process: function insertAnalytics() {
// read directory for analytics adapter file names, map the file names to String.replace,
// use a regex to remove file extensions, then return the array of adapter names
const files = fs.readdirSync('src/adapters/analytics')
.map(file => file.replace(/\.[^/.]+$/, ''));

let adapters = analyticsAdapters.map(adapter => adapter.length ? adapter : Object.keys(adapter)[0]);

let inserts = adapters.filter(adapter => {
if (files.includes(adapter)) {
return adapter;
} else {
console.log(`Prebid Warning: no adapter found for ${adapter}, continuing.`);
}
});

// if no matching adapters and no adapter files found, exit
if (!inserts || !inserts.length) {
return null;
}

// return the javascript strings to insert into adaptermanager.js
return inserts.map((adapter) => {
return `var ${adapter} = require('./adapters/analytics/${adapter}.js').default
|| require('./adapters/analytics/${adapter}.js');
exports.registerAnalyticsAdapter({ adapter: ${adapter}, code: '${adapter}' });\n`;
}).join('');
}
};

module.exports = blockLoader(options);
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
}
}
],
"analytics": [],
"author": "the prebid.js contributors",
"license": "Apache-2.0",
"devDependencies": {
Expand Down
43 changes: 40 additions & 3 deletions src/adaptermanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { BaseAdapter } from './adapters/baseAdapter';
var _bidderRegistry = {};
exports.bidderRegistry = _bidderRegistry;

var _analyticsRegistry = {};

function getBids({ bidderCode, requestId, bidderRequestId, adUnits }) {
return adUnits.map(adUnit => {
return adUnit.bids.filter(bid => bid.bidder === bidderCode)
Expand All @@ -23,7 +25,7 @@ function getBids({ bidderCode, requestId, bidderRequestId, adUnits }) {
}).reduce(flatten, []);
}

exports.callBids = ({ adUnits }) => {
exports.callBids = ({ adUnits, cbTimeout }) => {
const requestId = utils.getUniqueIdentifierStr();

getBidderCodes(adUnits).forEach(bidderCode => {
Expand All @@ -35,7 +37,8 @@ exports.callBids = ({ adUnits }) => {
requestId,
bidderRequestId,
bids: getBids({ bidderCode, requestId, bidderRequestId, adUnits }),
start: new Date().getTime()
start: new Date().getTime(),
timeout: cbTimeout
};
utils.logMessage(`CALLING BIDDER ======= ${bidderCode}`);
pbjs._bidsRequested.push(bidderRequest);
Expand Down Expand Up @@ -92,7 +95,41 @@ exports.aliasBidAdapter = function (bidderCode, alias) {
}
};

exports.registerAnalyticsAdapter = function ({ adapter, code }) {
if (adapter && code) {

if (typeof adapter.enableAnalytics === CONSTANTS.objectType_function) {
adapter.code = code;
_analyticsRegistry[code] = adapter;
} else {
utils.logError(`Prebid Error: Analytics adaptor error for analytics "${code}"
analytics adapter must implement an enableAnalytics() function`);
}
} else {
utils.logError('Prebid Error: analyticsAdapter or analyticsCode not specified');
}
};

exports.enableAnalytics = function (config) {
if (!utils.isArray(config)) {
config = [config];
}

utils._each(config, adapterConfig => {
var adapter = _analyticsRegistry[adapterConfig.provider];
if (adapter) {
adapter.enableAnalytics(adapterConfig);
} else {
utils.logError(`Prebid Error: no analytics adapter found in registry for
${adapterConfig.provider}.`);
}
});
};

/** INSERT ADAPTERS - DO NOT EDIT OR REMOVE */

// here be adapters
/** END INSERT ADAPTERS */

/** INSERT ANALYTICS - DO NOT EDIT OR REMOVE */

/** END INSERT ANALYTICS */
119 changes: 119 additions & 0 deletions src/adapters/analytics/AnalyticsAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import CONSTANTS from 'src/constants.json';
import { loadScript } from 'src/adloader';
import { ajax } from 'src/ajax';

const events = require('src/events');
const utils = require('../../utils');

const BID_REQUESTED = CONSTANTS.EVENTS.BID_REQUESTED;
const BID_TIMEOUT = CONSTANTS.EVENTS.BID_TIMEOUT;
const BID_RESPONSE = CONSTANTS.EVENTS.BID_RESPONSE;
const BID_WON = CONSTANTS.EVENTS.BID_WON;
const BID_ADJUSTMENT = CONSTANTS.EVENTS.BID_ADJUSTMENT;

const LIBRARY = 'library';
const ENDPOINT = 'endpoint';
const BUNDLE = 'bundle';

var _timedOutBidders = [];

export default function AnalyticsAdapter({ url, analyticsType, global, handler }) {
var _queue = [];
var _eventCount = 0;
var _enableCheck = true;

if (analyticsType === LIBRARY) {
loadScript(url, _emptyQueue);
}

if (analyticsType === ENDPOINT || BUNDLE) {
_emptyQueue();
}

return {
track: _track,
enqueue: _enqueue,
enableAnalytics: _enable,
getAdapterType: () => analyticsType,
getGlobal: () => global,
getHandler: () => handler,
getUrl: () => url
};

function _track({ eventType, args }) {
if (this.getAdapterType() === LIBRARY || BUNDLE) {
window[global](handler, eventType, args);
}

if (this.getAdapterType() === ENDPOINT) {
_callEndpoint(...arguments);
}
}

function _callEndpoint({ eventType, args, callback }) {
ajax(url, callback, JSON.stringify({ eventType, args }));
}

function _enqueue({ eventType, args }) {
const _this = this;

if (global && window[global] && eventType && args) {
this.track({ eventType, args });
} else {
_queue.push(function () {
_eventCount++;
_this.track({ eventType, args });
});
}
}

function _enable() {
var _this = this;

//first send all events fired before enableAnalytics called
events.getEvents().forEach(event => {
if (!event) {
return;
}

const { eventType, args } = event;

if (eventType === BID_TIMEOUT) {
_timedOutBidders = args.bidderCode;
} else {
_enqueue.call(_this, { eventType, args });
}
});

//Next register event listeners to send data immediately

//bidRequests
events.on(BID_REQUESTED, args => this.enqueue({ eventType: BID_REQUESTED, args }));
events.on(BID_RESPONSE, args => this.enqueue({ eventType: BID_RESPONSE, args }));
events.on(BID_TIMEOUT, args => this.enqueue({ eventType: BID_TIMEOUT, args }));
events.on(BID_WON, args => this.enqueue({ eventType: BID_WON, args }));
events.on(BID_ADJUSTMENT, args => this.enqueue({ eventType: BID_ADJUSTMENT, args }));

// finally set this function to return log message, prevents multiple adapter listeners
this.enableAnalytics = function _enable() {
return utils.logMessage(`Analytics adapter for "${global}" already enabled, unnecessary call to \`enableAnalytics\`.`);
};
}

function _emptyQueue() {
if (_enableCheck) {
for (var i = 0; i < _queue.length; i++) {
_queue[i]();
}

//override push to execute the command immediately from now on
_queue.push = function (fn) {
fn();
};

_enableCheck = false;
}

utils.logMessage(`event count sent to ${global}: ${_eventCount}`);
}
}
12 changes: 12 additions & 0 deletions src/adapters/analytics/appnexus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* appnexus.js - AppNexus Prebid Analytics Adapter
*/

import adapter from 'AnalyticsAdapter';

export default adapter({
global: 'AppNexusPrebidAnalytics',
handler: 'on',
analyticsType: 'bundle'
});

14 changes: 14 additions & 0 deletions src/adapters/analytics/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* example.js - analytics adapter for Example Analytics Library example
*/

import adapter from 'AnalyticsAdapter';

export default adapter(
{
url: 'http://localhost:9999/src/adapters/analytics/libraries/example.js',
global: 'ExampleAnalyticsGlobalObject',
handler: 'on',
analyticsType: 'library'
}
);
25 changes: 25 additions & 0 deletions src/adapters/analytics/example2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ajax } from 'src/ajax';

/**
* example2.js - analytics adapter for Example2 Analytics Endpoint example
*/

import adapter from 'AnalyticsAdapter';
const utils = require('../../utils');

const url = 'https://httpbin.org/post';
const analyticsType = 'endpoint';

export default utils.extend(adapter(
{
url,
analyticsType
}
),
{
// Override AnalyticsAdapter functions by supplying custom methods
track({ eventType, args }) {
console.log('track function override for Example2 Analytics');
ajax(url, (result) => console.log('Analytics Endpoint Example2: result = ' + result), JSON.stringify({ eventType, args }));
}
});
Loading

0 comments on commit e122486

Please sign in to comment.