UNPKG

@multiplayer-app/session-recorder-browser

Version:
189 lines 6.6 kB
import { trace, SpanStatusCode } from '@opentelemetry/api'; import { SessionType } from '@multiplayer-app/session-recorder-common'; const DEFAULT_CONFIG = { enabled: true, }; const DEFAULT_FRAMEWORK = 'web'; const DEFAULT_SOURCE = 'router'; export class NavigationRecorder { constructor() { this.config = { ...DEFAULT_CONFIG }; this.isRecording = false; this.currentRoute = null; this.stack = []; this.navigationStartTime = 0; this.sessionContext = { sessionId: null, sessionType: SessionType.MANUAL, }; this.api = { record: (signal) => this.record(signal), reset: () => this.reset(), getCurrentRoute: () => this.currentRoute, getStack: () => [...this.stack], }; } init(config) { this.config = { ...DEFAULT_CONFIG, ...config }; } start(context) { var _a, _b; if (!this.config.enabled) { return; } this.sessionContext = { sessionId: (_a = context === null || context === void 0 ? void 0 : context.sessionId) !== null && _a !== void 0 ? _a : null, sessionType: (_b = context === null || context === void 0 ? void 0 : context.sessionType) !== null && _b !== void 0 ? _b : SessionType.MANUAL, }; this.isRecording = true; this.navigationStartTime = Date.now(); } pause() { this.isRecording = false; } resume() { if (!this.config.enabled) { return; } this.isRecording = true; this.navigationStartTime = Date.now(); } stop() { this.isRecording = false; this.reset(); } record(signal) { var _a; if (!this.config.enabled || !this.isRecording) { return; } const timestamp = (_a = signal.timestamp) !== null && _a !== void 0 ? _a : Date.now(); const routeKey = this.resolveRouteKey(signal); const previousRoute = this.currentRoute; if (routeKey) { this.updateNavigationStack(routeKey, signal.navigationType); this.currentRoute = routeKey; } const metadata = { ...(signal.metadata || {}), framework: signal.framework || DEFAULT_FRAMEWORK, source: signal.source || DEFAULT_SOURCE, navigationType: signal.navigationType || (previousRoute ? 'push' : 'initial'), navigationDuration: timestamp - this.navigationStartTime, stackDepth: this.stack.length, }; if (previousRoute) { metadata.previousRoute = previousRoute; } if (signal.path) { metadata.path = signal.path; } if (signal.title) { metadata.documentTitle = signal.title; } if (signal.url) { metadata.url = signal.url; } if (signal.state !== undefined) { metadata.state = this.stringifySafe(signal.state); } const paramsString = signal.params ? this.stringifySafe(signal.params) : undefined; this.recordSpan({ timestamp, routeName: signal.routeName || routeKey, paramsString, metadata, }); this.navigationStartTime = timestamp; } recordSpan(payload) { try { const span = trace.getTracer('navigation').startSpan('Navigation.navigate', { startTime: payload.timestamp, attributes: { 'navigation.system': 'router', 'navigation.operation': 'navigate', 'navigation.type': 'navigate', 'navigation.platform': 'web', }, }); if (payload.routeName) { span.setAttribute('navigation.route_name', payload.routeName); } if (payload.paramsString) { span.setAttribute('navigation.params', payload.paramsString); } Object.entries(payload.metadata).forEach(([key, value]) => { const normalizedValue = typeof value === 'string' ? value : this.stringifySafe(value); span.setAttribute(`navigation.metadata.${key}`, normalizedValue); }); span.setStatus({ code: SpanStatusCode.OK }); span.end(payload.timestamp); } catch (error) { if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line no-console console.warn('[SessionRecorder][NavigationRecorder] Failed to record navigation span', error); } } } updateNavigationStack(routeName, navigationType) { const normalized = (navigationType || '').toLowerCase(); if (normalized === 'replace') { if (this.stack.length) { this.stack[this.stack.length - 1] = routeName; } else { this.stack.push(routeName); } return; } if (normalized === 'pop' || normalized === 'back' || normalized === 'goback') { const index = this.stack.lastIndexOf(routeName); if (index >= 0) { this.stack = this.stack.slice(0, index + 1); } else if (this.stack.length) { this.stack.pop(); } return; } if (!this.stack.length || this.stack[this.stack.length - 1] !== routeName) { this.stack.push(routeName); } } resolveRouteKey(signal) { if (signal.routeName && typeof signal.routeName === 'string') { return signal.routeName; } if (signal.path && typeof signal.path === 'string') { return signal.path; } if (signal.url && typeof signal.url === 'string') { try { return new URL(signal.url).pathname || signal.url; } catch (_error) { return signal.url; } } if (signal.title && typeof signal.title === 'string') { return signal.title; } return 'unknown'; } stringifySafe(value) { try { return JSON.stringify(value); } catch (_error) { return String(value); } } reset() { this.stack = []; this.currentRoute = null; this.navigationStartTime = Date.now(); } } //# sourceMappingURL=navigationRecorder.js.map