You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/mx/tools/ClientErrorReporter.ts

169 lines
6.5 KiB

3 weeks ago
import { DEV } from 'cc/env';
interface PfRealtimeLogger {
error?: (m: string) => void;
warn?: (m: string) => void;
}
/** 小游戏 pf / tt 上的 RealtimeLog 工厂 */
interface PfPlatformApi {
getRealtimeLogManager?: (opts?: { syncToConsole?: boolean }) => PfRealtimeLogger;
}
interface ClientErrorGg {
sdk?: {
pf?: PfPlatformApi;
pfLog?: (msg: string) => void;
};
}
/**
* 客户端已捕获异常:本地始终 console.error;平台仅 pfLog + RealtimeLog(不走 ZYSDK / PF-ERR)。
*/
export class ClientErrorReporter {
/** 同一 capture + 消息摘要的上平台间隔 */
private static readonly MSG_COOLDOWN_MS = 60000;
/** 同一 capture 任意内容上平台间隔 */
private static readonly CAPTURE_GAP_MS = 60000;
/** 每个 capture 整会话最多上平台次数 */
private static readonly CAPTURE_SESSION_MAX = 3;
/** 全会话滚动窗口(毫秒) */
private static readonly GLOBAL_WINDOW_MS = 600000;
/** 滚动窗口内平台上限 */
private static readonly GLOBAL_WINDOW_MAX = 20;
private static _lastMsgMsByKey: Record<string, number> = {};
private static _lastCapturePlatformMs: Record<string, number> = {};
private static _capturePlatformCount: Record<string, number> = {};
private static _globalWindowStart = 0;
private static _globalWindowCount = 0;
private static _silencedCaptureWarned: Record<string, boolean> = {};
private static _realtime: PfRealtimeLogger | null = null;
private static _realtimeReady = false;
/**
* 上报捕获点异常:本地必打;平台上报(pfLog)受会话/窗口上限约束,不上 ZYSDK。
* @param capture 捕获点标识
* @param err 异常或消息
*/
static report(capture: string, err: unknown): void {
const detail = ClientErrorReporter._stringify(err);
console.error(`[${capture}]`, detail);
const cap = ClientErrorReporter._sanitizeToken(capture, 28);
const msgCore = ClientErrorReporter._messageCore(detail);
if (!ClientErrorReporter._allowPlatformUpload(cap, msgCore)) return;
const det = ClientErrorReporter._sanitizeDetail(detail);
const platformMsg = `ERR-${cap}-${det}`.slice(0, 250);
ClientErrorReporter._recordPlatformUpload(cap, msgCore);
ClientErrorReporter._platformLog(platformMsg);
}
private static _allowPlatformUpload(cap: string, msgCore: string): boolean {
const now = Date.now();
if (ClientErrorReporter._globalWindowStart <= 0
|| now - ClientErrorReporter._globalWindowStart > ClientErrorReporter.GLOBAL_WINDOW_MS) {
ClientErrorReporter._globalWindowStart = now;
ClientErrorReporter._globalWindowCount = 0;
}
if (ClientErrorReporter._globalWindowCount >= ClientErrorReporter.GLOBAL_WINDOW_MAX) return false;
const count = ClientErrorReporter._capturePlatformCount[cap] ?? 0;
if (count >= ClientErrorReporter.CAPTURE_SESSION_MAX) {
if (!ClientErrorReporter._silencedCaptureWarned[cap]) {
ClientErrorReporter._silencedCaptureWarned[cap] = true;
console.warn('[ClientErrorReporter] capture 平台上报已达上限,后续仅本地日志', cap);
}
return false;
}
const lastCap = ClientErrorReporter._lastCapturePlatformMs[cap] ?? 0;
if (now - lastCap < ClientErrorReporter.CAPTURE_GAP_MS) return false;
const msgKey = `${cap}:${msgCore}`;
const lastMsg = ClientErrorReporter._lastMsgMsByKey[msgKey] ?? 0;
if (now - lastMsg < ClientErrorReporter.MSG_COOLDOWN_MS) return false;
return true;
}
private static _recordPlatformUpload(cap: string, msgCore: string): void {
const now = Date.now();
ClientErrorReporter._globalWindowCount++;
ClientErrorReporter._capturePlatformCount[cap] = (ClientErrorReporter._capturePlatformCount[cap] ?? 0) + 1;
ClientErrorReporter._lastCapturePlatformMs[cap] = now;
ClientErrorReporter._lastMsgMsByKey[`${cap}:${msgCore}`] = now;
}
private static _gg(): ClientErrorGg | undefined {
return (typeof globalThis !== 'undefined' ? (globalThis as { gg?: ClientErrorGg }).gg : undefined);
}
private static _platformLog(msg: string): void {
try {
ClientErrorReporter._gg()?.sdk?.pfLog?.(msg);
} catch {
/* ignore */
}
const logger = ClientErrorReporter._realtimeLogger();
if (logger?.error) {
try {
logger.error(msg);
} catch {
/* ignore */
}
}
}
private static _realtimeLogger(): PfRealtimeLogger | null {
if (ClientErrorReporter._realtimeReady) return ClientErrorReporter._realtime;
ClientErrorReporter._realtimeReady = true;
const pf: PfPlatformApi | null | undefined = ClientErrorReporter._gg()?.sdk?.pf
?? (typeof globalThis !== 'undefined' ? (globalThis as { tt?: PfPlatformApi }).tt : null);
if (!pf?.getRealtimeLogManager) {
ClientErrorReporter._realtime = null;
return null;
}
try {
ClientErrorReporter._realtime = pf.getRealtimeLogManager({ syncToConsole: !!DEV });
} catch {
ClientErrorReporter._realtime = null;
}
return ClientErrorReporter._realtime;
}
private static _stringify(err: unknown): string {
if (err instanceof Error) {
const stack = err.stack ? `\n${err.stack}` : '';
return `${err.message}${stack}`;
}
if (typeof err === 'string') return err;
try {
return JSON.stringify(err);
} catch {
return String(err);
}
}
/** 去重用消息摘要(不含 stack 行,避免每帧变体) */
private static _messageCore(detail: string): string {
const oneLine = String(detail ?? '').replace(/[\r\n]+/g, ' ').trim();
const bar = oneLine.indexOf(' | ');
const core = bar >= 0 ? oneLine.slice(0, bar) : oneLine;
return core.slice(0, 80);
}
private static _sanitizeToken(raw: string, maxLen: number): string {
const s = String(raw ?? '').replace(/[^a-zA-Z0-9_]/g, '_');
return s.length > 0 ? s.slice(0, maxLen) : 'unknown';
}
private static _sanitizeDetail(raw: string): string {
return String(raw ?? '')
.replace(/[\r\n]+/g, ' ')
.replace(/[^a-zA-Z0-9_\u4e00-\u9fa5 :|.\-_]/g, '_')
.slice(0, 160);
}
}