@multiplayer-app/session-recorder-react-native 1.3.16 → 1.3.21

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 (53) hide show
  1. package/lib/module/config/constants.js +3 -0
  2. package/lib/module/config/constants.js.map +1 -1
  3. package/lib/module/config/defaults.js +5 -1
  4. package/lib/module/config/defaults.js.map +1 -1
  5. package/lib/module/config/session-recorder.js +5 -1
  6. package/lib/module/config/session-recorder.js.map +1 -1
  7. package/lib/module/otel/CrashBufferSpanProcessor.js +41 -0
  8. package/lib/module/otel/CrashBufferSpanProcessor.js.map +1 -0
  9. package/lib/module/otel/index.js +28 -9
  10. package/lib/module/otel/index.js.map +1 -1
  11. package/lib/module/recorder/index.js +15 -1
  12. package/lib/module/recorder/index.js.map +1 -1
  13. package/lib/module/services/api.service.js +24 -2
  14. package/lib/module/services/api.service.js.map +1 -1
  15. package/lib/module/services/crashBuffer.service.js +248 -0
  16. package/lib/module/services/crashBuffer.service.js.map +1 -0
  17. package/lib/module/services/socket.service.js +9 -2
  18. package/lib/module/services/socket.service.js.map +1 -1
  19. package/lib/module/session-recorder.js +152 -6
  20. package/lib/module/session-recorder.js.map +1 -1
  21. package/lib/module/types/session-recorder.js.map +1 -1
  22. package/lib/typescript/src/config/constants.d.ts +1 -0
  23. package/lib/typescript/src/config/constants.d.ts.map +1 -1
  24. package/lib/typescript/src/config/defaults.d.ts.map +1 -1
  25. package/lib/typescript/src/config/session-recorder.d.ts.map +1 -1
  26. package/lib/typescript/src/otel/CrashBufferSpanProcessor.d.ts +18 -0
  27. package/lib/typescript/src/otel/CrashBufferSpanProcessor.d.ts.map +1 -0
  28. package/lib/typescript/src/otel/index.d.ts +8 -0
  29. package/lib/typescript/src/otel/index.d.ts.map +1 -1
  30. package/lib/typescript/src/recorder/index.d.ts +8 -1
  31. package/lib/typescript/src/recorder/index.d.ts.map +1 -1
  32. package/lib/typescript/src/services/api.service.d.ts +27 -2
  33. package/lib/typescript/src/services/api.service.d.ts.map +1 -1
  34. package/lib/typescript/src/services/crashBuffer.service.d.ts +46 -0
  35. package/lib/typescript/src/services/crashBuffer.service.d.ts.map +1 -0
  36. package/lib/typescript/src/services/socket.service.d.ts +4 -3
  37. package/lib/typescript/src/services/socket.service.d.ts.map +1 -1
  38. package/lib/typescript/src/session-recorder.d.ts +8 -0
  39. package/lib/typescript/src/session-recorder.d.ts.map +1 -1
  40. package/lib/typescript/src/types/session-recorder.d.ts +18 -0
  41. package/lib/typescript/src/types/session-recorder.d.ts.map +1 -1
  42. package/package.json +2 -2
  43. package/src/config/constants.ts +3 -0
  44. package/src/config/defaults.ts +5 -0
  45. package/src/config/session-recorder.ts +5 -0
  46. package/src/otel/CrashBufferSpanProcessor.ts +61 -0
  47. package/src/otel/index.ts +90 -34
  48. package/src/recorder/index.ts +30 -3
  49. package/src/services/api.service.ts +68 -13
  50. package/src/services/crashBuffer.service.ts +327 -0
  51. package/src/services/socket.service.ts +36 -22
  52. package/src/session-recorder.ts +226 -19
  53. package/src/types/session-recorder.ts +18 -0
package/src/otel/index.ts CHANGED
@@ -1,14 +1,22 @@
1
1
  import { resourceFromAttributes } from '@opentelemetry/resources';
2
- import { W3CTraceContextPropagator } from '@opentelemetry/core';
3
- import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
2
+ import {
3
+ W3CTraceContextPropagator,
4
+ type ExportResult,
5
+ } from '@opentelemetry/core';
6
+ import {
7
+ AlwaysOnSampler,
8
+ BatchSpanProcessor,
9
+ type ReadableSpan,
10
+ } from '@opentelemetry/sdk-trace-base';
4
11
  import * as SemanticAttributes from '@opentelemetry/semantic-conventions';
5
12
  import { registerInstrumentations } from '@opentelemetry/instrumentation';
6
13
  import {
7
14
  SessionType,
8
- ATTR_MULTIPLAYER_SESSION_ID,
15
+ SessionRecorderSdk,
9
16
  SessionRecorderIdGenerator,
10
- SessionRecorderTraceIdRatioBasedSampler,
11
17
  SessionRecorderBrowserTraceExporter,
18
+ ATTR_MULTIPLAYER_SESSION_ID,
19
+ MULTIPLAYER_TRACE_CLIENT_ID_LENGTH,
12
20
  } from '@multiplayer-app/session-recorder-common';
13
21
  import { type TracerReactNativeConfig } from '../types';
14
22
  import { getInstrumentations } from './instrumentations';
@@ -17,28 +25,37 @@ import { trace, SpanStatusCode, context, type Span } from '@opentelemetry/api';
17
25
 
18
26
  import { getPlatformAttributes } from '../utils/platform';
19
27
  import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
28
+ import { CrashBufferService } from '../services/crashBuffer.service';
29
+ import { CrashBufferSpanProcessor } from './CrashBufferSpanProcessor';
20
30
 
21
31
  export class TracerReactNativeSDK {
32
+ clientId = '';
22
33
  private tracerProvider?: WebTracerProvider;
23
34
  private config?: TracerReactNativeConfig;
24
35
 
25
36
  private sessionId = '';
26
37
  private idGenerator?: SessionRecorderIdGenerator;
27
- private exporter?: any;
38
+ private exporter?: SessionRecorderBrowserTraceExporter;
39
+ private batchSpanProcessor?: BatchSpanProcessor;
28
40
  private globalErrorHandlerRegistered = false;
41
+ private crashBuffer?: CrashBufferService;
29
42
 
30
- constructor() { }
43
+ constructor() {}
31
44
 
32
45
  private _setSessionId(
33
46
  sessionId: string,
34
47
  sessionType: SessionType = SessionType.MANUAL
35
48
  ) {
36
49
  this.sessionId = sessionId;
37
- this.idGenerator?.setSessionId(sessionId, sessionType);
50
+ this.idGenerator?.setSessionId(sessionId, sessionType, this.clientId);
38
51
  }
39
52
 
40
53
  init(options: TracerReactNativeConfig): void {
41
54
  this.config = options;
55
+ const clientIdGenerator = SessionRecorderSdk.getIdGenerator(
56
+ MULTIPLAYER_TRACE_CLIENT_ID_LENGTH
57
+ );
58
+ this.clientId = clientIdGenerator();
42
59
 
43
60
  const { application, version, environment } = this.config;
44
61
 
@@ -49,6 +66,8 @@ export class TracerReactNativeSDK {
49
66
  url: getExporterEndpoint(options.exporterEndpoint),
50
67
  });
51
68
 
69
+ this.batchSpanProcessor = new BatchSpanProcessor(this.exporter);
70
+
52
71
  this.tracerProvider = new WebTracerProvider({
53
72
  resource: resourceFromAttributes({
54
73
  [SemanticAttributes.SEMRESATTRS_SERVICE_NAME]: application,
@@ -57,12 +76,14 @@ export class TracerReactNativeSDK {
57
76
  ...getPlatformAttributes(),
58
77
  }),
59
78
  idGenerator: this.idGenerator,
60
- sampler: new SessionRecorderTraceIdRatioBasedSampler(
61
- this.config.sampleTraceRatio || 0.15
62
- ),
79
+ sampler: new AlwaysOnSampler(),
63
80
  spanProcessors: [
64
81
  this._getSpanSessionIdProcessor(),
65
- new BatchSpanProcessor(this.exporter),
82
+ new CrashBufferSpanProcessor(
83
+ this.batchSpanProcessor,
84
+ this.crashBuffer,
85
+ this.exporter.serializeSpan.bind(this.exporter)
86
+ ),
66
87
  ],
67
88
  });
68
89
 
@@ -79,17 +100,41 @@ export class TracerReactNativeSDK {
79
100
  this._registerGlobalErrorHandlers();
80
101
  }
81
102
 
103
+ setCrashBuffer(
104
+ crashBuffer: CrashBufferService | undefined,
105
+ windowMs?: number
106
+ ): void {
107
+ this.crashBuffer = crashBuffer;
108
+ if (
109
+ crashBuffer &&
110
+ typeof windowMs === 'number' &&
111
+ Number.isFinite(windowMs)
112
+ ) {
113
+ crashBuffer.setDefaultWindowMs(windowMs);
114
+ }
115
+ }
116
+
117
+ async exportTraces(
118
+ spans: ReadableSpan[]
119
+ ): Promise<ExportResult | undefined | void> {
120
+ if (this.batchSpanProcessor) {
121
+ spans.map((span) => {
122
+ this.batchSpanProcessor?.onEnd(span);
123
+ });
124
+ return Promise.resolve();
125
+ }
126
+
127
+ throw new Error('Buffer span processor not initialized');
128
+ }
129
+
82
130
  private _getSpanSessionIdProcessor() {
83
131
  return {
84
132
  onStart: (span: any) => {
85
133
  if (this.sessionId) {
86
134
  span.setAttribute(ATTR_MULTIPLAYER_SESSION_ID, this.sessionId);
87
135
  }
88
- // Add React Native specific attributes
89
- span.setAttribute('platform', 'react-native');
90
- span.setAttribute('timestamp', Date.now());
91
136
  },
92
- onEnd: () => { },
137
+ onEnd: () => {},
93
138
  shutdown: () => Promise.resolve(),
94
139
  forceFlush: () => Promise.resolve(),
95
140
  };
@@ -143,32 +188,36 @@ export class TracerReactNativeSDK {
143
188
  if (!error) return;
144
189
  // Prefer attaching to the active span to keep correlation intact
145
190
  try {
146
- const activeSpan = trace.getSpan(context.active())
191
+ const activeSpan = trace.getSpan(context.active());
147
192
  if (activeSpan) {
148
- this._recordException(activeSpan, error, errorInfo)
149
- return
193
+ this._recordException(activeSpan, error, errorInfo);
194
+ return;
150
195
  }
151
- } catch (_ignored) { }
196
+ } catch (_ignored) {}
152
197
 
153
198
  // Fallback: create a short-lived span to hold the exception details
154
199
  try {
155
- const tracer = trace.getTracer('exception')
156
- const span = tracer.startSpan(error.name || 'Error')
157
- this._recordException(span, error, errorInfo)
158
- span.end()
159
- } catch (_ignored) { }
200
+ const tracer = trace.getTracer('exception');
201
+ const span = tracer.startSpan(error.name || 'Error');
202
+ this._recordException(span, error, errorInfo);
203
+ span.end();
204
+ } catch (_ignored) {}
160
205
  }
161
206
 
162
- private _recordException(span: Span, error: Error, errorInfo?: Record<string, any>): void {
163
- span.recordException(error)
164
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message })
165
- span.setAttribute('exception.type', error.name || 'Error')
166
- span.setAttribute('exception.message', error.message)
167
- span.setAttribute('exception.stacktrace', error.stack || '')
207
+ private _recordException(
208
+ span: Span,
209
+ error: Error,
210
+ errorInfo?: Record<string, any>
211
+ ): void {
212
+ span.recordException(error);
213
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
214
+ span.setAttribute('exception.type', error.name || 'Error');
215
+ span.setAttribute('exception.message', error.message);
216
+ span.setAttribute('exception.stacktrace', error.stack || '');
168
217
  if (errorInfo) {
169
218
  Object.entries(errorInfo).forEach(([key, value]) => {
170
- span.setAttribute(`error_info.${key}`, value)
171
- })
219
+ span.setAttribute(`error_info.${key}`, value);
220
+ });
172
221
  }
173
222
  }
174
223
 
@@ -182,11 +231,18 @@ export class TracerReactNativeSDK {
182
231
  const previous = ErrorUtilsRef.getGlobalHandler?.();
183
232
  ErrorUtilsRef.setGlobalHandler((error: any, isFatal?: boolean) => {
184
233
  try {
185
- const err = error instanceof Error ? error : new Error(String(error?.message || error));
234
+ const err =
235
+ error instanceof Error
236
+ ? error
237
+ : new Error(String(error?.message || error));
186
238
  this.captureException(err);
187
239
  } finally {
188
240
  if (typeof previous === 'function') {
189
- try { previous(error, isFatal); } catch (_e) { /* ignore */ }
241
+ try {
242
+ previous(error, isFatal);
243
+ } catch (_e) {
244
+ /* ignore */
245
+ }
190
246
  }
191
247
  }
192
248
  });
@@ -1,6 +1,7 @@
1
1
  import { SessionType } from '@multiplayer-app/session-recorder-common';
2
2
  // import { pack } from '@rrweb/packer' // Removed to avoid blob creation issues in Hermes
3
3
  import { SocketService } from '../services/socket.service';
4
+ import { CrashBufferService } from '../services/crashBuffer.service';
4
5
  import { logger } from '../utils';
5
6
  import { ScreenRecorder } from './screenRecorder';
6
7
  import { GestureRecorder } from './gestureRecorder';
@@ -15,6 +16,9 @@ export class RecorderReactNativeSDK implements EventRecorder {
15
16
  private navigationTracker: NavigationTracker;
16
17
  private recordedEvents: eventWithTime[] = [];
17
18
  private socketService!: SocketService;
19
+ private crashBuffer?: CrashBufferService;
20
+ private bufferingEnabled: boolean = false;
21
+ private bufferWindowMs: number = 2 * 60 * 1000;
18
22
  private sessionId: string | null = null;
19
23
  private sessionType: SessionType = SessionType.MANUAL;
20
24
 
@@ -24,15 +28,25 @@ export class RecorderReactNativeSDK implements EventRecorder {
24
28
  this.navigationTracker = new NavigationTracker();
25
29
  }
26
30
 
27
- init(config: RecorderConfig, socketService: SocketService): void {
31
+ init(
32
+ config: RecorderConfig,
33
+ socketService: SocketService,
34
+ crashBuffer?: CrashBufferService,
35
+ buffering?: { enabled: boolean; windowMs: number }
36
+ ): void {
28
37
  this.config = config;
29
38
  this.socketService = socketService;
39
+ this.crashBuffer = crashBuffer;
40
+ this.bufferingEnabled = Boolean(buffering?.enabled);
41
+ this.bufferWindowMs = Math.max(
42
+ 10_000,
43
+ buffering?.windowMs || 1 * 60 * 1000
44
+ );
30
45
  this.screenRecorder.init(config, this);
31
46
  this.navigationTracker.init(config, this.screenRecorder);
32
47
  this.gestureRecorder.init(config, this, this.screenRecorder);
33
48
  }
34
49
 
35
-
36
50
  start(sessionId: string | null, sessionType: SessionType): void {
37
51
  if (!this.config) {
38
52
  throw new Error(
@@ -88,8 +102,21 @@ export class RecorderReactNativeSDK implements EventRecorder {
88
102
  return;
89
103
  }
90
104
 
105
+ // Buffer-only mode (no active debug session): persist locally.
106
+ if (!this.sessionId && this.crashBuffer && this.bufferingEnabled) {
107
+ void this.crashBuffer.appendEvent(
108
+ { ts: event.timestamp, event },
109
+ this.bufferWindowMs
110
+ );
111
+ return;
112
+ }
113
+
91
114
  if (this.socketService) {
92
- logger.debug('RecorderReactNativeSDK', 'Sending to socket service', event);
115
+ logger.debug(
116
+ 'RecorderReactNativeSDK',
117
+ 'Sending to socket service',
118
+ event
119
+ );
93
120
  // Skip packing to avoid blob creation issues in Hermes
94
121
  // const packedEvent = pack(event)
95
122
  this.socketService.send({
@@ -3,10 +3,8 @@ import type {
3
3
  ISessionAttributes,
4
4
  IResourceAttributes,
5
5
  } from '@multiplayer-app/session-recorder-common';
6
- import {
7
- type ApiServiceConfig,
8
-
9
- } from '../types';
6
+ import { type ApiServiceConfig } from '../types';
7
+ import type { eventWithTime } from '@rrweb/types';
10
8
 
11
9
  export interface StartSessionRequest {
12
10
  name?: string;
@@ -23,11 +21,14 @@ export interface StopSessionRequest {
23
21
  stoppedAt: string | number;
24
22
  }
25
23
 
26
-
27
24
  export interface CheckRemoteSessionRequest {
28
- sessionAttributes?: ISessionAttributes
29
- resourceAttributes?: IResourceAttributes
30
- userAttributes?: IUserAttributes
25
+ sessionAttributes?: ISessionAttributes;
26
+ resourceAttributes?: IResourceAttributes;
27
+ userAttributes?: IUserAttributes;
28
+ }
29
+
30
+ export interface CreateErrorSpanSessionRequest {
31
+ span: any;
31
32
  }
32
33
 
33
34
  export class ApiService {
@@ -72,6 +73,60 @@ export class ApiService {
72
73
  }
73
74
  }
74
75
 
76
+ /**
77
+ * Create a new error span session
78
+ * @param request - Session create error span request data
79
+ * @param signal - Optional AbortSignal for request cancellation
80
+ */
81
+ async createErrorSession(
82
+ request: CreateErrorSpanSessionRequest,
83
+ signal?: AbortSignal
84
+ ): Promise<any> {
85
+ return this.makeRequest(
86
+ '/debug-sessions/error-span/start',
87
+ 'POST',
88
+ request,
89
+ signal
90
+ );
91
+ }
92
+
93
+ async updateSessionAttributes(
94
+ sessionId: string,
95
+ requestBody: {
96
+ name?: string;
97
+ userAttributes?: IUserAttributes;
98
+ sessionAttributes?: ISessionAttributes;
99
+ resourceAttributes?: IResourceAttributes;
100
+ },
101
+ signal?: AbortSignal
102
+ ): Promise<any> {
103
+ return this.makeRequest(
104
+ `/debug-sessions/${sessionId}`,
105
+ 'PATCH',
106
+ requestBody,
107
+ signal
108
+ );
109
+ }
110
+
111
+ /**
112
+ * Export events to the session debugger API
113
+ * @param sessionId - ID of the session to export events
114
+ * @param requestBody - Request body containing events
115
+ * @param signal - Optional AbortSignal for request cancellation
116
+ */
117
+ async exportEvents(
118
+ sessionId: string,
119
+ requestBody: { events: eventWithTime[] },
120
+ signal?: AbortSignal
121
+ ): Promise<any> {
122
+ return this.makeRequest(
123
+ `/debug-sessions/${sessionId}/rrweb-events`,
124
+ 'POST',
125
+ requestBody,
126
+ signal
127
+ );
128
+ }
129
+
75
130
  /**
76
131
  * Make a request to the session debugger API
77
132
  * @param path - API endpoint path (relative to the base URL)
@@ -212,17 +267,17 @@ export class ApiService {
212
267
  }
213
268
 
214
269
  /**
215
- * Check debug session should be started remotely
216
- */
270
+ * Check debug session should be started remotely
271
+ */
217
272
  async checkRemoteSession(
218
273
  requestBody: CheckRemoteSessionRequest,
219
- signal?: AbortSignal,
274
+ signal?: AbortSignal
220
275
  ): Promise<{ state: 'START' | 'STOP' }> {
221
276
  return this.makeRequest(
222
277
  '/remote-debug-session/check',
223
278
  'POST',
224
279
  requestBody,
225
- signal,
226
- )
280
+ signal
281
+ );
227
282
  }
228
283
  }