mixpanel-browser 2.69.1 → 2.71.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/build.sh +1 -0
  3. package/dist/mixpanel-core.cjs.d.ts +440 -0
  4. package/dist/mixpanel-core.cjs.js +857 -56
  5. package/dist/mixpanel-recorder.js +5 -3
  6. package/dist/mixpanel-recorder.min.js +1 -1
  7. package/dist/mixpanel-recorder.min.js.map +1 -1
  8. package/dist/mixpanel-with-async-recorder.cjs.d.ts +440 -0
  9. package/dist/mixpanel-with-async-recorder.cjs.js +857 -56
  10. package/dist/mixpanel-with-recorder.d.ts +440 -0
  11. package/dist/mixpanel-with-recorder.js +861 -58
  12. package/dist/mixpanel-with-recorder.min.d.ts +440 -0
  13. package/dist/mixpanel-with-recorder.min.js +1 -1
  14. package/dist/mixpanel.amd.d.ts +440 -0
  15. package/dist/mixpanel.amd.js +861 -58
  16. package/dist/mixpanel.cjs.d.ts +440 -0
  17. package/dist/mixpanel.cjs.js +861 -58
  18. package/dist/mixpanel.globals.js +857 -56
  19. package/dist/mixpanel.min.js +170 -152
  20. package/dist/mixpanel.module.d.ts +440 -0
  21. package/dist/mixpanel.module.js +861 -58
  22. package/dist/mixpanel.umd.d.ts +440 -0
  23. package/dist/mixpanel.umd.js +861 -58
  24. package/dist/rrweb-compiled.js +4 -2
  25. package/package.json +2 -19
  26. package/rollup.config.mjs +28 -4
  27. package/src/autocapture/deadclick.js +254 -0
  28. package/src/autocapture/index.js +237 -41
  29. package/src/autocapture/shadow-dom-observer.js +100 -0
  30. package/src/autocapture/utils.js +230 -3
  31. package/src/config.js +1 -1
  32. package/src/flags/index.js +43 -18
  33. package/src/index.d.ts +16 -3
  34. package/src/loaders/loader-module-core.d.ts +1 -0
  35. package/src/loaders/loader-module-with-async-recorder.d.ts +1 -0
  36. package/src/loaders/loader-module.d.ts +1 -0
  37. package/src/utils.js +15 -0
@@ -1,17 +1,23 @@
1
1
  // stateless utils
2
2
  // mostly from https://github.com/mixpanel/mixpanel-js/blob/989ada50f518edab47b9c4fd9535f9fbd5ec5fc0/src/autotrack-utils.js
3
3
 
4
- import { _, console_with_prefix, document } from '../utils'; // eslint-disable-line camelcase
4
+ import { _, console_with_prefix, document, safewrap } from '../utils'; // eslint-disable-line camelcase
5
5
  import { window } from '../window';
6
6
 
7
7
  var EV_CHANGE = 'change';
8
8
  var EV_CLICK = 'click';
9
9
  var EV_HASHCHANGE = 'hashchange';
10
+ var EV_INPUT = 'input';
11
+ var EV_LOAD = 'load';
10
12
  var EV_MP_LOCATION_CHANGE = 'mp_locationchange';
11
13
  var EV_POPSTATE = 'popstate';
12
14
  // TODO scrollend isn't available in Safari: document or polyfill?
13
15
  var EV_SCROLLEND = 'scrollend';
16
+ var EV_SCROLL = 'scroll';
17
+ var EV_SELECT = 'select';
14
18
  var EV_SUBMIT = 'submit';
19
+ var EV_TOGGLE = 'toggle';
20
+ var EV_VISIBILITYCHANGE = 'visibilitychange';
15
21
 
16
22
  var CLICK_EVENT_PROPS = [
17
23
  'clientX', 'clientY',
@@ -28,6 +34,77 @@ var TRACKED_ATTRS = [
28
34
  'href', 'name', 'role', 'title', 'type'
29
35
  ];
30
36
 
37
+ var INTERACTIVE_ARIA_ROLES = {
38
+ 'button': true,
39
+ 'checkbox': true,
40
+ 'combobox': true,
41
+ 'grid': true,
42
+ 'link': true,
43
+ 'listbox': true,
44
+ 'menu': true,
45
+ 'menubar': true,
46
+ 'menuitem': true,
47
+ 'menuitemcheckbox': true,
48
+ 'menuitemradio': true,
49
+ 'navigation': true,
50
+ 'option': true,
51
+ 'radio': true,
52
+ 'radiogroup': true,
53
+ 'searchbox': true,
54
+ 'slider': true,
55
+ 'spinbutton': true,
56
+ 'switch': true,
57
+ 'tab': true,
58
+ 'tablist': true,
59
+ 'textbox': true,
60
+ 'tree': true,
61
+ 'treegrid': true,
62
+ 'treeitem': true
63
+ };
64
+
65
+ var ALWAYS_NON_INTERACTIVE_TAGS = {
66
+ // Document metadata
67
+ 'base': true,
68
+ 'head': true,
69
+ 'html': true,
70
+ 'link': true,
71
+ 'meta': true,
72
+ 'script': true,
73
+ 'style': true,
74
+ 'title': true,
75
+ // Text formatting
76
+ 'br': true,
77
+ 'hr': true,
78
+ 'wbr': true,
79
+ // Other
80
+ 'noscript': true,
81
+ 'picture': true,
82
+ 'source': true,
83
+ 'template': true,
84
+ 'track': true
85
+ };
86
+
87
+ // Common container tags that need additional checks
88
+ var TEXT_CONTAINER_TAGS = {
89
+ 'article': true,
90
+ 'div': true,
91
+ 'h1': true,
92
+ 'h2': true,
93
+ 'h3': true,
94
+ 'h4': true,
95
+ 'h5': true,
96
+ 'h6': true,
97
+ 'p': true,
98
+ 'section': true,
99
+ 'span': true
100
+ };
101
+
102
+ var EVENT_HANDLER_ATTRIBUTES = [
103
+ 'onclick', 'onmousedown', 'onmouseup', 'onpointerdown', 'onpointerup', 'ontouchend', 'ontouchstart'
104
+ ];
105
+
106
+ var MAX_DEPTH = 5;
107
+
31
108
  var logger = console_with_prefix('autocapture');
32
109
 
33
110
 
@@ -395,6 +472,10 @@ function minDOMApisSupported() {
395
472
  }
396
473
  }
397
474
 
475
+ function weakSetSupported() {
476
+ return typeof WeakSet !== 'undefined';
477
+ }
478
+
398
479
  /*
399
480
  * Check whether a DOM event should be "tracked" or if it may contain sensitive data
400
481
  * using a variety of heuristics.
@@ -520,12 +601,158 @@ function shouldTrackValue(value) {
520
601
  return true;
521
602
  }
522
603
 
604
+ /**
605
+ * Creates a cross-browser compatible scroll end function with appropriate event listener.
606
+ * For browsers that support scrollend, returns the original function with scrollend event.
607
+ * For browsers without scrollend support, returns a debounced function that triggers
608
+ * 100ms after the last scroll event to simulate scrollend behavior.
609
+ * @param {Function} originalFunction - The function to call when scrolling ends
610
+ * @returns {Object} Object containing listener function and eventType string
611
+ * @returns {Function} returns.listener - The wrapped function to use as event listener
612
+ * @returns {string} returns.eventType - The event type to listen for ('scrollend' or 'scroll')
613
+ */
614
+ function getPolyfillScrollEndFunction(originalFunction) {
615
+ var supportsScrollEnd = 'onscrollend' in window;
616
+ var polyfillFunction = safewrap(originalFunction);
617
+ var polyfillEvent = EV_SCROLLEND;
618
+ if (!supportsScrollEnd) {
619
+ // Polyfill for browsers without scrollend support: wait 100ms after the last scroll event
620
+ // https://developer.chrome.com/blog/scrollend-a-new-javascript-event
621
+ var scrollTimer = null;
622
+ var scrollDelayMs = 100;
623
+
624
+ polyfillFunction = safewrap(function() {
625
+ clearTimeout(scrollTimer);
626
+ scrollTimer = setTimeout(originalFunction, scrollDelayMs);
627
+ });
628
+
629
+ polyfillEvent = EV_SCROLL;
630
+ }
631
+
632
+ return {
633
+ listener: polyfillFunction,
634
+ eventType: polyfillEvent
635
+ };
636
+ }
637
+
638
+ function hasInlineEventHandlers(element) {
639
+ for (var i = 0; i < EVENT_HANDLER_ATTRIBUTES.length; i++) {
640
+ if (element.hasAttribute(EVENT_HANDLER_ATTRIBUTES[i])) {
641
+ return true;
642
+ }
643
+ }
644
+ return false;
645
+ }
646
+
647
+ function hasInteractiveAriaRole(element) {
648
+ var role = element.getAttribute('role');
649
+ if (!role) return false;
650
+
651
+ // Handle invalid markup where multiple roles might be specified
652
+ // Only the first token is recognized per ARIA spec
653
+ var primaryRole = role.trim().split(/\s+/)[0].toLowerCase();
654
+
655
+ return INTERACTIVE_ARIA_ROLES[primaryRole];
656
+ }
657
+
658
+ function hasAnyInteractivityIndicators(element) {
659
+ var tagName = element.tagName.toLowerCase();
660
+
661
+ // Check for interactive HTML elements
662
+ if (tagName === 'button' ||
663
+ tagName === 'input' ||
664
+ tagName === 'select' ||
665
+ tagName === 'textarea' ||
666
+ tagName === 'details' ||
667
+ tagName === 'dialog') {
668
+ return true;
669
+ }
670
+
671
+ if (element.isContentEditable) {
672
+ return true;
673
+ }
674
+
675
+ if (element.onclick || element.onmousedown || element.onmouseup || element.ontouchstart || element.ontouchend) {
676
+ return true;
677
+ }
678
+
679
+ if (hasInlineEventHandlers(element)) {
680
+ return true;
681
+ }
682
+
683
+ if (hasInteractiveAriaRole(element)) {
684
+ return true;
685
+ }
686
+
687
+ if (tagName === 'a' && element.hasAttribute('href')) {
688
+ return true;
689
+ }
690
+
691
+ if (element.hasAttribute('tabindex')) {
692
+ return true;
693
+ }
694
+
695
+ return false;
696
+ }
697
+
698
+
699
+ function isDefinitelyNonInteractive(element) {
700
+ if (!element || !element.tagName) {
701
+ return true;
702
+ }
703
+
704
+ var tagName = element.tagName.toLowerCase();
705
+
706
+ // These tags are definitely non-interactive
707
+ if (ALWAYS_NON_INTERACTIVE_TAGS[tagName]) {
708
+ return true;
709
+ }
710
+
711
+ // For all other elements, we can only be certain they're non-interactive if they lack ALL indicators of interactivity
712
+ // Check for any signs of interactivity
713
+ if (hasAnyInteractivityIndicators(element)) {
714
+ return false;
715
+ }
716
+
717
+ // Check parent chain for interactive context
718
+ var parent = element.parentElement;
719
+ var depth = 0;
720
+
721
+ while (parent && depth < MAX_DEPTH) {
722
+ if (hasAnyInteractivityIndicators(parent)) {
723
+ return false; // Element is inside an interactive parent
724
+ }
725
+
726
+ if (parent.getRootNode && parent.getRootNode() !== document) {
727
+ var root = parent.getRootNode();
728
+ if (root.host && hasAnyInteractivityIndicators(root.host)) {
729
+ return false; // Inside an interactive shadow host
730
+ }
731
+ }
732
+
733
+ parent = parent.parentElement;
734
+ depth++;
735
+ }
736
+
737
+ // Pure text containers without any interactive context
738
+ if (TEXT_CONTAINER_TAGS[tagName]) {
739
+ // These are non-interactive ONLY if they have no interactive indicators (already checked as part of hasAnyInteractivityIndicators)
740
+ return true;
741
+ }
742
+
743
+ // Default: we can't be certain it's non-interactive
744
+ return false;
745
+ }
746
+
523
747
  export {
748
+ EV_CHANGE, EV_CLICK, EV_HASHCHANGE, EV_INPUT, EV_LOAD,EV_MP_LOCATION_CHANGE, EV_POPSTATE,
749
+ EV_SCROLL, EV_SCROLLEND, EV_SELECT, EV_SUBMIT, EV_TOGGLE, EV_VISIBILITYCHANGE,
750
+ getPolyfillScrollEndFunction,
524
751
  getPropsForDOMEvent,
525
752
  getSafeText,
753
+ isDefinitelyNonInteractive,
526
754
  logger,
527
755
  minDOMApisSupported,
528
756
  shouldTrackDomEvent, shouldTrackElementDetails, shouldTrackValue,
529
- EV_CHANGE, EV_CLICK, EV_HASHCHANGE, EV_MP_LOCATION_CHANGE, EV_POPSTATE,
530
- EV_SCROLLEND, EV_SUBMIT
757
+ weakSetSupported
531
758
  };
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.69.0'
3
+ LIB_VERSION: '2.71.0'
4
4
  };
5
5
 
6
6
  export default Config;
@@ -1,7 +1,7 @@
1
- import { _, console_with_prefix, safewrapClass } from '../utils'; // eslint-disable-line camelcase
1
+ import { _, console_with_prefix, generateTraceparent, safewrapClass } from '../utils'; // eslint-disable-line camelcase
2
2
  import { window } from '../window';
3
+ import Config from '../config';
3
4
 
4
- var fetch = window['fetch'];
5
5
  var logger = console_with_prefix('flags');
6
6
 
7
7
  var FLAGS_CONFIG_KEY = 'flags';
@@ -15,6 +15,7 @@ CONFIG_DEFAULTS[CONFIG_CONTEXT] = {};
15
15
  * @constructor
16
16
  */
17
17
  var FeatureFlagManager = function(initOptions) {
18
+ this.fetch = window['fetch'];
18
19
  this.getFullApiRoute = initOptions.getFullApiRoute;
19
20
  this.getMpConfig = initOptions.getConfigFunc;
20
21
  this.setMpConfig = initOptions.setConfigFunc;
@@ -23,7 +24,7 @@ var FeatureFlagManager = function(initOptions) {
23
24
  };
24
25
 
25
26
  FeatureFlagManager.prototype.init = function() {
26
- if (!minApisSupported()) {
27
+ if (!this.minApisSupported()) {
27
28
  logger.critical('Feature Flags unavailable: missing minimum required APIs');
28
29
  return;
29
30
  }
@@ -86,18 +87,24 @@ FeatureFlagManager.prototype.fetchFlags = function() {
86
87
 
87
88
  var distinctId = this.getMpProperty('distinct_id');
88
89
  var deviceId = this.getMpProperty('$device_id');
90
+ var traceparent = generateTraceparent();
89
91
  logger.log('Fetching flags for distinct ID: ' + distinctId);
90
- var reqParams = {
91
- 'context': _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT))
92
- };
92
+
93
+ var context = _.extend({'distinct_id': distinctId, 'device_id': deviceId}, this.getConfig(CONFIG_CONTEXT));
94
+ var searchParams = new URLSearchParams();
95
+ searchParams.set('context', JSON.stringify(context));
96
+ searchParams.set('token', this.getMpConfig('token'));
97
+ searchParams.set('mp_lib', 'web');
98
+ searchParams.set('$lib_version', Config.LIB_VERSION);
99
+ var url = this.getFullApiRoute() + '?' + searchParams.toString();
100
+
93
101
  this._fetchInProgressStartTime = Date.now();
94
- this.fetchPromise = window['fetch'](this.getFullApiRoute(), {
95
- 'method': 'POST',
102
+ this.fetchPromise = this.fetch.call(window, url, {
103
+ 'method': 'GET',
96
104
  'headers': {
97
105
  'Authorization': 'Basic ' + btoa(this.getMpConfig('token') + ':'),
98
- 'Content-Type': 'application/octet-stream'
99
- },
100
- 'body': JSON.stringify(reqParams)
106
+ 'traceparent': traceparent
107
+ }
101
108
  }).then(function(response) {
102
109
  this.markFetchComplete();
103
110
  return response.json().then(function(responseBody) {
@@ -109,10 +116,14 @@ FeatureFlagManager.prototype.fetchFlags = function() {
109
116
  _.each(responseFlags, function(data, key) {
110
117
  flags.set(key, {
111
118
  'key': data['variant_key'],
112
- 'value': data['variant_value']
119
+ 'value': data['variant_value'],
120
+ 'experiment_id': data['experiment_id'],
121
+ 'is_experiment_active': data['is_experiment_active'],
122
+ 'is_qa_tester': data['is_qa_tester']
113
123
  });
114
124
  });
115
125
  this.flags = flags;
126
+ this._traceparent = traceparent;
116
127
  }.bind(this)).catch(function(error) {
117
128
  this.markFetchComplete();
118
129
  logger.error(error);
@@ -209,22 +220,36 @@ FeatureFlagManager.prototype.trackFeatureCheck = function(featureName, feature)
209
220
  return;
210
221
  }
211
222
  this.trackedFeatures.add(featureName);
212
- this.track('$experiment_started', {
223
+
224
+ var trackingProperties = {
213
225
  'Experiment name': featureName,
214
226
  'Variant name': feature['key'],
215
227
  '$experiment_type': 'feature_flag',
216
228
  'Variant fetch start time': new Date(this._fetchStartTime).toISOString(),
217
229
  'Variant fetch complete time': new Date(this._fetchCompleteTime).toISOString(),
218
- 'Variant fetch latency (ms)': this._fetchLatency
219
- });
230
+ 'Variant fetch latency (ms)': this._fetchLatency,
231
+ 'Variant fetch traceparent': this._traceparent,
232
+ };
233
+
234
+ if (feature['experiment_id'] !== 'undefined') {
235
+ trackingProperties['$experiment_id'] = feature['experiment_id'];
236
+ }
237
+ if (feature['is_experiment_active'] !== 'undefined') {
238
+ trackingProperties['$is_experiment_active'] = feature['is_experiment_active'];
239
+ }
240
+ if (feature['is_qa_tester'] !== 'undefined') {
241
+ trackingProperties['$is_qa_tester'] = feature['is_qa_tester'];
242
+ }
243
+
244
+ this.track('$experiment_started', trackingProperties);
220
245
  };
221
246
 
222
- function minApisSupported() {
223
- return !!fetch &&
247
+ FeatureFlagManager.prototype.minApisSupported = function() {
248
+ return !!this.fetch &&
224
249
  typeof Promise !== 'undefined' &&
225
250
  typeof Map !== 'undefined' &&
226
251
  typeof Set !== 'undefined';
227
- }
252
+ };
228
253
 
229
254
  safewrapClass(FeatureFlagManager);
230
255
 
package/src/index.d.ts CHANGED
@@ -40,7 +40,7 @@ export interface OutTrackingOptions extends ClearOptOutInOutOptions {
40
40
  delete_user: boolean;
41
41
  }
42
42
 
43
- export type RageClickConfig =
43
+ export type RageClickConfig =
44
44
  | boolean
45
45
  | {
46
46
  /** Distance threshold in pixels for clicks to be considered within the same area (default: 30) */
@@ -51,6 +51,13 @@ export type RageClickConfig =
51
51
  click_count?: number;
52
52
  };
53
53
 
54
+ export type DeadClickConfig =
55
+ | boolean
56
+ | {
57
+ /** Time in milliseconds to wait after a click before qualifying it as dead (default: 500) */
58
+ timeout_ms?: number;
59
+ };
60
+
54
61
  export interface RegisterOptions {
55
62
  persistent: boolean;
56
63
  }
@@ -83,6 +90,12 @@ export interface AutocaptureConfig {
83
90
  * @default true
84
91
  */
85
92
  rage_click?: RageClickConfig;
93
+ /**
94
+ * When set to `true`, Mixpanel will track dead clicks (clicks that produce no response).
95
+ * Can also be configured as an object to customize dead click detection parameters.
96
+ * @default true
97
+ */
98
+ dead_click?: DeadClickConfig;
86
99
  /**
87
100
  * When set, Mixpanel will collect page scrolls at specified scroll intervals.
88
101
  * @default true
@@ -191,12 +204,12 @@ export interface Config {
191
204
  batch_size: number;
192
205
  batch_flush_interval_ms: number;
193
206
  batch_request_timeout_ms: number;
194
- record_block_class: string;
207
+ record_block_class: string | RegExp;
195
208
  record_block_selector: string;
196
209
  record_collect_fonts: boolean;
197
210
  record_idle_timeout_ms: number;
198
211
  record_inline_images: boolean;
199
- record_mask_text_class: string;
212
+ record_mask_text_class: string | RegExp;
200
213
  record_mask_text_selector: string;
201
214
  record_min_ms: number;
202
215
  record_max_ms: number;
@@ -0,0 +1 @@
1
+ export * from '../index';
@@ -0,0 +1 @@
1
+ export * from '../index';
@@ -0,0 +1 @@
1
+ export * from '../index';
package/src/utils.js CHANGED
@@ -1677,6 +1677,20 @@ var cheap_guid = function(maxlen) {
1677
1677
  return maxlen ? guid.substring(0, maxlen) : guid;
1678
1678
  };
1679
1679
 
1680
+ /**
1681
+ * Generates a W3C traceparent header for easy interop with distributed tracing systems i.e Open Telemetry
1682
+ * https://www.w3.org/TR/trace-context/#traceparent-header
1683
+ */
1684
+ var generateTraceparent = function() {
1685
+ var traceID = _.UUID().replace(/-/g, '');
1686
+ var parentID = _.UUID().replace(/-/g, '').substring(0, 16);
1687
+
1688
+ // Sampled trace
1689
+ var traceFlags = '01';
1690
+
1691
+ return '00-' + traceID + '-' + parentID + '-' + traceFlags;
1692
+ };
1693
+
1680
1694
  // naive way to extract domain name (example.com) from full hostname (my.sub.example.com)
1681
1695
  var SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
1682
1696
  // this next one attempts to account for some ccSLDs, e.g. extracting oxford.ac.uk from www.oxford.ac.uk
@@ -1746,6 +1760,7 @@ export {
1746
1760
  console,
1747
1761
  document,
1748
1762
  extract_domain,
1763
+ generateTraceparent,
1749
1764
  JSONParse,
1750
1765
  JSONStringify,
1751
1766
  isOnline,