166 lines
4.6 KiB
JavaScript
166 lines
4.6 KiB
JavaScript
import { PATHS } from "./constants.js";
|
|
|
|
const RECENT_EVENT_TTL_MS = 15_000;
|
|
const recentEventMap = new Map();
|
|
const pendingEventMap = new Map();
|
|
let pendingFlushTimer = null;
|
|
|
|
function normalizeErrorDetail(detail) {
|
|
if (!detail) return "";
|
|
if (detail instanceof Error) {
|
|
return detail.stack || detail.message || String(detail);
|
|
}
|
|
if (typeof detail === "string") {
|
|
return detail;
|
|
}
|
|
try {
|
|
return JSON.stringify(detail);
|
|
} catch {
|
|
return String(detail);
|
|
}
|
|
}
|
|
|
|
function dedupeKey(level, message, detail, category) {
|
|
return `${level}::${category || ""}::${message}::${detail}`;
|
|
}
|
|
|
|
function normalizeFingerprintText(value) {
|
|
return String(value || "")
|
|
.replace(/[?&](m|t|token|expires|signature|X-Amz-[^=]+)=[^&\s]+/gi, "")
|
|
.replace(/(index|chunk|segment)[_-]?\d+(_\d+)?\.(ts|m4s|vtt)/gi, "<hls-fragment>")
|
|
.replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, "<uuid>")
|
|
.replace(/\bconn_[A-Za-z0-9:._-]+\b/g, "<connection>")
|
|
.replace(/\b\d{5,}\b/g, "<number>")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
function hashFingerprint(value) {
|
|
let hash = 5381;
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
hash = ((hash << 5) + hash) ^ value.charCodeAt(index);
|
|
}
|
|
return `client-${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
}
|
|
|
|
function buildFingerprint(level, message, detail, category, module) {
|
|
return hashFingerprint(
|
|
[
|
|
normalizeFingerprintText(level),
|
|
normalizeFingerprintText(category),
|
|
normalizeFingerprintText(module),
|
|
normalizeFingerprintText(message),
|
|
normalizeFingerprintText(detail),
|
|
].join("|"),
|
|
);
|
|
}
|
|
|
|
function shouldSkip(level, message, detail, category) {
|
|
const key = dedupeKey(level, message, detail, category);
|
|
const now = Date.now();
|
|
const lastSeenAt = recentEventMap.get(key);
|
|
recentEventMap.set(key, now);
|
|
|
|
for (const [entryKey, entryTime] of recentEventMap.entries()) {
|
|
if (now - entryTime > RECENT_EVENT_TTL_MS) {
|
|
recentEventMap.delete(entryKey);
|
|
}
|
|
}
|
|
|
|
return lastSeenAt && now - lastSeenAt < RECENT_EVENT_TTL_MS;
|
|
}
|
|
|
|
async function sendEarthClientLog(payload) {
|
|
try {
|
|
await fetch(PATHS.earthClientLogsApi, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
keepalive: true,
|
|
});
|
|
} catch {
|
|
// Swallow reporting failures to avoid recursive log noise.
|
|
}
|
|
}
|
|
|
|
function scheduleFlush() {
|
|
if (pendingFlushTimer) return;
|
|
pendingFlushTimer = window.setTimeout(() => {
|
|
pendingFlushTimer = null;
|
|
const pending = Array.from(pendingEventMap.values());
|
|
pendingEventMap.clear();
|
|
pending.forEach((entry) => {
|
|
void sendEarthClientLog({
|
|
level: entry.level,
|
|
message: entry.message,
|
|
category: entry.category,
|
|
module: entry.module,
|
|
url: window.location.href,
|
|
detail: entry.detail.slice(0, 4000),
|
|
fingerprint: entry.fingerprint,
|
|
occurrence_count: entry.occurrenceCount,
|
|
metadata: entry.metadata,
|
|
});
|
|
});
|
|
}, 1000);
|
|
}
|
|
|
|
export function reportEarthClientLog({
|
|
level = "error",
|
|
message,
|
|
category = "runtime",
|
|
module = "earth",
|
|
detail = "",
|
|
metadata = {},
|
|
}) {
|
|
if (!message) return;
|
|
const normalizedDetail = normalizeErrorDetail(detail);
|
|
const fingerprint = buildFingerprint(level, message, normalizedDetail, category, module);
|
|
const key = dedupeKey(level, message, normalizedDetail, category);
|
|
const existing = pendingEventMap.get(key);
|
|
if (existing) {
|
|
existing.occurrenceCount += 1;
|
|
existing.metadata = { ...existing.metadata, ...metadata };
|
|
} else {
|
|
pendingEventMap.set(key, {
|
|
level,
|
|
message,
|
|
category,
|
|
module,
|
|
detail: normalizedDetail,
|
|
fingerprint,
|
|
occurrenceCount: 1,
|
|
metadata,
|
|
});
|
|
}
|
|
|
|
shouldSkip(level, message, normalizedDetail, category);
|
|
scheduleFlush();
|
|
}
|
|
|
|
export function registerEarthClientErrorHandlers() {
|
|
window.addEventListener("error", (event) => {
|
|
console.error("全局错误:", event.error);
|
|
void reportEarthClientLog({
|
|
level: "error",
|
|
category: "window-error",
|
|
module: "main",
|
|
message: event.message || "Earth 页面发生未捕获错误",
|
|
detail: event.error || `${event.filename || ""}:${event.lineno || 0}:${event.colno || 0}`,
|
|
});
|
|
});
|
|
|
|
window.addEventListener("unhandledrejection", (event) => {
|
|
console.error("未处理的 Promise 错误:", event.reason);
|
|
void reportEarthClientLog({
|
|
level: "error",
|
|
category: "unhandledrejection",
|
|
module: "main",
|
|
message: "Earth 页面发生未处理 Promise 错误",
|
|
detail: event.reason,
|
|
});
|
|
});
|
|
}
|