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.
168 lines
6.5 KiB
168 lines
6.5 KiB
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);
|
|
}
|
|
}
|
|
|