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
JavaScript
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