mixpanel-browser 2.72.0 → 2.74.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 (44) hide show
  1. package/.claude/settings.local.json +5 -2
  2. package/.eslintrc.json +7 -4
  3. package/.github/workflows/integration-tests.yml +52 -0
  4. package/.github/workflows/unit-tests.yml +40 -0
  5. package/CHANGELOG.md +12 -0
  6. package/README.md +1 -1
  7. package/build.sh +1 -5
  8. package/dist/mixpanel-core.cjs.d.ts +49 -4
  9. package/dist/mixpanel-core.cjs.js +244 -26
  10. package/dist/mixpanel-recorder.js +5258 -688
  11. package/dist/mixpanel-recorder.min.js +1 -1
  12. package/dist/mixpanel-recorder.min.js.map +1 -1
  13. package/dist/mixpanel-with-async-recorder.cjs.d.ts +49 -4
  14. package/dist/mixpanel-with-async-recorder.cjs.js +244 -26
  15. package/dist/mixpanel-with-recorder.d.ts +49 -4
  16. package/dist/mixpanel-with-recorder.js +6858 -2099
  17. package/dist/mixpanel-with-recorder.min.d.ts +49 -4
  18. package/dist/mixpanel-with-recorder.min.js +1 -1
  19. package/dist/mixpanel.amd.d.ts +49 -4
  20. package/dist/mixpanel.amd.js +6858 -2099
  21. package/dist/mixpanel.cjs.d.ts +49 -4
  22. package/dist/mixpanel.cjs.js +6858 -2099
  23. package/dist/mixpanel.globals.js +244 -26
  24. package/dist/mixpanel.min.js +175 -171
  25. package/dist/mixpanel.module.d.ts +49 -4
  26. package/dist/mixpanel.module.js +6858 -2099
  27. package/dist/mixpanel.umd.d.ts +49 -4
  28. package/dist/mixpanel.umd.js +6858 -2099
  29. package/dist/rrweb-bundled.js +4315 -591
  30. package/dist/rrweb-compiled.js +4962 -641
  31. package/package.json +30 -5
  32. package/rollup.config.mjs +254 -224
  33. package/src/autocapture/utils.js +15 -7
  34. package/src/config.js +1 -1
  35. package/src/index.d.ts +49 -4
  36. package/src/mixpanel-core.js +215 -15
  37. package/src/recorder/masking.js +197 -0
  38. package/src/recorder/rrweb-entrypoint.js +2 -1
  39. package/src/recorder/session-recording.js +43 -4
  40. package/src/recorder/utils.js +5 -1
  41. package/src/utils.js +11 -2
  42. package/src/window.js +3 -1
  43. package/testServer.js +51 -7
  44. package/.github/workflows/tests.yml +0 -25
@@ -1,6 +1,7 @@
1
1
  // this file exists as an entry point to be able to transpile rrweb packages to es5
2
2
  // compatible code without needing to transpile the entire mixpanel-js codebase
3
3
  import {record, EventType, IncrementalSource} from '@mixpanel/rrweb';
4
+ import {classMatchesRegex} from '@mixpanel/rrweb-snapshot';
4
5
  import {getRecordConsolePlugin} from '@mixpanel/rrweb-plugin-console-record';
5
6
 
6
- export { record, EventType, IncrementalSource, getRecordConsolePlugin };
7
+ export { record, EventType, IncrementalSource, getRecordConsolePlugin, classMatchesRegex };
@@ -1,11 +1,17 @@
1
+ /**
2
+ * @typedef {import('../index').RecordPrivacyConfig} RecordPrivacyConfig
3
+ */
4
+
1
5
  import { window } from '../window';
2
- import { IncrementalSource, EventType, getRecordConsolePlugin } from './rrweb-entrypoint';
6
+ import { EventType, getRecordConsolePlugin, IncrementalSource } from './rrweb-entrypoint';
3
7
  import { MAX_RECORDING_MS, MAX_VALUE_FOR_MIN_RECORDING_MS, console_with_prefix, NOOP_FUNC, _, localStorageSupported, canUseCompressionStream, navigator, userAgent, windowOpera} from '../utils'; // eslint-disable-line camelcase
4
8
  import { IDBStorageWrapper, RECORDING_EVENTS_STORE_NAME } from '../storage/indexed-db';
5
9
  import { addOptOutCheckMixpanelLib } from '../gdpr-utils';
6
10
  import { RequestBatcher } from '../request-batcher';
11
+
7
12
  import Config from '../config';
8
13
  import { RECORD_ENQUEUE_THROTTLE_MS } from './utils';
14
+ import { shouldMaskInput, shouldMaskText, getPrivacyConfig } from './masking';
9
15
 
10
16
  var logger = console_with_prefix('recorder');
11
17
  var CompressionStream = window['CompressionStream'];
@@ -52,7 +58,7 @@ function isUserEvent(ev) {
52
58
  * @property {String} [options.replayId] - unique uuid for a single replay
53
59
  * @property {Function} [options.onIdleTimeout] - callback when a recording reaches idle timeout
54
60
  * @property {Function} [options.onMaxLengthReached] - callback when a recording reaches its maximum length
55
- * @property {Function} [options.rrwebRecord] - rrweb's `record` function
61
+ * @property {import('./rrweb-entrypoint').record} [options.rrwebRecord] - rrweb's `record` function
56
62
  * @property {Function} [options.onBatchSent] - callback when a batch of events is sent to the server
57
63
  * @property {Storage} [options.sharedLockStorage] - optional storage for shared lock, used for test dependency injection
58
64
  * optional properties for deserialization:
@@ -233,6 +239,8 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
233
239
  blockSelector = undefined;
234
240
  }
235
241
 
242
+ var privacyConfig = getPrivacyConfig(this._mixpanel);
243
+
236
244
  try {
237
245
  this._stopRecording = this._rrwebRecord({
238
246
  'emit': function (ev) {
@@ -262,9 +270,11 @@ SessionRecording.prototype.startRecording = function (shouldStopBatcher) {
262
270
  'type': 'image/webp',
263
271
  'quality': 0.6
264
272
  },
273
+ // mask all inputs and text by default, unmasking is applied via callbacks maskInputFn and maskTextFn
265
274
  'maskAllInputs': true,
266
- 'maskTextClass': this.getConfig('record_mask_text_class'),
267
- 'maskTextSelector': this.getConfig('record_mask_text_selector'),
275
+ 'maskTextSelector': '*',
276
+ 'maskInputFn': this._getMaskFn(shouldMaskInput, privacyConfig),
277
+ 'maskTextFn': this._getMaskFn(shouldMaskText, privacyConfig),
268
278
  'recordCanvas': this.getConfig('record_canvas'),
269
279
  'sampling': {
270
280
  'canvas': 15
@@ -536,4 +546,33 @@ SessionRecording.prototype._getRecordMinMs = function() {
536
546
  return configValue;
537
547
  };
538
548
 
549
+ /**
550
+ * Creates a masking function for rrweb's maskInputFn or maskTextFn
551
+ * @param {(element: HTMLElement, privacyConfig: RecordPrivacyConfig) => boolean} shouldMaskFn - Function that determines if element should be masked
552
+ * @param {RecordPrivacyConfig} privacyConfig - Privacy configuration
553
+ * @returns {(text: string, element: HTMLElement) => string} Function that masks text based on privacy config
554
+ */
555
+ SessionRecording.prototype._getMaskFn = function(shouldMaskFn, privacyConfig) {
556
+ return function(text, element) {
557
+ // prevent visual artifacts from random whitespace
558
+ if (!text.trim().length) {
559
+ return '';
560
+ }
561
+
562
+ var shouldMask = true;
563
+ try {
564
+ shouldMask = shouldMaskFn(element, privacyConfig);
565
+ } catch (err) {
566
+ this.reportError('Error checking if text should be masked, defaulting to masked', err);
567
+ }
568
+
569
+ if (shouldMask) {
570
+ var textLength = Math.min(text.length, 10000); // limit to 10000 chars to optimize performance
571
+ return '*'.repeat(textLength);
572
+ } else {
573
+ return text;
574
+ }
575
+ }.bind(this);
576
+ };
577
+
539
578
  export { SessionRecording };
@@ -7,6 +7,10 @@ var isRecordingExpired = function(serializedRecording) {
7
7
  return !serializedRecording || now > serializedRecording['maxExpires'] || now > serializedRecording['idleExpires'];
8
8
  };
9
9
 
10
+
10
11
  var RECORD_ENQUEUE_THROTTLE_MS = 250;
11
12
 
12
- export { isRecordingExpired, RECORD_ENQUEUE_THROTTLE_MS};
13
+ export {
14
+ isRecordingExpired,
15
+ RECORD_ENQUEUE_THROTTLE_MS
16
+ };
package/src/utils.js CHANGED
@@ -1115,8 +1115,17 @@ function _storageWrapper(storage, name, is_supported_fn) {
1115
1115
  };
1116
1116
  }
1117
1117
 
1118
- _.localStorage = _storageWrapper(window.localStorage, 'localStorage', localStorageSupported);
1119
- _.sessionStorage = _storageWrapper(window.sessionStorage, 'sessionStorage', sessionStorageSupported);
1118
+ // Safari errors out accessing localStorage/sessionStorage when cookies are disabled,
1119
+ // so create dummy storage wrappers that silently fail as a fallback.
1120
+ var windowLocalStorage = null, windowSessionStorage = null;
1121
+ try {
1122
+ windowLocalStorage = window.localStorage;
1123
+ windowSessionStorage = window.sessionStorage;
1124
+ // eslint-disable-next-line no-empty
1125
+ } catch (_err) {}
1126
+
1127
+ _.localStorage = _storageWrapper(windowLocalStorage, 'localStorage', localStorageSupported);
1128
+ _.sessionStorage = _storageWrapper(windowSessionStorage, 'sessionStorage', sessionStorageSupported);
1120
1129
 
1121
1130
  _.register_event = (function() {
1122
1131
  // written by Dean Edwards, 2005
package/src/window.js CHANGED
@@ -15,7 +15,9 @@ if (typeof(window) === 'undefined') {
15
15
  screen: { width: 0, height: 0 },
16
16
  location: loc,
17
17
  addEventListener: function() {},
18
- removeEventListener: function() {}
18
+ removeEventListener: function() {},
19
+ dispatchEvent: function() {},
20
+ CustomEvent: function () {}
19
21
  };
20
22
  } else {
21
23
  win = window;
package/testServer.js CHANGED
@@ -1,14 +1,40 @@
1
1
  'use strict';
2
2
 
3
- var express = require('express');
4
- var cookieParser = require('cookie-parser');
5
- var logger = require('morgan');
3
+ const express = require('express');
4
+ const cookieParser = require('cookie-parser');
5
+ const logger = require('morgan');
6
6
 
7
- var app = express();
7
+ const app = express();
8
+
9
+ // Test suite definitions
10
+ const TEST_SUITES = {
11
+ 'dev': {
12
+ name: 'Development Build',
13
+ description: 'Unminified library for easier debugging',
14
+ customLibUrl: './static/build/mixpanel.js',
15
+ snippetUrl: './static/src/loaders/mixpanel-jslib-snippet.js',
16
+ testUrl: './static/build/test/browser/snippet-test.js'
17
+ },
18
+ 'minified': {
19
+ name: 'Minified Build',
20
+ description: 'Production build (closure compiled, served via Mixpanel CDN)',
21
+ customLibUrl: './static/build/mixpanel.min.js',
22
+ snippetUrl: './static/build/mixpanel-jslib-snippet.min.test.js',
23
+ testUrl: './static/build/test/browser/snippet-test.js'
24
+ },
25
+ 'module-cjs': {
26
+ name: 'CommonJS Module',
27
+ description: 'Node.js compatible module (served via npm install)',
28
+ testUrl: './static/build/test/browser/module-cjs-test.js'
29
+ }
30
+ };
8
31
 
9
32
  app.use(cookieParser());
10
33
  app.use(logger('dev'));
11
34
 
35
+ app.set('views', __dirname + '/tests');
36
+ app.set('view engine', 'pug');
37
+
12
38
  app.use('/tests', express.static(__dirname + "/tests"));
13
39
  app.get('/tests/cookie_included/:cookieName', function(req, res) {
14
40
  if (req.cookies && req.cookies[req.params.cookieName]) {
@@ -19,9 +45,27 @@ app.get('/tests/cookie_included/:cookieName', function(req, res) {
19
45
  });
20
46
  app.use(express.static(__dirname));
21
47
  app.get('/', function(req, res) {
22
- res.redirect(301, '/tests/');
48
+ res.redirect(301, '/tests/new');
23
49
  });
24
50
 
25
- var server = app.listen(3000, function () {
26
- console.log('Mixpanel test app listening on port %s', server.address().port);
51
+ app.get('/tests/new', function(req, res) {
52
+ res.render('directory.pug', { testSuites: TEST_SUITES });
53
+ });
54
+
55
+ app.use('/tests/new/static', express.static(__dirname));
56
+
57
+ // register routes for each test suite
58
+ for (const [suiteId, suite] of Object.entries(TEST_SUITES)) {
59
+ app.get('/tests/new/' + suiteId, function(req, res) {
60
+ res.render('integration.pug', {
61
+ suiteName: suite.name,
62
+ customLibUrl: suite.customLibUrl,
63
+ snippetUrl: suite.snippetUrl,
64
+ testUrl: suite.testUrl
65
+ });
66
+ });
67
+ }
68
+
69
+ const server = app.listen(3001, function () {
70
+ console.log(`Mixpanel test app listening on port ${server.address().port}`);
27
71
  });
@@ -1,25 +0,0 @@
1
- name: Tests
2
-
3
- on:
4
- push:
5
- branches: [master]
6
- pull_request:
7
- branches: [master]
8
-
9
- jobs:
10
- build:
11
- runs-on: ubuntu-latest
12
-
13
- strategy:
14
- matrix:
15
- node-version: [20.x, 22.x]
16
-
17
- steps:
18
- - uses: actions/checkout@v4
19
- - name: Use Node.js ${{ matrix.node-version }}
20
- uses: actions/setup-node@v4
21
- with:
22
- node-version: ${{ matrix.node-version }}
23
- - run: npm ci
24
- - run: npm test
25
- - run: npm run build-dist