@multiplayer-app/session-recorder-react-native 1.3.22 → 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.
- package/README.md +22 -13
- package/lib/module/config/defaults.js +2 -2
- package/lib/module/config/defaults.js.map +1 -1
- package/lib/module/config/session-recorder.js.map +1 -1
- package/lib/module/otel/CrashBufferSpanProcessor.js +14 -13
- package/lib/module/otel/CrashBufferSpanProcessor.js.map +1 -1
- package/lib/module/otel/index.js +93 -71
- package/lib/module/otel/index.js.map +1 -1
- package/lib/module/recorder/index.js +1 -1
- package/lib/module/recorder/index.js.map +1 -1
- package/lib/module/services/api.service.js.map +1 -1
- package/lib/module/services/crashBuffer.service.js +51 -19
- package/lib/module/services/crashBuffer.service.js.map +1 -1
- package/lib/module/services/socket.service.js +2 -2
- package/lib/module/services/socket.service.js.map +1 -1
- package/lib/module/session-recorder.js +29 -78
- package/lib/module/session-recorder.js.map +1 -1
- package/lib/module/types/session-recorder.js.map +1 -1
- package/lib/module/utils/rrweb-events.js +1 -1
- package/lib/module/utils/rrweb-events.js.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 +3 -4
- package/lib/typescript/src/otel/CrashBufferSpanProcessor.d.ts.map +1 -1
- package/lib/typescript/src/otel/index.d.ts +2 -5
- package/lib/typescript/src/otel/index.d.ts.map +1 -1
- package/lib/typescript/src/services/api.service.d.ts +2 -1
- package/lib/typescript/src/services/api.service.d.ts.map +1 -1
- package/lib/typescript/src/services/crashBuffer.service.d.ts +4 -23
- package/lib/typescript/src/services/crashBuffer.service.d.ts.map +1 -1
- package/lib/typescript/src/services/socket.service.d.ts +4 -1
- package/lib/typescript/src/services/socket.service.d.ts.map +1 -1
- package/lib/typescript/src/session-recorder.d.ts +0 -3
- package/lib/typescript/src/session-recorder.d.ts.map +1 -1
- package/lib/typescript/src/types/session-recorder.d.ts +6 -13
- package/lib/typescript/src/types/session-recorder.d.ts.map +1 -1
- package/lib/typescript/src/utils/rrweb-events.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/config/defaults.ts +29 -29
- package/src/config/session-recorder.ts +8 -2
- package/src/otel/CrashBufferSpanProcessor.ts +17 -24
- package/src/otel/index.ts +124 -76
- package/src/recorder/index.ts +1 -1
- package/src/services/api.service.ts +2 -1
- package/src/services/crashBuffer.service.ts +73 -31
- package/src/services/socket.service.ts +5 -2
- package/src/session-recorder.ts +43 -98
- package/src/types/session-recorder.ts +15 -18
- package/src/utils/rrweb-events.ts +3 -3
package/src/otel/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
type ExportResult,
|
|
5
5
|
} from '@opentelemetry/core';
|
|
6
6
|
import {
|
|
7
|
-
AlwaysOnSampler,
|
|
8
7
|
BatchSpanProcessor,
|
|
9
8
|
type ReadableSpan,
|
|
10
9
|
} from '@opentelemetry/sdk-trace-base';
|
|
@@ -17,17 +16,21 @@ import {
|
|
|
17
16
|
SessionRecorderBrowserTraceExporter,
|
|
18
17
|
ATTR_MULTIPLAYER_SESSION_ID,
|
|
19
18
|
MULTIPLAYER_TRACE_CLIENT_ID_LENGTH,
|
|
19
|
+
SessionRecorderTraceIdRatioBasedSampler,
|
|
20
20
|
} from '@multiplayer-app/session-recorder-common';
|
|
21
21
|
import { type TracerReactNativeConfig } from '../types';
|
|
22
22
|
import { getInstrumentations } from './instrumentations';
|
|
23
23
|
import { getExporterEndpoint } from './helpers';
|
|
24
|
-
import { trace, SpanStatusCode, context, type Span } from '@opentelemetry/api';
|
|
25
24
|
|
|
26
25
|
import { getPlatformAttributes } from '../utils/platform';
|
|
27
26
|
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
|
|
28
27
|
import { CrashBufferService } from '../services/crashBuffer.service';
|
|
29
28
|
import { CrashBufferSpanProcessor } from './CrashBufferSpanProcessor';
|
|
30
29
|
|
|
30
|
+
const clientIdGenerator = SessionRecorderSdk.getIdGenerator(
|
|
31
|
+
MULTIPLAYER_TRACE_CLIENT_ID_LENGTH
|
|
32
|
+
);
|
|
33
|
+
|
|
31
34
|
export class TracerReactNativeSDK {
|
|
32
35
|
clientId = '';
|
|
33
36
|
private tracerProvider?: WebTracerProvider;
|
|
@@ -36,7 +39,6 @@ export class TracerReactNativeSDK {
|
|
|
36
39
|
private sessionId = '';
|
|
37
40
|
private idGenerator?: SessionRecorderIdGenerator;
|
|
38
41
|
private exporter?: SessionRecorderBrowserTraceExporter;
|
|
39
|
-
private batchSpanProcessor?: BatchSpanProcessor;
|
|
40
42
|
private globalErrorHandlerRegistered = false;
|
|
41
43
|
private crashBuffer?: CrashBufferService;
|
|
42
44
|
|
|
@@ -47,42 +49,50 @@ export class TracerReactNativeSDK {
|
|
|
47
49
|
sessionType: SessionType = SessionType.MANUAL
|
|
48
50
|
) {
|
|
49
51
|
this.sessionId = sessionId;
|
|
50
|
-
|
|
52
|
+
|
|
53
|
+
if (!this.idGenerator) {
|
|
54
|
+
throw new Error('Id generator not initialized');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.idGenerator.setSessionId(sessionId, sessionType, this.clientId);
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
init(options: TracerReactNativeConfig): void {
|
|
54
61
|
this.config = options;
|
|
55
|
-
const clientIdGenerator = SessionRecorderSdk.getIdGenerator(
|
|
56
|
-
MULTIPLAYER_TRACE_CLIENT_ID_LENGTH
|
|
57
|
-
);
|
|
58
62
|
this.clientId = clientIdGenerator();
|
|
59
63
|
|
|
60
64
|
const { application, version, environment } = this.config;
|
|
61
65
|
|
|
62
66
|
this.idGenerator = new SessionRecorderIdGenerator();
|
|
63
67
|
|
|
68
|
+
this._setSessionId('', SessionType.SESSION_CACHE);
|
|
69
|
+
|
|
64
70
|
this.exporter = new SessionRecorderBrowserTraceExporter({
|
|
65
71
|
apiKey: options.apiKey,
|
|
66
72
|
url: getExporterEndpoint(options.exporterEndpoint),
|
|
67
73
|
});
|
|
68
74
|
|
|
69
|
-
|
|
75
|
+
const resourceAttributes = resourceFromAttributes({
|
|
76
|
+
[SemanticAttributes.SEMRESATTRS_SERVICE_NAME]: application,
|
|
77
|
+
[SemanticAttributes.SEMRESATTRS_SERVICE_VERSION]: version,
|
|
78
|
+
[SemanticAttributes.SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: environment,
|
|
79
|
+
...getPlatformAttributes(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
SessionRecorderSdk.setResourceAttributes(resourceAttributes.attributes);
|
|
70
83
|
|
|
71
84
|
this.tracerProvider = new WebTracerProvider({
|
|
72
|
-
resource:
|
|
73
|
-
[SemanticAttributes.SEMRESATTRS_SERVICE_NAME]: application,
|
|
74
|
-
[SemanticAttributes.SEMRESATTRS_SERVICE_VERSION]: version,
|
|
75
|
-
[SemanticAttributes.SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: environment,
|
|
76
|
-
...getPlatformAttributes(),
|
|
77
|
-
}),
|
|
85
|
+
resource: resourceAttributes,
|
|
78
86
|
idGenerator: this.idGenerator,
|
|
79
|
-
sampler: new
|
|
87
|
+
sampler: new SessionRecorderTraceIdRatioBasedSampler(
|
|
88
|
+
this.config.sampleTraceRatio
|
|
89
|
+
),
|
|
80
90
|
spanProcessors: [
|
|
81
91
|
this._getSpanSessionIdProcessor(),
|
|
92
|
+
new BatchSpanProcessor(this.exporter),
|
|
82
93
|
new CrashBufferSpanProcessor(
|
|
83
|
-
this.batchSpanProcessor,
|
|
84
94
|
this.crashBuffer,
|
|
85
|
-
this.exporter.serializeSpan
|
|
95
|
+
this.exporter.serializeSpan
|
|
86
96
|
),
|
|
87
97
|
],
|
|
88
98
|
});
|
|
@@ -117,27 +127,22 @@ export class TracerReactNativeSDK {
|
|
|
117
127
|
async exportTraces(
|
|
118
128
|
spans: ReadableSpan[]
|
|
119
129
|
): Promise<ExportResult | undefined | void> {
|
|
120
|
-
if (this.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
if (!this.exporter) {
|
|
131
|
+
throw new Error('Trace exporter not initialized');
|
|
132
|
+
}
|
|
133
|
+
if (!spans || spans.length === 0) {
|
|
124
134
|
return Promise.resolve();
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
|
|
128
|
-
|
|
137
|
+
const readableSpans = spans.map((s: any) =>
|
|
138
|
+
TracerReactNativeSDK._toReadableSpanLike(s)
|
|
139
|
+
);
|
|
129
140
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
onEnd: () => {},
|
|
138
|
-
shutdown: () => Promise.resolve(),
|
|
139
|
-
forceFlush: () => Promise.resolve(),
|
|
140
|
-
};
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
this.exporter?.exportBuffer(readableSpans, (result) => {
|
|
143
|
+
resolve(result);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
start(sessionId: string, sessionType: SessionType): void {
|
|
@@ -157,7 +162,7 @@ export class TracerReactNativeSDK {
|
|
|
157
162
|
);
|
|
158
163
|
}
|
|
159
164
|
|
|
160
|
-
this._setSessionId('');
|
|
165
|
+
this._setSessionId('', SessionType.SESSION_CACHE);
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
setApiKey(apiKey: string): void {
|
|
@@ -167,16 +172,7 @@ export class TracerReactNativeSDK {
|
|
|
167
172
|
);
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
this.exporter.setApiKey
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
setSessionId(sessionId: string, sessionType: SessionType): void {
|
|
174
|
-
this._setSessionId(sessionId, sessionType);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Shutdown (React Native specific)
|
|
178
|
-
shutdown(): Promise<void> {
|
|
179
|
-
return Promise.resolve();
|
|
175
|
+
this.exporter.setApiKey(apiKey);
|
|
180
176
|
}
|
|
181
177
|
|
|
182
178
|
/**
|
|
@@ -186,39 +182,20 @@ export class TracerReactNativeSDK {
|
|
|
186
182
|
*/
|
|
187
183
|
captureException(error: Error, errorInfo?: Record<string, any>): void {
|
|
188
184
|
if (!error) return;
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const activeSpan = trace.getSpan(context.active());
|
|
192
|
-
if (activeSpan) {
|
|
193
|
-
this._recordException(activeSpan, error, errorInfo);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
} catch (_ignored) {}
|
|
197
|
-
|
|
198
|
-
// Fallback: create a short-lived span to hold the exception details
|
|
199
|
-
try {
|
|
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) {}
|
|
185
|
+
SessionRecorderSdk.captureException(error, errorInfo);
|
|
205
186
|
}
|
|
206
187
|
|
|
207
|
-
private
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
Object.entries(errorInfo).forEach(([key, value]) => {
|
|
219
|
-
span.setAttribute(`error_info.${key}`, value);
|
|
220
|
-
});
|
|
221
|
-
}
|
|
188
|
+
private _getSpanSessionIdProcessor() {
|
|
189
|
+
return {
|
|
190
|
+
onStart: (span: any) => {
|
|
191
|
+
if (this.sessionId) {
|
|
192
|
+
span.setAttribute(ATTR_MULTIPLAYER_SESSION_ID, this.sessionId);
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
onEnd: () => {},
|
|
196
|
+
shutdown: () => Promise.resolve(),
|
|
197
|
+
forceFlush: () => Promise.resolve(),
|
|
198
|
+
};
|
|
222
199
|
}
|
|
223
200
|
|
|
224
201
|
private _registerGlobalErrorHandlers(): void {
|
|
@@ -249,4 +226,75 @@ export class TracerReactNativeSDK {
|
|
|
249
226
|
this.globalErrorHandlerRegistered = true;
|
|
250
227
|
}
|
|
251
228
|
}
|
|
229
|
+
|
|
230
|
+
private static _toReadableSpanLike(span: any): ReadableSpan {
|
|
231
|
+
if (
|
|
232
|
+
span &&
|
|
233
|
+
typeof span.spanContext === 'function' &&
|
|
234
|
+
span.instrumentationScope
|
|
235
|
+
) {
|
|
236
|
+
return span as ReadableSpan;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const spanContext =
|
|
240
|
+
typeof span?.spanContext === 'function'
|
|
241
|
+
? span.spanContext()
|
|
242
|
+
: span?._spanContext;
|
|
243
|
+
const normalizedCtx =
|
|
244
|
+
spanContext ||
|
|
245
|
+
({
|
|
246
|
+
traceId: span?.traceId,
|
|
247
|
+
spanId: span?.spanId,
|
|
248
|
+
traceFlags: span?.traceFlags,
|
|
249
|
+
traceState: span?.traceState,
|
|
250
|
+
} as any);
|
|
251
|
+
|
|
252
|
+
const instrumentationScope =
|
|
253
|
+
span?.instrumentationScope ||
|
|
254
|
+
span?.instrumentationLibrary ||
|
|
255
|
+
({
|
|
256
|
+
name: 'multiplayer-buffer',
|
|
257
|
+
version: undefined,
|
|
258
|
+
schemaUrl: undefined,
|
|
259
|
+
} as any);
|
|
260
|
+
|
|
261
|
+
const normalizedScope = {
|
|
262
|
+
name: instrumentationScope?.name || 'multiplayer-buffer',
|
|
263
|
+
version: instrumentationScope?.version,
|
|
264
|
+
schemaUrl: instrumentationScope?.schemaUrl,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const resource = span?.resource || {
|
|
268
|
+
attributes: {},
|
|
269
|
+
asyncAttributesPending: false,
|
|
270
|
+
};
|
|
271
|
+
const parentSpanId = span?.parentSpanId;
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
name: span?.name || '',
|
|
275
|
+
kind: span?.kind,
|
|
276
|
+
spanContext: () => normalizedCtx,
|
|
277
|
+
parentSpanContext: parentSpanId
|
|
278
|
+
? ({
|
|
279
|
+
traceId: normalizedCtx?.traceId,
|
|
280
|
+
spanId: parentSpanId,
|
|
281
|
+
traceFlags: normalizedCtx?.traceFlags,
|
|
282
|
+
traceState: normalizedCtx?.traceState,
|
|
283
|
+
} as any)
|
|
284
|
+
: undefined,
|
|
285
|
+
startTime: span?.startTime,
|
|
286
|
+
endTime: span?.endTime ?? span?.startTime,
|
|
287
|
+
duration: span?.duration,
|
|
288
|
+
status: span?.status,
|
|
289
|
+
attributes: span?.attributes || {},
|
|
290
|
+
links: span?.links || [],
|
|
291
|
+
events: span?.events || [],
|
|
292
|
+
ended: typeof span?.ended === 'boolean' ? span.ended : true,
|
|
293
|
+
droppedAttributesCount: span?.droppedAttributesCount || 0,
|
|
294
|
+
droppedEventsCount: span?.droppedEventsCount || 0,
|
|
295
|
+
droppedLinksCount: span?.droppedLinksCount || 0,
|
|
296
|
+
resource,
|
|
297
|
+
instrumentationScope: normalizedScope as any,
|
|
298
|
+
} as any;
|
|
299
|
+
}
|
|
252
300
|
}
|
package/src/recorder/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ export class RecorderReactNativeSDK implements EventRecorder {
|
|
|
40
40
|
this.bufferingEnabled = Boolean(buffering?.enabled);
|
|
41
41
|
this.bufferWindowMs = Math.max(
|
|
42
42
|
10_000,
|
|
43
|
-
buffering?.windowMs ||
|
|
43
|
+
buffering?.windowMs || 0.5 * 60 * 1000
|
|
44
44
|
);
|
|
45
45
|
this.screenRecorder.init(config, this);
|
|
46
46
|
this.navigationTracker.init(config, this.screenRecorder);
|
|
@@ -93,7 +93,8 @@ export class ApiService {
|
|
|
93
93
|
async updateSessionAttributes(
|
|
94
94
|
sessionId: string,
|
|
95
95
|
requestBody: {
|
|
96
|
-
|
|
96
|
+
startedAt?: string;
|
|
97
|
+
stoppedAt?: string;
|
|
97
98
|
userAttributes?: IUserAttributes;
|
|
98
99
|
sessionAttributes?: ISessionAttributes;
|
|
99
100
|
resourceAttributes?: IResourceAttributes;
|
|
@@ -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 =
|
|
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:
|
|
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
|
-
|
|
120
|
-
|
|
132
|
+
record.ts,
|
|
133
|
+
record,
|
|
121
134
|
windowMs ?? this.defaultWindowMs
|
|
122
135
|
);
|
|
123
136
|
}
|
|
124
137
|
|
|
125
138
|
async appendSpans(
|
|
126
|
-
payload:
|
|
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 ||
|
|
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
|
|
285
|
-
const
|
|
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')
|
|
292
|
-
if (e.kind === 'span')
|
|
304
|
+
if (e.kind === 'rrweb') allEvents.push(payload);
|
|
305
|
+
if (e.kind === 'span') allSpans.push(payload);
|
|
293
306
|
}
|
|
294
307
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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(
|
|
220
|
-
|
|
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> {
|