index.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. var request = require("request");
  2. var uuid = require("uuid");
  3. var querystring = require("querystring");
  4. var utils = require("./utils");
  5. var config = require("./config");
  6. var url = require("url");
  7. var debug = require("debug")("universal-analytics");
  8. module.exports = init;
  9. function init (tid, cid, options) {
  10. return new Visitor(tid, cid, options);
  11. }
  12. var Visitor = module.exports.Visitor = function (tid, cid, options, context, persistentParams) {
  13. if (typeof tid === 'object') {
  14. options = tid;
  15. tid = cid = null;
  16. } else if (typeof cid === 'object') {
  17. options = cid;
  18. cid = null;
  19. }
  20. this._queue = [];
  21. this.options = options || {};
  22. if(this.options.hostname) {
  23. config.hostname = this.options.hostname;
  24. }
  25. if(this.options.path) {
  26. config.path = this.options.path;
  27. }
  28. if (this.options.http) {
  29. var parsedHostname = url.parse(config.hostname);
  30. config.hostname = 'http://' + parsedHostname.host;
  31. }
  32. if(this.options.enableBatching !== undefined) {
  33. config.batching = options.enableBatching;
  34. }
  35. if(this.options.batchSize) {
  36. config.batchSize = this.options.batchSize;
  37. }
  38. this._context = context || {};
  39. this._persistentParams = persistentParams || {};
  40. this.tid = tid || this.options.tid;
  41. this.cid = this._determineCid(cid, this.options.cid, (this.options.strictCidFormat !== false));
  42. if(this.options.uid) {
  43. this.uid = this.options.uid;
  44. }
  45. }
  46. module.exports.middleware = function (tid, options) {
  47. this.tid = tid;
  48. this.options = options;
  49. var cookieName = (this.options || {}).cookieName || "_ga";
  50. return function (req, res, next) {
  51. req.visitor = module.exports.createFromSession(req.session);
  52. if (req.visitor) return next();
  53. var cid;
  54. if (req.cookies && req.cookies[cookieName]) {
  55. var gaSplit = req.cookies[cookieName].split('.');
  56. cid = gaSplit[2] + "." + gaSplit[3];
  57. }
  58. req.visitor = init(tid, cid, options);
  59. if (req.session) {
  60. req.session.cid = req.visitor.cid;
  61. }
  62. next();
  63. }
  64. }
  65. module.exports.createFromSession = function (session) {
  66. if (session && session.cid) {
  67. return init(this.tid, session.cid, this.options);
  68. }
  69. }
  70. Visitor.prototype = {
  71. debug: function (d) {
  72. debug.enabled = arguments.length === 0 ? true : d;
  73. debug("visitor.debug() is deprecated: set DEBUG=universal-analytics to enable logging")
  74. return this;
  75. },
  76. reset: function () {
  77. this._context = null;
  78. return this;
  79. },
  80. set: function (key, value) {
  81. this._persistentParams = this._persistentParams || {};
  82. this._persistentParams[key] = value;
  83. },
  84. pageview: function (path, hostname, title, params, fn) {
  85. if (typeof path === 'object' && path != null) {
  86. params = path;
  87. if (typeof hostname === 'function') {
  88. fn = hostname
  89. }
  90. path = hostname = title = null;
  91. } else if (typeof hostname === 'function') {
  92. fn = hostname
  93. hostname = title = null;
  94. } else if (typeof title === 'function') {
  95. fn = title;
  96. title = null;
  97. } else if (typeof params === 'function') {
  98. fn = params;
  99. params = null;
  100. }
  101. params = this._translateParams(params);
  102. params = Object.assign({}, this._persistentParams || {}, params);
  103. params.dp = path || params.dp || this._context.dp;
  104. params.dh = hostname || params.dh || this._context.dh;
  105. params.dt = title || params.dt || this._context.dt;
  106. this._tidyParameters(params);
  107. if (!params.dp && !params.dl) {
  108. return this._handleError("Please provide either a page path (dp) or a document location (dl)", fn);
  109. }
  110. return this._withContext(params)._enqueue("pageview", params, fn);
  111. },
  112. screenview: function (screenName, appName, appVersion, appId, appInstallerId, params, fn) {
  113. if (typeof screenName === 'object' && screenName != null) {
  114. params = screenName;
  115. if (typeof appName === 'function') {
  116. fn = appName
  117. }
  118. screenName = appName = appVersion = appId = appInstallerId = null;
  119. } else if (typeof appName === 'function') {
  120. fn = appName
  121. appName = appVersion = appId = appInstallerId = null;
  122. } else if (typeof appVersion === 'function') {
  123. fn = appVersion;
  124. appVersion = appId = appInstallerId = null;
  125. } else if (typeof appId === 'function') {
  126. fn = appId;
  127. appId = appInstallerId = null;
  128. } else if (typeof appInstallerId === 'function') {
  129. fn = appInstallerId;
  130. appInstallerId = null;
  131. } else if (typeof params === 'function') {
  132. fn = params;
  133. params = null;
  134. }
  135. params = this._translateParams(params);
  136. params = Object.assign({}, this._persistentParams || {}, params);
  137. params.cd = screenName || params.cd || this._context.cd;
  138. params.an = appName || params.an || this._context.an;
  139. params.av = appVersion || params.av || this._context.av;
  140. params.aid = appId || params.aid || this._context.aid;
  141. params.aiid = appInstallerId || params.aiid || this._context.aiid;
  142. this._tidyParameters(params);
  143. if (!params.cd || !params.an) {
  144. return this._handleError("Please provide at least a screen name (cd) and an app name (an)", fn);
  145. }
  146. return this._withContext(params)._enqueue("screenview", params, fn);
  147. },
  148. event: function (category, action, label, value, params, fn) {
  149. if (typeof category === 'object' && category != null) {
  150. params = category;
  151. if (typeof action === 'function') {
  152. fn = action
  153. }
  154. category = action = label = value = null;
  155. } else if (typeof label === 'function') {
  156. fn = label;
  157. label = value = null;
  158. } else if (typeof value === 'function') {
  159. fn = value;
  160. value = null;
  161. } else if (typeof params === 'function') {
  162. fn = params;
  163. params = null;
  164. }
  165. params = this._translateParams(params);
  166. params = Object.assign({}, this._persistentParams || {}, params);
  167. params.ec = category || params.ec || this._context.ec;
  168. params.ea = action || params.ea || this._context.ea;
  169. params.el = label || params.el || this._context.el;
  170. params.ev = value || params.ev || this._context.ev;
  171. params.p = params.p || params.dp || this._context.p || this._context.dp;
  172. delete params.dp;
  173. this._tidyParameters(params);
  174. if (!params.ec || !params.ea) {
  175. return this._handleError("Please provide at least an event category (ec) and an event action (ea)", fn);
  176. }
  177. return this._withContext(params)._enqueue("event", params, fn);
  178. },
  179. transaction: function (transaction, revenue, shipping, tax, affiliation, params, fn) {
  180. if (typeof transaction === 'object') {
  181. params = transaction;
  182. if (typeof revenue === 'function') {
  183. fn = revenue
  184. }
  185. transaction = revenue = shipping = tax = affiliation = null;
  186. } else if (typeof revenue === 'function') {
  187. fn = revenue;
  188. revenue = shipping = tax = affiliation = null;
  189. } else if (typeof shipping === 'function') {
  190. fn = shipping;
  191. shipping = tax = affiliation = null;
  192. } else if (typeof tax === 'function') {
  193. fn = tax;
  194. tax = affiliation = null;
  195. } else if (typeof affiliation === 'function') {
  196. fn = affiliation;
  197. affiliation = null;
  198. } else if (typeof params === 'function') {
  199. fn = params;
  200. params = null;
  201. }
  202. params = this._translateParams(params);
  203. params = Object.assign({}, this._persistentParams || {}, params);
  204. params.ti = transaction || params.ti || this._context.ti;
  205. params.tr = revenue || params.tr || this._context.tr;
  206. params.ts = shipping || params.ts || this._context.ts;
  207. params.tt = tax || params.tt || this._context.tt;
  208. params.ta = affiliation || params.ta || this._context.ta;
  209. params.p = params.p || this._context.p || this._context.dp;
  210. this._tidyParameters(params);
  211. if (!params.ti) {
  212. return this._handleError("Please provide at least a transaction ID (ti)", fn);
  213. }
  214. return this._withContext(params)._enqueue("transaction", params, fn);
  215. },
  216. item: function (price, quantity, sku, name, variation, params, fn) {
  217. if (typeof price === 'object') {
  218. params = price;
  219. if (typeof quantity === 'function') {
  220. fn = quantity
  221. }
  222. price = quantity = sku = name = variation = null;
  223. } else if (typeof quantity === 'function') {
  224. fn = quantity;
  225. quantity = sku = name = variation = null;
  226. } else if (typeof sku === 'function') {
  227. fn = sku;
  228. sku = name = variation = null;
  229. } else if (typeof name === 'function') {
  230. fn = name;
  231. name = variation = null;
  232. } else if (typeof variation === 'function') {
  233. fn = variation;
  234. variation = null;
  235. } else if (typeof params === 'function') {
  236. fn = params;
  237. params = null;
  238. }
  239. params = this._translateParams(params);
  240. params = Object.assign({}, this._persistentParams || {}, params);
  241. params.ip = price || params.ip || this._context.ip;
  242. params.iq = quantity || params.iq || this._context.iq;
  243. params.ic = sku || params.ic || this._context.ic;
  244. params.in = name || params.in || this._context.in;
  245. params.iv = variation || params.iv || this._context.iv;
  246. params.p = params.p || this._context.p || this._context.dp;
  247. params.ti = params.ti || this._context.ti;
  248. this._tidyParameters(params);
  249. if (!params.ti) {
  250. return this._handleError("Please provide at least an item transaction ID (ti)", fn);
  251. }
  252. return this._withContext(params)._enqueue("item", params, fn);
  253. },
  254. exception: function (description, fatal, params, fn) {
  255. if (typeof description === 'object') {
  256. params = description;
  257. if (typeof fatal === 'function') {
  258. fn = fatal;
  259. }
  260. description = fatal = null;
  261. } else if (typeof fatal === 'function') {
  262. fn = fatal;
  263. fatal = 0;
  264. } else if (typeof params === 'function') {
  265. fn = params;
  266. params = null;
  267. }
  268. params = this._translateParams(params);
  269. params = Object.assign({}, this._persistentParams || {}, params);
  270. params.exd = description || params.exd || this._context.exd;
  271. params.exf = +!!(fatal || params.exf || this._context.exf);
  272. if (params.exf === 0) {
  273. delete params.exf;
  274. }
  275. this._tidyParameters(params);
  276. return this._withContext(params)._enqueue("exception", params, fn);
  277. },
  278. timing: function (category, variable, time, label, params, fn) {
  279. if (typeof category === 'object') {
  280. params = category;
  281. if (typeof variable === 'function') {
  282. fn = variable;
  283. }
  284. category = variable = time = label = null;
  285. } else if (typeof variable === 'function') {
  286. fn = variable;
  287. variable = time = label = null;
  288. } else if (typeof time === 'function') {
  289. fn = time;
  290. time = label = null;
  291. } else if (typeof label === 'function') {
  292. fn = label;
  293. label = null;
  294. } else if (typeof params === 'function') {
  295. fn = params;
  296. params = null;
  297. }
  298. params = this._translateParams(params);
  299. params = Object.assign({}, this._persistentParams || {}, params);
  300. params.utc = category || params.utc || this._context.utc;
  301. params.utv = variable || params.utv || this._context.utv;
  302. params.utt = time || params.utt || this._context.utt;
  303. params.utl = label || params.utl || this._context.utl;
  304. this._tidyParameters(params);
  305. return this._withContext(params)._enqueue("timing", params, fn);
  306. },
  307. send: function (fn) {
  308. var self = this;
  309. var count = 1;
  310. var fn = fn || function () {};
  311. debug("Sending %d tracking call(s)", self._queue.length);
  312. var getBody = function(params) {
  313. return params.map(function(x) { return querystring.stringify(x); }).join("\n");
  314. }
  315. var onFinish = function (err) {
  316. debug("Finished sending tracking calls")
  317. fn.call(self, err || null, count - 1);
  318. }
  319. var iterator = function () {
  320. if (!self._queue.length) {
  321. return onFinish(null);
  322. }
  323. var params = [];
  324. if(config.batching) {
  325. params = self._queue.splice(0, Math.min(self._queue.length, config.batchSize));
  326. } else {
  327. params.push(self._queue.shift());
  328. }
  329. var useBatchPath = params.length > 1;
  330. var path = config.hostname + (useBatchPath ? config.batchPath :config.path);
  331. debug("%d: %o", count++, params);
  332. var options = Object.assign({}, self.options.requestOptions, {
  333. body: getBody(params),
  334. headers: self.options.headers || {}
  335. });
  336. request.post(path, options, nextIteration);
  337. }
  338. function nextIteration(err) {
  339. if (err) return onFinish(err);
  340. iterator();
  341. }
  342. iterator();
  343. },
  344. _enqueue: function (type, params, fn) {
  345. if (typeof params === 'function') {
  346. fn = params;
  347. params = {};
  348. }
  349. params = this._translateParams(params) || {};
  350. Object.assign(params, {
  351. v: config.protocolVersion,
  352. tid: this.tid,
  353. cid: this.cid,
  354. t: type
  355. });
  356. if(this.uid) {
  357. params.uid = this.uid;
  358. }
  359. this._queue.push(params);
  360. if (debug.enabled) {
  361. this._checkParameters(params);
  362. }
  363. debug("Enqueued %s (%o)", type, params);
  364. if (fn) {
  365. this.send(fn);
  366. }
  367. return this;
  368. },
  369. _handleError: function (message, fn) {
  370. debug("Error: %s", message)
  371. fn && fn.call(this, new Error(message))
  372. return this;
  373. },
  374. _determineCid: function () {
  375. var args = Array.prototype.splice.call(arguments, 0);
  376. var id;
  377. var lastItem = args.length-1;
  378. var strict = args[lastItem];
  379. if (strict) {
  380. for (var i = 0; i < lastItem; i++) {
  381. id = utils.ensureValidCid(args[i]);
  382. if (id !== false) return id;
  383. if (id != null) debug("Warning! Invalid UUID format '%s'", args[i]);
  384. }
  385. } else {
  386. for (var i = 0; i < lastItem; i++) {
  387. if (args[i]) return args[i];
  388. }
  389. }
  390. return uuid.v4();
  391. },
  392. _checkParameters: function (params) {
  393. for (var param in params) {
  394. if (config.acceptedParameters.indexOf(param) !== -1 || config.acceptedParametersRegex.filter(function (r) {
  395. return r.test(param);
  396. }).length) {
  397. continue;
  398. }
  399. debug("Warning! Unsupported tracking parameter %s (%s)", param, params[param]);
  400. }
  401. },
  402. _translateParams: function (params) {
  403. var translated = {};
  404. for (var key in params) {
  405. if (config.parametersMap.hasOwnProperty(key)) {
  406. translated[config.parametersMap[key]] = params[key];
  407. } else {
  408. translated[key] = params[key];
  409. }
  410. }
  411. return translated;
  412. },
  413. _tidyParameters: function (params) {
  414. for (var param in params) {
  415. if (params[param] === null || params[param] === undefined) {
  416. delete params[param];
  417. }
  418. }
  419. return params;
  420. },
  421. _withContext: function (context) {
  422. var visitor = new Visitor(this.tid, this.cid, this.options, context, this._persistentParams);
  423. visitor._queue = this._queue;
  424. return visitor;
  425. }
  426. }
  427. Visitor.prototype.pv = Visitor.prototype.pageview
  428. Visitor.prototype.e = Visitor.prototype.event
  429. Visitor.prototype.t = Visitor.prototype.transaction
  430. Visitor.prototype.i = Visitor.prototype.item