@multiplayer-app/session-recorder-react-native 1.3.23 → 1.3.32

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 (49) hide show
  1. package/README.md +22 -13
  2. package/lib/module/config/defaults.js +2 -2
  3. package/lib/module/config/defaults.js.map +1 -1
  4. package/lib/module/config/session-recorder.js.map +1 -1
  5. package/lib/module/otel/CrashBufferSpanProcessor.js +14 -13
  6. package/lib/module/otel/CrashBufferSpanProcessor.js.map +1 -1
  7. package/lib/module/otel/index.js +85 -59
  8. package/lib/module/otel/index.js.map +1 -1
  9. package/lib/module/recorder/index.js +1 -1
  10. package/lib/module/recorder/index.js.map +1 -1
  11. package/lib/module/services/api.service.js.map +1 -1
  12. package/lib/module/services/crashBuffer.service.js +51 -19
  13. package/lib/module/services/crashBuffer.service.js.map +1 -1
  14. package/lib/module/services/socket.service.js +2 -2
  15. package/lib/module/services/socket.service.js.map +1 -1
  16. package/lib/module/session-recorder.js +29 -78
  17. package/lib/module/session-recorder.js.map +1 -1
  18. package/lib/module/types/session-recorder.js.map +1 -1
  19. package/lib/module/utils/rrweb-events.js +1 -1
  20. package/lib/module/utils/rrweb-events.js.map +1 -1
  21. package/lib/typescript/src/config/defaults.d.ts.map +1 -1
  22. package/lib/typescript/src/config/session-recorder.d.ts.map +1 -1
  23. package/lib/typescript/src/otel/CrashBufferSpanProcessor.d.ts +3 -4
  24. package/lib/typescript/src/otel/CrashBufferSpanProcessor.d.ts.map +1 -1
  25. package/lib/typescript/src/otel/index.d.ts +2 -3
  26. package/lib/typescript/src/otel/index.d.ts.map +1 -1
  27. package/lib/typescript/src/services/api.service.d.ts +2 -1
  28. package/lib/typescript/src/services/api.service.d.ts.map +1 -1
  29. package/lib/typescript/src/services/crashBuffer.service.d.ts +4 -23
  30. package/lib/typescript/src/services/crashBuffer.service.d.ts.map +1 -1
  31. package/lib/typescript/src/services/socket.service.d.ts +4 -1
  32. package/lib/typescript/src/services/socket.service.d.ts.map +1 -1
  33. package/lib/typescript/src/session-recorder.d.ts +0 -3
  34. package/lib/typescript/src/session-recorder.d.ts.map +1 -1
  35. package/lib/typescript/src/types/session-recorder.d.ts +6 -13
  36. package/lib/typescript/src/types/session-recorder.d.ts.map +1 -1
  37. package/lib/typescript/src/utils/rrweb-events.d.ts.map +1 -1
  38. package/package.json +2 -2
  39. package/src/config/defaults.ts +29 -29
  40. package/src/config/session-recorder.ts +8 -2
  41. package/src/otel/CrashBufferSpanProcessor.ts +17 -24
  42. package/src/otel/index.ts +110 -61
  43. package/src/recorder/index.ts +1 -1
  44. package/src/services/api.service.ts +2 -1
  45. package/src/services/crashBuffer.service.ts +73 -31
  46. package/src/services/socket.service.ts +5 -2
  47. package/src/session-recorder.ts +43 -98
  48. package/src/types/session-recorder.ts +15 -18
  49. package/src/utils/rrweb-events.ts +3 -3
@@ -1,9 +1,14 @@
1
1
  import { Platform } from 'react-native';
2
+ import { EventType } from '@rrweb/types';
2
3
  import type {
3
4
  CrashBufferEventMap,
4
5
  CrashBufferEventName,
5
6
  CrashBufferErrorSpanAppendedEvent,
6
7
  CrashBuffer,
8
+ CrashBufferRrwebEventPayload,
9
+ CrashBufferOtelSpanPayload,
10
+ CrashBufferOtelSpanBatchPayload,
11
+ CrashBufferSnapshot,
7
12
  } from '@multiplayer-app/session-recorder-common';
8
13
  import { SpanStatusCode } from '@opentelemetry/api';
9
14
 
@@ -43,15 +48,6 @@ const RECORD_PREFIX = 'mp_crash_buffer_rec_v1:';
43
48
  const randomId = (): string =>
44
49
  `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
45
50
 
46
- export type CrashBufferSnapshot = {
47
- rrwebEvents: Array<{ ts: number; event: any }>;
48
- otelSpans: Array<{ ts: number; span: any }>;
49
- attrs: any | null;
50
- windowMs: number;
51
- fromTs: number;
52
- toTs: number;
53
- };
54
-
55
51
  export class CrashBufferService implements CrashBuffer {
56
52
  private static instance: CrashBufferService | null = null;
57
53
 
@@ -59,7 +55,9 @@ export class CrashBufferService implements CrashBuffer {
59
55
  private indexLoaded = false;
60
56
  private lastPruneAt = 0;
61
57
  private opChain: Promise<any> = Promise.resolve();
62
- private defaultWindowMs: number = 1 * 60 * 1000;
58
+ private defaultWindowMs: number = 0.5 * 60 * 1000;
59
+ private lastSeenEventTs: number = 0;
60
+
63
61
  private listeners = new Map<
64
62
  CrashBufferEventName,
65
63
  Set<(payload: CrashBufferEventMap[CrashBufferEventName]) => void>
@@ -111,19 +109,34 @@ export class CrashBufferService implements CrashBuffer {
111
109
  }
112
110
 
113
111
  async appendEvent(
114
- payload: { ts: number; event: any },
112
+ payload: CrashBufferRrwebEventPayload,
115
113
  windowMs?: number
116
114
  ): Promise<void> {
115
+ const ts = payload?.ts ?? Date.now();
116
+ this.lastSeenEventTs = Math.max(this.lastSeenEventTs, ts);
117
+
118
+ const rawEventType =
119
+ (payload as any)?.event?.eventType ?? (payload as any)?.event?.type;
120
+ const isFullSnapshot =
121
+ Boolean(payload.isFullSnapshot) ||
122
+ rawEventType === EventType.FullSnapshot;
123
+
124
+ const record: CrashBufferRrwebEventPayload = {
125
+ ...payload,
126
+ ts,
127
+ isFullSnapshot,
128
+ };
129
+
117
130
  return this.appendRecord(
118
131
  'rrweb',
119
- payload.ts,
120
- payload,
132
+ record.ts,
133
+ record,
121
134
  windowMs ?? this.defaultWindowMs
122
135
  );
123
136
  }
124
137
 
125
138
  async appendSpans(
126
- payload: Array<{ ts: number; span: any }>,
139
+ payload: CrashBufferOtelSpanBatchPayload,
127
140
  windowMs?: number
128
141
  ): Promise<void> {
129
142
  if (!payload.length) return;
@@ -161,7 +174,7 @@ export class CrashBufferService implements CrashBuffer {
161
174
  }
162
175
 
163
176
  setDefaultWindowMs(windowMs: number): void {
164
- this.defaultWindowMs = Math.max(10_000, windowMs || 1 * 60 * 1000);
177
+ this.defaultWindowMs = Math.max(10_000, windowMs || 0.5 * 60 * 1000);
165
178
  }
166
179
 
167
180
  on<E extends CrashBufferEventName>(
@@ -281,32 +294,60 @@ export class CrashBufferService implements CrashBuffer {
281
294
  }
282
295
  }
283
296
 
284
- const rrwebEvents: Array<{ ts: number; event: any }> = [];
285
- const otelSpans: Array<{ ts: number; span: any }> = [];
297
+ const allEvents: CrashBufferRrwebEventPayload[] = [];
298
+ const allSpans: CrashBufferOtelSpanPayload[] = [];
286
299
 
287
300
  for (const e of entries.sort((a, b) => a.ts - b.ts)) {
288
301
  const key = `${RECORD_PREFIX}${e.id}`;
289
302
  const payload = byKey.get(key);
290
303
  if (!payload) continue;
291
- if (e.kind === 'rrweb') rrwebEvents.push(payload);
292
- if (e.kind === 'span') otelSpans.push(payload);
304
+ if (e.kind === 'rrweb') allEvents.push(payload);
305
+ if (e.kind === 'span') allSpans.push(payload);
293
306
  }
294
307
 
295
- let attrs: any | null = null;
296
- try {
297
- const raw = AsyncStorage ? await AsyncStorage.getItem(ATTRS_KEY) : null;
298
- attrs = raw ? JSON.parse(raw) : null;
299
- } catch (_e) {
300
- attrs = null;
308
+ // Mirror browser semantics:
309
+ // - Ensure the rrweb stream starts at Meta -> FullSnapshot (or is empty).
310
+ // - Only include spans from the replayable window onward.
311
+ const eventsSorted = allEvents.slice().sort((a, b) => a.ts - b.ts);
312
+ const firstSnapshotIdx = eventsSorted.findIndex((e) => {
313
+ const t = (e as any)?.event?.eventType ?? (e as any)?.event?.type;
314
+ return t === EventType.FullSnapshot;
315
+ });
316
+
317
+ if (firstSnapshotIdx < 0) {
318
+ return {
319
+ events: [],
320
+ spans: [],
321
+ startedAt: fromTs,
322
+ stoppedAt: toTs,
323
+ };
324
+ }
325
+
326
+ let startIdx = firstSnapshotIdx;
327
+ for (let i = firstSnapshotIdx - 1; i >= 0; i--) {
328
+ const t =
329
+ (eventsSorted[i] as any)?.event?.eventType ??
330
+ (eventsSorted[i] as any)?.event?.type;
331
+ if (t === EventType.Meta) {
332
+ startIdx = i;
333
+ break;
334
+ }
301
335
  }
302
336
 
337
+ const rrwebEvents = eventsSorted.slice(startIdx);
338
+ const firstEvent = rrwebEvents[0];
339
+ const replayStartedAt =
340
+ typeof firstEvent?.ts === 'number' ? firstEvent.ts : fromTs;
341
+
342
+ const otelSpans = allSpans
343
+ .filter((s) => typeof s?.ts === 'number' && s.ts >= replayStartedAt)
344
+ .sort((a, b) => a.ts - b.ts);
345
+
303
346
  return {
304
- rrwebEvents,
305
- otelSpans,
306
- attrs,
307
- windowMs: effectiveWindowMs,
308
- fromTs,
309
- toTs,
347
+ spans: otelSpans,
348
+ events: rrwebEvents,
349
+ stoppedAt: toTs,
350
+ startedAt: replayStartedAt,
310
351
  };
311
352
  });
312
353
  }
@@ -317,6 +358,7 @@ export class CrashBufferService implements CrashBuffer {
317
358
  await this.ensureIndexLoaded();
318
359
  const keys = this.index.map((e) => `${RECORD_PREFIX}${e.id}`);
319
360
  this.index = [];
361
+ this.lastSeenEventTs = 0;
320
362
  try {
321
363
  await AsyncStorage.multiRemove([INDEX_KEY, ATTRS_KEY, ...keys]);
322
364
  } catch (_e) {
@@ -216,8 +216,11 @@ export class SocketService extends Observable<SocketServiceEvents> {
216
216
  }
217
217
  }
218
218
 
219
- public setUser(userAttributes: IUserAttributes | null): void {
220
- this.emitSocketEvent(SOCKET_SET_USER_EVENT, userAttributes);
219
+ public setUser(data: {
220
+ userAttributes: IUserAttributes | null;
221
+ clientId?: string;
222
+ }): void {
223
+ this.emitSocketEvent(SOCKET_SET_USER_EVENT, data);
221
224
  }
222
225
 
223
226
  public close(): Promise<void> {
@@ -180,95 +180,32 @@ class SessionRecorder
180
180
  }
181
181
  }
182
182
 
183
- public async flushBuffer(payload?: { reason?: string }): Promise<any> {
184
- if (!this._configs?.buffering?.enabled) return null;
185
- if (this._isFlushingBuffer) return null;
186
- if (this.sessionState !== SessionState.stopped || this.sessionId)
187
- return null;
188
-
189
- const windowMs = Math.max(
190
- 10_000,
191
- (this._configs.buffering.windowMinutes || 2) * 60 * 1000
192
- );
193
-
194
- this._isFlushingBuffer = true;
195
- try {
196
- const reason = payload?.reason || 'manual';
197
- await this._crashBuffer.setAttrs({
198
- sessionAttributes: this.sessionAttributes,
199
- resourceAttributes: getNavigatorInfo(),
200
- userAttributes: this._userAttributes,
201
- });
202
-
203
- const snapshot = await this._crashBuffer.snapshot(windowMs);
204
- if (
205
- snapshot.rrwebEvents.length === 0 &&
206
- snapshot.otelSpans.length === 0
207
- ) {
208
- return null;
209
- }
210
-
211
- const request: StartSessionRequest = {
212
- name: `${this._configs.application} ${getFormattedDate(new Date())}`,
213
- stoppedAt: new Date().toISOString(),
214
- sessionAttributes: this.sessionAttributes,
215
- resourceAttributes: getNavigatorInfo(),
216
- ...(this._userAttributes
217
- ? { userAttributes: this._userAttributes }
218
- : {}),
219
- debugSessionData: {
220
- meta: {
221
- reason,
222
- windowMs: snapshot.windowMs,
223
- fromTs: snapshot.fromTs,
224
- toTs: snapshot.toTs,
225
- },
226
- events: snapshot.rrwebEvents,
227
- spans: snapshot.otelSpans.map((s) => s.span),
228
- attrs: snapshot.attrs,
229
- },
230
- };
231
-
232
- try {
233
- const res = await this._apiService.startSession(request);
234
- await this._crashBuffer.clear();
235
- return res;
236
- } catch (_e) {
237
- // swallow: flush is best-effort; never throw into app code
238
- return null;
239
- }
240
- } finally {
241
- this._isFlushingBuffer = false;
242
- }
243
- }
244
-
245
183
  private async _flushBuffer(sessionId: string): Promise<any> {
246
- if (!this._configs?.buffering?.enabled) return null;
247
- if (this._isFlushingBuffer) return null;
248
- if (this.sessionState !== SessionState.stopped || this.sessionId)
184
+ if (
185
+ !sessionId ||
186
+ !this._crashBuffer ||
187
+ this._isFlushingBuffer ||
188
+ !this._configs?.buffering?.enabled ||
189
+ this.sessionState !== SessionState.stopped
190
+ ) {
249
191
  return null;
250
-
251
- const windowMs = Math.max(
252
- 10_000,
253
- (this._configs.buffering.windowMinutes || 2) * 60 * 1000
254
- );
192
+ }
255
193
 
256
194
  this._isFlushingBuffer = true;
257
195
  try {
258
- const snapshot = await this._crashBuffer.snapshot(windowMs);
259
- if (
260
- snapshot.rrwebEvents.length === 0 &&
261
- snapshot.otelSpans.length === 0
262
- ) {
196
+ const { events, spans, startedAt, stoppedAt } =
197
+ await this._crashBuffer.snapshot();
198
+ if (events.length === 0 && spans.length === 0) {
263
199
  return null;
264
200
  }
265
- const spans = snapshot.otelSpans.map((s) => s.span);
266
- const events = snapshot.rrwebEvents.map((e) => e.event);
267
201
  await Promise.all([
268
- this._tracer.exportTraces(spans),
269
- this._apiService.exportEvents(sessionId, { events }),
202
+ this._tracer.exportTraces(spans.map((s) => s.span)),
203
+ this._apiService.exportEvents(sessionId, {
204
+ events: events.map((e) => e.event),
205
+ }),
270
206
  this._apiService.updateSessionAttributes(sessionId, {
271
- name: this._getSessionName(),
207
+ startedAt: new Date(startedAt).toISOString(),
208
+ stoppedAt: new Date(stoppedAt).toISOString(),
272
209
  sessionAttributes: this.sessionAttributes,
273
210
  resourceAttributes: getNavigatorInfo(),
274
211
  userAttributes: this._userAttributes || undefined,
@@ -285,7 +222,7 @@ class SessionRecorder
285
222
  private async _createExceptionSession(span: any): Promise<void> {
286
223
  try {
287
224
  const session = await this._apiService.createErrorSession({ span });
288
- if (session) {
225
+ if (session?._id) {
289
226
  void this._flushBuffer(session._id);
290
227
  }
291
228
  } catch (_ignored) {
@@ -341,7 +278,7 @@ class SessionRecorder
341
278
  const bufferEnabled = Boolean(this._configs.buffering?.enabled);
342
279
  const windowMs = Math.max(
343
280
  10_000,
344
- (this._configs.buffering?.windowMinutes || 2) * 60 * 1000
281
+ (this._configs.buffering?.windowMinutes || 0.5) * 60 * 1000
345
282
  );
346
283
  this._tracer.setCrashBuffer(
347
284
  bufferEnabled ? this._crashBuffer : undefined,
@@ -365,8 +302,13 @@ class SessionRecorder
365
302
  );
366
303
 
367
304
  this._crashBuffer.on('error-span-appended', (payload) => {
368
- if (this.sessionState !== SessionState.stopped || this.sessionId) return;
369
- if (!payload.span) return;
305
+ if (
306
+ !payload.span ||
307
+ this.sessionId ||
308
+ this.sessionState !== SessionState.stopped
309
+ ) {
310
+ return;
311
+ }
370
312
  this._createExceptionSession(payload.span);
371
313
  });
372
314
 
@@ -387,21 +329,20 @@ class SessionRecorder
387
329
  }
388
330
 
389
331
  private _startBufferOnlyRecording(): void {
390
- if (!this._configs?.buffering?.enabled) return;
391
- if (this.sessionState !== SessionState.stopped || this.sessionId) return;
332
+ if (
333
+ this.sessionId ||
334
+ !this._crashBuffer ||
335
+ !this._configs?.buffering?.enabled ||
336
+ this.sessionState !== SessionState.stopped
337
+ ) {
338
+ return;
339
+ }
392
340
 
393
341
  const windowMs = Math.max(
394
342
  10_000,
395
- (this._configs.buffering.windowMinutes || 2) * 60 * 1000
343
+ (this._configs.buffering.windowMinutes || 0.5) * 60 * 1000
396
344
  );
397
345
 
398
- // Best-effort: persist current attrs so flush has context.
399
- this._crashBuffer.setAttrs({
400
- sessionAttributes: this.sessionAttributes,
401
- resourceAttributes: getNavigatorInfo(),
402
- userAttributes: this._userAttributes,
403
- });
404
-
405
346
  // Wire buffer into tracer + recorder (only used when sessionId is null).
406
347
  this._tracer.setCrashBuffer(this._crashBuffer, windowMs);
407
348
  this._recorder.init(this._configs, this._socketService, this._crashBuffer, {
@@ -448,9 +389,7 @@ class SessionRecorder
448
389
  });
449
390
 
450
391
  this._socketService.on(SESSION_SAVE_BUFFER_EVENT, (payload: any) => {
451
- if (payload?.debugSession?._id) {
452
- void this._flushBuffer(payload.debugSession._id);
453
- }
392
+ this._flushBuffer(payload?.debugSession?._id);
454
393
  });
455
394
  }
456
395
 
@@ -628,7 +567,13 @@ class SessionRecorder
628
567
  return;
629
568
  }
630
569
  this._userAttributes = userAttributes;
631
- this._socketService.setUser(this._userAttributes);
570
+
571
+ const data = {
572
+ userAttributes: this._userAttributes,
573
+ clientId: this._tracer.clientId,
574
+ };
575
+
576
+ this._socketService.setUser(data);
632
577
  }
633
578
 
634
579
  /**
@@ -1,8 +1,11 @@
1
1
  import { type Span } from '@opentelemetry/api';
2
- import { type ISession, type SessionType, type IUserAttributes } from '@multiplayer-app/session-recorder-common';
2
+ import {
3
+ type ISession,
4
+ type SessionType,
5
+ type IUserAttributes,
6
+ } from '@multiplayer-app/session-recorder-common';
3
7
  import { type PropagateTraceHeaderCorsUrls } from '@opentelemetry/sdk-trace-web';
4
8
 
5
-
6
9
  // WidgetButtonPlacement moved to configs.ts
7
10
 
8
11
  export enum SessionState {
@@ -157,11 +160,11 @@ export interface SessionRecorderOptions {
157
160
  };
158
161
 
159
162
  /**
160
- * @description
161
- * If true, webSocket will be used to manage remote recording sessions.
162
- * @default true
163
- */
164
- useWebsocket?: boolean
163
+ * @description
164
+ * If true, webSocket will be used to manage remote recording sessions.
165
+ * @default true
166
+ */
167
+ useWebsocket?: boolean;
165
168
 
166
169
  /**
167
170
  * (Optional) Client-side crash buffer configuration.
@@ -169,11 +172,11 @@ export interface SessionRecorderOptions {
169
172
  * even if the user did not start a manual/continuous recording.
170
173
  */
171
174
  buffering?: {
172
- /** Enable/disable buffering. @default true */
173
- enabled?: boolean
174
- /** Rolling window size (minutes). @default 1 */
175
- windowMinutes?: number
176
- }
175
+ /** Enable/disable buffering. @default false */
176
+ enabled?: boolean;
177
+ /** Rolling window size (minutes). @default 0.5 */
178
+ windowMinutes?: number;
179
+ };
177
180
  }
178
181
 
179
182
  /**
@@ -361,12 +364,6 @@ export interface ISessionRecorder {
361
364
  * Capture an exception and send it as an error trace
362
365
  */
363
366
  captureException(error: unknown, errorInfo?: Record<string, any>): void;
364
-
365
- /**
366
- * Flush the local crash buffer by creating a debug session and uploading buffered data.
367
- * No-op if a live recording is currently active.
368
- */
369
- flushBuffer(payload?: { reason?: string }): Promise<any>;
370
367
  }
371
368
 
372
369
  /**
@@ -1,11 +1,11 @@
1
1
  import { Dimensions } from 'react-native';
2
2
  import {
3
- EventType,
4
- type eventWithTime,
5
3
  NodeType,
6
- type serializedNodeWithId,
4
+ EventType,
7
5
  IncrementalSource,
8
6
  type mutationData,
7
+ type eventWithTime,
8
+ type serializedNodeWithId,
9
9
  } from '@rrweb/types';
10
10
  import { getAppMetadata } from './platform';
11
11