Files
planet/frontend/public/earth/js/client-logs.js
linkong acbbfdf9e2
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.69.0
2026-06-03 17:27:00 +08:00

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,
});
});
}