Files
dukang/apps/mini-user/src/lib/client-error.ts
T
jacy 205c1110c2 feat(ops): add client error reporting API and frontend hooks
Collect mini-user/shop/partner JS errors via POST /common/client-errors, persist logs, and push fatal/error to WeCom.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 16:27:08 +08:00

126 lines
3.3 KiB
TypeScript

import Taro from '@tarojs/taro';
import { CLIENT_APP, getToken, API_BASE } from './api';
export type ClientErrorLevel = 'fatal' | 'error' | 'warn';
export type ClientErrorCategory =
| 'js_error'
| 'unhandled_rejection'
| 'api_error'
| 'network'
| 'render'
| 'bridge'
| 'other';
export type ClientErrorPayload = {
level: ClientErrorLevel;
category: ClientErrorCategory;
message: string;
stack?: string;
pagePath?: string;
extra?: Record<string, unknown>;
};
function currentPagePath(): string | undefined {
try {
const pages = Taro.getCurrentPages();
const cur = pages[pages.length - 1] as { route?: string; $taroPath?: string } | undefined;
return cur?.$taroPath || cur?.route || undefined;
} catch {
return undefined;
}
}
/** 上报客户端错误(失败静默,避免递归) */
export function reportClientError(payload: ClientErrorPayload): void {
const body = {
level: payload.level,
category: payload.category,
message: String(payload.message || 'unknown').slice(0, 1000),
stack: payload.stack ? String(payload.stack).slice(0, 4000) : undefined,
pagePath: payload.pagePath || currentPagePath(),
clientApp: CLIENT_APP,
extra: payload.extra,
};
const header: Record<string, string> = {
'Content-Type': 'application/json',
'X-Client-App': CLIENT_APP,
};
const token = getToken();
if (token) header.Authorization = `Bearer ${token}`;
void Taro.request({
url: `${API_BASE}/common/client-errors`,
method: 'POST',
data: body,
header,
}).catch(() => {});
}
let installed = false;
/** 安装小程序/H5 全局未捕获错误钩子(幂等) */
export function installClientErrorReporting(): void {
if (installed) return;
installed = true;
try {
Taro.onError?.((msg) => {
reportClientError({
level: 'fatal',
category: 'js_error',
message: typeof msg === 'string' ? msg : String(msg),
});
});
} catch {
/* ignore */
}
try {
Taro.onUnhandledRejection?.((res) => {
const reason = (res as { reason?: unknown })?.reason;
const message =
reason instanceof Error
? reason.message
: typeof reason === 'string'
? reason
: JSON.stringify(reason);
const stack = reason instanceof Error ? reason.stack : undefined;
reportClientError({
level: 'error',
category: 'unhandled_rejection',
message: message || 'UnhandledRejection',
stack,
});
});
} catch {
/* ignore */
}
if (typeof window !== 'undefined') {
window.addEventListener('error', (ev) => {
reportClientError({
level: 'fatal',
category: 'js_error',
message: ev.message || 'window.error',
stack: ev.error instanceof Error ? ev.error.stack : undefined,
extra: { filename: ev.filename, lineno: ev.lineno, colno: ev.colno },
});
});
window.addEventListener('unhandledrejection', (ev) => {
const reason = ev.reason;
reportClientError({
level: 'error',
category: 'unhandled_rejection',
message:
reason instanceof Error
? reason.message
: typeof reason === 'string'
? reason
: 'unhandledrejection',
stack: reason instanceof Error ? reason.stack : undefined,
});
});
}
}