135 lines
3.9 KiB
JavaScript
135 lines
3.9 KiB
JavaScript
const MAX_CONTEXT_LEN = 2000;
|
|
const RECENT_WINDOW_MS = 5000;
|
|
const UI_ERROR_REPORTING_KEY = 'shine-ui-send-errors-to-server-v1';
|
|
|
|
let transport = null;
|
|
let transportDepth = 0;
|
|
const recentFingerprints = new Map();
|
|
let notifySent = null;
|
|
|
|
function nowTs() {
|
|
return Date.now();
|
|
}
|
|
|
|
function cleanString(value, maxLen = 1000) {
|
|
if (value == null) return '';
|
|
const normalized = String(value).replace(/\s+/g, ' ').trim();
|
|
if (normalized.length <= maxLen) return normalized;
|
|
return `${normalized.slice(0, Math.max(0, maxLen - 3))}...`;
|
|
}
|
|
|
|
function stringifyContext(context) {
|
|
if (context == null) return '';
|
|
try {
|
|
const raw = JSON.stringify(context);
|
|
if (!raw) return '';
|
|
if (raw.length <= MAX_CONTEXT_LEN) return raw;
|
|
return `${raw.slice(0, MAX_CONTEXT_LEN - 3)}...`;
|
|
} catch (error) {
|
|
return cleanString(`context_json_error:${error?.message || error}`, MAX_CONTEXT_LEN);
|
|
}
|
|
}
|
|
|
|
function makeFingerprint(payload) {
|
|
return [
|
|
payload.kind,
|
|
payload.message,
|
|
payload.sourceUrl,
|
|
payload.lineNumber,
|
|
payload.columnNumber,
|
|
payload.requestOp,
|
|
].join('|');
|
|
}
|
|
|
|
function isDuplicate(fingerprint) {
|
|
const ts = nowTs();
|
|
const prev = recentFingerprints.get(fingerprint);
|
|
recentFingerprints.set(fingerprint, ts);
|
|
|
|
for (const [key, time] of recentFingerprints.entries()) {
|
|
if (ts - time > RECENT_WINDOW_MS) {
|
|
recentFingerprints.delete(key);
|
|
}
|
|
}
|
|
|
|
return prev != null && ts - prev < RECENT_WINDOW_MS;
|
|
}
|
|
|
|
function buildPayload(details = {}) {
|
|
return {
|
|
kind: cleanString(details.kind || 'client_error', 64),
|
|
message: cleanString(details.message || details.reason || 'Unknown client error', 500),
|
|
stack: cleanString(details.stack || details.error?.stack || '', 8000),
|
|
sourceUrl: cleanString(details.sourceUrl || details.fileName || '', 240),
|
|
lineNumber: Number.isFinite(details.lineNumber) ? details.lineNumber : null,
|
|
columnNumber: Number.isFinite(details.columnNumber) ? details.columnNumber : null,
|
|
route: cleanString(details.route || window.location?.hash || '', 200),
|
|
href: cleanString(details.href || window.location?.href || '', 240),
|
|
userAgent: cleanString(details.userAgent || navigator.userAgent || '', 240),
|
|
clientTs: Number.isFinite(details.clientTs) ? details.clientTs : nowTs(),
|
|
requestOp: cleanString(details.requestOp || '', 64),
|
|
requestIdRef: cleanString(details.requestIdRef || '', 128),
|
|
contextJson: stringifyContext({
|
|
title: document.title || '',
|
|
pageVisibility: document.visibilityState || '',
|
|
...details.context,
|
|
}),
|
|
};
|
|
}
|
|
|
|
export function setClientErrorTransport(fn) {
|
|
transport = typeof fn === 'function' ? fn : null;
|
|
}
|
|
|
|
export function setClientErrorSentNotifier(fn) {
|
|
notifySent = typeof fn === 'function' ? fn : null;
|
|
}
|
|
|
|
export function isClientErrorReportingEnabled() {
|
|
try {
|
|
return localStorage.getItem(UI_ERROR_REPORTING_KEY) === '1';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function setClientErrorReportingEnabled(enabled) {
|
|
try {
|
|
localStorage.setItem(UI_ERROR_REPORTING_KEY, enabled ? '1' : '0');
|
|
} catch {
|
|
// ignore storage errors
|
|
}
|
|
}
|
|
|
|
export async function captureClientError(details = {}) {
|
|
const payload = buildPayload(details);
|
|
if (!payload.message) return false;
|
|
|
|
const fingerprint = details.dedupeKey || makeFingerprint(payload);
|
|
if (isDuplicate(fingerprint)) return false;
|
|
|
|
console.error('[client-error]', payload.kind, payload.message, details.error || '');
|
|
|
|
if (!transport || details.skipTransport === true || transportDepth > 0 || !isClientErrorReportingEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
transportDepth += 1;
|
|
await transport(payload);
|
|
if (notifySent) {
|
|
try {
|
|
notifySent(payload);
|
|
} catch {
|
|
// ignore notifier errors
|
|
}
|
|
}
|
|
return true;
|
|
} catch (error) {
|
|
console.warn('client error transport failed', error);
|
|
return false;
|
|
} finally {
|
|
transportDepth = Math.max(0, transportDepth - 1);
|
|
}
|
|
}
|