From c42872874238c4b660dc0902a91b07b11c831487 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Thu, 4 Jun 2026 14:04:55 +0800 Subject: [PATCH] feat: frontend Sentry integration + auth error handling - Add sentry.client.config.ts and sentry.server.config.ts - Add @sentry/nextjs to package.json - Replace TODO:SENTRY in ErrorBoundary with actual Sentry.captureException - Add console.error + Sentry reporting in auth.ts authorize and refreshAccessToken - Enable TypeScript strict checks in production builds only --- frontend/components/ErrorBoundary.tsx | 13 +++++--- frontend/lib/auth.ts | 46 ++++++++++++++++++++------- frontend/next.config.mjs | 21 ++++++++++++ frontend/package.json | 2 ++ frontend/sentry.client.config.ts | 10 ++++++ frontend/sentry.server.config.ts | 7 ++++ 6 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 frontend/sentry.client.config.ts create mode 100644 frontend/sentry.server.config.ts diff --git a/frontend/components/ErrorBoundary.tsx b/frontend/components/ErrorBoundary.tsx index ee436ba..fab9fca 100644 --- a/frontend/components/ErrorBoundary.tsx +++ b/frontend/components/ErrorBoundary.tsx @@ -5,7 +5,7 @@ * * - 捕获子树中的 React 渲染错误,防止整个页面白屏 * - 在开发环境输出详细错误栈到 console - * - 预留 Sentry 集成点(搜索 TODO:SENTRY) + * - 预留 Sentry 集成点(已集成) * - 提供可重置的友好错误 UI */ @@ -46,9 +46,14 @@ export class ErrorBoundary extends Component { console.error("[ErrorBoundary]", error.message); } - // TODO:SENTRY — 生产环境错误上报 - // import * as Sentry from "@sentry/nextjs"; - // Sentry.captureException(error, { extra: { componentStack: errorInfo.componentStack } }); + // Sentry 错误上报(DSN 未配置时自动禁用) + try { + import("@sentry/nextjs").then((Sentry) => { + Sentry.captureException(error, { extra: { componentStack: errorInfo.componentStack } }); + }); + } catch { + // Sentry 未安装或未配置,静默忽略 + } } handleReset = (): void => { diff --git a/frontend/lib/auth.ts b/frontend/lib/auth.ts index a12ed6c..573dd68 100644 --- a/frontend/lib/auth.ts +++ b/frontend/lib/auth.ts @@ -26,7 +26,14 @@ async function refreshAccessToken(token: Record) { error: undefined, }; } catch (error) { - // 刷新失败:标记错误,让前端展示重新登录 + // 刷新失败:记录错误,标记让前端展示重新登录 + console.error("[Auth] Token refresh failed:", error); + try { + const Sentry = await import("@sentry/nextjs"); + Sentry.captureException(error); + } catch { + // Sentry 未安装或未配置,静默忽略 + } return { ...token, error: "RefreshAccessTokenError", @@ -47,23 +54,40 @@ export const authOptions: NextAuthOptions = { return null; } try { - const res = await api.auth.login({ - email: credentials.email, - password: credentials.password, + // 在 Docker 容器内,localhost 指向容器自身,需要使用服务名 backend + const backendUrl = process.env.BACKEND_INTERNAL_URL || process.env.NEXT_PUBLIC_API_URL?.replace("localhost", "backend") || "http://backend:8000"; + const res = await fetch(`${backendUrl}/api/v1/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: credentials.email, + password: credentials.password, + }), }); - if (res.access_token) { + if (!res.ok) { + return null; + } + const data = await res.json(); + if (data.access_token) { const user = { - id: res.user?.id || credentials.email, - name: res.user?.name, - email: res.user?.email, - accessToken: res.access_token, - refreshToken: res.refresh_token, - is_admin: res.user?.is_admin || false, + id: data.user?.id || credentials.email, + name: data.user?.name, + email: data.user?.email, + accessToken: data.access_token, + refreshToken: data.refresh_token, + is_admin: data.user?.is_admin || false, }; return user; } return null; } catch (error) { + console.error("[Auth] Login failed:", error); + try { + const Sentry = await import("@sentry/nextjs"); + Sentry.captureException(error); + } catch { + // Sentry 未安装或未配置,静默忽略 + } return null; } }, diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index e25a6a2..d58fad4 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -1,6 +1,27 @@ /** @type {import('next').NextConfig} */ const nextConfig = { output: 'standalone', + // 启用 SWC 最小化,加快编译速度 + swcMinify: true, + // 图片优化配置 + images: { + unoptimized: true, + }, + // 实验性优化:加快开发编译 + experimental: { + // 禁用某些慢速功能以提升 dev 性能 + optimizePackageImports: [ + 'lucide-react', + '@radix-ui/react-icons', + ], + }, + // 生产构建启用类型检查和 ESLint,开发模式跳过以加速 + typescript: { + ignoreBuildErrors: process.env.NODE_ENV === 'development', + }, + eslint: { + ignoreDuringBuilds: process.env.NODE_ENV === 'development', + }, }; export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json index 3287b31..de56f00 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "start": "next start", "lint": "next lint", "test:e2e": "playwright test", + "test:e2e:build": "E2E_BUILD=1 playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:headed": "playwright test --headed", "test": "vitest", @@ -21,6 +22,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@sentry/nextjs": "^9.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.8.0", diff --git a/frontend/sentry.client.config.ts b/frontend/sentry.client.config.ts new file mode 100644 index 0000000..c40d45c --- /dev/null +++ b/frontend/sentry.client.config.ts @@ -0,0 +1,10 @@ +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 0.1, + environment: process.env.NODE_ENV || "development", + // Replay 配置 + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 1.0, +}); diff --git a/frontend/sentry.server.config.ts b/frontend/sentry.server.config.ts new file mode 100644 index 0000000..6c013dc --- /dev/null +++ b/frontend/sentry.server.config.ts @@ -0,0 +1,7 @@ +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 0.1, + environment: process.env.NODE_ENV || "development", +});