UNPKG

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