@multiplayer-app/session-recorder-browser 2.0.23 → 2.0.24

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/dist/index.umd.js CHANGED
@@ -24436,7 +24436,7 @@ const CONTINUOUS_DEBUGGING_TIMEOUT = 60000; // 1 minutes
24436
24436
  const DEBUG_SESSION_MAX_DURATION_SECONDS = 10 * 60 + 30; // TODO: move to shared config otel core
24437
24437
  const REMOTE_SESSION_RECORDING_START = 'remote-session-recording:start';
24438
24438
  const REMOTE_SESSION_RECORDING_STOP = 'remote-session-recording:stop';
24439
- const PACKAGE_VERSION_EXPORT = "2.0.23" || 0;
24439
+ const PACKAGE_VERSION_EXPORT = "2.0.24" || 0;
24440
24440
  // Regex patterns for OpenTelemetry ignore URLs
24441
24441
  const OTEL_IGNORE_URLS = [
24442
24442
  // Traces endpoint
@@ -24538,7 +24538,7 @@ const BASE_CONFIG = {
24538
24538
  widgetTextOverrides: DEFAULT_WIDGET_TEXT_CONFIG,
24539
24539
  useWebsocket: true,
24540
24540
  buffering: {
24541
- enabled: false,
24541
+ enabled: true,
24542
24542
  windowMinutes: 0.5,
24543
24543
  snapshotIntervalMs: 20000,
24544
24544
  },
@@ -26144,6 +26144,7 @@ if (typeof XMLHttpRequest !== 'undefined') {
26144
26144
 
26145
26145
  class RecorderBrowserSDK {
26146
26146
  constructor() {
26147
+ this.generation = 0;
26147
26148
  this.intervals = {
26148
26149
  restart: null,
26149
26150
  bufferSnapshot: null,
@@ -26179,12 +26180,15 @@ class RecorderBrowserSDK {
26179
26180
  throw new Error('Configuration not initialized. Call init() before start().');
26180
26181
  }
26181
26182
  this.startedAt = new Date().toISOString();
26183
+ const gen = ++this.generation;
26182
26184
  this.stopFn = (0,rrweb__WEBPACK_IMPORTED_MODULE_6__.record)({
26183
26185
  ...this._buildRecordOptions(),
26184
26186
  emit: async (event) => {
26187
+ if (gen !== this.generation)
26188
+ return;
26185
26189
  const ts = event.timestamp;
26186
26190
  if (!sessionId) {
26187
- await this._handleBufferOnlyEvent(event, ts);
26191
+ await this._handleBufferOnlyEvent(event, ts, gen);
26188
26192
  return;
26189
26193
  }
26190
26194
  this._handleLiveSessionEvent(event, ts, sessionId, sessionType);
@@ -26200,6 +26204,7 @@ class RecorderBrowserSDK {
26200
26204
  var _a;
26201
26205
  try {
26202
26206
  (_a = this.stopFn) === null || _a === void 0 ? void 0 : _a.call(this);
26207
+ this.stopFn = undefined;
26203
26208
  this.start(sessionId, sessionType);
26204
26209
  }
26205
26210
  catch (_e) {
@@ -26213,6 +26218,7 @@ class RecorderBrowserSDK {
26213
26218
  var _a, _b, _c;
26214
26219
  (_a = this.stopFn) === null || _a === void 0 ? void 0 : _a.call(this);
26215
26220
  this.stopFn = undefined;
26221
+ this.generation++;
26216
26222
  if (!((_b = this.config) === null || _b === void 0 ? void 0 : _b.useWebsocket)) {
26217
26223
  (_c = this.socketService) === null || _c === void 0 ? void 0 : _c.close();
26218
26224
  }
@@ -26252,13 +26258,15 @@ class RecorderBrowserSDK {
26252
26258
  * @param event - Event.
26253
26259
  * @param ts - Timestamp.
26254
26260
  */
26255
- async _handleBufferOnlyEvent(event, ts) {
26261
+ async _handleBufferOnlyEvent(event, ts, gen) {
26256
26262
  if (!this.crashBuffer)
26257
26263
  return;
26258
26264
  try {
26259
26265
  this._applyConsoleMasking(event);
26260
26266
  const packedEvent = (0,_rrweb_packer__WEBPACK_IMPORTED_MODULE_0__.pack)(event);
26261
26267
  this.stoppedAt = new Date(ts).toISOString();
26268
+ if (gen !== this.generation)
26269
+ return;
26262
26270
  await this.crashBuffer.appendEvent({
26263
26271
  ts,
26264
26272
  isFullSnapshot: event.type === _rrweb_types__WEBPACK_IMPORTED_MODULE_1__.EventType.FullSnapshot,
@@ -26530,10 +26538,11 @@ class ApiService {
26530
26538
 
26531
26539
 
26532
26540
  class CrashBufferService {
26533
- constructor(db, tabId, windowMs) {
26541
+ constructor(db, tabId, windowMs, ready = Promise.resolve()) {
26534
26542
  this.db = db;
26535
26543
  this.tabId = tabId;
26536
26544
  this.windowMs = windowMs;
26545
+ this.ready = ready;
26537
26546
  this.lastPruneAt = 0;
26538
26547
  this.pruneInFlight = null;
26539
26548
  this.isActive = true;
@@ -26556,6 +26565,7 @@ class CrashBufferService {
26556
26565
  if (!this.isActive)
26557
26566
  return;
26558
26567
  const isFullSnapshot = Boolean(payload.isFullSnapshot);
26568
+ await this.ready;
26559
26569
  await this._safe(async () => {
26560
26570
  await this.db.appendEvent({
26561
26571
  tabId: this.tabId,
@@ -26576,6 +26586,7 @@ class CrashBufferService {
26576
26586
  if (!this.isActive)
26577
26587
  return;
26578
26588
  let errorEvent = null;
26589
+ await this.ready;
26579
26590
  await this._safe(async () => {
26580
26591
  const records = payload.map((p) => {
26581
26592
  var _a, _b;
@@ -26624,6 +26635,7 @@ class CrashBufferService {
26624
26635
  }
26625
26636
  async snapshot(_windowMs, now = Date.now()) {
26626
26637
  var _a, _b;
26638
+ await this.ready;
26627
26639
  const stoppedAt = now;
26628
26640
  const startedAt = Math.max(0, stoppedAt - this.windowMs);
26629
26641
  // Always include a full snapshot "anchor" if one exists at/before the window start.
@@ -27261,6 +27273,8 @@ class SocketService extends lib0_observable__WEBPACK_IMPORTED_MODULE_4__.Observa
27261
27273
  this.sessionId = null;
27262
27274
  this.usePostMessage = false;
27263
27275
  this.isInitialized = false;
27276
+ this.lastUserPayload = null;
27277
+ this.lastSubscribeSession = null;
27264
27278
  this.options = {
27265
27279
  apiKey: '',
27266
27280
  socketUrl: '',
@@ -27323,6 +27337,15 @@ class SocketService extends lib0_observable__WEBPACK_IMPORTED_MODULE_4__.Observa
27323
27337
  this.isConnecting = false;
27324
27338
  this.isConnected = true;
27325
27339
  this.usePostMessage = false;
27340
+ // Re-establish identity and session on every (re)connect: socket.io's auto-reconnect
27341
+ // fires 'ready' again after a drop, but the server has no memory of the previous
27342
+ // setUser/subscribe calls — the client must replay them.
27343
+ if (this.lastUserPayload && this.socket) {
27344
+ this.socket.emit(_config__WEBPACK_IMPORTED_MODULE_2__.SOCKET_SET_USER_EVENT, this.lastUserPayload);
27345
+ }
27346
+ if (this.lastSubscribeSession) {
27347
+ this._emitSubscribe(this.lastSubscribeSession);
27348
+ }
27326
27349
  this.flushQueue();
27327
27350
  });
27328
27351
  this.socket.on('disconnect', (err) => {
@@ -27389,16 +27412,33 @@ class SocketService extends lib0_observable__WEBPACK_IMPORTED_MODULE_4__.Observa
27389
27412
  this.emitSocketEvent(_config__WEBPACK_IMPORTED_MODULE_2__.SESSION_ADD_EVENT, event);
27390
27413
  }
27391
27414
  subscribeToSession(session) {
27415
+ // Remember the session so reconnects replay subscribe + started.
27416
+ this.lastSubscribeSession = session;
27417
+ this._emitSubscribe(session);
27418
+ }
27419
+ _emitSubscribe(session) {
27392
27420
  this.sessionId = session.shortId || session._id;
27393
- const payload = {
27421
+ const subscribePayload = {
27394
27422
  projectId: session.project,
27395
27423
  workspaceId: session.workspace,
27396
27424
  debugSessionId: this.sessionId,
27397
27425
  sessionType: session.creationType,
27398
27426
  };
27399
- this.emitSocketEvent(_config__WEBPACK_IMPORTED_MODULE_2__.SESSION_SUBSCRIBE_EVENT, payload);
27400
- // use long id instead of short id
27401
- this.emitSocketEvent(_config__WEBPACK_IMPORTED_MODULE_2__.SESSION_STARTED_EVENT, { debugSessionId: session._id });
27427
+ const startedPayload = { debugSessionId: session._id };
27428
+ // Send directly (not via the queue). The queue would cause a duplicate emit on
27429
+ // the next 'ready' alongside the replay, since 'ready' also replays lastSubscribeSession.
27430
+ if (this.usePostMessage) {
27431
+ this.sendViaPostMessage(_config__WEBPACK_IMPORTED_MODULE_2__.SESSION_SUBSCRIBE_EVENT, subscribePayload);
27432
+ this.sendViaPostMessage(_config__WEBPACK_IMPORTED_MODULE_2__.SESSION_STARTED_EVENT, startedPayload);
27433
+ }
27434
+ else if (this.socket && this.isConnected) {
27435
+ this.socket.emit(_config__WEBPACK_IMPORTED_MODULE_2__.SESSION_SUBSCRIBE_EVENT, subscribePayload);
27436
+ this.socket.emit(_config__WEBPACK_IMPORTED_MODULE_2__.SESSION_STARTED_EVENT, startedPayload);
27437
+ }
27438
+ else {
27439
+ // Not connected: 'ready' will replay via lastSubscribeSession once the socket is up.
27440
+ this._initConnection();
27441
+ }
27402
27442
  }
27403
27443
  unsubscribeFromSession(stopSession) {
27404
27444
  if (this.sessionId) {
@@ -27407,9 +27447,23 @@ class SocketService extends lib0_observable__WEBPACK_IMPORTED_MODULE_4__.Observa
27407
27447
  this.emitSocketEvent(_config__WEBPACK_IMPORTED_MODULE_2__.SESSION_STOPPED_EVENT, {});
27408
27448
  }
27409
27449
  }
27450
+ this.lastSubscribeSession = null;
27410
27451
  }
27411
27452
  setUser(data) {
27412
- this.emitSocketEvent(_config__WEBPACK_IMPORTED_MODULE_2__.SOCKET_SET_USER_EVENT, data);
27453
+ // Remember the last identity so 'ready' (initial connect + every reconnect) can replay it.
27454
+ // Send directly here rather than queuing: the queue would cause a duplicate emit on the
27455
+ // next 'ready' alongside the replay.
27456
+ this.lastUserPayload = data;
27457
+ if (this.usePostMessage) {
27458
+ this.sendViaPostMessage(_config__WEBPACK_IMPORTED_MODULE_2__.SOCKET_SET_USER_EVENT, data);
27459
+ }
27460
+ else if (this.socket && this.isConnected) {
27461
+ this.socket.emit(_config__WEBPACK_IMPORTED_MODULE_2__.SOCKET_SET_USER_EVENT, data);
27462
+ }
27463
+ else {
27464
+ // Not connected yet: 'ready' will replay lastUserPayload once the socket is up.
27465
+ this._initConnection();
27466
+ }
27413
27467
  }
27414
27468
  close() {
27415
27469
  return new Promise((resolve) => {
@@ -27651,13 +27705,11 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27651
27705
  this._start();
27652
27706
  }
27653
27707
  else {
27654
- // Buffer-only recording when there is no active debug session.
27655
27708
  this._startBufferOnlyRecording();
27656
27709
  }
27657
27710
  this._registerWidgetEvents();
27658
27711
  this._registerSocketServiceListeners();
27659
27712
  _services_messaging_service__WEBPACK_IMPORTED_MODULE_9__["default"].sendMessage('state-change', this.sessionState);
27660
- // Emit init observable event
27661
27713
  this.emit('init', [this]);
27662
27714
  }
27663
27715
  _setupCrashBuffer() {
@@ -27665,7 +27717,8 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27665
27717
  if ((_a = this._configs.buffering) === null || _a === void 0 ? void 0 : _a.enabled) {
27666
27718
  const windowMinutes = this._configs.buffering.windowMinutes || 0.5;
27667
27719
  const windowMs = Math.max(10000, windowMinutes * 60 * 1000);
27668
- this._crashBuffer = new _services_crashBuffer_service__WEBPACK_IMPORTED_MODULE_13__.CrashBufferService(this._bufferDb, this._tabId, windowMs);
27720
+ const ready = this._bufferDb.clearTab(this._tabId).catch(() => undefined);
27721
+ this._crashBuffer = new _services_crashBuffer_service__WEBPACK_IMPORTED_MODULE_13__.CrashBufferService(this._bufferDb, this._tabId, windowMs, ready);
27669
27722
  this._recorder.setCrashBuffer(this._crashBuffer);
27670
27723
  this._tracer.setCrashBuffer(this._crashBuffer);
27671
27724
  this._crashBuffer.on('error-span-appended', (payload) => {
@@ -27717,10 +27770,14 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27717
27770
  }
27718
27771
  _startBufferOnlyRecording() {
27719
27772
  var _a, _b;
27720
- if (this.sessionId ||
27721
- !this._crashBuffer ||
27722
- !((_b = (_a = this._configs) === null || _a === void 0 ? void 0 : _a.buffering) === null || _b === void 0 ? void 0 : _b.enabled) ||
27723
- this.sessionState !== _types__WEBPACK_IMPORTED_MODULE_4__.SessionState.stopped) {
27773
+ // NOTE: `this.sessionId` is intentionally NOT checked. `_stop()` runs
27774
+ // before `_clearSession()` (the stopSession API call sits between them),
27775
+ // so the clear().then() chain fires while sessionId is still set.
27776
+ // Bailing on sessionId here meant rrweb never restarted after manual
27777
+ // stop, leaving the buffer with no FullSnapshot and silently breaking
27778
+ // exception-triggered flushBuffer. `_recorder.restart(null, ...)` passes
27779
+ // null explicitly, so it's safe regardless of `this.sessionId`.
27780
+ if (!this._crashBuffer || !((_b = (_a = this._configs) === null || _a === void 0 ? void 0 : _a.buffering) === null || _b === void 0 ? void 0 : _b.enabled) || this.sessionState !== _types__WEBPACK_IMPORTED_MODULE_4__.SessionState.stopped) {
27724
27781
  return;
27725
27782
  }
27726
27783
  void this._recorder.restart(null, _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_0__.SessionType.MANUAL);
@@ -27790,9 +27847,10 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27790
27847
  async stop(comment) {
27791
27848
  try {
27792
27849
  this._checkOperation('stop');
27850
+ const sid = this.sessionId;
27793
27851
  this._stop();
27794
27852
  if (this.continuousRecording) {
27795
- await this._apiService.stopContinuousDebugSession(this.sessionId);
27853
+ await this._apiService.stopContinuousDebugSession(sid);
27796
27854
  this.sessionType = _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_0__.SessionType.MANUAL;
27797
27855
  }
27798
27856
  else {
@@ -27800,10 +27858,9 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27800
27858
  sessionAttributes: { comment },
27801
27859
  stoppedAt: this._recorder.stoppedAt,
27802
27860
  };
27803
- const response = await this._apiService.stopSession(this.sessionId, request);
27861
+ const response = await this._apiService.stopSession(sid, request);
27804
27862
  _eventBus__WEBPACK_IMPORTED_MODULE_7__.recorderEventBus.emit(_config__WEBPACK_IMPORTED_MODULE_5__.SESSION_RESPONSE, response);
27805
27863
  }
27806
- this._clearSession();
27807
27864
  }
27808
27865
  catch (error) {
27809
27866
  this.error = error.message;
@@ -27839,15 +27896,15 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27839
27896
  async cancel() {
27840
27897
  try {
27841
27898
  this._checkOperation('cancel');
27899
+ const sid = this.sessionId;
27842
27900
  this._stop();
27843
27901
  if (this.continuousRecording) {
27844
- await this._apiService.stopContinuousDebugSession(this.sessionId);
27902
+ await this._apiService.stopContinuousDebugSession(sid);
27845
27903
  this.sessionType = _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_0__.SessionType.MANUAL;
27846
27904
  }
27847
27905
  else {
27848
- await this._apiService.cancelSession(this.sessionId);
27906
+ await this._apiService.cancelSession(sid);
27849
27907
  }
27850
- this._clearSession();
27851
27908
  }
27852
27909
  catch (error) {
27853
27910
  this.error = error.message;
@@ -27899,12 +27956,7 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27899
27956
  }
27900
27957
  }
27901
27958
  async _flushBuffer(sessionId) {
27902
- var _a, _b;
27903
- if (!sessionId ||
27904
- !this._crashBuffer ||
27905
- this._isFlushingBuffer ||
27906
- !((_b = (_a = this._configs) === null || _a === void 0 ? void 0 : _a.buffering) === null || _b === void 0 ? void 0 : _b.enabled) ||
27907
- this.sessionState !== _types__WEBPACK_IMPORTED_MODULE_4__.SessionState.stopped) {
27959
+ if (!sessionId || !this._crashBuffer || this._isFlushingBuffer) {
27908
27960
  return null;
27909
27961
  }
27910
27962
  this._isFlushingBuffer = true;
@@ -28094,6 +28146,8 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
28094
28146
  });
28095
28147
  this._socketService.on(_config__WEBPACK_IMPORTED_MODULE_5__.SESSION_SAVE_BUFFER_EVENT, (payload) => {
28096
28148
  var _a;
28149
+ if (this.sessionState !== _types__WEBPACK_IMPORTED_MODULE_4__.SessionState.stopped)
28150
+ return;
28097
28151
  this._flushBuffer((_a = payload === null || payload === void 0 ? void 0 : payload.debugSession) === null || _a === void 0 ? void 0 : _a._id);
28098
28152
  });
28099
28153
  }
@@ -28164,7 +28218,18 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
28164
28218
  this._tracer.stop();
28165
28219
  this._recorder.stop();
28166
28220
  this._navigationRecorder.stop();
28167
- this._startBufferOnlyRecording();
28221
+ // Clear session identity synchronously. The buffer-restart chain and the
28222
+ // error-span-appended listener both gate on `this.sessionId === null`;
28223
+ // deferring this to `_clearSession()` (after the network stopSession
28224
+ // call) left them seeing a stale id and silently no-oping. Callers that
28225
+ // need the id for the stop/cancel API must capture it before _stop().
28226
+ this.session = null;
28227
+ this.sessionId = null;
28228
+ // rrweb assigns new node IDs on each record() call, so the next buffer
28229
+ // segment must not carry events from the previous generation. Await the
28230
+ // clear so its IDB tx can't race past the fresh FullSnapshot.
28231
+ const cleared = this._crashBuffer ? this._crashBuffer.clear() : Promise.resolve();
28232
+ void cleared.catch(() => undefined).then(() => this._startBufferOnlyRecording());
28168
28233
  }
28169
28234
  /**
28170
28235
  * Pause the session tracing and recording
@@ -56821,6 +56886,7 @@ __webpack_require__.r(__webpack_exports__);
56821
56886
  /* harmony export */ MULTIPLAYER_TRACE_SESSION_CACHE_PREFIX: () => (/* reexport safe */ _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_7__.MULTIPLAYER_TRACE_SESSION_CACHE_PREFIX),
56822
56887
  /* harmony export */ MULTIPLAYER_TRACE_SESSION_PREFIX: () => (/* reexport safe */ _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_7__.MULTIPLAYER_TRACE_SESSION_PREFIX),
56823
56888
  /* harmony export */ NavigationRecorder: () => (/* reexport safe */ _navigation__WEBPACK_IMPORTED_MODULE_6__.NavigationRecorder),
56889
+ /* harmony export */ SessionRecorder: () => (/* binding */ SessionRecorderInstance),
56824
56890
  /* harmony export */ SessionRecorderBrowserTraceExporter: () => (/* reexport safe */ _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_7__.SessionRecorderBrowserTraceExporter),
56825
56891
  /* harmony export */ SessionRecorderIdGenerator: () => (/* reexport safe */ _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_7__.SessionRecorderIdGenerator),
56826
56892
  /* harmony export */ SessionRecorderSdk: () => (/* reexport safe */ _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_7__.SessionRecorderSdk),
@@ -56868,6 +56934,7 @@ else {
56868
56934
  SessionRecorderInstance = new _session_recorder__WEBPACK_IMPORTED_MODULE_3__.SessionRecorder();
56869
56935
  }
56870
56936
 
56937
+
56871
56938
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (SessionRecorderInstance);
56872
56939
 
56873
56940
  })();