mixpanel-browser 2.40.0 → 2.42.1

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.
package/doc/build-docs.js CHANGED
@@ -97,6 +97,22 @@ function doxToMD(items) {
97
97
  });
98
98
  }
99
99
 
100
+ // Captures prototype bracket notation property assignment, e.g.:
101
+ // `MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) {`
102
+ // Based on https://github.com/tj/dox/blob/9fe92e17dfcd31c9b6512f6e5bf0b52c2b6b84d4/lib/dox.js#L592
103
+ dox.contextPatternMatchers.push(function (str) {
104
+ if (/^\s*([\w$.]+)\s*\.\s*prototype\s*\['\s*([\w$]+)'\]\s*=\s*([^\n;]+)/.exec(str)) {
105
+ return {
106
+ type: 'property'
107
+ , constructor: RegExp.$1
108
+ , cons: RegExp.$1
109
+ , name: RegExp.$2
110
+ , value: RegExp.$3.trim()
111
+ , string: RegExp.$1 + '.prototype.' + RegExp.$2
112
+ };
113
+ }
114
+ });
115
+
100
116
  const rawCode = fs.readFileSync(SOURCE_FILE).toString().trim();
101
117
  const parsed = dox.parseComments(rawCode);
102
118
 
@@ -990,6 +990,24 @@ mixpanel.people.unset(['gender', 'Company']);
990
990
  # mixpanel.group
991
991
 
992
992
 
993
+ ___
994
+ ## mixpanel.group.delete
995
+ Permanently delete a group.
996
+
997
+
998
+ ### Usage:
999
+
1000
+ ```javascript
1001
+ mixpanel.get_group('company', 'mixpanel').delete();
1002
+ ```
1003
+
1004
+
1005
+
1006
+ | Argument | Type | Description |
1007
+ | ------------- | ------------- | ----- |
1008
+ | **callback** | <span class="mp-arg-type">Function</span></br></span><span class="mp-arg-optional">optional</span> | If provided, the callback will be called after the tracking event |
1009
+
1010
+
993
1011
  ___
994
1012
  ## mixpanel.group.remove
995
1013
  Remove a property from a group. The value will be ignored if doesn't exist.
@@ -9,25 +9,6 @@ var MIXPANEL_LIB_URL = '//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js';
9
9
  (function(document, mixpanel) {
10
10
  // Only stub out if this is the first time running the snippet.
11
11
  if (!mixpanel['__SV']) {
12
- var win = window;
13
-
14
- // grab the hash params for ce editor immediately in case
15
- // host website changes hash after init
16
- try {
17
- var getHashParam, matches, state, loc = win.location, hash = loc.hash;
18
- getHashParam = function(hash, param) {
19
- matches = hash.match(new RegExp(param + '=([^&]*)'));
20
- return matches ? matches[1] : null;
21
- };
22
- if (hash && getHashParam(hash, 'state')) {
23
- state = JSON.parse(decodeURIComponent(getHashParam(hash, 'state')));
24
- if (state['action'] === 'mpeditor') {
25
- win.sessionStorage.setItem('_mpcehash', hash);
26
- history.replaceState(state['desiredHash'] || '', document.title, loc.pathname + loc.search); // remove ce editor hash
27
- }
28
- }
29
- } catch (e) {}
30
-
31
12
  var script, first_script, gen_fn, functions, i, lib_name = "mixpanel";
32
13
  window[lib_name] = mixpanel;
33
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixpanel-browser",
3
- "version": "2.40.0",
3
+ "version": "2.42.1",
4
4
  "description": "The official Mixpanel JavaScript browser client library",
5
5
  "main": "dist/mixpanel.cjs.js",
6
6
  "directories": {
@@ -46,10 +46,10 @@
46
46
  "jsdom": "11.12.0",
47
47
  "jsdom-global": "3.0.2",
48
48
  "localStorage": "1.0.4",
49
- "lodash": "4.17.19",
49
+ "lodash": "4.17.21",
50
50
  "mocha": "7.1.1",
51
51
  "morgan": "1.9.1",
52
- "rdme": "3.0.0",
52
+ "rdme": "4.0.0",
53
53
  "request": "2.88.0",
54
54
  "rollup": "0.25.8",
55
55
  "rollup-plugin-npm": "1.4.0",
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.40.0'
3
+ LIB_VERSION: '2.42.1'
4
4
  };
5
5
 
6
6
  export default Config;
package/src/gdpr-utils.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * These functions are used internally by the SDK and are not intended to be publicly exposed.
12
12
  */
13
13
 
14
- import { _, window } from './utils';
14
+ import { _, console, window } from './utils';
15
15
 
16
16
  /**
17
17
  * A function used to track a Mixpanel event (e.g. MixpanelLib.track)
@@ -83,9 +83,14 @@ export function hasOptedIn(token, options) {
83
83
  */
84
84
  export function hasOptedOut(token, options) {
85
85
  if (_hasDoNotTrackFlagOn(options)) {
86
+ console.warn('This browser has "Do Not Track" enabled. This will prevent the Mixpanel SDK from sending any data. To ignore the "Do Not Track" browser setting, initialize the Mixpanel instance with the config "ignore_dnt: true"');
86
87
  return true;
87
88
  }
88
- return _getStorageValue(token, options) === '0';
89
+ var optedOut = _getStorageValue(token, options) === '0';
90
+ if (optedOut) {
91
+ console.warn('You are opted out of Mixpanel tracking. This will prevent the Mixpanel SDK from sending any data.');
92
+ }
93
+ return optedOut;
89
94
  }
90
95
 
91
96
  /**
@@ -1,7 +1,6 @@
1
1
  /* eslint camelcase: "off" */
2
2
  import Config from './config';
3
- import { _, console, userAgent, window, document, navigator, determine_eligibility, slice } from './utils';
4
- import { autotrack } from './autotrack';
3
+ import { _, console, userAgent, window, document, navigator, slice } from './utils';
5
4
  import { FormTracker, LinkTracker } from './dom-trackers';
6
5
  import { RequestBatcher } from './request-batcher';
7
6
  import { MixpanelGroup } from './mixpanel-group';
@@ -88,7 +87,6 @@ var DEFAULT_CONFIG = {
88
87
  'api_method': 'POST',
89
88
  'api_transport': 'XHR',
90
89
  'app_host': 'https://mixpanel.com',
91
- 'autotrack': true,
92
90
  'cdn': 'https://cdn.mxpnl.com',
93
91
  'cross_site_cookie': false,
94
92
  'cross_subdomain_cookie': true,
@@ -119,7 +117,7 @@ var DEFAULT_CONFIG = {
119
117
  'inapp_protocol': '//',
120
118
  'inapp_link_new_window': false,
121
119
  'ignore_dnt': false,
122
- 'batch_requests': false, // for now
120
+ 'batch_requests': true,
123
121
  'batch_size': 50,
124
122
  'batch_flush_interval_ms': 5000,
125
123
  'batch_request_timeout_ms': 90000,
@@ -171,21 +169,6 @@ var create_mplib = function(token, config, name) {
171
169
  // global debug to be true
172
170
  Config.DEBUG = Config.DEBUG || instance.get_config('debug');
173
171
 
174
- instance['__autotrack_enabled'] = instance.get_config('autotrack');
175
- if (instance.get_config('autotrack')) {
176
- var num_buckets = 100;
177
- var num_enabled_buckets = 100;
178
- if (!autotrack.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) {
179
- instance['__autotrack_enabled'] = false;
180
- console.log('Not in active bucket: disabling Automatic Event Collection.');
181
- } else if (!autotrack.isBrowserSupported()) {
182
- instance['__autotrack_enabled'] = false;
183
- console.log('Disabling Automatic Event Collection because this browser is not supported');
184
- } else {
185
- autotrack.init(instance);
186
- }
187
- }
188
-
189
172
  // if target is not defined, we called init after the lib already
190
173
  // loaded, so there won't be an array of things to execute
191
174
  if (!_.isUndefined(target) && _.isArray(target)) {
@@ -253,17 +236,7 @@ MixpanelLib.prototype._init = function(token, config, name) {
253
236
  this['config'] = {};
254
237
  this['_triggered_notifs'] = [];
255
238
 
256
- // rollout: enable batch_requests by default for 60% of projects
257
- // (only if they have not specified a value in their init config
258
- // and they aren't using a custom API host)
259
- var variable_features = {};
260
- var api_host = config['api_host'];
261
- var is_custom_api = !!api_host && !api_host.match(/\.mixpanel\.com$/);
262
- if (!('batch_requests' in config) && !is_custom_api && determine_eligibility(token, 'batch', 60)) {
263
- variable_features['batch_requests'] = true;
264
- }
265
-
266
- this.set_config(_.extend({}, DEFAULT_CONFIG, variable_features, config, {
239
+ this.set_config(_.extend({}, DEFAULT_CONFIG, config, {
267
240
  'name': name,
268
241
  'token': token,
269
242
  'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc'
@@ -289,15 +262,32 @@ MixpanelLib.prototype._init = function(token, config, name) {
289
262
  } else {
290
263
  this.init_batchers();
291
264
  if (sendBeacon && window.addEventListener) {
292
- window.addEventListener('unload', _.bind(function() {
293
- // Before page closes, attempt to flush any events queued up via navigator.sendBeacon.
294
- // Since sendBeacon doesn't report success/failure, events will not be removed from
295
- // the persistent store; if the site is loaded again, the events will be flushed again
296
- // on startup and deduplicated on the Mixpanel server side.
265
+ // Before page closes or hides (user tabs away etc), attempt to flush any events
266
+ // queued up via navigator.sendBeacon. Since sendBeacon doesn't report success/failure,
267
+ // events will not be removed from the persistent store; if the site is loaded again,
268
+ // the events will be flushed again on startup and deduplicated on the Mixpanel server
269
+ // side.
270
+ // There is no reliable way to capture only page close events, so we lean on the
271
+ // visibilitychange and pagehide events as recommended at
272
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/unload_event#usage_notes.
273
+ // These events fire when the user clicks away from the current page/tab, so will occur
274
+ // more frequently than page unload, but are the only mechanism currently for capturing
275
+ // this scenario somewhat reliably.
276
+ var flush_on_unload = _.bind(function() {
297
277
  if (!this.request_batchers.events.stopped) {
298
278
  this.request_batchers.events.flush({unloading: true});
299
279
  }
300
- }, this));
280
+ }, this);
281
+ window.addEventListener('pagehide', function(ev) {
282
+ if (ev['persisted']) {
283
+ flush_on_unload();
284
+ }
285
+ });
286
+ window.addEventListener('visibilitychange', function() {
287
+ if (document['visibilityState'] === 'hidden') {
288
+ flush_on_unload();
289
+ }
290
+ });
301
291
  }
302
292
  }
303
293
  }
@@ -2057,7 +2047,7 @@ export function init_from_snippet() {
2057
2047
  }
2058
2048
  if (mixpanel_master['__loaded'] || (mixpanel_master['config'] && mixpanel_master['persistence'])) {
2059
2049
  // lib has already been loaded at least once; we don't want to override the global object this time so bomb early
2060
- console.error('Mixpanel library has already been downloaded at least once.');
2050
+ console.critical('The Mixpanel library has already been downloaded at least once. Ensure that the Mixpanel code snippet only appears once on the page (and is not double-loaded by a tag manager) in order to avoid errors.');
2061
2051
  return;
2062
2052
  }
2063
2053
  var snippet_version = mixpanel_master['__SV'] || 0;
@@ -110,9 +110,13 @@ MixpanelGroup.prototype.union = addOptOutCheckMixpanelGroup(function(list_name,
110
110
  * Permanently delete a group.
111
111
  *
112
112
  * ### Usage:
113
+ *
113
114
  * mixpanel.get_group('company', 'mixpanel').delete();
115
+ *
116
+ * @param {Function} [callback] If provided, the callback will be called after the tracking event
114
117
  */
115
118
  MixpanelGroup.prototype['delete'] = addOptOutCheckMixpanelGroup(function(callback) {
119
+ // bracket notation above prevents a minification error related to reserved words
116
120
  var data = this.delete_action();
117
121
  return this._send_request(data, callback);
118
122
  });
@@ -148,9 +148,9 @@ RequestBatcher.prototype.flush = function(options) {
148
148
  } else if (
149
149
  _.isObject(res) &&
150
150
  res.xhr_req &&
151
- (res.xhr_req['status'] >= 500 || res.xhr_req['status'] <= 0)
151
+ (res.xhr_req['status'] >= 500 || res.xhr_req['status'] === 429 || res.error === 'timeout')
152
152
  ) {
153
- // network or API error, retry
153
+ // network or API error, or 429 Too Many Requests, retry
154
154
  var retryMS = this.flushInterval * 2;
155
155
  var headers = res.xhr_req['responseHeaders'];
156
156
  if (headers) {
package/src/utils.js CHANGED
@@ -67,6 +67,19 @@ var console = {
67
67
  }
68
68
  },
69
69
  /** @type {function(...*)} */
70
+ warn: function() {
71
+ if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
72
+ var args = ['Mixpanel warning:'].concat(_.toArray(arguments));
73
+ try {
74
+ windowConsole.warn.apply(windowConsole, args);
75
+ } catch (err) {
76
+ _.each(args, function(arg) {
77
+ windowConsole.warn(arg);
78
+ });
79
+ }
80
+ }
81
+ },
82
+ /** @type {function(...*)} */
70
83
  error: function() {
71
84
  if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) {
72
85
  var args = ['Mixpanel error:'].concat(_.toArray(arguments));
@@ -266,10 +279,6 @@ _.values = function(obj) {
266
279
  return results;
267
280
  };
268
281
 
269
- _.identity = function(value) {
270
- return value;
271
- };
272
-
273
282
  _.include = function(obj, target) {
274
283
  var found = false;
275
284
  if (obj === null) {
@@ -932,9 +941,39 @@ _.UUID = (function() {
932
941
  // _.isBlockedUA()
933
942
  // This is to block various web spiders from executing our JS and
934
943
  // sending false tracking data
944
+ var BLOCKED_UA_STRS = [
945
+ 'ahrefsbot',
946
+ 'baiduspider',
947
+ 'bingbot',
948
+ 'bingpreview',
949
+ 'facebookexternal',
950
+ 'petalbot',
951
+ 'pinterest',
952
+ 'screaming frog',
953
+ 'yahoo! slurp',
954
+ 'yandexbot',
955
+
956
+ // a whole bunch of goog-specific crawlers
957
+ // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
958
+ 'adsbot-google',
959
+ 'apis-google',
960
+ 'duplexweb-google',
961
+ 'feedfetcher-google',
962
+ 'google favicon',
963
+ 'google web preview',
964
+ 'google-read-aloud',
965
+ 'googlebot',
966
+ 'googleweblight',
967
+ 'mediapartners-google',
968
+ 'storebot-google'
969
+ ];
935
970
  _.isBlockedUA = function(ua) {
936
- if (/(google web preview|baiduspider|yandexbot|bingbot|googlebot|yahoo! slurp)/i.test(ua)) {
937
- return true;
971
+ var i;
972
+ ua = ua.toLowerCase();
973
+ for (i = 0; i < BLOCKED_UA_STRS.length; i++) {
974
+ if (ua.indexOf(BLOCKED_UA_STRS[i]) !== -1) {
975
+ return true;
976
+ }
938
977
  }
939
978
  return false;
940
979
  };
@@ -979,10 +1018,6 @@ _.getQueryParam = function(url, param) {
979
1018
  }
980
1019
  };
981
1020
 
982
- _.getHashParam = function(hash, param) {
983
- var matches = hash.match(new RegExp(param + '=([^&]*)'));
984
- return matches ? matches[1] : null;
985
- };
986
1021
 
987
1022
  // _.cookie
988
1023
  // Methods partially borrowed from quirksmode.org/js/cookies.html
@@ -1649,28 +1684,6 @@ var cheap_guid = function(maxlen) {
1649
1684
  return maxlen ? guid.substring(0, maxlen) : guid;
1650
1685
  };
1651
1686
 
1652
- /**
1653
- * Check deterministically whether to include or exclude from a feature rollout/test based on the
1654
- * given string and the desired percentage to include.
1655
- * @param {String} str - string to run the check against (for instance a project's token)
1656
- * @param {String} feature - name of feature (for inclusion in hash, to ensure different results
1657
- * for different features)
1658
- * @param {Number} percent_allowed - percentage chance that a given string will be included
1659
- * @returns {Boolean} whether the given string should be included
1660
- */
1661
- var determine_eligibility = _.safewrap(function(str, feature, percent_allowed) {
1662
- str = str + feature;
1663
-
1664
- // Bernstein's hash: http://www.cse.yorku.ca/~oz/hash.html#djb2
1665
- var hash = 5381;
1666
- for (var i = 0; i < str.length; i++) {
1667
- hash = ((hash << 5) + hash) + str.charCodeAt(i);
1668
- hash = hash & hash;
1669
- }
1670
- var dart = (hash >>> 0) % 100;
1671
- return dart < percent_allowed;
1672
- });
1673
-
1674
1687
  // naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
1675
1688
  var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
1676
1689
  // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
@@ -1729,7 +1742,6 @@ export {
1729
1742
  navigator,
1730
1743
  cheap_guid,
1731
1744
  console_with_prefix,
1732
- determine_eligibility,
1733
1745
  extract_domain,
1734
1746
  localStorageSupported,
1735
1747
  JSONStringify,
package/tunnel.log ADDED
File without changes
package/.travis.yml DELETED
@@ -1,6 +0,0 @@
1
- node_js:
2
- - '10'
3
- - '12'
4
- language: node_js
5
- script:
6
- - npm test
@@ -1,192 +0,0 @@
1
- import { _ } from './utils';
2
-
3
- /*
4
- * Get the className of an element, accounting for edge cases where element.className is an object
5
- * @param {Element} el - element to get the className of
6
- * @returns {string} the element's class
7
- */
8
- export function getClassName(el) {
9
- switch(typeof el.className) {
10
- case 'string':
11
- return el.className;
12
- case 'object': // handle cases where className might be SVGAnimatedString or some other type
13
- return el.className.baseVal || el.getAttribute('class') || '';
14
- default: // future proof
15
- return '';
16
- }
17
- }
18
-
19
- /*
20
- * Get the direct text content of an element, protecting against sensitive data collection.
21
- * Concats textContent of each of the element's text node children; this avoids potential
22
- * collection of sensitive data that could happen if we used element.textContent and the
23
- * element had sensitive child elements, since element.textContent includes child content.
24
- * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
25
- * @param {Element} el - element to get the text of
26
- * @returns {string} the element's direct text content
27
- */
28
- export function getSafeText(el) {
29
- var elText = '';
30
-
31
- if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) {
32
- _.each(el.childNodes, function(child) {
33
- if (isTextNode(child) && child.textContent) {
34
- elText += _.trim(child.textContent)
35
- // scrub potentially sensitive values
36
- .split(/(\s+)/).filter(shouldTrackValue).join('')
37
- // normalize whitespace
38
- .replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ')
39
- // truncate
40
- .substring(0, 255);
41
- }
42
- });
43
- }
44
-
45
- return _.trim(elText);
46
- }
47
-
48
- /*
49
- * Check whether an element has nodeType Node.ELEMENT_NODE
50
- * @param {Element} el - element to check
51
- * @returns {boolean} whether el is of the correct nodeType
52
- */
53
- export function isElementNode(el) {
54
- return el && el.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability
55
- }
56
-
57
- /*
58
- * Check whether an element is of a given tag type.
59
- * Due to potential reference discrepancies (such as the webcomponents.js polyfill),
60
- * we want to match tagNames instead of specific references because something like
61
- * element === document.body won't always work because element might not be a native
62
- * element.
63
- * @param {Element} el - element to check
64
- * @param {string} tag - tag name (e.g., "div")
65
- * @returns {boolean} whether el is of the given tag type
66
- */
67
- export function isTag(el, tag) {
68
- return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase();
69
- }
70
-
71
- /*
72
- * Check whether an element has nodeType Node.TEXT_NODE
73
- * @param {Element} el - element to check
74
- * @returns {boolean} whether el is of the correct nodeType
75
- */
76
- export function isTextNode(el) {
77
- return el && el.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability
78
- }
79
-
80
- /*
81
- * Check whether a DOM event should be "tracked" or if it may contain sentitive data
82
- * using a variety of heuristics.
83
- * @param {Element} el - element to check
84
- * @param {Event} event - event to check
85
- * @returns {boolean} whether the event should be tracked
86
- */
87
- export function shouldTrackDomEvent(el, event) {
88
- if (!el || isTag(el, 'html') || !isElementNode(el)) {
89
- return false;
90
- }
91
- var tag = el.tagName.toLowerCase();
92
- switch (tag) {
93
- case 'html':
94
- return false;
95
- case 'form':
96
- return event.type === 'submit';
97
- case 'input':
98
- if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) {
99
- return event.type === 'change';
100
- } else {
101
- return event.type === 'click';
102
- }
103
- case 'select':
104
- case 'textarea':
105
- return event.type === 'change';
106
- default:
107
- return event.type === 'click';
108
- }
109
- }
110
-
111
- /*
112
- * Check whether a DOM element should be "tracked" or if it may contain sentitive data
113
- * using a variety of heuristics.
114
- * @param {Element} el - element to check
115
- * @returns {boolean} whether the element should be tracked
116
- */
117
- export function shouldTrackElement(el) {
118
- for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
119
- var classes = getClassName(curEl).split(' ');
120
- if (_.includes(classes, 'mp-sensitive') || _.includes(classes, 'mp-no-track')) {
121
- return false;
122
- }
123
- }
124
-
125
- if (_.includes(getClassName(el).split(' '), 'mp-include')) {
126
- return true;
127
- }
128
-
129
- // don't send data from inputs or similar elements since there will always be
130
- // a risk of clientside javascript placing sensitive data in attributes
131
- if (
132
- isTag(el, 'input') ||
133
- isTag(el, 'select') ||
134
- isTag(el, 'textarea') ||
135
- el.getAttribute('contenteditable') === 'true'
136
- ) {
137
- return false;
138
- }
139
-
140
- // don't include hidden or password fields
141
- var type = el.type || '';
142
- 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"]
143
- switch(type.toLowerCase()) {
144
- case 'hidden':
145
- return false;
146
- case 'password':
147
- return false;
148
- }
149
- }
150
-
151
- // filter out data from fields that look like sensitive fields
152
- var name = el.name || el.id || '';
153
- 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"]
154
- var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i;
155
- if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) {
156
- return false;
157
- }
158
- }
159
-
160
- return true;
161
- }
162
-
163
- /*
164
- * Check whether a string value should be "tracked" or if it may contain sentitive data
165
- * using a variety of heuristics.
166
- * @param {string} value - string value to check
167
- * @returns {boolean} whether the element should be tracked
168
- */
169
- export function shouldTrackValue(value) {
170
- if (value === null || _.isUndefined(value)) {
171
- return false;
172
- }
173
-
174
- if (typeof value === 'string') {
175
- value = _.trim(value);
176
-
177
- // check to see if input value looks like a credit card number
178
- // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html
179
- 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}))$/;
180
- if (ccRegex.test((value || '').replace(/[- ]/g, ''))) {
181
- return false;
182
- }
183
-
184
- // check to see if input value looks like a social security number
185
- var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/;
186
- if (ssnRegex.test(value)) {
187
- return false;
188
- }
189
- }
190
-
191
- return true;
192
- }