UNPKG

rusty-replay

Version:

Lightweight error tracking and replay system for React apps using rrweb and Rust-powered backend integration.

332 lines (325 loc) 10.8 kB
var __defProp = Object.defineProperty; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; // src/environment.ts function getBrowserInfo() { const ua = navigator.userAgent; let browser = "unknown", os = "unknown"; if (ua.includes("Firefox")) browser = "Firefox"; else if (ua.includes("SamsungBrowser")) browser = "Samsung Browser"; else if (ua.includes("Opera") || ua.includes("OPR")) browser = "Opera"; else if (ua.includes("Trident")) browser = "IE"; else if (ua.includes("Edge")) browser = "Edge (Legacy)"; else if (ua.includes("Edg")) browser = "Edge"; else if (ua.includes("Chrome")) browser = "Chrome"; else if (ua.includes("Safari")) browser = "Safari"; if (ua.includes("Windows")) os = "Windows"; else if (ua.includes("Mac")) os = "macOS"; else if (ua.includes("Linux")) os = "Linux"; else if (ua.includes("Android")) os = "Android"; else if (ua.includes("like Mac")) os = "iOS"; return { browser, os, userAgent: ua }; } function getEnvironment() { if (true) return "development"; return "production"; } // src/error-batcher.ts import axios from "axios"; var ErrorBatcher = class { constructor(opts) { this.opts = opts; this.queue = []; this.isFlushing = false; var _a; this.apiKey = opts.apiKey; const interval = (_a = opts.flushIntervalMs) != null ? _a : 3e4; this.flushTimer = window.setInterval(() => this.flush(), interval); window.addEventListener("beforeunload", () => this.flushOnUnload()); } getApiKey() { return this.apiKey; } capture(evt) { var _a; const id = this.makeId(); const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const record2 = __spreadValues({ id, timestamp }, evt); if (this.queue.length >= ((_a = this.opts.maxBufferSize) != null ? _a : 64)) { this.queue.shift(); } this.queue.push(record2); return id; } async flush() { if (this.isFlushing || this.queue.length === 0) return; this.isFlushing = true; const batch = this.queue.splice(0, this.queue.length); try { await axios.post( this.opts.endpoint, { events: batch }, { maxBodyLength: 1e3 * 1024 * 1024, // 10MB maxContentLength: 1e3 * 1024 * 1024, // 10MB timeout: 3e4, headers: { "Content-Type": "application/json" } } ); } catch (e) { this.queue.unshift(...batch); } finally { this.isFlushing = false; } } flushOnUnload() { if (!navigator.sendBeacon || this.queue.length === 0) return; const payload = JSON.stringify({ events: this.queue }); navigator.sendBeacon(this.opts.endpoint, payload); } makeId() { return "xxxx-xxxx-4xxx-yxxx".replace(/[xy]/g, (c) => { const r = Math.random() * 16 | 0; const v = c === "x" ? r : r & 3 | 8; return v.toString(16); }); } destroy() { clearInterval(this.flushTimer); } }; // src/recorder.ts import { record } from "rrweb"; var events = []; var MAX_EVENTS = 1e3; var stopFn = void 0; function startRecording() { events = []; stopFn == null ? void 0 : stopFn(); stopFn = record({ emit(event) { if (event.type === 2) console.log("[rrweb] FullSnapshot \uAE30\uB85D\uB428:", event); events.push(event); if (events.length > MAX_EVENTS) { events = events.slice(-MAX_EVENTS); } }, // checkoutEveryNms: 1000, // 1초마다 체크아웃 checkoutEveryNms: 15e3, // 15초마다 한 번 checkoutEveryNth: 100, // 100개 이벤트마다 한 번 maskAllInputs: true, sampling: { mouseInteraction: { MouseUp: false, MouseDown: false, Click: false, ContextMenu: false, DblClick: false, Focus: false, Blur: false, TouchStart: false, TouchEnd: false } } }); } function getRecordedEvents(beforeErrorSec = 10, errorTime = Date.now(), source = events) { const sliced = source.filter( (e) => errorTime - e.timestamp < beforeErrorSec * 1e3 ); const snapshotCandidates = source.filter((e) => e.type === 2); const lastSnapshot = [...snapshotCandidates].reverse().find((e) => e.timestamp <= errorTime); if (lastSnapshot && !sliced.includes(lastSnapshot)) { return [lastSnapshot, ...sliced]; } if (!sliced.some((e) => e.type === 2)) { console.warn("\u26A0\uFE0F Snapshot \uC5C6\uC774 \uC798\uB9B0 replay\uC785\uB2C8\uB2E4. \uBCF5\uC6D0 \uBD88\uAC00\uB2A5\uD560 \uC218 \uC788\uC74C."); } return sliced; } function clearEvents() { events = []; } function getCurrentEvents() { return events.slice(); } // src/utils.ts import { zlibSync, unzlibSync } from "fflate"; function compressToBase64(obj) { const json = JSON.stringify(obj); const compressed = zlibSync(new TextEncoder().encode(json)); return btoa(String.fromCharCode(...compressed)); } function decompressFromBase64(base64) { const binary = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); const json = new TextDecoder().decode(unzlibSync(binary)); return JSON.parse(json); } // src/reporter.ts var batcher; var globalOpts = { beforeErrorSec: 30 }; function init(options) { var _a; globalOpts.beforeErrorSec = (_a = options.beforeErrorSec) != null ? _a : 10; batcher = new ErrorBatcher({ endpoint: options.endpoint, apiKey: options.apiKey, flushIntervalMs: options.flushIntervalMs, maxBufferSize: options.maxBufferSize }); if (typeof window !== "undefined") { if (document.readyState === "complete") { requestAnimationFrame(() => startRecording()); } else { window.addEventListener("load", () => { requestAnimationFrame(() => startRecording()); }); } } } function captureException(error, additionalInfo, userId) { var _a, _b; const errorTime = Date.now(); const eventsSnapshot = getCurrentEvents(); const rawReplay = getRecordedEvents( globalOpts.beforeErrorSec, errorTime, eventsSnapshot ); clearEvents(); const { browser, os, userAgent } = getBrowserInfo(); const compressedReplay = compressToBase64(rawReplay); return batcher.capture({ message: (_a = error.message) != null ? _a : "", stacktrace: (_b = error.stack) != null ? _b : "", replay: compressedReplay, environment: getEnvironment(), browser, os, userAgent, userId, additionalInfo, appVersion: "1.0.0", apiKey: batcher.getApiKey() }); } // src/handler.ts function setupGlobalErrorHandler() { if (window.__errorHandlerSetup) return; const origOnError = window.onerror; window.onerror = function thisWindowOnError(message, source, lineno, colno, error) { origOnError == null ? void 0 : origOnError.call(this, message, source, lineno, colno, error); captureException( error != null ? error : new Error(typeof message === "string" ? message : "Unknown error") ); return false; }; const origOnUnhandledRejection = window.onunhandledrejection; window.onunhandledrejection = function thisWindowOnRejection(event) { origOnUnhandledRejection == null ? void 0 : origOnUnhandledRejection.call(this, event); const err = event.reason instanceof Error ? event.reason : new Error(JSON.stringify(event.reason)); captureException(err); }; window.__errorHandlerSetup = true; } // src/front-end-tracer.ts import { W3CTraceContextPropagator } from "@opentelemetry/core"; import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; import { resourceFromAttributes, osDetector, detectResources } from "@opentelemetry/resources"; import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; var initOtel = async (config = {}) => { var _a, _b, _c, _d; const finalConfig = { serviceName: (_a = config.serviceName) != null ? _a : "replay", endpoint: (_b = config.endpoint) != null ? _b : "http://localhost:8081/traces", isSyntheticRequest: (_c = config.isSyntheticRequest) != null ? _c : false, scheduledDelayMillis: (_d = config.scheduledDelayMillis) != null ? _d : 500, customHeaders: __spreadValues({ "Content-Type": "application/x-protobuf" }, config.customHeaders) }; const { ZoneContextManager } = await import("@opentelemetry/context-zone"); let resource = resourceFromAttributes({ [ATTR_SERVICE_NAME]: finalConfig.serviceName }); if (finalConfig.isSyntheticRequest) { resource = resource.merge( resourceFromAttributes({ "app.synthetic_request": "true" }) ); } resource = resource.merge(detectResources({ detectors: [osDetector] })); const spanProcessor = new BatchSpanProcessor( new OTLPTraceExporter({ url: finalConfig.endpoint, headers: finalConfig.customHeaders }), { scheduledDelayMillis: finalConfig.scheduledDelayMillis } ); const provider = new WebTracerProvider({ resource, spanProcessors: [spanProcessor] }); provider.register({ contextManager: new ZoneContextManager(), propagator: new W3CTraceContextPropagator() }); if (typeof window !== "undefined") { const backendOrigin = new URL(finalConfig.endpoint).origin; const [{ FetchInstrumentation }, { XMLHttpRequestInstrumentation }] = await Promise.all([ import("@opentelemetry/instrumentation-fetch"), import("@opentelemetry/instrumentation-xml-http-request") ]); const fetchInst = new FetchInstrumentation({ propagateTraceHeaderCorsUrls: [backendOrigin], clearTimingResources: true }); const xhrInst = new XMLHttpRequestInstrumentation({ propagateTraceHeaderCorsUrls: [backendOrigin], clearTimingResources: true }); fetchInst.setTracerProvider(provider); xhrInst.setTracerProvider(provider); fetchInst.enable(); xhrInst.enable(); } console.log("OpenTelemetry initialized successfully"); return provider; }; export { ErrorBatcher, captureException, decompressFromBase64, getBrowserInfo, getEnvironment, getRecordedEvents, init, initOtel, setupGlobalErrorHandler, startRecording }; //# sourceMappingURL=index.js.map