From 7a700c062db882c9c954e464e0efe467cdc9df08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E4=B8=B4?= <155876693+wsxiaolin@users.noreply.github.com> Date: Mon, 4 May 2026 15:52:46 +0800 Subject: [PATCH 1/2] Improve API error validation and console stack logging --- src/services/api/getData.ts | 67 +++++++++++++++++++++++++++++++------ src/services/errorLogger.ts | 18 ++++------ 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/services/api/getData.ts b/src/services/api/getData.ts index d83ff67..e394c99 100644 --- a/src/services/api/getData.ts +++ b/src/services/api/getData.ts @@ -4,6 +4,7 @@ import i18n from "@i18n/index.ts"; import { detectBrowserLanguage, toApiLanguage } from "@i18n/index.ts"; import { getDeviceInfo, getVisitorId } from "./getDevice.ts"; import { showMessage } from "@popup/naiveui.ts"; +import { showAPiError } from "@popup/apiError.ts"; import { getPath } from "../utils.ts"; import { normalizePath } from "./types.ts"; @@ -23,6 +24,16 @@ import { normalizePath } from "./types.ts"; import type { ApiPath, APIParam, APIResult } from "./types.ts"; import type { Device, ResultOf, Users } from "../../pl-serve-type-main/type/main"; +type ApiResultLike = { + Status?: number; + Message?: string; + [key: string]: unknown; +}; + +function isApiResultLike(value: unknown): value is ApiResultLike { + return typeof value === "object" && value !== null; +} + export function getData( path: Path, body: APIParam, @@ -43,6 +54,11 @@ export function getData(path: string, body?: unknown): Promise { // Mabe the anonymous token storage or local storage or authentication (CheckLogin) has a problem // 可能是匿名Token的存储或者本地存储或者鉴权(CheckLogin)出了问题 + const buildRetry = () => () => getData(path, body); + const showRetryableApiError = (message: string) => { + showAPiError(i18n.global.t("errors.networkError") as string, message, buildRetry()); + }; + return fetch(getPath(`/@api${npath}`), { method: "POST", body: JSON.stringify(body), @@ -71,17 +87,30 @@ export function getData(path: string, body?: unknown): Promise { undefined, body, ); - return response.json().then(() => { - // 这里的错误处理仅处理API本身非2xx的错误,及服务器本身出了问题 - // 而Response.data中的错误是API本身的错误(如权限不足、参数错误等),需要在调用API时处理 - // This error handling only deals with non-2xx errors from the API itself, and server issues. - // Errors in Response.data are API-specific errors (like insufficient permissions, parameter errors - showMessage("error", i18n.global.t("errors.networkError"), { - duration: 5000, - }); + return response.json().catch(() => undefined).then(() => { + showMessage("error", i18n.global.t("errors.networkError"), { duration: 5000 }); + showRetryableApiError(`${npath} failed with HTTP ${response.status}`); }); } - return response.json().then((data) => { + return response.json().then((data: unknown) => { + if (!isApiResultLike(data)) { + const invalidError = new Error(`Invalid API response shape from ${npath}`); + window.$ErrorLogger.captureError({ + type: "api", + message: invalidError.message, + stack: invalidError.stack, + error: invalidError, + method: "POST", + url: npath, + statusCode: response.status, + responseData: data, + requestData: body, + breadcrumbs: [...window.$ErrorLogger.getBreadcrumbs()], + }); + showRetryableApiError(`Invalid response from ${npath}`); + return data; + } + if (npath !== "/Users/GetUser") { window.$ErrorLogger.addBreadcrumb("api", `${npath} success`, { statusCode: 200, @@ -93,11 +122,14 @@ export function getData(path: string, body?: unknown): Promise { if (data.Status !== 200) { window.$ErrorLogger.captureApiError( "POST", - path, - data.Status, + npath, + typeof data.Status === "number" ? data.Status : -1, data, body, ); + showRetryableApiError( + `${npath} returned business status ${String(data.Status ?? "unknown")}`, + ); } const afterRes = afterRequest(data); if (afterRes.continue === false) { @@ -105,6 +137,19 @@ export function getData(path: string, body?: unknown): Promise { } return data; + }).catch((error) => { + window.$ErrorLogger.captureError({ + type: "network", + message: `Failed to parse API response: ${npath}`, + stack: error?.stack, + error, + method: "POST", + url: npath, + requestData: body, + breadcrumbs: [...window.$ErrorLogger.getBreadcrumbs()], + }); + showRetryableApiError(`Failed to parse response from ${npath}`); + throw error; }); }); } diff --git a/src/services/errorLogger.ts b/src/services/errorLogger.ts index 10101ea..5f020ba 100644 --- a/src/services/errorLogger.ts +++ b/src/services/errorLogger.ts @@ -143,8 +143,8 @@ class ErrorLogger { url: log.url, type: log.type, message: log.message, - // keep only the first line of stack to save space and still be useful - stack: log.stack ? String(log.stack).split("\n")[0] : undefined, + // keep full stack to preserve actionable debugging details + stack: log.stack ? String(log.stack) : undefined, statusCode: log.statusCode, responseData: log.responseData, requestData: log.requestData, @@ -271,22 +271,18 @@ class ErrorLogger { this.showErrorNotification(errorLog, this.lastErrorCount); } - // Console output: be concise to avoid double noise (browser already prints native errors) + // Console output: always print a standard error entry so stack frames stay visible in dev/prod. const rawContext = { ...context }; - console.groupCollapsed( - `%c[${context.type.toUpperCase()}] ${context.message}`, - "color: red; font-weight: bold", - ); if (rawContext.error) { - // print the original Error object so it can be expanded when needed - console.error("Error object:", rawContext.error); + console.error(`[${context.type.toUpperCase()}] ${context.message}`, rawContext.error); } else if (context.stack) { - console.error("Stack:", context.stack); + console.error(`[${context.type.toUpperCase()}] ${context.message}\n${context.stack}`); + } else { + console.error(`[${context.type.toUpperCase()}] ${context.message}`); } if (this.debugMode) { console.debug("Full context (sanitized):", errorLog); } - console.groupEnd(); } /** From d8747bd916c39e36c798f046d787308598c13154 Mon Sep 17 00:00:00 2001 From: gushishang <117088703+gushishang@users.noreply.github.com> Date: Sat, 16 May 2026 21:41:04 +0800 Subject: [PATCH 2/2] update --- src/services/api/getData.ts | 4 ++-- src/services/errorLogger.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/services/api/getData.ts b/src/services/api/getData.ts index e394c99..3be1d34 100644 --- a/src/services/api/getData.ts +++ b/src/services/api/getData.ts @@ -31,7 +31,7 @@ type ApiResultLike = { }; function isApiResultLike(value: unknown): value is ApiResultLike { - return typeof value === "object" && value !== null; + return typeof value === "object" && value !== null && !Array.isArray(value); } export function getData( @@ -108,7 +108,7 @@ export function getData(path: string, body?: unknown): Promise { breadcrumbs: [...window.$ErrorLogger.getBreadcrumbs()], }); showRetryableApiError(`Invalid response from ${npath}`); - return data; + return {}; } if (npath !== "/Users/GetUser") { diff --git a/src/services/errorLogger.ts b/src/services/errorLogger.ts index 5f020ba..4bcded1 100644 --- a/src/services/errorLogger.ts +++ b/src/services/errorLogger.ts @@ -42,7 +42,7 @@ export interface ErrorLog extends ErrorContext { class ErrorLogger { private logs = ref([]); - private maxLogs = 1000; + private maxLogs = 200; private breadcrumbs: Breadcrumb[] = []; private maxBreadcrumbs = 50; private sessionId: string; @@ -65,6 +65,11 @@ class ErrorLogger { } private initDebugMode() { + // Dev 环境默认开启 debug,生产环境 follow 用户配置 + if (import.meta.env.DEV) { + this.debugMode = true; + return; + } const debugConfig = storageManager.getObj("userConfig").value?.debugger; this.debugMode = debugConfig === "on" || debugConfig === "export"; }