@multiplayer-app/session-recorder-browser 2.0.18 → 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.18" || 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
@@ -24521,8 +24521,11 @@ const BASE_CONFIG = {
24521
24521
  apiBaseUrl: _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_0__.MULTIPLAYER_BASE_API_URL,
24522
24522
  exporterEndpoint: _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_0__.MULTIPLAYER_OTEL_DEFAULT_TRACES_EXPORTER_HTTP_URL,
24523
24523
  recordCanvas: false,
24524
- inlineImages: true,
24525
- inlineStylesheet: true,
24524
+ // Off by default: inlining reads pixels via canvas and requires CORS on cross-origin hosts (e.g. S3).
24525
+ // Set true when asset URLs send Access-Control-Allow-Origin for your app origin.
24526
+ inlineImages: false,
24527
+ // Off by default: inlining fetches stylesheet text; cross-origin <link> assets need CORS when enabled.
24528
+ inlineStylesheet: false,
24526
24529
  recordNavigation: true,
24527
24530
  schemifyDocSpanPayload: true,
24528
24531
  ignoreUrls: [],
@@ -24535,7 +24538,7 @@ const BASE_CONFIG = {
24535
24538
  widgetTextOverrides: DEFAULT_WIDGET_TEXT_CONFIG,
24536
24539
  useWebsocket: true,
24537
24540
  buffering: {
24538
- enabled: false,
24541
+ enabled: true,
24539
24542
  windowMinutes: 0.5,
24540
24543
  snapshotIntervalMs: 20000,
24541
24544
  },
@@ -26141,6 +26144,7 @@ if (typeof XMLHttpRequest !== 'undefined') {
26141
26144
 
26142
26145
  class RecorderBrowserSDK {
26143
26146
  constructor() {
26147
+ this.generation = 0;
26144
26148
  this.intervals = {
26145
26149
  restart: null,
26146
26150
  bufferSnapshot: null,
@@ -26176,12 +26180,15 @@ class RecorderBrowserSDK {
26176
26180
  throw new Error('Configuration not initialized. Call init() before start().');
26177
26181
  }
26178
26182
  this.startedAt = new Date().toISOString();
26183
+ const gen = ++this.generation;
26179
26184
  this.stopFn = (0,rrweb__WEBPACK_IMPORTED_MODULE_6__.record)({
26180
26185
  ...this._buildRecordOptions(),
26181
26186
  emit: async (event) => {
26187
+ if (gen !== this.generation)
26188
+ return;
26182
26189
  const ts = event.timestamp;
26183
26190
  if (!sessionId) {
26184
- await this._handleBufferOnlyEvent(event, ts);
26191
+ await this._handleBufferOnlyEvent(event, ts, gen);
26185
26192
  return;
26186
26193
  }
26187
26194
  this._handleLiveSessionEvent(event, ts, sessionId, sessionType);
@@ -26197,6 +26204,7 @@ class RecorderBrowserSDK {
26197
26204
  var _a;
26198
26205
  try {
26199
26206
  (_a = this.stopFn) === null || _a === void 0 ? void 0 : _a.call(this);
26207
+ this.stopFn = undefined;
26200
26208
  this.start(sessionId, sessionType);
26201
26209
  }
26202
26210
  catch (_e) {
@@ -26210,6 +26218,7 @@ class RecorderBrowserSDK {
26210
26218
  var _a, _b, _c;
26211
26219
  (_a = this.stopFn) === null || _a === void 0 ? void 0 : _a.call(this);
26212
26220
  this.stopFn = undefined;
26221
+ this.generation++;
26213
26222
  if (!((_b = this.config) === null || _b === void 0 ? void 0 : _b.useWebsocket)) {
26214
26223
  (_c = this.socketService) === null || _c === void 0 ? void 0 : _c.close();
26215
26224
  }
@@ -26249,13 +26258,15 @@ class RecorderBrowserSDK {
26249
26258
  * @param event - Event.
26250
26259
  * @param ts - Timestamp.
26251
26260
  */
26252
- async _handleBufferOnlyEvent(event, ts) {
26261
+ async _handleBufferOnlyEvent(event, ts, gen) {
26253
26262
  if (!this.crashBuffer)
26254
26263
  return;
26255
26264
  try {
26256
26265
  this._applyConsoleMasking(event);
26257
26266
  const packedEvent = (0,_rrweb_packer__WEBPACK_IMPORTED_MODULE_0__.pack)(event);
26258
26267
  this.stoppedAt = new Date(ts).toISOString();
26268
+ if (gen !== this.generation)
26269
+ return;
26259
26270
  await this.crashBuffer.appendEvent({
26260
26271
  ts,
26261
26272
  isFullSnapshot: event.type === _rrweb_types__WEBPACK_IMPORTED_MODULE_1__.EventType.FullSnapshot,
@@ -26527,10 +26538,11 @@ class ApiService {
26527
26538
 
26528
26539
 
26529
26540
  class CrashBufferService {
26530
- constructor(db, tabId, windowMs) {
26541
+ constructor(db, tabId, windowMs, ready = Promise.resolve()) {
26531
26542
  this.db = db;
26532
26543
  this.tabId = tabId;
26533
26544
  this.windowMs = windowMs;
26545
+ this.ready = ready;
26534
26546
  this.lastPruneAt = 0;
26535
26547
  this.pruneInFlight = null;
26536
26548
  this.isActive = true;
@@ -26553,6 +26565,7 @@ class CrashBufferService {
26553
26565
  if (!this.isActive)
26554
26566
  return;
26555
26567
  const isFullSnapshot = Boolean(payload.isFullSnapshot);
26568
+ await this.ready;
26556
26569
  await this._safe(async () => {
26557
26570
  await this.db.appendEvent({
26558
26571
  tabId: this.tabId,
@@ -26573,6 +26586,7 @@ class CrashBufferService {
26573
26586
  if (!this.isActive)
26574
26587
  return;
26575
26588
  let errorEvent = null;
26589
+ await this.ready;
26576
26590
  await this._safe(async () => {
26577
26591
  const records = payload.map((p) => {
26578
26592
  var _a, _b;
@@ -26621,6 +26635,7 @@ class CrashBufferService {
26621
26635
  }
26622
26636
  async snapshot(_windowMs, now = Date.now()) {
26623
26637
  var _a, _b;
26638
+ await this.ready;
26624
26639
  const stoppedAt = now;
26625
26640
  const startedAt = Math.max(0, stoppedAt - this.windowMs);
26626
26641
  // Always include a full snapshot "anchor" if one exists at/before the window start.
@@ -27258,6 +27273,8 @@ class SocketService extends lib0_observable__WEBPACK_IMPORTED_MODULE_4__.Observa
27258
27273
  this.sessionId = null;
27259
27274
  this.usePostMessage = false;
27260
27275
  this.isInitialized = false;
27276
+ this.lastUserPayload = null;
27277
+ this.lastSubscribeSession = null;
27261
27278
  this.options = {
27262
27279
  apiKey: '',
27263
27280
  socketUrl: '',
@@ -27320,6 +27337,15 @@ class SocketService extends lib0_observable__WEBPACK_IMPORTED_MODULE_4__.Observa
27320
27337
  this.isConnecting = false;
27321
27338
  this.isConnected = true;
27322
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
+ }
27323
27349
  this.flushQueue();
27324
27350
  });
27325
27351
  this.socket.on('disconnect', (err) => {
@@ -27386,16 +27412,33 @@ class SocketService extends lib0_observable__WEBPACK_IMPORTED_MODULE_4__.Observa
27386
27412
  this.emitSocketEvent(_config__WEBPACK_IMPORTED_MODULE_2__.SESSION_ADD_EVENT, event);
27387
27413
  }
27388
27414
  subscribeToSession(session) {
27415
+ // Remember the session so reconnects replay subscribe + started.
27416
+ this.lastSubscribeSession = session;
27417
+ this._emitSubscribe(session);
27418
+ }
27419
+ _emitSubscribe(session) {
27389
27420
  this.sessionId = session.shortId || session._id;
27390
- const payload = {
27421
+ const subscribePayload = {
27391
27422
  projectId: session.project,
27392
27423
  workspaceId: session.workspace,
27393
27424
  debugSessionId: this.sessionId,
27394
27425
  sessionType: session.creationType,
27395
27426
  };
27396
- this.emitSocketEvent(_config__WEBPACK_IMPORTED_MODULE_2__.SESSION_SUBSCRIBE_EVENT, payload);
27397
- // use long id instead of short id
27398
- 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
+ }
27399
27442
  }
27400
27443
  unsubscribeFromSession(stopSession) {
27401
27444
  if (this.sessionId) {
@@ -27404,9 +27447,23 @@ class SocketService extends lib0_observable__WEBPACK_IMPORTED_MODULE_4__.Observa
27404
27447
  this.emitSocketEvent(_config__WEBPACK_IMPORTED_MODULE_2__.SESSION_STOPPED_EVENT, {});
27405
27448
  }
27406
27449
  }
27450
+ this.lastSubscribeSession = null;
27407
27451
  }
27408
27452
  setUser(data) {
27409
- 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
+ }
27410
27467
  }
27411
27468
  close() {
27412
27469
  return new Promise((resolve) => {
@@ -27648,13 +27705,11 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27648
27705
  this._start();
27649
27706
  }
27650
27707
  else {
27651
- // Buffer-only recording when there is no active debug session.
27652
27708
  this._startBufferOnlyRecording();
27653
27709
  }
27654
27710
  this._registerWidgetEvents();
27655
27711
  this._registerSocketServiceListeners();
27656
27712
  _services_messaging_service__WEBPACK_IMPORTED_MODULE_9__["default"].sendMessage('state-change', this.sessionState);
27657
- // Emit init observable event
27658
27713
  this.emit('init', [this]);
27659
27714
  }
27660
27715
  _setupCrashBuffer() {
@@ -27662,7 +27717,8 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27662
27717
  if ((_a = this._configs.buffering) === null || _a === void 0 ? void 0 : _a.enabled) {
27663
27718
  const windowMinutes = this._configs.buffering.windowMinutes || 0.5;
27664
27719
  const windowMs = Math.max(10000, windowMinutes * 60 * 1000);
27665
- 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);
27666
27722
  this._recorder.setCrashBuffer(this._crashBuffer);
27667
27723
  this._tracer.setCrashBuffer(this._crashBuffer);
27668
27724
  this._crashBuffer.on('error-span-appended', (payload) => {
@@ -27714,10 +27770,14 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27714
27770
  }
27715
27771
  _startBufferOnlyRecording() {
27716
27772
  var _a, _b;
27717
- if (this.sessionId ||
27718
- !this._crashBuffer ||
27719
- !((_b = (_a = this._configs) === null || _a === void 0 ? void 0 : _a.buffering) === null || _b === void 0 ? void 0 : _b.enabled) ||
27720
- 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) {
27721
27781
  return;
27722
27782
  }
27723
27783
  void this._recorder.restart(null, _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_0__.SessionType.MANUAL);
@@ -27787,9 +27847,10 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27787
27847
  async stop(comment) {
27788
27848
  try {
27789
27849
  this._checkOperation('stop');
27850
+ const sid = this.sessionId;
27790
27851
  this._stop();
27791
27852
  if (this.continuousRecording) {
27792
- await this._apiService.stopContinuousDebugSession(this.sessionId);
27853
+ await this._apiService.stopContinuousDebugSession(sid);
27793
27854
  this.sessionType = _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_0__.SessionType.MANUAL;
27794
27855
  }
27795
27856
  else {
@@ -27797,10 +27858,9 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27797
27858
  sessionAttributes: { comment },
27798
27859
  stoppedAt: this._recorder.stoppedAt,
27799
27860
  };
27800
- const response = await this._apiService.stopSession(this.sessionId, request);
27861
+ const response = await this._apiService.stopSession(sid, request);
27801
27862
  _eventBus__WEBPACK_IMPORTED_MODULE_7__.recorderEventBus.emit(_config__WEBPACK_IMPORTED_MODULE_5__.SESSION_RESPONSE, response);
27802
27863
  }
27803
- this._clearSession();
27804
27864
  }
27805
27865
  catch (error) {
27806
27866
  this.error = error.message;
@@ -27836,15 +27896,15 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27836
27896
  async cancel() {
27837
27897
  try {
27838
27898
  this._checkOperation('cancel');
27899
+ const sid = this.sessionId;
27839
27900
  this._stop();
27840
27901
  if (this.continuousRecording) {
27841
- await this._apiService.stopContinuousDebugSession(this.sessionId);
27902
+ await this._apiService.stopContinuousDebugSession(sid);
27842
27903
  this.sessionType = _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_0__.SessionType.MANUAL;
27843
27904
  }
27844
27905
  else {
27845
- await this._apiService.cancelSession(this.sessionId);
27906
+ await this._apiService.cancelSession(sid);
27846
27907
  }
27847
- this._clearSession();
27848
27908
  }
27849
27909
  catch (error) {
27850
27910
  this.error = error.message;
@@ -27896,12 +27956,7 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
27896
27956
  }
27897
27957
  }
27898
27958
  async _flushBuffer(sessionId) {
27899
- var _a, _b;
27900
- if (!sessionId ||
27901
- !this._crashBuffer ||
27902
- this._isFlushingBuffer ||
27903
- !((_b = (_a = this._configs) === null || _a === void 0 ? void 0 : _a.buffering) === null || _b === void 0 ? void 0 : _b.enabled) ||
27904
- this.sessionState !== _types__WEBPACK_IMPORTED_MODULE_4__.SessionState.stopped) {
27959
+ if (!sessionId || !this._crashBuffer || this._isFlushingBuffer) {
27905
27960
  return null;
27906
27961
  }
27907
27962
  this._isFlushingBuffer = true;
@@ -28091,6 +28146,8 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
28091
28146
  });
28092
28147
  this._socketService.on(_config__WEBPACK_IMPORTED_MODULE_5__.SESSION_SAVE_BUFFER_EVENT, (payload) => {
28093
28148
  var _a;
28149
+ if (this.sessionState !== _types__WEBPACK_IMPORTED_MODULE_4__.SessionState.stopped)
28150
+ return;
28094
28151
  this._flushBuffer((_a = payload === null || payload === void 0 ? void 0 : payload.debugSession) === null || _a === void 0 ? void 0 : _a._id);
28095
28152
  });
28096
28153
  }
@@ -28161,7 +28218,18 @@ class SessionRecorder extends lib0_observable__WEBPACK_IMPORTED_MODULE_16__.Obse
28161
28218
  this._tracer.stop();
28162
28219
  this._recorder.stop();
28163
28220
  this._navigationRecorder.stop();
28164
- 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());
28165
28233
  }
28166
28234
  /**
28167
28235
  * Pause the session tracing and recording
@@ -56818,6 +56886,7 @@ __webpack_require__.r(__webpack_exports__);
56818
56886
  /* harmony export */ MULTIPLAYER_TRACE_SESSION_CACHE_PREFIX: () => (/* reexport safe */ _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_7__.MULTIPLAYER_TRACE_SESSION_CACHE_PREFIX),
56819
56887
  /* harmony export */ MULTIPLAYER_TRACE_SESSION_PREFIX: () => (/* reexport safe */ _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_7__.MULTIPLAYER_TRACE_SESSION_PREFIX),
56820
56888
  /* harmony export */ NavigationRecorder: () => (/* reexport safe */ _navigation__WEBPACK_IMPORTED_MODULE_6__.NavigationRecorder),
56889
+ /* harmony export */ SessionRecorder: () => (/* binding */ SessionRecorderInstance),
56821
56890
  /* harmony export */ SessionRecorderBrowserTraceExporter: () => (/* reexport safe */ _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_7__.SessionRecorderBrowserTraceExporter),
56822
56891
  /* harmony export */ SessionRecorderIdGenerator: () => (/* reexport safe */ _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_7__.SessionRecorderIdGenerator),
56823
56892
  /* harmony export */ SessionRecorderSdk: () => (/* reexport safe */ _multiplayer_app_session_recorder_common__WEBPACK_IMPORTED_MODULE_7__.SessionRecorderSdk),
@@ -56865,6 +56934,7 @@ else {
56865
56934
  SessionRecorderInstance = new _session_recorder__WEBPACK_IMPORTED_MODULE_3__.SessionRecorder();
56866
56935
  }
56867
56936
 
56937
+
56868
56938
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (SessionRecorderInstance);
56869
56939
 
56870
56940
  })();