1 |
|
2 | var request = require("request");
|
3 | var uuid = require("uuid");
|
4 | var querystring = require("querystring");
|
5 |
|
6 | var utils = require("./utils");
|
7 | var config = require("./config");
|
8 | var url = require("url");
|
9 |
|
10 | var debug = require("debug")("universal-analytics");
|
11 |
|
12 | module.exports = init;
|
13 |
|
14 |
|
15 | function init (tid, cid, options) {
|
16 | return new Visitor(tid, cid, options);
|
17 | }
|
18 |
|
19 | var 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 |
|
66 | module.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 |
|
97 | module.exports.createFromSession = function (session) {
|
98 | if (session && session.cid) {
|
99 | return init(this.tid, session.cid, this.options);
|
100 | }
|
101 | }
|
102 |
|
103 |
|
104 |
|
105 | Visitor.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 |
|
560 | Visitor.prototype.pv = Visitor.prototype.pageview
|
561 | Visitor.prototype.e = Visitor.prototype.event
|
562 | Visitor.prototype.t = Visitor.prototype.transaction
|
563 | Visitor.prototype.i = Visitor.prototype.item
|