UNPKG

@multiplayer-app/session-recorder-browser

Version:
252 lines 12.6 kB
import { resourceFromAttributes } from '@opentelemetry/resources'; import { W3CTraceContextPropagator } from '@opentelemetry/core'; import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; import * as SemanticAttributes from '@opentelemetry/semantic-conventions'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web'; import { SessionType, ATTR_MULTIPLAYER_SESSION_ID, SessionRecorderIdGenerator, SessionRecorderBrowserTraceExporter, SessionRecorderTraceIdRatioBasedSampler, } from '@multiplayer-app/session-recorder-common'; import { trace, SpanStatusCode, context } from '@opentelemetry/api'; import { OTEL_IGNORE_URLS } from '../config'; import { processHttpPayload, headersToObject, extractResponseBody, getExporterEndpoint, getElementTextContent, getElementInnerText, } from './helpers'; export class TracerBrowserSDK { constructor() { this.sessionId = ''; this.globalErrorListenersRegistered = false; } setSessionId(sessionId, sessionType = SessionType.PLAIN) { this.sessionId = sessionId; this.idGenerator.setSessionId(sessionId, sessionType); } init(options) { this.config = options; const { application, version, environment } = this.config; this.idGenerator = new SessionRecorderIdGenerator(); this.exporter = new SessionRecorderBrowserTraceExporter({ apiKey: options.apiKey, url: getExporterEndpoint(options.exporterEndpoint), usePostMessageFallback: options.usePostMessageFallback, }); this.tracerProvider = new WebTracerProvider({ resource: resourceFromAttributes({ [SemanticAttributes.SEMRESATTRS_SERVICE_NAME]: application, [SemanticAttributes.SEMRESATTRS_SERVICE_VERSION]: version, [SemanticAttributes.SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: environment, }), idGenerator: this.idGenerator, sampler: new SessionRecorderTraceIdRatioBasedSampler(this.config.sampleTraceRatio), spanProcessors: [ this._getSpanSessionIdProcessor(), new BatchSpanProcessor(this.exporter), ], }); this.tracerProvider.register({ // contextManager: new ZoneContextManager(), propagator: new W3CTraceContextPropagator(), }); registerInstrumentations({ tracerProvider: this.tracerProvider, instrumentations: [ getWebAutoInstrumentations({ '@opentelemetry/instrumentation-xml-http-request': { clearTimingResources: true, ignoreUrls: [ ...OTEL_IGNORE_URLS, ...(this.config.ignoreUrls || []), ], propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls, applyCustomAttributesOnSpan: (span, xhr) => { if (!this.config) return; const { captureBody, captureHeaders } = this.config; try { if (!captureBody && !captureHeaders) { return; } // @ts-ignore const networkRequest = xhr.networkRequest; const requestBody = networkRequest.requestBody; const responseBody = networkRequest.responseBody; const requestHeaders = networkRequest.requestHeaders || {}; const responseHeaders = networkRequest.responseHeaders || {}; const payload = { requestBody, responseBody, requestHeaders, responseHeaders, }; processHttpPayload(payload, this.config, span); } catch (error) { // eslint-disable-next-line console.error('[MULTIPLAYER_SESSION_RECORDER] Failed to capture xml-http payload', error); } }, }, '@opentelemetry/instrumentation-fetch': { clearTimingResources: true, ignoreUrls: [ ...OTEL_IGNORE_URLS, ...(this.config.ignoreUrls || []), ], propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls, applyCustomAttributesOnSpan: async (span, request, response) => { if (!this.config) return; const { captureBody, captureHeaders } = this.config; try { if (!captureBody && !captureHeaders) { return; } // Try to get data from our fetch wrapper first // @ts-ignore const networkRequest = response === null || response === void 0 ? void 0 : response.networkRequest; let requestBody = null; let responseBody = null; let requestHeaders = {}; let responseHeaders = {}; if (networkRequest) { // Use data captured by our fetch wrapper requestBody = networkRequest.requestBody; responseBody = networkRequest.responseBody; requestHeaders = networkRequest.requestHeaders || {}; responseHeaders = networkRequest.responseHeaders || {}; } else { // Fallback to original OpenTelemetry approach requestBody = request.body; requestHeaders = headersToObject(request.headers); responseHeaders = headersToObject(response instanceof Response ? response.headers : undefined); if (response instanceof Response && response.body) { responseBody = await extractResponseBody(response); } } const payload = { requestBody, responseBody, requestHeaders, responseHeaders, }; processHttpPayload(payload, this.config, span); } catch (error) { // eslint-disable-next-line console.error('[MULTIPLAYER_SESSION_RECORDER] Failed to capture fetch payload', error); } }, }, '@opentelemetry/instrumentation-user-interaction': { shouldPreventSpanCreation: (_event, element, span) => { if (span['parentSpanContext']) { return true; } span.setAttribute('target.innerText', getElementInnerText(element)); span.setAttribute('target.textContent', getElementTextContent(element)); Array.from(element.attributes).forEach(attribute => { span.setAttribute(`target.attribute.${attribute.name}`, attribute.value); }); return false; }, }, }), ], }); this._registerGlobalErrorListeners(); } start(sessionId, sessionType) { if (!this.tracerProvider) { throw new Error('Configuration not initialized. Call init() before start().'); } this.setSessionId(sessionId, sessionType); } stop() { if (!this.tracerProvider) { throw new Error('Configuration not initialized. Call init() before start().'); } this.setSessionId(''); } setApiKey(apiKey) { if (!this.exporter) { throw new Error('Configuration not initialized. Call init() before setApiKey().'); } this.exporter.setApiKey(apiKey); } /** * Capture an exception as an error span/event. * If there is an active span, the exception will be recorded on it. * Otherwise, a short-lived span will be created to hold the exception event. */ captureException(error, errorInfo) { if (!error) return; // Prefer attaching to the active span to keep correlation intact try { const activeSpan = trace.getSpan(context.active()); if (activeSpan) { this._recordException(activeSpan, error, errorInfo); return; } // eslint-disable-next-line } catch (_ignored) { } // Fallback: create a short-lived span to hold the exception details try { const tracer = trace.getTracer('exception'); const span = tracer.startSpan(error.name || 'Error'); this._recordException(span, error, errorInfo); span.end(); // eslint-disable-next-line } catch (_ignored) { } } _recordException(span, error, errorInfo) { span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); span.setAttribute('exception.type', error.name || 'Error'); span.setAttribute('exception.message', error.message); span.setAttribute('exception.stacktrace', error.stack || ''); if (errorInfo) { Object.entries(errorInfo).forEach(([key, value]) => { span.setAttribute(`error_info.${key}`, value); }); } } _getSpanSessionIdProcessor() { return { forceFlush: () => Promise.resolve(), onEnd: () => { }, shutdown: () => Promise.resolve(), onStart: (span) => { var _a; if ((_a = this.sessionId) === null || _a === void 0 ? void 0 : _a.length) { span.setAttribute(ATTR_MULTIPLAYER_SESSION_ID, this.sessionId); } }, }; } _registerGlobalErrorListeners() { if (this.globalErrorListenersRegistered) return; if (typeof window === 'undefined') return; // eslint-disable-next-line const errorHandler = (event) => { const err = (event === null || event === void 0 ? void 0 : event.error) instanceof Error ? event.error : new Error((event === null || event === void 0 ? void 0 : event.message) || 'Script error'); this.captureException(err); }; // eslint-disable-next-line const rejectionHandler = (event) => { const reason = (event && 'reason' in event) ? event.reason : undefined; const err = reason instanceof Error ? reason : new Error(typeof reason === 'string' ? reason : 'Unhandled promise rejection'); this.captureException(err); }; window.addEventListener('error', errorHandler); window.addEventListener('unhandledrejection', rejectionHandler); this.globalErrorListenersRegistered = true; } } //# sourceMappingURL=index.js.map