@multiplayer-app/session-recorder-browser
Version:
Multiplayer Fullstack Session Recorder for Browser
189 lines • 6.6 kB
JavaScript
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