diff --git a/src/services/api/getData.ts b/src/services/api/getData.ts index d83ff67..3be1d34 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 && !Array.isArray(value); +} + 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 {}; + } + 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..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"; } @@ -143,8 +148,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 +276,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(); } /**