mixpanel-browser 2.57.1 → 2.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,434 @@
1
+ // stateless utils
2
+ // mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
3
+
4
+ import { _, console_with_prefix, document } from '../utils'; // eslint-disable-line camelcase
5
+ import { window } from '../window';
6
+
7
+ var EV_CHANGE = 'change';
8
+ var EV_CLICK = 'click';
9
+ var EV_HASHCHANGE = 'hashchange';
10
+ var EV_MP_LOCATION_CHANGE = 'mp_locationchange';
11
+ var EV_POPSTATE = 'popstate';
12
+ // TODO scrollend isn't available in Safari: document or polyfill?
13
+ var EV_SCROLLEND = 'scrollend';
14
+ var EV_SUBMIT = 'submit';
15
+
16
+ var CLICK_EVENT_PROPS = [
17
+ 'clientX', 'clientY',
18
+ 'offsetX', 'offsetY',
19
+ 'pageX', 'pageY',
20
+ 'screenX', 'screenY',
21
+ 'x', 'y'
22
+ ];
23
+ var OPT_IN_CLASSES = ['mp-include'];
24
+ var OPT_OUT_CLASSES = ['mp-no-track'];
25
+ var SENSITIVE_DATA_CLASSES = OPT_OUT_CLASSES.concat(['mp-sensitive']);
26
+ var TRACKED_ATTRS = [
27
+ 'aria-label', 'aria-labelledby', 'aria-describedby',
28
+ 'href', 'name', 'role', 'title', 'type'
29
+ ];
30
+
31
+ var logger = console_with_prefix('autocapture');
32
+
33
+
34
+ function getClasses(el) {
35
+ var classes = {};
36
+ var classList = getClassName(el).split(' ');
37
+ for (var i = 0; i < classList.length; i++) {
38
+ var cls = classList[i];
39
+ if (cls) {
40
+ classes[cls] = true;
41
+ }
42
+ }
43
+ return classes;
44
+ }
45
+
46
+ /*
47
+ * Get the className of an element, accounting for edge cases where element.className is an object
48
+ * @param {Element} el - element to get the className of
49
+ * @returns {string} the element's class
50
+ */
51
+ function getClassName(el) {
52
+ switch(typeof el.className) {
53
+ case 'string':
54
+ return el.className;
55
+ case 'object': // handle cases where className might be SVGAnimatedString or some other type
56
+ return el.className.baseVal || el.getAttribute('class') || '';
57
+ default: // future proof
58
+ return '';
59
+ }
60
+ }
61
+
62
+ function getPreviousElementSibling(el) {
63
+ if (el.previousElementSibling) {
64
+ return el.previousElementSibling;
65
+ } else {
66
+ do {
67
+ el = el.previousSibling;
68
+ } while (el && !isElementNode(el));
69
+ return el;
70
+ }
71
+ }
72
+
73
+ function getPropertiesFromElement(el) {
74
+ var props = {
75
+ '$classes': getClassName(el).split(' '),
76
+ '$tag_name': el.tagName.toLowerCase()
77
+ };
78
+ var elId = el.id;
79
+ if (elId) {
80
+ props['$id'] = elId;
81
+ }
82
+
83
+ if (shouldTrackElement(el)) {
84
+ _.each(TRACKED_ATTRS, function(attr) {
85
+ if (el.hasAttribute(attr)) {
86
+ var attrVal = el.getAttribute(attr);
87
+ if (shouldTrackValue(attrVal)) {
88
+ props['$attr-' + attr] = attrVal;
89
+ }
90
+ }
91
+ });
92
+ }
93
+
94
+ var nthChild = 1;
95
+ var nthOfType = 1;
96
+ var currentElem = el;
97
+ while (currentElem = getPreviousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign
98
+ nthChild++;
99
+ if (currentElem.tagName === el.tagName) {
100
+ nthOfType++;
101
+ }
102
+ }
103
+ props['$nth_child'] = nthChild;
104
+ props['$nth_of_type'] = nthOfType;
105
+
106
+ return props;
107
+ }
108
+
109
+ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
110
+ blockSelectors = blockSelectors || [];
111
+ var props = null;
112
+
113
+ var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
114
+ if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html)
115
+ target = target.parentNode;
116
+ }
117
+
118
+ if (shouldTrackDomEvent(target, ev)) {
119
+ var targetElementList = [target];
120
+ var curEl = target;
121
+ while (curEl.parentNode && !isTag(curEl, 'body')) {
122
+ targetElementList.push(curEl.parentNode);
123
+ curEl = curEl.parentNode;
124
+ }
125
+
126
+ var elementsJson = [];
127
+ var href, explicitNoTrack = false;
128
+ _.each(targetElementList, function(el) {
129
+ var shouldTrackEl = shouldTrackElement(el);
130
+
131
+ // if the element or a parent element is an anchor tag
132
+ // include the href as a property
133
+ if (el.tagName.toLowerCase() === 'a') {
134
+ href = el.getAttribute('href');
135
+ href = shouldTrackEl && shouldTrackValue(href) && href;
136
+ }
137
+
138
+ // allow users to programmatically prevent tracking of elements by adding classes such as 'mp-no-track'
139
+ var classes = getClasses(el);
140
+ _.each(OPT_OUT_CLASSES, function(cls) {
141
+ if (classes[cls]) {
142
+ explicitNoTrack = true;
143
+ }
144
+ });
145
+
146
+ if (!explicitNoTrack) {
147
+ // programmatically prevent tracking of elements that match CSS selectors
148
+ _.each(blockSelectors, function(sel) {
149
+ try {
150
+ if (el['matches'](sel)) {
151
+ explicitNoTrack = true;
152
+ }
153
+ } catch (err) {
154
+ logger.critical('Error while checking selector: ' + sel, err);
155
+ }
156
+ });
157
+ }
158
+
159
+ elementsJson.push(getPropertiesFromElement(el));
160
+ }, this);
161
+
162
+ if (!explicitNoTrack) {
163
+ var docElement = document['documentElement'];
164
+ props = {
165
+ '$event_type': ev.type,
166
+ '$host': window.location.host,
167
+ '$pathname': window.location.pathname,
168
+ '$elements': elementsJson,
169
+ '$el_attr__href': href,
170
+ '$viewportHeight': Math.max(docElement['clientHeight'], window['innerHeight'] || 0),
171
+ '$viewportWidth': Math.max(docElement['clientWidth'], window['innerWidth'] || 0)
172
+ };
173
+
174
+ if (captureTextContent) {
175
+ elementText = getSafeText(target);
176
+ if (elementText && elementText.length) {
177
+ props['$el_text'] = elementText;
178
+ }
179
+ }
180
+
181
+ if (ev.type === EV_CLICK) {
182
+ _.each(CLICK_EVENT_PROPS, function(prop) {
183
+ if (prop in ev) {
184
+ props['$' + prop] = ev[prop];
185
+ }
186
+ });
187
+ target = guessRealClickTarget(ev);
188
+ }
189
+ // prioritize text content from "real" click target if different from original target
190
+ if (captureTextContent) {
191
+ var elementText = getSafeText(target);
192
+ if (elementText && elementText.length) {
193
+ props['$el_text'] = elementText;
194
+ }
195
+ }
196
+
197
+ if (target) {
198
+ var targetProps = getPropertiesFromElement(target);
199
+ props['$target'] = targetProps;
200
+ // pull up more props onto main event props
201
+ props['$el_classes'] = targetProps['$classes'];
202
+ _.extend(props, _.strip_empty_properties({
203
+ '$el_id': targetProps['$id'],
204
+ '$el_tag_name': targetProps['$tag_name']
205
+ }));
206
+ }
207
+ }
208
+ }
209
+
210
+ return props;
211
+ }
212
+
213
+
214
+ /*
215
+ * Get the direct text content of an element, protecting against sensitive data collection.
216
+ * Concats textContent of each of the element's text node children; this avoids potential
217
+ * collection of sensitive data that could happen if we used element.textContent and the
218
+ * element had sensitive child elements, since element.textContent includes child content.
219
+ * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
220
+ * @param {Element} el - element to get the text of
221
+ * @returns {string} the element's direct text content
222
+ */
223
+ function getSafeText(el) {
224
+ var elText = '';
225
+
226
+ if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) {
227
+ _.each(el.childNodes, function(child) {
228
+ if (isTextNode(child) && child.textContent) {
229
+ elText += _.trim(child.textContent)
230
+ // scrub potentially sensitive values
231
+ .split(/(\s+)/).filter(shouldTrackValue).join('')
232
+ // normalize whitespace
233
+ .replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ')
234
+ // truncate
235
+ .substring(0, 255);
236
+ }
237
+ });
238
+ }
239
+
240
+ return _.trim(elText);
241
+ }
242
+
243
+ function guessRealClickTarget(ev) {
244
+ var target = ev.target;
245
+ var composedPath = ev['composedPath']();
246
+ for (var i = 0; i < composedPath.length; i++) {
247
+ var node = composedPath[i];
248
+ if (
249
+ isTag(node, 'a') ||
250
+ isTag(node, 'button') ||
251
+ isTag(node, 'input') ||
252
+ isTag(node, 'select') ||
253
+ (node.getAttribute && node.getAttribute('role') === 'button')
254
+ ) {
255
+ target = node;
256
+ break;
257
+ }
258
+ if (node === target) {
259
+ break;
260
+ }
261
+ }
262
+ return target;
263
+ }
264
+
265
+ /*
266
+ * Check whether a DOM node has nodeType Node.ELEMENT_NODE
267
+ * @param {Node} node - node to check
268
+ * @returns {boolean} whether node is of the correct nodeType
269
+ */
270
+ function isElementNode(node) {
271
+ return node && node.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability
272
+ }
273
+
274
+ /*
275
+ * Check whether an element is of a given tag type.
276
+ * Due to potential reference discrepancies (such as the webcomponents.js polyfill),
277
+ * we want to match tagNames instead of specific references because something like
278
+ * element === document.body won't always work because element might not be a native
279
+ * element.
280
+ * @param {Element} el - element to check
281
+ * @param {string} tag - tag name (e.g., "div")
282
+ * @returns {boolean} whether el is of the given tag type
283
+ */
284
+ function isTag(el, tag) {
285
+ return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase();
286
+ }
287
+
288
+ /*
289
+ * Check whether a DOM node is a TEXT_NODE
290
+ * @param {Node} node - node to check
291
+ * @returns {boolean} whether node is of type Node.TEXT_NODE
292
+ */
293
+ function isTextNode(node) {
294
+ return node && node.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability
295
+ }
296
+
297
+ function minDOMApisSupported() {
298
+ try {
299
+ var testEl = document.createElement('div');
300
+ return !!testEl['matches'];
301
+ } catch (err) {
302
+ return false;
303
+ }
304
+ }
305
+
306
+ /*
307
+ * Check whether a DOM event should be "tracked" or if it may contain sensitive data
308
+ * using a variety of heuristics.
309
+ * @param {Element} el - element to check
310
+ * @param {Event} ev - event to check
311
+ * @returns {boolean} whether the event should be tracked
312
+ */
313
+ function shouldTrackDomEvent(el, ev) {
314
+ if (!el || isTag(el, 'html') || !isElementNode(el)) {
315
+ return false;
316
+ }
317
+ var tag = el.tagName.toLowerCase();
318
+ switch (tag) {
319
+ case 'form':
320
+ return ev.type === EV_SUBMIT;
321
+ case 'input':
322
+ if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) {
323
+ return ev.type === EV_CHANGE;
324
+ } else {
325
+ return ev.type === EV_CLICK;
326
+ }
327
+ case 'select':
328
+ case 'textarea':
329
+ return ev.type === EV_CHANGE;
330
+ default:
331
+ return ev.type === EV_CLICK;
332
+ }
333
+ }
334
+
335
+ /*
336
+ * Check whether a DOM element should be "tracked" or if it may contain sensitive data
337
+ * using a variety of heuristics.
338
+ * @param {Element} el - element to check
339
+ * @returns {boolean} whether the element should be tracked
340
+ */
341
+ function shouldTrackElement(el) {
342
+ var i;
343
+
344
+ for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
345
+ var classes = getClasses(curEl);
346
+ for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
347
+ if (classes[SENSITIVE_DATA_CLASSES[i]]) {
348
+ return false;
349
+ }
350
+ }
351
+ }
352
+
353
+ var elClasses = getClasses(el);
354
+ for (i = 0; i < OPT_IN_CLASSES.length; i++) {
355
+ if (elClasses[OPT_IN_CLASSES[i]]) {
356
+ return true;
357
+ }
358
+ }
359
+
360
+ // don't send data from inputs or similar elements since there will always be
361
+ // a risk of clientside javascript placing sensitive data in attributes
362
+ if (
363
+ isTag(el, 'input') ||
364
+ isTag(el, 'select') ||
365
+ isTag(el, 'textarea') ||
366
+ el.getAttribute('contenteditable') === 'true'
367
+ ) {
368
+ return false;
369
+ }
370
+
371
+ // don't include hidden or password fields
372
+ var type = el.type || '';
373
+ if (typeof type === 'string') { // it's possible for el.type to be a DOM element if el is a form with a child input[name="type"]
374
+ switch(type.toLowerCase()) {
375
+ case 'hidden':
376
+ return false;
377
+ case 'password':
378
+ return false;
379
+ }
380
+ }
381
+
382
+ // filter out data from fields that look like sensitive fields
383
+ var name = el.name || el.id || '';
384
+ if (typeof name === 'string') { // it's possible for el.name or el.id to be a DOM element if el is a form with a child input[name="name"]
385
+ var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
386
+ if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
387
+ return false;
388
+ }
389
+ }
390
+
391
+ return true;
392
+ }
393
+
394
+
395
+ /*
396
+ * Check whether a string value should be "tracked" or if it may contain sensitive data
397
+ * using a variety of heuristics.
398
+ * @param {string} value - string value to check
399
+ * @returns {boolean} whether the element should be tracked
400
+ */
401
+ function shouldTrackValue(value) {
402
+ if (value === null || _.isUndefined(value)) {
403
+ return false;
404
+ }
405
+
406
+ if (typeof value === 'string') {
407
+ value = _.trim(value);
408
+
409
+ // check to see if input value looks like a credit card number
410
+ // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
411
+ var ccRegex = /^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11}))$/;
412
+ if (ccRegex.test((value || '').replace(/[- ]/g, ''))) {
413
+ return false;
414
+ }
415
+
416
+ // check to see if input value looks like a social security number
417
+ var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/;
418
+ if (ssnRegex.test(value)) {
419
+ return false;
420
+ }
421
+ }
422
+
423
+ return true;
424
+ }
425
+
426
+ export {
427
+ getPropsForDOMEvent,
428
+ getSafeText,
429
+ logger,
430
+ minDOMApisSupported,
431
+ shouldTrackDomEvent, shouldTrackElement, shouldTrackValue,
432
+ EV_CHANGE, EV_CLICK, EV_HASHCHANGE, EV_MP_LOCATION_CHANGE, EV_POPSTATE,
433
+ EV_SCROLLEND, EV_SUBMIT
434
+ };
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.57.1'
3
+ LIB_VERSION: '2.59.0'
4
4
  };
5
5
 
6
6
  export default Config;
@@ -2,6 +2,7 @@
2
2
  import Config from './config';
3
3
  import { MAX_RECORDING_MS, _, console, userAgent, document, navigator, slice } from './utils';
4
4
  import { window } from './window';
5
+ import { Autocapture } from './autocapture';
5
6
  import { FormTracker, LinkTracker } from './dom-trackers';
6
7
  import { RequestBatcher } from './request-batcher';
7
8
  import { MixpanelGroup } from './mixpanel-group';
@@ -105,6 +106,7 @@ var DEFAULT_CONFIG = {
105
106
  'api_transport': 'XHR',
106
107
  'api_payload_format': PAYLOAD_TYPE_BASE64,
107
108
  'app_host': 'https://mixpanel.com',
109
+ 'autocapture': false,
108
110
  'cdn': 'https://cdn.mxpnl.com',
109
111
  'cross_site_cookie': false,
110
112
  'cross_subdomain_cookie': true,
@@ -147,6 +149,7 @@ var DEFAULT_CONFIG = {
147
149
  'hooks': {},
148
150
  'record_block_class': new RegExp('^(mp-block|fs-exclude|amp-block|rr-block|ph-no-capture)$'),
149
151
  'record_block_selector': 'img, video',
152
+ 'record_canvas': false,
150
153
  'record_collect_fonts': false,
151
154
  'record_idle_timeout_ms': 30 * 60 * 1000, // 30 minutes
152
155
  'record_mask_text_class': new RegExp('^(mp-mask|fs-mask|amp-mask|rr-mask|ph-mask)$'),
@@ -363,10 +366,8 @@ MixpanelLib.prototype._init = function(token, config, name) {
363
366
  }, '');
364
367
  }
365
368
 
366
- var track_pageview_option = this.get_config('track_pageview');
367
- if (track_pageview_option) {
368
- this._init_url_change_tracking(track_pageview_option);
369
- }
369
+ this.autocapture = new Autocapture(this);
370
+ this.autocapture.init();
370
371
 
371
372
  if (this.get_config('record_sessions_percent') > 0 && Math.random() * 100 <= this.get_config('record_sessions_percent')) {
372
373
  this.start_session_recording();
@@ -491,55 +492,6 @@ MixpanelLib.prototype._track_dom = function(DomClass, args) {
491
492
  return dt.track.apply(dt, args);
492
493
  };
493
494
 
494
- MixpanelLib.prototype._init_url_change_tracking = function(track_pageview_option) {
495
- var previous_tracked_url = '';
496
- var tracked = this.track_pageview();
497
- if (tracked) {
498
- previous_tracked_url = _.info.currentUrl();
499
- }
500
-
501
- if (_.include(['full-url', 'url-with-path-and-query-string', 'url-with-path'], track_pageview_option)) {
502
- window.addEventListener('popstate', function() {
503
- window.dispatchEvent(new Event('mp_locationchange'));
504
- });
505
- window.addEventListener('hashchange', function() {
506
- window.dispatchEvent(new Event('mp_locationchange'));
507
- });
508
- var nativePushState = window.history.pushState;
509
- if (typeof nativePushState === 'function') {
510
- window.history.pushState = function(state, unused, url) {
511
- nativePushState.call(window.history, state, unused, url);
512
- window.dispatchEvent(new Event('mp_locationchange'));
513
- };
514
- }
515
- var nativeReplaceState = window.history.replaceState;
516
- if (typeof nativeReplaceState === 'function') {
517
- window.history.replaceState = function(state, unused, url) {
518
- nativeReplaceState.call(window.history, state, unused, url);
519
- window.dispatchEvent(new Event('mp_locationchange'));
520
- };
521
- }
522
- window.addEventListener('mp_locationchange', function() {
523
- var current_url = _.info.currentUrl();
524
- var should_track = false;
525
- if (track_pageview_option === 'full-url') {
526
- should_track = current_url !== previous_tracked_url;
527
- } else if (track_pageview_option === 'url-with-path-and-query-string') {
528
- should_track = current_url.split('#')[0] !== previous_tracked_url.split('#')[0];
529
- } else if (track_pageview_option === 'url-with-path') {
530
- should_track = current_url.split('#')[0].split('?')[0] !== previous_tracked_url.split('#')[0].split('?')[0];
531
- }
532
-
533
- if (should_track) {
534
- var tracked = this.track_pageview();
535
- if (tracked) {
536
- previous_tracked_url = current_url;
537
- }
538
- }
539
- }.bind(this));
540
- }
541
- };
542
-
543
495
  /**
544
496
  * _prepare_callback() should be called by callers of _send_request for use
545
497
  * as the callback argument.
@@ -1797,6 +1749,10 @@ MixpanelLib.prototype.set_config = function(config) {
1797
1749
  this['persistence'].update_config(this['config']);
1798
1750
  }
1799
1751
  Config.DEBUG = Config.DEBUG || this.get_config('debug');
1752
+
1753
+ if ('autocapture' in config && this.autocapture) {
1754
+ this.autocapture.init();
1755
+ }
1800
1756
  }
1801
1757
  };
1802
1758
 
@@ -53,6 +53,7 @@ var SessionRecording = function(options) {
53
53
 
54
54
  this.seqNo = 0;
55
55
  this.replayStartTime = null;
56
+ this.replayStartUrl = null;
56
57
  this.batchStartUrl = null;
57
58
 
58
59
  this.idleTimeoutId = null;
@@ -104,6 +105,7 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
104
105
 
105
106
  this.replayStartTime = new Date().getTime();
106
107
  this.batchStartUrl = _.info.currentUrl();
108
+ this.replayStartUrl = _.info.currentUrl();
107
109
 
108
110
  if (shouldStopBatcher || this.recordMinMs > 0) {
109
111
  // the primary case for shouldStopBatcher is when we're starting recording after a reset
@@ -140,9 +142,17 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
140
142
  'blockClass': this.getConfig('record_block_class'),
141
143
  'blockSelector': blockSelector,
142
144
  'collectFonts': this.getConfig('record_collect_fonts'),
145
+ 'dataURLOptions': { // canvas image options (https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL)
146
+ 'type': 'image/webp',
147
+ 'quality': 0.6
148
+ },
143
149
  'maskAllInputs': true,
144
150
  'maskTextClass': this.getConfig('record_mask_text_class'),
145
- 'maskTextSelector': this.getConfig('record_mask_text_selector')
151
+ 'maskTextSelector': this.getConfig('record_mask_text_selector'),
152
+ 'recordCanvas': this.getConfig('record_canvas'),
153
+ 'sampling': {
154
+ 'canvas': 15
155
+ }
146
156
  });
147
157
 
148
158
  if (typeof this._stopRecording !== 'function') {
@@ -260,6 +270,7 @@ SessionRecording.prototype._flushEvents = addOptOutCheckMixpanelLib(function (da
260
270
  'replay_id': replayId,
261
271
  'replay_length_ms': replayLengthMs,
262
272
  'replay_start_time': this.replayStartTime / 1000,
273
+ 'replay_start_url': this.replayStartUrl,
263
274
  'seq': this.seqNo
264
275
  };
265
276
  var eventsJson = _.JSONEncode(data);
package/src/utils.js CHANGED
@@ -110,6 +110,29 @@ var console_with_prefix = function(prefix) {
110
110
  };
111
111
 
112
112
 
113
+ var safewrap = function(f) {
114
+ return function() {
115
+ try {
116
+ return f.apply(this, arguments);
117
+ } catch (e) {
118
+ console.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.');
119
+ if (Config.DEBUG){
120
+ console.critical(e);
121
+ }
122
+ }
123
+ };
124
+ };
125
+
126
+ var safewrapClass = function(klass) {
127
+ var proto = klass.prototype;
128
+ for (var func in proto) {
129
+ if (typeof(proto[func]) === 'function') {
130
+ proto[func] = safewrap(proto[func]);
131
+ }
132
+ }
133
+ };
134
+
135
+
113
136
  // UNDERSCORE
114
137
  // Embed part of the Underscore Library
115
138
  _.bind = function(func, context) {
@@ -888,6 +911,7 @@ _.UUID = (function() {
888
911
  var BLOCKED_UA_STRS = [
889
912
  'ahrefsbot',
890
913
  'ahrefssiteaudit',
914
+ 'amazonbot',
891
915
  'baiduspider',
892
916
  'bingbot',
893
917
  'bingpreview',
@@ -897,7 +921,7 @@ var BLOCKED_UA_STRS = [
897
921
  'pinterest',
898
922
  'screaming frog',
899
923
  'yahoo! slurp',
900
- 'yandexbot',
924
+ 'yandex',
901
925
 
902
926
  // a whole bunch of goog-specific crawlers
903
927
  // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
@@ -1732,6 +1756,8 @@ export {
1732
1756
  MAX_RECORDING_MS,
1733
1757
  MAX_VALUE_FOR_MIN_RECORDING_MS,
1734
1758
  navigator,
1759
+ safewrap,
1760
+ safewrapClass,
1735
1761
  slice,
1736
1762
  userAgent,
1737
1763
  };
package/src/window.js CHANGED
@@ -7,11 +7,14 @@ if (typeof(window) === 'undefined') {
7
7
  win = {
8
8
  navigator: { userAgent: '', onLine: true },
9
9
  document: {
10
+ createElement: function() { return {}; },
10
11
  location: loc,
11
12
  referrer: ''
12
13
  },
13
14
  screen: { width: 0, height: 0 },
14
- location: loc
15
+ location: loc,
16
+ addEventListener: function() {},
17
+ removeEventListener: function() {}
15
18
  };
16
19
  } else {
17
20
  win = window;