Files
NextEdu/docs/superpowers/specs/2026-07-07-logging-refactor-design.md
SpecialX 5d9981fd7d
Some checks failed
CI / scheduled-backup (push) Has been skipped
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
docs(architecture): update impact map, data, audit reports, superpowers docs
- Update 004_architecture_impact_map.md and 005_architecture_data.json

- Add audit reports: data-access-audit-framework-v1, data-access-audit-v1-data.json,

  data-access-audit-v1, g1-g5 audit outputs

- Add superpowers plans and specs (logging-refactor, documentation-system-redesign)

- Update troubleshooting/known-issues.md
2026-07-07 16:23:35 +08:00

722 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 日志系统重构设计文档
| 字段 | 值 |
|------|---|
| 文档版本 | v1 |
| 创建日期 | 2026-07-07 |
| 作者 | Trae 协作生成 |
| 状态 | 待用户审查 |
| 范围 | pino logger 抽象 + Request ID 贯穿 + 静默失败修复 + error.tsx 上报 + track-event 去重 |
| 策略 | 应用层 SDK 改造不部署外部可观测性服务OTel / Sentry / Prometheus 等) |
---
## 1. 背景与现状
### 1.1 现状调研摘要
基于 2026-07-07 全项目调研:
| 维度 | 状态 | 说明 |
|------|------|------|
| 业务审计日志 | ✅ 已成熟 | audit / login / change logger + 3 张 DB 表 + audit 模块管理后台 |
| 应用层结构化日志 | ❌ 未实现 | 88 处散乱 `console.*`,无级别、无 JSON、无 request ID |
| Request Correlation | ❌ 未实现 | proxy.ts 中间件零日志、不注入请求 ID |
| 错误边界上报 | ❌ 全部丢弃 | 130 个 `error.tsx` 接收 `error` prop 后直接丢弃,前端运行时错误对开发者不可见 |
| 静默失败 | ⚠️ 严重 | 三个 audit-logger 的 catch 块完全吞没错误,运维无法感知审计写入失败 |
| 统一错误处理 | ⚠️ 部分 | `handleActionError` / `handleApiError``console.error`,前缀混乱(`[ExamAction]` / `[ActionError]` / `[ApiError]` / 无前缀混用) |
| track-event | ⚠️ 重复 | 4 个 no-op stubshared / rbac / course-plans / questions 各一份) |
| 架构图覆盖 | ❌ 未覆盖 | 004 / 005 完全未记录可观测性基础设施 |
### 1.2 console.* 分布
| 类型 | 次数 | 文件数 | 主要位置 |
|------|------|--------|----------|
| `console.log` | 2 | 1 | `questions/utils/parse-content.ts` |
| `console.error` | 67 | 30 | data-access 的 catch 块、`action-utils.ts``api-response.ts`、各 Server Action |
| `console.warn` | 5 | 5 | `api/web-vitals/route.ts`、redis/store 等 |
| `console.info` | 10 | 6 | `track-event.ts``api/web-vitals/route.ts`、notifications channels |
| `console.debug` | 4 | 3 | 各模块 `track-event.ts`(仅 dev |
### 1.3 关键约束
- **proxy.ts 在 Edge Runtime 运行**[proxy.ts:64](file:///e:/Desktop/CICD/src/proxy.ts#L64) 注释明确说明),`AsyncLocalStorage` 不可用(依赖 `node:async_hooks`Edge 不支持)
- 其余部分RSC、Server Action、Route Handler、data-access运行在 Node.js runtime`AsyncLocalStorage` 完全支持
- 项目自部署(`next.config.ts``output: "standalone"`),运维通过 `docker logs` 查看日志
---
## 2. 目标与非目标
### 2.1 目标
1. **统一 logger 抽象**pino 替换 88 处散乱 `console.*`,提供 `info/error/warn/debug` + `createModuleLogger(module)` 子 logger
2. **级别控制**`LOG_LEVEL` 环境变量控制输出阈值debug/info/warn/errorZod 校验
3. **结构化输出**:生产环境 JSON含 timestamp/level/module/requestId/msg开发环境 `pino-pretty` 彩色文本
4. **Request ID 贯穿**proxy.ts 生成 ID 并注入请求头Node.js runtime 通过 `AsyncLocalStorage` 贯穿到 data-access / audit-logger
5. **静默失败告警**:三个 audit-logger 的 catch 块从 silent 改为 `logger.warn`
6. **统一错误处理接入**`handleActionError` / `handleApiError` / `safeActionCall` 接入 logger前缀通过 `module` 字段规范化
7. **track-event 去重合并**4 个 no-op stub 合并为 1 个,统一通过 `createModuleLogger("track")`
8. **error.tsx 错误上报**130 个 error.tsx 通过 `useErrorReport` Hook 上报到 `/api/client-error`,含节流防风暴
9. **架构图同步**004 / 005 / known-issues.md 全量更新
### 2.2 非目标YAGNI
- OpenTelemetry / 链路追踪trace
- Prometheus / `/metrics` 端点
- Sentry / Bugsnag / Datadog 等 SaaS 错误监控
- Web Vitals 后端持久化(保留现有 `console.warn`
- 数据库查询日志 / 慢查询日志
- 日志文件轮转(`docker logs` 已足够)
- next-auth events 回调改造(与本次重构解耦)
---
## 3. 架构设计
### 3.1 整体数据流
```
[Client] ──HTTP──> [proxy.ts (Edge Runtime)]
│ requestId = crypto.randomUUID() // Web Crypto API
│ NextResponse.next({ request: { headers } })
│ 注入 x-request-id 到下游请求头
[RSC / Server Action / Route Handler (Node.js Runtime)]
│ withRequestContext(fn):
│ 1. headers().get("x-request-id") 读取
│ 2. requestContextStorage.run({ requestId }, fn)
[data-access / audit-logger / 业务逻辑 (Node.js Runtime)]
│ logger.info({...}, "msg") 调用
│ pino mixin 自动从 getRequestContext() 取 requestId
[stdout: {"level":"info","time":...,"requestId":"abc-123","module":"audit","msg":"..."}]
```
### 3.2 新增文件清单
```
src/shared/lib/
├─ logger.ts # pino 实例 + createModuleLogger 工厂
├─ request-context.ts # AsyncLocalStorage仅 Node.js runtime
└─ with-request-context.ts # Server Action / Route Handler 入口包装
src/shared/hooks/
└─ use-error-report.ts # error.tsx 公共上报 Hook含节流
src/app/api/client-error/
└─ route.ts # 客户端错误接收端点
```
### 3.3 修改文件清单
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `package.json` | 新增依赖 | `pino``pino-pretty`dev |
| `src/env.mjs` | 新增字段 | `LOG_LEVEL`(默认 `info`Zod enum |
| `src/proxy.ts` | 修改 | 生成 requestId 并通过 `NextResponse.next` 注入请求头 |
| `src/shared/lib/action-utils.ts` | 修改 | `handleActionError` / `safeActionCall` 用 logger |
| `src/shared/lib/api-response.ts` | 修改 | `handleApiError` 用 logger |
| `src/shared/lib/audit-logger.ts` | 修改 | catch 块从 silent 改为 `logger.warn` |
| `src/shared/lib/change-logger.ts` | 修改 | 同上 |
| `src/shared/lib/login-logger.ts` | 修改 | 同上 |
| `src/shared/lib/track-event.ts` | 修改 | 改为 `createModuleLogger("track")`,删除 no-op 输出 |
| `src/modules/rbac/lib/track.ts` | 删除 | 引用方改为从 `@/shared/lib/track-event` 导入 |
| `src/modules/course-plans/lib/track-event.ts` | 删除 | 同上 |
| `src/modules/questions/utils/track-event.ts` | 删除 | 同上 |
| 88 处 `console.*` 调用点 | 修改 | 替换为 `logger.*``createModuleLogger(module)` |
| 130 个 `error.tsx` | 修改 | 在 useEffect 中调用 `useErrorReport(error)` |
| `next.config.ts` | 修改 | 在现有 `serverExternalPackages` 数组中添加 `"pino"`(当前已有 `mysql2`/`tencentcloud-sdk-nodejs`/`exceljs` |
| `.eslintrc` / `eslint.config.mjs` | 修改 | 新增 `no-console` 规则,仅允许 `logger.ts` 中使用 console |
---
## 4. 核心模块设计
### 4.1 `src/shared/lib/logger.ts`
```ts
import pino, { type Logger } from "pino"
import { env } from "@/env.mjs"
import { getRequestContext } from "./request-context"
/**
* 全局 logger 实例。
*
* - 生产环境JSON 输出到 stdoutdocker logs 友好)
* - 开发环境pino-pretty 彩色文本
* - 自动从 AsyncLocalStorage 混入 requestId / userId若存在
*/
export const logger = pino({
level: env.LOG_LEVEL,
base: { service: "cicd-app" },
formatters: {
level: (label) => ({ level: label }),
},
mixin: () => getRequestContext(),
...(env.NODE_ENV === "development" && {
transport: {
target: "pino-pretty",
options: { colorize: true, translateTime: "SYS:standard" },
},
}),
})
/**
* 创建模块级子 logger自动绑定 module 字段。
*
* @example
* ```ts
* const log = createModuleLogger("audit")
* log.info({ userId }, "User action logged")
* // 输出: {"level":"info","module":"audit","msg":"User action logged", ...}
* ```
*/
export function createModuleLogger(module: string): Logger {
return logger.child({ module })
}
export type { Logger }
```
### 4.2 `src/shared/lib/request-context.ts`
```ts
import { AsyncLocalStorage } from "node:async_hooks"
/**
* 请求上下文,贯穿整个请求生命周期。
*
* 仅在 Node.js Runtime 中可用proxy.ts 是 Edge Runtime不导入此模块
* 通过 withRequestContext 高阶函数注入。
*/
export interface RequestContext {
requestId: string
userId?: string
module?: string
}
export const requestContextStorage = new AsyncLocalStorage<RequestContext>()
/**
* 获取当前请求上下文(若存在)。
*
* - 在 withRequestContext 包装的调用栈内:返回完整上下文
* - 在调用栈外(如顶层模块初始化、定时任务):返回空对象
*
* pino logger 的 mixin 配置会自动调用此函数混入 requestId。
*/
export function getRequestContext(): Partial<RequestContext> {
return requestContextStorage.getStore() ?? {}
}
```
### 4.3 `src/shared/lib/with-request-context.ts`
```ts
import { headers } from "next/headers"
import { randomUUID } from "node:crypto"
import { requestContextStorage, type RequestContext } from "./request-context"
/**
* 包装 Server Action / Route Handler注入 requestId 到 AsyncLocalStorage。
*
* 工作流程:
* 1. 通过 `headers()` 读取 proxy.ts 注入的 `x-request-id`
* 2. 若请求头无此字段(如直接调用的内部函数),生成新 UUID
* 3. 通过 `requestContextStorage.run()` 注入到 AsyncLocalStorage
* 4. 在调用栈内的所有 logger 调用自动获得 requestId
*
* @example
* ```ts
* export const createUserAction = withRequestContext(
* async (state: ActionState<User>, input: CreateUserInput) => {
* // 此处 logger.info 会自动带 requestId
* return handleAction(...)
* }
* )
* ```
*/
export function withRequestContext<TArgs extends unknown[], TResult>(
fn: (...args: TArgs) => Promise<TResult>
): (...args: TArgs) => Promise<TResult> {
return async (...args: TArgs) => {
const headersList = await headers()
const requestId =
headersList.get("x-request-id") ?? randomUUID()
const ctx: RequestContext = { requestId }
return requestContextStorage.run(ctx, () => fn(...args))
}
}
```
### 4.4 `src/proxy.ts` 改造
```ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
import { getToken } from "next-auth/jwt"
// ... 原有 imports
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl
// 生成或复用 requestIdWeb Crypto APIEdge 兼容)
const requestId =
request.headers.get("x-request-id") ?? crypto.randomUUID()
// 跳过静态资源和登录页
if (
pathname.startsWith("/_next") ||
pathname.startsWith("/api/auth") ||
pathname === "/login" ||
pathname === "/register" ||
pathname === "/favicon.ico"
) {
return NextResponse.next({
request: { headers: injectRequestId(request, requestId) },
})
}
// ... 原有 token / onboarding / 权限检查逻辑
// 所有 NextResponse.next() / NextResponse.redirect() 调用保留,
// 但 NextResponse.next() 调用统一传入 request.headers
const response = NextResponse.next({
request: { headers: injectRequestId(request, requestId) },
})
response.headers.set("x-request-id", requestId)
return response
}
/**
* 创建包含 x-request-id 的新 Headers 对象。
* 通过 NextResponse.next({ request: { headers } }) 注入到下游 RSC 请求。
*/
function injectRequestId(request: NextRequest, requestId: string): Headers {
const headers = new Headers(request.headers)
headers.set("x-request-id", requestId)
return headers
}
```
> **说明**proxy.ts 不导入 `request-context.ts`,避免在 Edge Runtime 中加载 `node:async_hooks` 导致构建错误。
### 4.5 `src/shared/hooks/use-error-report.ts`
```ts
"use client"
import { useEffect } from "react"
interface ClientErrorPayload {
message: string
stack?: string
digest?: string
url: string
userAgent: string
timestamp: string
}
/**
* 客户端错误上报 Hook。
*
* 用于 error.tsx 接收 error prop 后上报到 /api/client-error。
*
* 节流策略:
* - 同一 digest或 message在 sessionStorage 中标记,避免 React 重渲染或快速刷新时多次上报
* - 上报失败时静默忽略,避免无限循环
*/
export function useErrorReport(error: Error & { digest?: string }): void {
useEffect(() => {
if (!error) return
const digest = error.digest ?? error.message
const storageKey = `error-reported:${digest}`
if (sessionStorage.getItem(storageKey)) return
sessionStorage.setItem(storageKey, "1")
const payload: ClientErrorPayload = {
message: error.message,
stack: error.stack,
digest: error.digest,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
}
fetch("/api/client-error", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
keepalive: true, // 即使页面卸载也尝试发送
}).catch(() => {
// 上报失败时不再上报,避免无限循环
})
}, [error])
}
```
### 4.6 `src/app/api/client-error/route.ts`
```ts
import { NextResponse } from "next/server"
import { createModuleLogger } from "@/shared/lib/logger"
import { withRequestContext } from "@/shared/lib/with-request-context"
const log = createModuleLogger("client-error")
interface ClientErrorPayload {
message: string
stack?: string
digest?: string
url: string
userAgent: string
timestamp: string
}
/**
* 接收客户端 error.tsx 上报的错误。
*
* 注意:客户端错误的 requestId 与原始请求不同(来自 /api/client-error 的 HTTP 请求),
* 但 digest 字段可用于关联原始错误。
*/
export const POST = withRequestContext(async (request: Request) => {
try {
const body = (await request.json()) as ClientErrorPayload
log.error(
{
clientMessage: body.message,
stack: body.stack,
digest: body.digest,
url: body.url,
userAgent: body.userAgent,
clientTimestamp: body.timestamp,
},
"Client error reported"
)
return NextResponse.json({ ok: true })
} catch (error) {
log.error({ err: error }, "Failed to parse client error payload")
return NextResponse.json({ ok: false }, { status: 400 })
}
})
```
### 4.7 `src/shared/lib/audit-logger.ts` 改造(静默失败 → 告警)
```ts
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("audit-logger")
export async function logAudit(params: LogAuditParams): Promise<void> {
try {
// ... 原有写入逻辑
} catch (error) {
// 旧catch { /* Silently fail */ }
// 新:记录到 logger运维可感知
log.warn(
{ err: error, action: params.action, module: params.module },
"Audit log write failed"
)
}
}
```
### 4.8 `src/shared/lib/action-utils.ts` 改造
```ts
import { createModuleLogger } from "@/shared/lib/logger"
import type { ActionState } from "@/shared/types/action-state"
import { PermissionDeniedError } from "@/shared/lib/errors"
const log = createModuleLogger("action")
export function handleActionError(e: unknown): ActionState<never> {
if (e instanceof PermissionDeniedError) {
return { success: false, message: e.message }
}
if (e instanceof BusinessError) {
return { success: false, message: e.message, errorCode: e.code }
}
if (e instanceof Error) {
// 旧console.error("[ActionError]", e.name, e.message, e.stack)
log.error({ err: e }, "Action failed")
return { success: false, message: "操作失败,请稍后重试", errorCode: "unexpected" }
}
log.error({ err: e }, "Unknown action error")
return { success: false, message: "操作失败,请稍后重试", errorCode: "unexpected" }
}
export async function safeActionCall<T>(
action: () => Promise<ActionState<T>>,
options?: {
onError?: (error: unknown) => void
onFinally?: () => void
}
): Promise<ActionState<T> | null> {
try {
return await action()
} catch (e) {
options?.onError?.(e)
// 旧console.error("[SafeActionCall]", e)
log.error({ err: e }, "Safe action call threw")
return null
} finally {
options?.onFinally?.()
}
}
```
### 4.9 `src/shared/lib/track-event.ts` 改造
```ts
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("track")
/**
* 业务埋点接口。
*
* 不再是 no-op stub通过 logger.info 输出结构化事件,
* 后续可扩展为接入外部 analytics 服务。
*/
export function trackEvent(
name: string,
props?: Record<string, unknown>
): void {
log.info({ event: name, ...props }, "track event")
}
export function trackExamEvent(
name: string,
props?: Record<string, unknown>
): void {
trackEvent(`exam.${name}`, props)
}
export function trackAuthEvent(
name: string,
props?: Record<string, unknown>
): void {
trackEvent(`auth.${name}`, props)
}
```
### 4.10 `src/env.mjs` 改造
```ts
// 在 server schema 中添加:
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
```
### 4.11 ESLint 规则
```js
// eslint.config.mjs
{
rules: {
"no-console": ["error", { allow: [], allowWithImplicit: false }]
},
// logger.ts 豁免pino 内部需用 console
overrides: [
{
files: ["src/shared/lib/logger.ts"],
rules: { "no-console": "off" }
}
]
}
```
---
## 5. 改造范围与策略
### 5.1 console.* 替换映射
| 类型 | 数量 | 替换为 | 调用方式 |
|------|------|--------|----------|
| `console.log` | 2 | `log.debug` | `const log = createModuleLogger("questions")` |
| `console.error` | 67 | `log.error` | 按文件所属模块创建子 logger |
| `console.warn` | 5 | `log.warn` | 同上 |
| `console.info` | 10 | `log.info` | 同上 |
| `console.debug` | 4 | `log.debug` | 同上 |
### 5.2 模块前缀规范化
当前散乱前缀 → 统一通过 `module` 字段:
| 当前前缀 | 新 module 字段 |
|---------|----------------|
| `[ExamAction]` | `exams` |
| `[ActionError]` | `action` |
| `[ApiError]` | `api` |
| `[AuditLogger]` | `audit-logger` |
| `[SafeActionCall]` | `action` |
| `[Files]` / `[files]` | `files` |
| 无前缀 | 按文件所属模块 |
### 5.3 Server Action 包装策略
由于 Server Action 通过 `"use server"` 自动成为 RPC不能简单用 `withRequestContext` 包装导出函数(会丢失 Next.js 类型推断)。且 `handleActionError` 是同步函数,无法 `await headers()` 读取请求头。
**最终策略**:在每个 Server Action 模块的入口点调用 `withRequestContext` 包装:
```ts
// src/modules/audit/actions.ts
"use server"
import { withRequestContext } from "@/shared/lib/with-request-context"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("audit")
export const createAuditLogAction = withRequestContext(
async (state: ActionState<void>, input: CreateAuditInput) => {
// 此处 logger 自动带 requestId
log.info({ input }, "Creating audit log")
return handleAction(...)
}
)
```
**data-access 层无需显式包装**:因为 data-access 总是从 Server Action 调用AsyncLocalStorage 上下文会自动贯穿到调用栈下游。data-access 中的 logger 调用会自动获得 requestId。
**实施时验证项**
- Next.js 16 是否允许高阶函数包装 Server Action保留 NextServerAction 标记)
- 若不允许,回退方案:在每个 Server Action 函数体首行调用 `await initRequestContext()`,该函数内部读取 headers 并写入 AsyncLocalStorage
### 5.4 error.tsx 改造模式
130 个 error.tsx 统一改为:
```tsx
"use client"
import { useEffect } from "react"
import { useErrorReport } from "@/shared/hooks/use-error-report"
// ... 原有 imports
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useErrorReport(error)
// ... 原有 UI 渲染
}
```
每个 error.tsx 仅增加 2 行import + 调用 Hook
---
## 6. 测试策略
### 6.1 单元测试
| 测试文件 | 覆盖内容 |
|---------|---------|
| `__tests__/logger.test.ts` | level 控制、JSON 输出格式、mixin 注入 requestId、createModuleLogger |
| `__tests__/request-context.test.ts` | AsyncLocalStorage 读写、嵌套调用、空上下文 |
| `__tests__/use-error-report.test.tsx` | 节流sessionStorage 标记、fetch 调用、错误处理 |
### 6.2 集成测试
- 启动开发服务器,发起请求,验证日志包含 requestId
- 触发 Server Action 错误,验证日志包含 module + requestId + error stack
- 触发 client error.tsx验证 `/api/client-error` 收到请求并记录日志
### 6.3 回归验证
- `npx tsc --noEmit` 零错误
- `npm run lint` 零错误(含新增 `no-console` 规则)
- 现有 vitest 测试套件全部通过
---
## 7. 风险与权衡
| 风险 | 影响 | 缓解 |
|------|------|------|
| pino 在 Next.js bundling 中可能有问题 | 高 | 已配置 `serverExternalPackages`pino 仅服务端导入;开发期 `pino-pretty` 通过 transport 配置启用 |
| `AsyncLocalStorage` 在 Server Action 中可能不工作 | 中 | Server Action 运行在 Node.js runtime完全支持实施时先做最小验证 |
| Server Action 高阶函数包装可能丢失 Next.js 类型 | 中 | 实施时验证,必要时回退为显式 `headers()` 调用 |
| 88 处 console.* 替换可能遗漏 | 低 | 通过 ESLint `no-console` 规则强制,仅允许 `logger.ts` 中使用 |
| 130 个 error.tsx 改造量大 | 中 | 提取公共 `useErrorReport` Hook每个 error.tsx 仅加 2 行 |
| track-event 合并可能破坏调用方 | 低 | 删除前 grep 所有引用,统一改为从 `@/shared/lib/track-event` 导入 |
| 客户端错误风暴(无限循环上报) | 中 | sessionStorage 节流 + fetch 失败静默 + `keepalive` 选项 |
| proxy.ts 改造可能影响 Edge Runtime 构建 | 中 | 不导入任何 Node.js 模块,仅用 Web Crypto API |
---
## 8. 架构图同步(强制)
按项目规则"改码必同步图",重构后必须更新:
### 8.1 `docs/architecture/004_architecture_impact_map.md`
新增章节:
- `shared/lib/logger.ts` — pino 实例 + createModuleLogger
- `shared/lib/request-context.ts` — AsyncLocalStorage 请求上下文
- `shared/lib/with-request-context.ts` — Server Action / Route Handler 入口包装
- `shared/hooks/use-error-report.ts` — 客户端错误上报 Hook
- `app/api/client-error/route.ts` — 客户端错误接收端点
修改章节:
- `proxy.ts` — 增加 requestId 注入逻辑
- `shared/lib/audit-logger.ts` / `change-logger.ts` / `login-logger.ts` — 静默失败改为 logger.warn
- `shared/lib/track-event.ts` — 实现层从 no-op 改为 logger.info
- `shared/lib/action-utils.ts` / `api-response.ts` — 接入 logger
删除记录:
- `modules/rbac/lib/track.ts`
- `modules/course-plans/lib/track-event.ts`
- `modules/questions/utils/track-event.ts`
### 8.2 `docs/architecture/005_architecture_data.json`
- `modules.shared.lib.exports` 新增 `logger` / `createModuleLogger` / `requestContextStorage` / `getRequestContext` / `withRequestContext`
- `modules.shared.hooks.exports` 新增 `useErrorReport`
- `modules.app.api.client-error.exports` 新增 `POST`
- `dependencyMatrix` 更新proxy.ts → request-context仅注入请求头不导入
- 删除 `modules.rbac.lib.track``modules.course-plans.lib.track-event``modules.questions.utils.track-event`
### 8.3 `docs/troubleshooting/known-issues.md`
新增规则条目:
- pino 集成:`serverExternalPackages` 配置 + `pino-pretty` 仅 dev
- Edge Runtime 限制proxy.ts 不能导入 `node:async_hooks`
- Server Action 包装:`withRequestContext` 高阶函数使用模式
- error.tsx 错误上报:`useErrorReport` Hook + 节流策略
- ESLint `no-console` 规则与豁免
---
## 9. 实施顺序建议
1. **Phase 1基础底座**
- 装 pino / pino-pretty
- 创建 `logger.ts` / `request-context.ts` / `with-request-context.ts`
-`env.mjs` 添加 LOG_LEVEL
- 单元测试
2. **Phase 2核心接入**
- proxy.ts 注入 requestId
- `action-utils.ts` / `api-response.ts` 接入 logger
- 三个 audit-logger 静默失败 → logger.warn
- track-event 去重合并
3. **Phase 3批量替换**
- 88 处 console.* 替换为 logger.*
- 前缀规范化
- ESLint `no-console` 规则启用
4. **Phase 4error.tsx 上报**
- 创建 `use-error-report` Hook
- 创建 `/api/client-error` Route Handler
- 130 个 error.tsx 接入
5. **Phase 5架构同步**
- 更新 004 / 005 / known-issues.md
- 验证 tsc / lint / 测试通过