2025-04-24 17:03:28 +08:00

564 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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