@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.
- package/lib/module/config/constants.js +3 -0
- package/lib/module/config/constants.js.map +1 -1
- package/lib/module/config/defaults.js +5 -1
- package/lib/module/config/defaults.js.map +1 -1
- package/lib/module/config/session-recorder.js +5 -1
- package/lib/module/config/session-recorder.js.map +1 -1
- package/lib/module/otel/CrashBufferSpanProcessor.js +41 -0
- package/lib/module/otel/CrashBufferSpanProcessor.js.map +1 -0
- package/lib/module/otel/index.js +28 -9
- package/lib/module/otel/index.js.map +1 -1
- package/lib/module/recorder/index.js +15 -1
- package/lib/module/recorder/index.js.map +1 -1
- package/lib/module/services/api.service.js +24 -2
- package/lib/module/services/api.service.js.map +1 -1
- package/lib/module/services/crashBuffer.service.js +248 -0
- package/lib/module/services/crashBuffer.service.js.map +1 -0
- package/lib/module/services/socket.service.js +9 -2
- package/lib/module/services/socket.service.js.map +1 -1
- package/lib/module/session-recorder.js +152 -6
- package/lib/module/session-recorder.js.map +1 -1
- package/lib/module/types/session-recorder.js.map +1 -1
- package/lib/typescript/src/config/constants.d.ts +1 -0
- package/lib/typescript/src/config/constants.d.ts.map +1 -1
- package/lib/typescript/src/config/defaults.d.ts.map +1 -1
- package/lib/typescript/src/config/session-recorder.d.ts.map +1 -1
- package/lib/typescript/src/otel/CrashBufferSpanProcessor.d.ts +18 -0
- package/lib/typescript/src/otel/CrashBufferSpanProcessor.d.ts.map +1 -0
- package/lib/typescript/src/otel/index.d.ts +8 -0
- package/lib/typescript/src/otel/index.d.ts.map +1 -1
- package/lib/typescript/src/recorder/index.d.ts +8 -1
- package/lib/typescript/src/recorder/index.d.ts.map +1 -1
- package/lib/typescript/src/services/api.service.d.ts +27 -2
- package/lib/typescript/src/services/api.service.d.ts.map +1 -1
- package/lib/typescript/src/services/crashBuffer.service.d.ts +46 -0
- package/lib/typescript/src/services/crashBuffer.service.d.ts.map +1 -0
- package/lib/typescript/src/services/socket.service.d.ts +4 -3
- package/lib/typescript/src/services/socket.service.d.ts.map +1 -1
- package/lib/typescript/src/session-recorder.d.ts +8 -0
- package/lib/typescript/src/session-recorder.d.ts.map +1 -1
- package/lib/typescript/src/types/session-recorder.d.ts +18 -0
- package/lib/typescript/src/types/session-recorder.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/config/constants.ts +3 -0
- package/src/config/defaults.ts +5 -0
- package/src/config/session-recorder.ts +5 -0
- package/src/otel/CrashBufferSpanProcessor.ts +61 -0
- package/src/otel/index.ts +90 -34
- package/src/recorder/index.ts +30 -3
- package/src/services/api.service.ts +68 -13
- package/src/services/crashBuffer.service.ts +327 -0
- package/src/services/socket.service.ts +36 -22
- package/src/session-recorder.ts +226 -19
- 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 {
|
|
3
|
-
|
|
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
|
-
|
|
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?:
|
|
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
|
|
61
|
-
this.config.sampleTraceRatio || 0.15
|
|
62
|
-
),
|
|
79
|
+
sampler: new AlwaysOnSampler(),
|
|
63
80
|
spanProcessors: [
|
|
64
81
|
this._getSpanSessionIdProcessor(),
|
|
65
|
-
new
|
|
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(
|
|
163
|
-
span
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
span.
|
|
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 =
|
|
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 {
|
|
241
|
+
try {
|
|
242
|
+
previous(error, isFatal);
|
|
243
|
+
} catch (_e) {
|
|
244
|
+
/* ignore */
|
|
245
|
+
}
|
|
190
246
|
}
|
|
191
247
|
}
|
|
192
248
|
});
|
package/src/recorder/index.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|