---
name: prod-error-triage
description: >
  当用户报告生产/UAT 环境出现错误页面、白屏、"Application error"、接口 500、
  或贴来浏览器截图/报错信息时使用。按"先查日志、再定位代码"的顺序排查，
  优先通过 SSH 读取生产日志而不是凭空猜测。
  触发短语：生产报错、线上报错、UAT 报错、Application error、客户端异常、
  白屏、500、接口报错、production error、prod error triage。
  不触发：本地开发环境报错（直接看终端/浏览器即可）、部署失败（用 troubleshoot / deploy-ops）。
---

# 生产错误分诊技能

## 核心原则

**不要凭压缩后的错误信息猜代码。** 先上生产机读日志，拿到原始堆栈或上下文后再动手。
用户贴的 "Application error: a client-side exception has occurred" 这类文案是 Next.js
的兜底，本身没信息量。

## SSR vs CSR 的判定

Next.js 的错误分两类，日志定位方式完全不同：

| 特征 | 判定 | 日志位置 |
|------|------|---------|
| 页面返回 500 / 服务端渲染阶段崩 | **SSR 错** | 生产机 frontend 容器/PM2 stdout |
| 页面能渲染出框架，交互后/hydrate 后崩 | **CSR 错** | 浏览器 console（服务端日志通常没有） |
| "Application error: a client-side exception has occurred" | 多数是 CSR hydrate 失败 | 需要 source map 还原 |
| API 4xx/5xx | **后端错** | 生产机 backend 容器/PM2 stdout |

## 排查流程

### 第 1 步：向用户确认最少信息

- 出错 URL / 页面
- 大致发生时间（用于日志时间窗 grep）
- 是打开就报错还是操作后报错
- 如果能贴浏览器 console 里压缩堆栈的前几行，一起要过来

### 第 2 步：SSH 到生产机读日志

服务器与路径参考 `docs/ops/01-server-infrastructure.md` 与 `deploy-ops` skill。

```bash
# 进入服务器后
pm2 status                                   # 确认哪些进程在跑
pm2 logs <frontend-process> --lines 200 --nostream   # 前端 (SSR) 日志
pm2 logs <backend-process>  --lines 200 --nostream   # 后端日志
docker ps                                    # 如果是容器化部署
docker logs --since 15m <frontend-container> 2>&1 | tail -200
docker logs --since 15m <backend-container>  2>&1 | tail -200
```

按时间窗过滤：

```bash
pm2 logs <proc> --lines 2000 --nostream | grep -E "2026-04-24T0[89]|Error|TypeError|ReferenceError"
```

### 第 3 步：按判定分支走

**若是 SSR 错（日志里有完整 stack）**
- 直接按栈定位到源码，最小修改。
- 注意生产走的是构建产物，本地改完要重部署才能验证。

**若是后端错**
- 看 backend 日志里的请求路径 / Prisma 报错 / 参数。
- 在对应 controller / service 修复，按 `backend-main` 规范补测试。

**若是 CSR 错且服务端日志没东西**
1. 向用户要浏览器 console 里完整压缩栈 + chunk 文件名。
2. 首选：在生产构建临时打开 source map：
   - `next.config.js` 设 `productionBrowserSourceMaps: true`，部署一次拿明牌堆栈。
   - 验证后务必回退配置（源码地图对外暴露有安全影响）。
3. 次选：在本地用相同 commit `npm run build && npm run start`，复现同一交互路径。
4. 常见根因清单（按命中频率）：
   - hydrate 不一致：服务端/客户端 render 结果不同（`Date.now()` / `Math.random()` / `typeof window` 分支）
   - 访问 `window` / `document` / `localStorage` 却没包在 `useEffect` / 客户端守卫里
   - 返回数据结构与 TS interface 不一致（后端新字段漏了、列表接口返回 `null` 未兜底）
   - i18n key 缺失导致 throw
   - 第三方组件 SSR 不兼容，未用 `next/dynamic({ ssr: false })`

### 第 4 步：修复与验证

- 最小改动原则，先把根因修掉，再考虑边界加固。
- 前后端契约变动走 L0 契约校验（见 CLAUDE.md 测试规则）。
- 按 `deploy-ops` skill 重新部署到 UAT → 生产。
- 验证：浏览器实打实点一遍原复现路径；同时盯住 `pm2 logs --err` 15 分钟确认无回归。

## 快查命令卡

```bash
# 找最近 15 分钟的错误
docker logs --since 15m <container> 2>&1 | grep -iE "error|exception|unhandled" | tail -50

# 把时间戳转成本地时间比对
pm2 logs --lines 500 --nostream --timestamp

# 检查 Next.js 构建产物的 chunk 是否存在（防止 CDN 缓存旧引用）
ls -la /srv/apps/ffoa/frontend/.next/static/chunks/ | grep <chunk-hash>
```

## 升级策略

| 级别 | 场景 | 行动 |
|------|------|------|
| L1 自行解决 | 日志里拿到明栈且根因清晰 | 修一个小 PR 走正常流程 |
| L2 需要协助 | 日志无信息且无法在本地复现 | 拉用户一起看浏览器 + 考虑开 source map 或接 Sentry |
| L3 紧急 | 全站白屏影响可用性 | 先按 `deploy-ops` 回滚到上一个已知好版本，再慢慢查根因 |

## 报告问题时务必提供

- 出错 URL、时间、用户角色
- 浏览器 console 原始堆栈（哪怕是压缩的）
- 生产日志 grep 到的相关片段
- 最近一次部署的 commit hash

## 与其他 skill 的关系

- **troubleshoot**：侧重服务不可用/502/磁盘/DB 连接等基础设施层；本 skill 侧重应用代码层的报错。
- **deploy-ops**：回滚、重新部署走那边。
- **code-review**：定位到 PR 引入的回归时，可交叉检查 PR 改动。
