564 lines
15 KiB
JavaScript
564 lines
15 KiB
JavaScript
|
|
|||
|
var request = require("request");
|
|||
|
var uuid = require("uuid");
|
|||
|
var querystring = require("querystring");
|
|||
|
|
|||
|
var utils = require("./utils");
|
|||
|
var config = require("./config");
|
|||
|
var url = require("url");
|
|||
|
|
|||
|
var debug = require("debug")("universal-analytics");
|
|||
|
|
|||
|
module.exports = init;
|
|||
|
|
|||
|
|
|||
|
function init (tid, cid, options) {
|
|||
|
return new Visitor(tid, cid, options);
|
|||
|
}
|
|||
|
|
|||
|
var Visitor = module.exports.Visitor = function (tid, cid, options, context, persistentParams) {
|
|||
|
|
|||
|
if (typeof tid === 'object') {
|
|||
|
options = tid;
|
|||
|
tid = cid = null;
|
|||
|
} else if (typeof cid === 'object') {
|
|||
|
options = cid;
|
|||
|
cid = null;
|
|||
|
}
|
|||
|
|
|||
|
this._queue = [];
|
|||
|
|
|||
|
this.options = options || {};
|
|||
|
|
|||
|
if(this.options.hostname) {
|
|||
|
config.hostname = this.options.hostname;
|
|||
|
}
|
|||
|
if(this.options.path) {
|
|||
|
config.path = this.options.path;
|
|||
|
}
|
|||
|
|
|||
|
if (this.options.http) {
|
|||
|
var parsedHostname = url.parse(config.hostname);
|
|||
|
config.hostname = 'http://' + parsedHostname.host;
|
|||
|
}
|
|||
|
|
|||
|
if(this.options.enableBatching !== undefined) {
|
|||
|
config.batching = options.enableBatching;
|
|||
|
}
|
|||
|
|
|||
|
if(this.options.batchSize) {
|
|||
|
config.batchSize = this.options.batchSize;
|
|||
|
}
|
|||
|
|
|||
|
this._context = context || {};
|
|||
|
this._persistentParams = persistentParams || {};
|
|||
|
|
|||
|
this.tid = tid || this.options.tid;
|
|||
|
this.cid = this._determineCid(cid, this.options.cid, (this.options.strictCidFormat !== false));
|
|||
|
if(this.options.uid) {
|
|||
|
this.uid = this.options.uid;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
module.exports.middleware = function (tid, options) {
|
|||
|
|
|||
|
this.tid = tid;
|
|||
|
this.options = options;
|
|||
|
|
|||
|
var cookieName = (this.options || {}).cookieName || "_ga";
|
|||
|
|
|||
|
return function (req, res, next) {
|
|||
|
|
|||
|
req.visitor = module.exports.createFromSession(req.session);
|
|||
|
|
|||
|
if (req.visitor) return next();
|
|||
|
|
|||
|
var cid;
|
|||
|
if (req.cookies && req.cookies[cookieName]) {
|
|||
|
var gaSplit = req.cookies[cookieName].split('.');
|
|||
|
cid = gaSplit[2] + "." + gaSplit[3];
|
|||
|
}
|
|||
|
|
|||
|
req.visitor = init(tid, cid, options);
|
|||
|
|
|||
|
if (req.session) {
|
|||
|
req.session.cid = req.visitor.cid;
|
|||
|
}
|
|||
|
|
|||
|
next();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
module.exports.createFromSession = function (session) {
|
|||
|
if (session && session.cid) {
|
|||
|
return init(this.tid, session.cid, this.options);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
|
|||
|
Visitor.prototype = {
|
|||
|
|
|||
|
debug: function (d) {
|
|||
|
debug.enabled = arguments.length === 0 ? true : d;
|
|||
|
debug("visitor.debug() is deprecated: set DEBUG=universal-analytics to enable logging")
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
|
|||
|
reset: function () {
|
|||
|
this._context = null;
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
set: function (key, value) {
|
|||
|
this._persistentParams = this._persistentParams || {};
|
|||
|
this._persistentParams[key] = value;
|
|||
|
},
|
|||
|
|
|||
|
pageview: function (path, hostname, title, params, fn) {
|
|||
|
|
|||
|
if (typeof path === 'object' && path != null) {
|
|||
|
params = path;
|
|||
|
if (typeof hostname === 'function') {
|
|||
|
fn = hostname
|
|||
|
}
|
|||
|
path = hostname = title = null;
|
|||
|
} else if (typeof hostname === 'function') {
|
|||
|
fn = hostname
|
|||
|
hostname = title = null;
|
|||
|
} else if (typeof title === 'function') {
|
|||
|
fn = title;
|
|||
|
title = null;
|
|||
|
} else if (typeof params === 'function') {
|
|||
|
fn = params;
|
|||
|
params = null;
|
|||
|
}
|
|||
|
|
|||
|
params = this._translateParams(params);
|
|||
|
|
|||
|
params = Object.assign({}, this._persistentParams || {}, params);
|
|||
|
|
|||
|
params.dp = path || params.dp || this._context.dp;
|
|||
|
params.dh = hostname || params.dh || this._context.dh;
|
|||
|
params.dt = title || params.dt || this._context.dt;
|
|||
|
|
|||
|
this._tidyParameters(params);
|
|||
|
|
|||
|
if (!params.dp && !params.dl) {
|
|||
|
return this._handleError("Please provide either a page path (dp) or a document location (dl)", fn);
|
|||
|
}
|
|||
|
|
|||
|
return this._withContext(params)._enqueue("pageview", params, fn);
|
|||
|
},
|
|||
|
|
|||
|
|
|||
|
screenview: function (screenName, appName, appVersion, appId, appInstallerId, params, fn) {
|
|||
|
|
|||
|
if (typeof screenName === 'object' && screenName != null) {
|
|||
|
params = screenName;
|
|||
|
if (typeof appName === 'function') {
|
|||
|
fn = appName
|
|||
|
}
|
|||
|
screenName = appName = appVersion = appId = appInstallerId = null;
|
|||
|
} else if (typeof appName === 'function') {
|
|||
|
fn = appName
|
|||
|
appName = appVersion = appId = appInstallerId = null;
|
|||
|
} else if (typeof appVersion === 'function') {
|
|||
|
fn = appVersion;
|
|||
|
appVersion = appId = appInstallerId = null;
|
|||
|
} else if (typeof appId === 'function') {
|
|||
|
fn = appId;
|
|||
|
appId = appInstallerId = null;
|
|||
|
} else if (typeof appInstallerId === 'function') {
|
|||
|
fn = appInstallerId;
|
|||
|
appInstallerId = null;
|
|||
|
} else if (typeof params === 'function') {
|
|||
|
fn = params;
|
|||
|
params = null;
|
|||
|
}
|
|||
|
|
|||
|
params = this._translateParams(params);
|
|||
|
|
|||
|
params = Object.assign({}, this._persistentParams || {}, params);
|
|||
|
|
|||
|
params.cd = screenName || params.cd || this._context.cd;
|
|||
|
params.an = appName || params.an || this._context.an;
|
|||
|
params.av = appVersion || params.av || this._context.av;
|
|||
|
params.aid = appId || params.aid || this._context.aid;
|
|||
|
params.aiid = appInstallerId || params.aiid || this._context.aiid;
|
|||
|
|
|||
|
this._tidyParameters(params);
|
|||
|
|
|||
|
if (!params.cd || !params.an) {
|
|||
|
return this._handleError("Please provide at least a screen name (cd) and an app name (an)", fn);
|
|||
|
}
|
|||
|
|
|||
|
return this._withContext(params)._enqueue("screenview", params, fn);
|
|||
|
},
|
|||
|
|
|||
|
|
|||
|
event: function (category, action, label, value, params, fn) {
|
|||
|
|
|||
|
if (typeof category === 'object' && category != null) {
|
|||
|
params = category;
|
|||
|
if (typeof action === 'function') {
|
|||
|
fn = action
|
|||
|
}
|
|||
|
category = action = label = value = null;
|
|||
|
} else if (typeof label === 'function') {
|
|||
|
fn = label;
|
|||
|
label = value = null;
|
|||
|
} else if (typeof value === 'function') {
|
|||
|
fn = value;
|
|||
|
value = null;
|
|||
|
} else if (typeof params === 'function') {
|
|||
|
fn = params;
|
|||
|
params = null;
|
|||
|
}
|
|||
|
|
|||
|
params = this._translateParams(params);
|
|||
|
|
|||
|
params = Object.assign({}, this._persistentParams || {}, params);
|
|||
|
|
|||
|
params.ec = category || params.ec || this._context.ec;
|
|||
|
params.ea = action || params.ea || this._context.ea;
|
|||
|
params.el = label || params.el || this._context.el;
|
|||
|
params.ev = value || params.ev || this._context.ev;
|
|||
|
params.p = params.p || params.dp || this._context.p || this._context.dp;
|
|||
|
|
|||
|
delete params.dp;
|
|||
|
this._tidyParameters(params);
|
|||
|
|
|||
|
if (!params.ec || !params.ea) {
|
|||
|
return this._handleError("Please provide at least an event category (ec) and an event action (ea)", fn);
|
|||
|
}
|
|||
|
|
|||
|
return this._withContext(params)._enqueue("event", params, fn);
|
|||
|
},
|
|||
|
|
|||
|
|
|||
|
transaction: function (transaction, revenue, shipping, tax, affiliation, params, fn) {
|
|||
|
if (typeof transaction === 'object') {
|
|||
|
params = transaction;
|
|||
|
if (typeof revenue === 'function') {
|
|||
|
fn = revenue
|
|||
|
}
|
|||
|
transaction = revenue = shipping = tax = affiliation = null;
|
|||
|
} else if (typeof revenue === 'function') {
|
|||
|
fn = revenue;
|
|||
|
revenue = shipping = tax = affiliation = null;
|
|||
|
} else if (typeof shipping === 'function') {
|
|||
|
fn = shipping;
|
|||
|
shipping = tax = affiliation = null;
|
|||
|
} else if (typeof tax === 'function') {
|
|||
|
fn = tax;
|
|||
|
tax = affiliation = null;
|
|||
|
} else if (typeof affiliation === 'function') {
|
|||
|
fn = affiliation;
|
|||
|
affiliation = null;
|
|||
|
} else if (typeof params === 'function') {
|
|||
|
fn = params;
|
|||
|
params = null;
|
|||
|
}
|
|||
|
|
|||
|
params = this._translateParams(params);
|
|||
|
|
|||
|
params = Object.assign({}, this._persistentParams || {}, params);
|
|||
|
|
|||
|
params.ti = transaction || params.ti || this._context.ti;
|
|||
|
params.tr = revenue || params.tr || this._context.tr;
|
|||
|
params.ts = shipping || params.ts || this._context.ts;
|
|||
|
params.tt = tax || params.tt || this._context.tt;
|
|||
|
params.ta = affiliation || params.ta || this._context.ta;
|
|||
|
params.p = params.p || this._context.p || this._context.dp;
|
|||
|
|
|||
|
this._tidyParameters(params);
|
|||
|
|
|||
|
if (!params.ti) {
|
|||
|
return this._handleError("Please provide at least a transaction ID (ti)", fn);
|
|||
|
}
|
|||
|
|
|||
|
return this._withContext(params)._enqueue("transaction", params, fn);
|
|||
|
},
|
|||
|
|
|||
|
|
|||
|
item: function (price, quantity, sku, name, variation, params, fn) {
|
|||
|
if (typeof price === 'object') {
|
|||
|
params = price;
|
|||
|
if (typeof quantity === 'function') {
|
|||
|
fn = quantity
|
|||
|
}
|
|||
|
price = quantity = sku = name = variation = null;
|
|||
|
} else if (typeof quantity === 'function') {
|
|||
|
fn = quantity;
|
|||
|
quantity = sku = name = variation = null;
|
|||
|
} else if (typeof sku === 'function') {
|
|||
|
fn = sku;
|
|||
|
sku = name = variation = null;
|
|||
|
} else if (typeof name === 'function') {
|
|||
|
fn = name;
|
|||
|
name = variation = null;
|
|||
|
} else if (typeof variation === 'function') {
|
|||
|
fn = variation;
|
|||
|
variation = null;
|
|||
|
} else if (typeof params === 'function') {
|
|||
|
fn = params;
|
|||
|
params = null;
|
|||
|
}
|
|||
|
|
|||
|
params = this._translateParams(params);
|
|||
|
|
|||
|
params = Object.assign({}, this._persistentParams || {}, params);
|
|||
|
|
|||
|
params.ip = price || params.ip || this._context.ip;
|
|||
|
params.iq = quantity || params.iq || this._context.iq;
|
|||
|
params.ic = sku || params.ic || this._context.ic;
|
|||
|
params.in = name || params.in || this._context.in;
|
|||
|
params.iv = variation || params.iv || this._context.iv;
|
|||
|
params.p = params.p || this._context.p || this._context.dp;
|
|||
|
params.ti = params.ti || this._context.ti;
|
|||
|
|
|||
|
this._tidyParameters(params);
|
|||
|
|
|||
|
if (!params.ti) {
|
|||
|
return this._handleError("Please provide at least an item transaction ID (ti)", fn);
|
|||
|
}
|
|||
|
|
|||
|
return this._withContext(params)._enqueue("item", params, fn);
|
|||
|
|
|||
|
},
|
|||
|
|
|||
|
exception: function (description, fatal, params, fn) {
|
|||
|
|
|||
|
if (typeof description === 'object') {
|
|||
|
params = description;
|
|||
|
if (typeof fatal === 'function') {
|
|||
|
fn = fatal;
|
|||
|
}
|
|||
|
description = fatal = null;
|
|||
|
} else if (typeof fatal === 'function') {
|
|||
|
fn = fatal;
|
|||
|
fatal = 0;
|
|||
|
} else if (typeof params === 'function') {
|
|||
|
fn = params;
|
|||
|
params = null;
|
|||
|
}
|
|||
|
|
|||
|
params = this._translateParams(params);
|
|||
|
|
|||
|
params = Object.assign({}, this._persistentParams || {}, params);
|
|||
|
|
|||
|
params.exd = description || params.exd || this._context.exd;
|
|||
|
params.exf = +!!(fatal || params.exf || this._context.exf);
|
|||
|
|
|||
|
if (params.exf === 0) {
|
|||
|
delete params.exf;
|
|||
|
}
|
|||
|
|
|||
|
this._tidyParameters(params);
|
|||
|
|
|||
|
return this._withContext(params)._enqueue("exception", params, fn);
|
|||
|
},
|
|||
|
|
|||
|
timing: function (category, variable, time, label, params, fn) {
|
|||
|
|
|||
|
if (typeof category === 'object') {
|
|||
|
params = category;
|
|||
|
if (typeof variable === 'function') {
|
|||
|
fn = variable;
|
|||
|
}
|
|||
|
category = variable = time = label = null;
|
|||
|
} else if (typeof variable === 'function') {
|
|||
|
fn = variable;
|
|||
|
variable = time = label = null;
|
|||
|
} else if (typeof time === 'function') {
|
|||
|
fn = time;
|
|||
|
time = label = null;
|
|||
|
} else if (typeof label === 'function') {
|
|||
|
fn = label;
|
|||
|
label = null;
|
|||
|
} else if (typeof params === 'function') {
|
|||
|
fn = params;
|
|||
|
params = null;
|
|||
|
}
|
|||
|
|
|||
|
params = this._translateParams(params);
|
|||
|
|
|||
|
params = Object.assign({}, this._persistentParams || {}, params);
|
|||
|
|
|||
|
params.utc = category || params.utc || this._context.utc;
|
|||
|
params.utv = variable || params.utv || this._context.utv;
|
|||
|
params.utt = time || params.utt || this._context.utt;
|
|||
|
params.utl = label || params.utl || this._context.utl;
|
|||
|
|
|||
|
this._tidyParameters(params);
|
|||
|
|
|||
|
return this._withContext(params)._enqueue("timing", params, fn);
|
|||
|
},
|
|||
|
|
|||
|
|
|||
|
send: function (fn) {
|
|||
|
var self = this;
|
|||
|
var count = 1;
|
|||
|
var fn = fn || function () {};
|
|||
|
debug("Sending %d tracking call(s)", self._queue.length);
|
|||
|
|
|||
|
var getBody = function(params) {
|
|||
|
return params.map(function(x) { return querystring.stringify(x); }).join("\n");
|
|||
|
}
|
|||
|
|
|||
|
var onFinish = function (err) {
|
|||
|
debug("Finished sending tracking calls")
|
|||
|
fn.call(self, err || null, count - 1);
|
|||
|
}
|
|||
|
|
|||
|
var iterator = function () {
|
|||
|
if (!self._queue.length) {
|
|||
|
return onFinish(null);
|
|||
|
}
|
|||
|
var params = [];
|
|||
|
|
|||
|
if(config.batching) {
|
|||
|
params = self._queue.splice(0, Math.min(self._queue.length, config.batchSize));
|
|||
|
} else {
|
|||
|
params.push(self._queue.shift());
|
|||
|
}
|
|||
|
|
|||
|
var useBatchPath = params.length > 1;
|
|||
|
|
|||
|
var path = config.hostname + (useBatchPath ? config.batchPath :config.path);
|
|||
|
|
|||
|
debug("%d: %o", count++, params);
|
|||
|
|
|||
|
var options = Object.assign({}, self.options.requestOptions, {
|
|||
|
body: getBody(params),
|
|||
|
headers: self.options.headers || {}
|
|||
|
});
|
|||
|
|
|||
|
request.post(path, options, nextIteration);
|
|||
|
}
|
|||
|
|
|||
|
function nextIteration(err) {
|
|||
|
if (err) return onFinish(err);
|
|||
|
iterator();
|
|||
|
}
|
|||
|
|
|||
|
iterator();
|
|||
|
|
|||
|
},
|
|||
|
|
|||
|
_enqueue: function (type, params, fn) {
|
|||
|
|
|||
|
if (typeof params === 'function') {
|
|||
|
fn = params;
|
|||
|
params = {};
|
|||
|
}
|
|||
|
|
|||
|
params = this._translateParams(params) || {};
|
|||
|
|
|||
|
Object.assign(params, {
|
|||
|
v: config.protocolVersion,
|
|||
|
tid: this.tid,
|
|||
|
cid: this.cid,
|
|||
|
t: type
|
|||
|
});
|
|||
|
if(this.uid) {
|
|||
|
params.uid = this.uid;
|
|||
|
}
|
|||
|
|
|||
|
this._queue.push(params);
|
|||
|
|
|||
|
if (debug.enabled) {
|
|||
|
this._checkParameters(params);
|
|||
|
}
|
|||
|
|
|||
|
debug("Enqueued %s (%o)", type, params);
|
|||
|
|
|||
|
if (fn) {
|
|||
|
this.send(fn);
|
|||
|
}
|
|||
|
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
|
|||
|
_handleError: function (message, fn) {
|
|||
|
debug("Error: %s", message)
|
|||
|
fn && fn.call(this, new Error(message))
|
|||
|
return this;
|
|||
|
},
|
|||
|
|
|||
|
|
|||
|
|
|||
|
_determineCid: function () {
|
|||
|
var args = Array.prototype.splice.call(arguments, 0);
|
|||
|
var id;
|
|||
|
var lastItem = args.length-1;
|
|||
|
var strict = args[lastItem];
|
|||
|
if (strict) {
|
|||
|
for (var i = 0; i < lastItem; i++) {
|
|||
|
id = utils.ensureValidCid(args[i]);
|
|||
|
if (id !== false) return id;
|
|||
|
if (id != null) debug("Warning! Invalid UUID format '%s'", args[i]);
|
|||
|
}
|
|||
|
} else {
|
|||
|
for (var i = 0; i < lastItem; i++) {
|
|||
|
if (args[i]) return args[i];
|
|||
|
}
|
|||
|
}
|
|||
|
return uuid.v4();
|
|||
|
},
|
|||
|
|
|||
|
|
|||
|
_checkParameters: function (params) {
|
|||
|
for (var param in params) {
|
|||
|
if (config.acceptedParameters.indexOf(param) !== -1 || config.acceptedParametersRegex.filter(function (r) {
|
|||
|
return r.test(param);
|
|||
|
}).length) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
debug("Warning! Unsupported tracking parameter %s (%s)", param, params[param]);
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
_translateParams: function (params) {
|
|||
|
var translated = {};
|
|||
|
for (var key in params) {
|
|||
|
if (config.parametersMap.hasOwnProperty(key)) {
|
|||
|
translated[config.parametersMap[key]] = params[key];
|
|||
|
} else {
|
|||
|
translated[key] = params[key];
|
|||
|
}
|
|||
|
}
|
|||
|
return translated;
|
|||
|
},
|
|||
|
|
|||
|
_tidyParameters: function (params) {
|
|||
|
for (var param in params) {
|
|||
|
if (params[param] === null || params[param] === undefined) {
|
|||
|
delete params[param];
|
|||
|
}
|
|||
|
}
|
|||
|
return params;
|
|||
|
},
|
|||
|
|
|||
|
_withContext: function (context) {
|
|||
|
var visitor = new Visitor(this.tid, this.cid, this.options, context, this._persistentParams);
|
|||
|
visitor._queue = this._queue;
|
|||
|
return visitor;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
Visitor.prototype.pv = Visitor.prototype.pageview
|
|||
|
Visitor.prototype.e = Visitor.prototype.event
|
|||
|
Visitor.prototype.t = Visitor.prototype.transaction
|
|||
|
Visitor.prototype.i = Visitor.prototype.item
|