mixpanel-browser 2.76.0 → 2.78.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 (52) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/.github/dependabot.yml +8 -0
  3. package/.github/workflows/integration-tests.yml +2 -2
  4. package/.github/workflows/unit-tests.yml +2 -2
  5. package/CHANGELOG.md +8 -0
  6. package/dist/async-modules/mixpanel-recorder-BjSlYaNJ.min.js +2 -0
  7. package/dist/async-modules/mixpanel-recorder-BjSlYaNJ.min.js.map +1 -0
  8. package/dist/async-modules/{mixpanel-recorder-bIS4LMGd.js → mixpanel-recorder-zMBXIyeG.js} +84 -10
  9. package/dist/async-modules/{mixpanel-targeting-VOeN7RWY.min.js → mixpanel-targeting-BSHal4N9.min.js} +2 -2
  10. package/dist/async-modules/{mixpanel-targeting-VOeN7RWY.min.js.map → mixpanel-targeting-BSHal4N9.min.js.map} +1 -1
  11. package/dist/async-modules/{mixpanel-targeting-BcAPS-Mz.js → mixpanel-targeting-UHf4eBfC.js} +1 -1
  12. package/dist/mixpanel-core.cjs.d.ts +3 -1
  13. package/dist/mixpanel-core.cjs.js +292 -130
  14. package/dist/mixpanel-recorder.js +84 -10
  15. package/dist/mixpanel-recorder.min.js +1 -1
  16. package/dist/mixpanel-recorder.min.js.map +1 -1
  17. package/dist/mixpanel-targeting.js +1 -1
  18. package/dist/mixpanel-targeting.min.js +1 -1
  19. package/dist/mixpanel-targeting.min.js.map +1 -1
  20. package/dist/mixpanel-with-async-modules.cjs.d.ts +3 -1
  21. package/dist/mixpanel-with-async-modules.cjs.js +294 -132
  22. package/dist/mixpanel-with-async-recorder.cjs.d.ts +3 -1
  23. package/dist/mixpanel-with-async-recorder.cjs.js +294 -132
  24. package/dist/mixpanel-with-recorder.d.ts +3 -1
  25. package/dist/mixpanel-with-recorder.js +381 -168
  26. package/dist/mixpanel-with-recorder.min.d.ts +3 -1
  27. package/dist/mixpanel-with-recorder.min.js +1 -1
  28. package/dist/mixpanel.amd.d.ts +3 -1
  29. package/dist/mixpanel.amd.js +381 -168
  30. package/dist/mixpanel.cjs.d.ts +3 -1
  31. package/dist/mixpanel.cjs.js +381 -168
  32. package/dist/mixpanel.globals.js +294 -132
  33. package/dist/mixpanel.min.js +191 -186
  34. package/dist/mixpanel.module.d.ts +3 -1
  35. package/dist/mixpanel.module.js +381 -168
  36. package/dist/mixpanel.umd.d.ts +3 -1
  37. package/dist/mixpanel.umd.js +381 -168
  38. package/dist/rrweb-bundled.js +61 -9
  39. package/dist/rrweb-compiled.js +56 -9
  40. package/package.json +6 -5
  41. package/src/config.js +1 -1
  42. package/src/flags/CLAUDE.md +24 -0
  43. package/src/flags/index.js +109 -80
  44. package/src/index.d.ts +3 -1
  45. package/src/mixpanel-core.js +4 -2
  46. package/src/recorder/session-recording.js +5 -1
  47. package/src/recorder/utils.js +27 -1
  48. package/src/recorder-manager.js +110 -2
  49. package/testServer.js +16 -1
  50. package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js +0 -2
  51. package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js.map +0 -1
  52. /package/src/loaders/{loader-module-with-async-recorder.d.ts → loader-module-with-async-modules.d.ts} +0 -0
@@ -1,12 +1,18 @@
1
1
  /* eslint camelcase: "off" */
2
+
2
3
  import {RECORDER_FILENAME, TARGETING_FILENAME, RECORDER_GLOBAL_NAME} from './config';
3
- import { _, console, safewrap, safewrapClass } from './utils';
4
+ import { _, console, console_with_prefix, safewrap, safewrapClass } from './utils';
4
5
  import { window } from './window';
5
6
  import { Promise } from './promise-polyfill';
6
7
  import { IDBStorageWrapper, RECORDING_REGISTRY_STORE_NAME } from './storage/indexed-db';
7
- import { isRecordingExpired } from './recorder/utils';
8
+ import { isRecordingExpired, validateAllowedOrigins } from './recorder/utils';
8
9
  import { getTargetingPromise } from './targeting/loader';
9
10
 
11
+ var logger = console_with_prefix('recorder');
12
+
13
+ var IFRAME_HANDSHAKE_REQUEST = 'mp_iframe_handshake_request';
14
+ var IFRAME_HANDSHAKE_RESPONSE = 'mp_iframe_handshake_response';
15
+
10
16
 
11
17
  /**
12
18
  * RecorderManager: manages session recording initialization, lifecycle and state
@@ -27,6 +33,8 @@ var RecorderManager = function(initOptions) {
27
33
  this.libBasePath = initOptions.libBasePath;
28
34
 
29
35
  this._recorder = null;
36
+ this._parentReplayId = null;
37
+ this._parentFrameRetryInterval = null;
30
38
  };
31
39
 
32
40
  RecorderManager.prototype.shouldLoadRecorder = function() {
@@ -80,6 +88,22 @@ RecorderManager.prototype.checkAndStartSessionRecording = function(force_start,
80
88
  }, this));
81
89
  }, this);
82
90
 
91
+ // Cross-origin iframe handling
92
+ var allowedOrigins = validateAllowedOrigins(this.getMpConfig('record_allowed_iframe_origins'), logger);
93
+ var isCrossOriginRecordingEnabled = allowedOrigins.length > 0;
94
+
95
+ if (isCrossOriginRecordingEnabled) {
96
+ // listen for handshake requests from their own child iframes (including nested)
97
+ this._setupParentFrameListener(allowedOrigins);
98
+
99
+ if (window.parent !== window) {
100
+ // also wait for parent's replay ID
101
+ this._setupChildFrameListener(allowedOrigins, loadRecorder);
102
+ this._sendParentFrameRequestWithRetry(allowedOrigins);
103
+ return Promise.resolve();
104
+ }
105
+ }
106
+
83
107
  /**
84
108
  * If the user is sampled or start_session_recording is called, we always load the recorder since it's guaranteed a recording should start.
85
109
  * Otherwise, if the recording registry has any records then it's likely there's a recording in progress or orphaned data that needs to be flushed.
@@ -199,6 +223,10 @@ RecorderManager.prototype.getSessionReplayUrl = function() {
199
223
  };
200
224
 
201
225
  RecorderManager.prototype.getSessionReplayId = function() {
226
+ // Child iframe uses parent's replay ID
227
+ if (this._parentReplayId) {
228
+ return this._parentReplayId;
229
+ }
202
230
  var replay_id = null;
203
231
  if (this._recorder) {
204
232
  replay_id = this._recorder['replayId'];
@@ -211,6 +239,86 @@ RecorderManager.prototype.getRecorder = function() {
211
239
  return this._recorder;
212
240
  };
213
241
 
242
+ RecorderManager.prototype._setupChildFrameListener = function(allowedOrigins, loadRecorder) {
243
+ if (this._childFrameMessageHandler) {
244
+ return;
245
+ }
246
+ var self = this;
247
+ this._childFrameMessageHandler = function(event) {
248
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
249
+ var data = event.data;
250
+ if (data && data['type'] === IFRAME_HANDSHAKE_RESPONSE && data['token'] === self.getMpConfig('token') && data['replayId']) {
251
+ self._parentReplayId = data['replayId'];
252
+ if (data['distinctId']) {
253
+ self.mixpanelInstance['identify'](data['distinctId']);
254
+ }
255
+ self._parentFrameRetryActive = false;
256
+ window.removeEventListener('message', self._childFrameMessageHandler);
257
+ self._childFrameMessageHandler = null;
258
+ loadRecorder(true);
259
+ }
260
+ };
261
+ window.addEventListener('message', this._childFrameMessageHandler);
262
+ };
263
+
264
+ RecorderManager.prototype._sendParentFrameRequest = function(allowedOrigins) {
265
+ var message = {};
266
+ message['type'] = IFRAME_HANDSHAKE_REQUEST;
267
+ message['token'] = this.getMpConfig('token');
268
+ for (var i = 0; i < allowedOrigins.length; i++) {
269
+ try {
270
+ window.parent.postMessage(message, allowedOrigins[i]);
271
+ } catch (e) {
272
+ // origin mismatch - ignore
273
+ }
274
+ }
275
+ };
276
+
277
+ RecorderManager.prototype._sendParentFrameRequestWithRetry = function(allowedOrigins) {
278
+ var self = this;
279
+ var maxRetries = 10;
280
+ var retryCount = 0;
281
+ var delay = 50;
282
+ this._parentFrameRetryActive = true;
283
+
284
+ this._sendParentFrameRequest(allowedOrigins);
285
+
286
+ function scheduleRetry() {
287
+ setTimeout(function() {
288
+ if (!self._parentFrameRetryActive || self._parentReplayId || ++retryCount >= maxRetries) {
289
+ return;
290
+ }
291
+ self._sendParentFrameRequest(allowedOrigins);
292
+ delay *= 2;
293
+ scheduleRetry();
294
+ }, delay);
295
+ }
296
+ scheduleRetry();
297
+ };
298
+
299
+ RecorderManager.prototype._setupParentFrameListener = function(allowedOrigins) {
300
+ if (this._parentFrameMessageHandler) {
301
+ return;
302
+ }
303
+ var self = this;
304
+ this._parentFrameMessageHandler = function(event) {
305
+ if (allowedOrigins.indexOf(event.origin) === -1) return;
306
+ var data = event.data;
307
+ if (data && data['type'] === IFRAME_HANDSHAKE_REQUEST && data['token'] === self.getMpConfig('token')) {
308
+ var replayId = self.getSessionReplayId();
309
+ if (replayId) {
310
+ var response = {};
311
+ response['type'] = IFRAME_HANDSHAKE_RESPONSE;
312
+ response['token'] = self.getMpConfig('token');
313
+ response['replayId'] = replayId;
314
+ response['distinctId'] = self.getDistinctId();
315
+ event.source.postMessage(response, event.origin);
316
+ }
317
+ }
318
+ };
319
+ window.addEventListener('message', this._parentFrameMessageHandler);
320
+ };
321
+
214
322
  safewrapClass(RecorderManager);
215
323
 
216
324
  export { RecorderManager };
package/testServer.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const express = require('express');
4
4
  const cookieParser = require('cookie-parser');
5
5
  const logger = require('morgan');
6
+ const { PARENT_PORT, CHILD_PORT } = require('./tests/browser/test-ports');
6
7
 
7
8
  const app = express();
8
9
 
@@ -119,8 +120,22 @@ for (const [suiteId, suite] of Object.entries(TEST_SUITES)) {
119
120
  testUrl: suite.testUrl
120
121
  });
121
122
  });
123
+
124
+ // Cross-origin child iframe page for session recording tests
125
+ app.get('/tests/new/' + suiteId + '-cross-origin-page', function(req, res) {
126
+ res.render('cross-origin-page.pug', {
127
+ customLibUrl: suite.customLibUrl || './static/build/mixpanel.js',
128
+ snippetUrl: suite.snippetUrl || './static/src/loaders/mixpanel-jslib-snippet.js',
129
+ testUrl: './static/build/test/browser/cross-origin-page.js'
130
+ });
131
+ });
122
132
  }
123
133
 
124
- const server = app.listen(3001, function () {
134
+ const server = app.listen(PARENT_PORT, function () {
125
135
  console.log(`Mixpanel test app listening on port ${server.address().port}`);
126
136
  });
137
+
138
+ // Second port for cross-origin iframe tests
139
+ const server2 = app.listen(CHILD_PORT, function () {
140
+ console.log(`Mixpanel cross-origin test server listening on port ${server2.address().port}`);
141
+ });