Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 56 additions & 11 deletions src/services/api/getData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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 extends ApiPath>(
path: Path,
body: APIParam<Path>,
Expand All @@ -43,6 +54,11 @@ export function getData(path: string, body?: unknown): Promise<unknown> {
// 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),
Expand Down Expand Up @@ -71,17 +87,30 @@ export function getData(path: string, body?: unknown): Promise<unknown> {
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,
Expand All @@ -93,18 +122,34 @@ export function getData(path: string, body?: unknown): Promise<unknown> {
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")}`,
);
Comment on lines +130 to +132
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit retry modal to truly retryable failures

This now opens the global retry dialog for every business-level non-200 response, including expected validation outcomes that the caller already handles (for example /Users/Follow returns 400 for self-follow and the UI shows a specific message). The result is duplicate/conflicting error UX and a misleading “network error” retry prompt for non-retryable user-input errors.

Useful? React with 👍 / 👎.

}
const afterRes = afterRequest(data);
if (afterRes.continue === false) {
return afterRes.data;
}

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;
});
});
}
Expand Down
25 changes: 13 additions & 12 deletions src/services/errorLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export interface ErrorLog extends ErrorContext {

class ErrorLogger {
private logs = ref<ErrorLog[]>([]);
private maxLogs = 1000;
private maxLogs = 200;
private breadcrumbs: Breadcrumb[] = [];
private maxBreadcrumbs = 50;
private sessionId: string;
Expand All @@ -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";
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}

/**
Expand Down