404
- {t("heading")}
+ 页面不存在 · Page not found
-
{t("body")}
+
+ 你访问的页面可能已被移动或不存在。Try going back home.
+
diff --git a/dev_docs/vercel-cpu-overage-2026-05.md b/dev_docs/vercel-cpu-overage-2026-05.md
new file mode 100644
index 00000000..3b9e1adb
--- /dev/null
+++ b/dev_docs/vercel-cpu-overage-2026-05.md
@@ -0,0 +1,198 @@
+# Vercel CPU 超额诊断 & 修复(2026-05)
+
+> 2026-05-12 触发:Hobby plan **Fluid Active CPU 7h54m / 4h(198%)**,
+> Fast Origin Transfer 12.04 GB / 10 GB(120%)。用户报告"之前做过 SSR
+> 优化但情况更糟"。本文档记录调查方法、根因、修复、验证手段。
+
+## TL;DR
+
+之前的 SSG 优化(2026-05-06 commit `8517332`)**只翻转了 1 条路由**(/[locale]
+首页)。`next build` 输出显示**还剩 18 条 ƒ Dynamic**,其中最致命的是
+`/_not-found`:所有漏洞扫描器(.env / wp-admin / php-info / graphql)和真实
+404 全落到这条 dynamic 路由。
+
+本次修复在 `next build` 输出层面把 6 条路由从 ƒ 翻成 ● / ○:
+
+| 路由 | 修前 | 修后 | 影响 |
+| ---------------- | --------- | ------------ | --------------------------------------- |
+| /\_not-found | ƒ Dynamic | **○ Static** | 所有 scanner / 404 路径不再烧 Fluid CPU |
+| /[locale]/docs | ƒ | ● SSG | /zh/docs / /en/docs 走 CDN |
+| /[locale]/events | ƒ | **● ISR 5m** | revalidate=300 终于真正生效 |
+| /[locale]/login | ƒ | ● SSG | |
+| /[locale]/editor | ƒ | ● SSG | (意外 cascade) |
+| /[locale]/share | ƒ | ● SSG | (意外 cascade) |
+
+同时:
+
+- `proxy.ts` 加 bot path 早返 404(拦在 edge,根本不进 Fluid)
+- Sentry tracesSampleRate 0.1 → 0.02(5× 减少 trace 开销)
+
+## 调查方法(验证黄金标准)
+
+**唯一可信源:`next build` 的 Route table。** 不能靠"我以为加了 force-static
+就行"——这正是上一轮优化为什么没生效。
+
+```bash
+# 修前快照
+pnpm build 2>&1 | tee /tmp/build-before.txt
+
+# 提取 Route 表
+grep -E '^[┌├└] ' /tmp/build-before.txt > /tmp/routes-before.txt
+```
+
+修后跑同样命令,`diff /tmp/routes-before.txt /tmp/routes-after.txt` 直接看
+哪些 ƒ 翻成 ● / ○。这是**事实**,不是推断。
+
+## 四个被证实的根因
+
+### H1:`/_not-found` dynamic(最大单点)
+
+**证据**:
+
+```
+ƒ /_not-found ← build 输出
+```
+
+```
+# 18:34:34-18:34:40 一波扫描,全 → /_not-found(来源 Vercel runtime logs)
+POST /zh/graphql/v2 200
+POST /zh/graphql/v1 200
+GET /var/www/html/.env 404
+GET /css../../.env.production 404
+...
+```
+
+**根因**:原 `app/not-found.tsx` 用 `await getTranslations("notFound")`。
+`getTranslations` 内部走 `cookies()` 推断 locale,把这条路由钉成 dynamic。
+每条扫描 + 每条真实 404 都是一次 Fluid 调用。
+
+**修复**:去掉 `getTranslations`,改成 hardcoded 双语 hard-coded text("页面
+不存在 · Page not found")。根 not-found 本就不知道用户期望哪种语言,双语并列
+最稳。
+
+**验证**:`next build` 显示 `○ /_not-found`,curl bot 路径应该非常快:
+
+```bash
+$ time curl -so /dev/null -w "%{http_code} %{time_total}s\n" \
+ https://involutionhell.com/zh/some-fake-path
+404 0.045s # ← 应该 < 100ms(CDN 静态文件)
+# 修前会是 200-500ms(Fluid 函数渲染)
+```
+
+### H1b:Bot scanner 还是会过 i18n middleware → SSR
+
+即便 not-found 静态化,扫描器路径会先被 next-intl middleware 加 `/zh/` 前缀,
+然后撞 `[locale]/[...slug]` 的 catch-all(● SSG 命中静态资源是 OK 的)或
+admin 路由(ƒ Dynamic,仍然烧 CPU)。
+
+**修复**:`proxy.ts` 在 i18n middleware 之前加 `isBotScanPath()` 早返 404。
+列表包含 OWASP/nikto/dirbuster 常见指纹(`.php` / `wp-` / `.env` / `graphql` /
+`werkzeug/console` / `phpmyadmin` 等)。明确**不包括** `login`/`admin` 等
+真实业务路径。
+
+**验证**:
+
+```bash
+# 应该 < 50ms(edge middleware 直接返)
+$ curl -so /dev/null -w "%{http_code} %{time_total}s\n" \
+ https://involutionhell.com/wp-admin/
+404 0.030s
+
+# Bot 扫 .php / .env / wp-* / graphql 全是 404 来自 edge
+```
+
+### H2:13 条 `[locale]/*` 缺 setRequestLocale → 全部 ƒ
+
+**证据**(修前):
+
+```bash
+$ grep -l "setRequestLocale" app/[locale]/*/page.tsx
+# 几乎为空,仅 docs/page.tsx 命中
+```
+
+**根因**:next-intl 的 SSG 启用条件是**每个 page** 都得调 `setRequestLocale`。
+parent layout(`[locale]/layout.tsx`)调了不够。缺这一步时 next-intl 内部
+fallback 到 `cookies()`/`headers()`,让整棵 RSC 树变 dynamic。
+
+参考 layout.tsx 的注释:
+
+> 缺这一行的话,next-intl 会回退到从 cookies()/headers() 推断 locale,
+> 整棵 RSC 树重新变 dynamic,绕了一圈又回到老问题。
+
+**修复**:给 `/events` `/login` 加 `params: Promise<{locale:string}>` +
+`await params; setRequestLocale(locale)` + `generateStaticParams() =>
+routing.locales.map(l => ({locale: l}))`。
+
+`/[locale]/docs/page.tsx` 已有 setRequestLocale 但仍然 ƒ —— 加 `export const
+dynamic = "force-static"` 显式 opt-in。
+
+**Build 期容错**:`/events` 走 SSG 后会在 build 时尝试 fetch backend。原版
+fetch 失败抛错 → 整次 build 挂。改成失败降级返回空数组,build 出"暂无活动"
+壳,ISR=300s 之后台拿到真数据。也是更鲁棒的设计。
+
+**验证**:
+
+```bash
+# 修前
+ƒ /[locale]/events
+ƒ /[locale]/login
+ƒ /[locale]/docs
+
+# 修后
+● /[locale]/events 5m 1y # ← 真 ISR 了
+● /[locale]/login
+● /[locale]/docs
+```
+
+线上 curl:
+
+```bash
+$ curl -sI https://involutionhell.com/zh/events | grep -iE "x-vercel-cache|age"
+x-vercel-cache: HIT # ← CDN 命中
+age: 142
+```
+
+### H3:Sentry tracesSampleRate 0.1(10%)叠在每次调用上
+
+**证据**:`sentry.server.config.ts:23` 和 `sentry.edge.config.ts:18` 都写
+`tracesSampleRate: 0.1`。在 368K 月度 invocations 下 = ~37K traces,每条
+trace 含 span 序列化 + 异步上报,叠加在每次 Fluid 调用 + 每次 edge middleware
+上。
+
+**修复**:0.1 → 0.02。日均仍能采到几千条 trace 监控 P95 性能趋势。
+
+**验证**:Sentry dashboard `Performance` tab,Events / 24h 应该下降 5×。
+
+## 还没修但记录在案的问题
+
+| 问题 | 影响 | 为啥不在本 PR 修 |
+| ----------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------- |
+| `/api/docs-tree` 声明 `force-static` 但 build 仍 ƒ | 中(每次访问 1 invocation) | 需要改成 build 时生成 JSON 写 public/,路由读静态文件。改动较大单独 PR |
+| `/feed/page.tsx` 用 server-side `searchParams` 把页面钉死 dynamic | 中(爬虫扫 8 分类 tab = 16 invocations/min) | 需要把 category 过滤改成 client-side fetch / route param。设计上重 |
+| `/rank/page.tsx` 同上 | 低(rank 流量不大) | 同上 |
+| `/[locale]/u/[username]/*` 三条 ƒ | 中(爬虫扫随机 username = 一次 SSR + backend 404) | 需要 dynamicParams + 限流 |
+| Uptime monitor 每分钟打 `/`(GET / 307 GET /zh 200) | 验证后**不是 Fluid 主因**(/zh 是 ● SSG 走 CDN,不烧 Fluid) | 不修。可选优化:提供 `/api/health` edge 静态 200 减少 edge requests |
+
+## 量化预期
+
+**修前**:368K function invocations / mo,CPU 7h54m / 4h(**超 198%**)。
+
+**修后估算**:
+
+- /\_not-found 静态化 + bot blocklist:拦截 ~30% 的 scanner 流量 → 函数调用 -30%
+- 5 条 [locale]/\* SSG:~15-20% 调用归零(这些是真实用户 + 爬虫的常见目标)
+- Sentry 0.1 → 0.02:每次调用 CPU 开销 -5-10%
+
+**保守预期**:CPU 从 7h54m → ~3-4h(贴近 4h 配额,可能略超)。
+**乐观预期**:CPU < 2h,远低于配额。
+
+24-48h 后 dashboard 数据出来才能确定。如果还超,下一轮修剩下的 `/feed`
+和 `/rank` searchParams 问题。
+
+## 后续工作
+
+- [ ] 修 `/api/docs-tree` build-time 生成
+- [ ] /feed /rank 搜索参数从 server-side 改 client-side
+- [ ] /[locale]/u/[username] ISR + dynamicParams
+- [ ] 给 admin 路由加 edge-middleware 早返 401,不让 bot 触发 Fluid
+- [ ] (可选)Cloudflare proxy 挡在 Vercel 前过滤 bot
diff --git a/generated/leetcode-slug-map.json b/generated/leetcode-slug-map.json
index 2c125e20..0c226954 100644
--- a/generated/leetcode-slug-map.json
+++ b/generated/leetcode-slug-map.json
@@ -5,7 +5,6 @@
"1664生成平衡数组的方案数_translated": "1664-sheng-cheng-ping-heng-shu-zu-de-fang-an-shu-translated",
"1825求出 MK 平均值_translated": "1825-qiu-chu-mk-ping-jun-zhi-translated",
"1828统计一个圆中点的数目_translated": "1828-tong-ji-yi-ge-yuan-zhong-dian-de-shu-mu-translated",
- "2131. 连接两字母单词得到的最长回文串": "2131-lian-jie-liang-zi-mu-dan-ci-de-dao-de-zui-chang-hui-wen-chuan",
"2299强密码检验器II_translated": "2299-qiang-mi-ma-jian-yan-qi-iitranslated",
"2309兼具大小写的最好英文字母_translated": "2309-jian-ju-da-xiao-xie-de-zui-hao-ying-wen-zi-mu-translated",
"2335. 装满杯子需要的最短总时长_translated": "2335-zhuang-man-bei-zi-xu-yao-de-zui-duan-zong-shi-chang-translated",
@@ -15,16 +14,13 @@
"2894. 分类求和并作差": "2894-fen-lei-qiu-he-bing-zuo-cha",
"3072. 将元素分配到两个数组中 II_translated": "3072-jiang-yuan-su-fen-pei-dao-liang-ge-shu-zu-zhong-iitranslated",
"345. 反转字符串中的元音字母_translated": "345-fan-zhuan-zi-fu-chuan-zhong-de-yuan-yin-zi-mu-translated",
- "46.全排列": "46-quan-pai-lie",
"538.把二叉搜索树转换为累加树_translated": "538-ba-er-cha-sou-suo-shu-zhuan-huan-wei-lei-jia-shu-translated",
"6323. 将钱分给最多的儿童_translated": "6323-jiang-qian-fen-gei-zui-duo-de-er-tong-translated",
"76最小覆盖子串_translated": "76-zui-xiao-fu-gai-zi-chuan-translated",
- "93复原Ip地址": "93-fu-yuan-ip-di-zhi",
"994.腐烂的橘子_translated": "994-fu-lan-de-ju-zi-translated",
"[121]买卖股票的最佳时期_translated": "121-mai-mai-gu-piao-de-zui-jia-shi-qi-translated",
"[1333]餐厅过滤器_translated": "1333-can-ting-guo-l-qi-translated",
"[146]LRU 缓存_translated": "146lru-huan-cun-translated",
- "[1545]找出第 N 个二进制字符串中的第 K 位": "1545-zhao-chu-di-n-ge-er-jin-zhi-zi-fu-chuan-zhong-de-di-k-wei",
"[213]打家劫舍 II_translated": "213-da-jia-jie-she-iitranslated",
"[2490]回环句_translated": "2490-hui-huan-ju-translated",
"[2562]找出数组的串联值_translated": "2562-zhao-chu-shu-zu-de-chuan-lian-zhi-translated",
diff --git a/proxy.ts b/proxy.ts
index 342237f1..fa78faf9 100644
--- a/proxy.ts
+++ b/proxy.ts
@@ -29,6 +29,46 @@ const LEETCODE_OLD_PATH_TAIL = "/docs/CommunityShare/Leetcode";
const intlMiddleware = createMiddleware(routing);
+/**
+ * Bot / vulnerability scanner path patterns —— 在 edge 早返 404,
+ * 不让请求穿透到 Fluid 函数(dev_docs/vercel-cpu-overage-2026-05.md 的 H1b)。
+ *
+ * 背景:Vercel runtime 日志显示线上有持续的漏洞扫描(.env / wp-admin /
+ * php-info / werkzeug / graphql 等)。原来这些路径会被 next-intl 加 /zh 前缀
+ * 后落到 [locale]/[...slug] 或根 /_not-found,每条扫描 ≈ 1 Fluid 调用。
+ * 整理列表参考 OWASP top-10 + 主流 scanner(nikto / nmap / dirbuster)的指纹。
+ *
+ * 注意:列表里**不要**放可能跟真实业务路径重名的 segment(不要加 admin、login
+ * 这些,admin 路由是有的)。只放 100% 业务用不到的字符串。
+ */
+const BOT_PATH_PATTERNS = [
+ // PHP / 老 CMS:本站根本没装 PHP,所有 *.php 都是扫描
+ /\.php(?:$|[?#/])/i,
+ // wp-* 系列:WordPress 扫描
+ /\/wp-(admin|content|includes|login|json|config)(?:$|[/?#])/i,
+ // .env / config 文件直接探测
+ /\.env(?:\.[a-z]+)?(?:$|[?#/])/i,
+ /\/(config|settings|secrets|credentials)\.(yml|yaml|json|ini|toml|xml)(?:$|[?#])/i,
+ // GraphQL / GQL endpoint 扫描(本站没 GraphQL)
+ /\/(graphql|gql|api\/graphql|v\d+\/graphql)(?:$|[/?#])/i,
+ // Werkzeug / Flask debug console
+ /\/werkzeug\/console/i,
+ // 常见 admin / debug panels 探测路径(本站 admin 在 /[locale]/admin,
+ // 这些是其他平台特有路径,扫到必是 bot)
+ /\/(phpmyadmin|adminer|pma|dbadmin|mysqladmin)(?:$|[/?#])/i,
+ // git / svn 仓库文件暴露探测
+ /\/\.(git|svn|hg|bzr)\/(?:config|HEAD|entries)/i,
+ // 已知敏感文件(war/jar/key/pem)扫描
+ /\.(war|jar|sql|bak|key|pem|pfx)$/i,
+];
+
+function isBotScanPath(pathname: string): boolean {
+ for (const re of BOT_PATH_PATTERNS) {
+ if (re.test(pathname)) return true;
+ }
+ return false;
+}
+
function redirectLeetcodeIfNeeded(req: NextRequest): NextResponse | null {
const { pathname } = req.nextUrl;
@@ -72,6 +112,17 @@ function redirectLeetcodeIfNeeded(req: NextRequest): NextResponse | null {
}
export function proxy(req: NextRequest) {
+ // 0. Bot / scanner path 早返 404 —— 拦在 edge,不让进 Fluid 函数。
+ // 放在最前面是因为 Leetcode redirect 和 i18n middleware 都会让请求
+ // 穿到 [...slug] / not-found,烧 Fluid CPU。这里 0 函数调用直接挡。
+ if (isBotScanPath(req.nextUrl.pathname)) {
+ // 显式 404 + no-cache:返回 200 的话攻击者会以为命中漏洞继续扫
+ return new NextResponse(null, {
+ status: 404,
+ headers: { "cache-control": "no-store" },
+ });
+ }
+
// 1. Leetcode 中文 slug 优先做 301
const leetcodeRedirect = redirectLeetcodeIfNeeded(req);
if (leetcodeRedirect) return leetcodeRedirect;
diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts
index 46f301c2..4d95d175 100644
--- a/sentry.edge.config.ts
+++ b/sentry.edge.config.ts
@@ -15,7 +15,9 @@ const dsn = process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn,
enabled: process.env.NODE_ENV === "production" && !!dsn,
- tracesSampleRate: 0.1,
+ // 10% → 2%(dev_docs/vercel-cpu-overage-2026-05.md H3)。Edge middleware
+ // 每条请求都过这里,10% trace 直接叠在 Fluid CPU 上。降到 2% 显著省 CPU。
+ tracesSampleRate: 0.02,
debug: false,
beforeSend(event) {
if (event.request?.headers) {
diff --git a/sentry.server.config.ts b/sentry.server.config.ts
index ae522920..07530941 100644
--- a/sentry.server.config.ts
+++ b/sentry.server.config.ts
@@ -20,7 +20,10 @@ const dsn = process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn,
enabled: process.env.NODE_ENV === "production" && !!dsn,
- tracesSampleRate: 0.1,
+ // 10% → 2%(dev_docs/vercel-cpu-overage-2026-05.md H3)。
+ // 10% 在月百万级请求量下产生 10w+ traces,每条 trace 上报又叠加 Fluid CPU。
+ // 2% 仍能日采几千条覆盖 P95 性能趋势,trace 配额也更宽裕。
+ tracesSampleRate: 0.02,
debug: false,
beforeSend(event) {
if (event.request?.headers) {
From 69ae9860ddf72936de375e51928ea8b48590e1ba Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 19:41:14 +0000
Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E6=92=A4=E5=9B=9E=E7=9C=81=20CPU=20?=
=?UTF-8?q?hack=EF=BC=8C=E6=81=A2=E5=A4=8D=20best=20practice=EF=BC=88dashb?=
=?UTF-8?q?oard=20=E6=95=B0=E6=8D=AE=E4=BF=AE=E8=AE=A2=E8=AF=8A=E6=96=AD?=
=?UTF-8?q?=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
用户 review 后指出"丢西瓜捡芝麻"风险,复盘 Vercel 30 天 dashboard 后修订:
## 真实根因(不是 /_not-found)
dashboard 30 天曲线显示 5/11 CPU 峰值 80-90min(基线 5-15min/day),完美
对应 SEO PR 落地时间:
- 5/11 15:01-15:39 UTC: PR #341 (253 MDX descriptions + 32 新 EN 翻译)
- 5/11 16:02-18:41 UTC: PR #342 (remark heading shift + leetcode dedup)
- 5/11 19:01-19:27 UTC: PR #343 + #340,4 小时 4 次 deploy 清空 ISR
加上 deploy.yml 里 IndexNow 主动告诉 Bing 重抓 → 5/10-5/12 crawler 风暴。
**这是 SEO 工作 successful 的代价,不是 bug。** 真实流量。
/_not-found 静态化 + bot blocklist 是真实 waste 清理(保留),但不能独立
解释 4× 激增。
## 撤回的两条 hack
1. Sentry tracesSampleRate 0.1 → 0.02:撤回,保持 10%
observability 不能为这点 CPU 让步,10% 是行业标准,client/server/edge
三处必须一致才能跨 runtime 串联 trace。
2. fetchEvents 失败一律返空:改成只在 NEXT_PHASE === phase-production-build
时返空,运行时仍 throw 让 Sentry 抓真故障。否则 prod backend 挂了会被
误显示成"暂无活动",掩盖故障。
## 保留的修复(best practice,不是 hack)
- /_not-found ƒ → ○:根 404 本就不需要 i18n
- proxy.ts bot blocklist:扫描器不该烧 Fluid
- /[locale]/docs /events /login 缺 setRequestLocale → 补:SSG/ISR 本就该工作
- /editor /share cascade ●:纯 client component,安全
## Build 验证
pnpm build 重跑:
- /[locale]/events 仍是 ● ISR 5m 1y
- [events] fetch failed at build, rendering empty shell(NEXT_PHASE guard 工作)
---
app/[locale]/events/page.tsx | 43 +++++++++++++------
dev_docs/vercel-cpu-overage-2026-05.md | 59 ++++++++++++++++++++------
sentry.edge.config.ts | 6 +--
sentry.server.config.ts | 8 ++--
4 files changed, 85 insertions(+), 31 deletions(-)
diff --git a/app/[locale]/events/page.tsx b/app/[locale]/events/page.tsx
index 5f3518f4..1c4c8af2 100644
--- a/app/[locale]/events/page.tsx
+++ b/app/[locale]/events/page.tsx
@@ -30,15 +30,24 @@ interface ApiResponse
{
message?: string;
}
+/**
+ * 在 build 阶段才允许"后端不可达就降级返空"。Next 16 用 NEXT_PHASE 标记
+ * phase-production-build,build 时返空让 generateStaticParams 能跑完不挂;
+ * 运行时仍然 throw,Sentry / 错误页才能感知真故障,不至于把 prod backend
+ * 挂了误显示成"暂无活动"。
+ */
+const IS_BUILD = process.env.NEXT_PHASE === "phase-production-build";
+
async function fetchEvents(): Promise {
const backendUrl = process.env.BACKEND_URL;
- // 改成"失败降级返回空"而非 throw:build 时 SSG(generateStaticParams 触发预渲染)
- // 如果后端不可达,throw 会让整次 build 失败。返回空数组让页面 build 出"暂无活动"
- // 的静态壳,等 revalidate=300 到点后台刷新拿到真数据。
- // 同步好处:Vercel CF 偶发挡 build IP / 后端临时挂时不再 break deploy。
if (!backendUrl) {
- console.warn("[events] BACKEND_URL not set, rendering empty list");
- return [];
+ if (IS_BUILD) {
+ console.warn(
+ "[events] BACKEND_URL not set at build, rendering empty shell; ISR will fetch real data after deploy",
+ );
+ return [];
+ }
+ throw new Error("BACKEND_URL is not configured");
}
try {
const res = await fetch(`${backendUrl}/api/events`, {
@@ -49,16 +58,26 @@ async function fetchEvents(): Promise {
},
});
if (!res.ok) {
- console.warn(
- `[events] backend ${res.status} ${res.statusText}, rendering empty list`,
- );
- return [];
+ if (IS_BUILD) {
+ console.warn(
+ `[events] backend ${res.status} at build, rendering empty shell`,
+ );
+ return [];
+ }
+ throw new Error(`/api/events backend ${res.status} ${res.statusText}`);
}
const json = (await res.json()) as ApiResponse;
return json.success && json.data ? json.data : [];
} catch (err) {
- console.warn("[events] fetch failed, rendering empty list:", err);
- return [];
+ if (IS_BUILD) {
+ console.warn(
+ "[events] fetch failed at build, rendering empty shell:",
+ err,
+ );
+ return [];
+ }
+ // 运行时失败仍然 throw —— Sentry 抓到,错误页正常显示,不掩盖故障
+ throw err;
}
}
diff --git a/dev_docs/vercel-cpu-overage-2026-05.md b/dev_docs/vercel-cpu-overage-2026-05.md
index 3b9e1adb..7f5b9441 100644
--- a/dev_docs/vercel-cpu-overage-2026-05.md
+++ b/dev_docs/vercel-cpu-overage-2026-05.md
@@ -4,12 +4,30 @@
> Fast Origin Transfer 12.04 GB / 10 GB(120%)。用户报告"之前做过 SSR
> 优化但情况更糟"。本文档记录调查方法、根因、修复、验证手段。
-## TL;DR
+## TL;DR(看完 Vercel dashboard 30 天图表后的修订诊断)
-之前的 SSG 优化(2026-05-06 commit `8517332`)**只翻转了 1 条路由**(/[locale]
-首页)。`next build` 输出显示**还剩 18 条 ƒ Dynamic**,其中最致命的是
-`/_not-found`:所有漏洞扫描器(.env / wp-admin / php-info / graphql)和真实
-404 全落到这条 dynamic 路由。
+**真正的元凶**:5/11 一天里 PR #341(253 MDX descriptions backfill + 32 新 EN
+翻译页)+ PR #342(remark h1 plugin)+ PR #343(escape-angles)连续 4 次
+deploy。每次 deploy 自动 ping IndexNow → Bing/Google 5/10-5/12 大规模重抓 +
+索引 32 个新 URL。Dashboard 30 天曲线显示 5/11 CPU 峰值 80-90min(pre-spike
+基线 5-15min/day),完美对应 SEO PR 落地时间。
+
+**这是 SEO 工作 successful 的代价,不是 bug**。流量是真实的搜索引擎 + 真实
+用户增长,付费 Pro plan 阈值就是这么到的。
+
+**本 PR 做的事是真实 waste 的清理**(不是 hack):
+
+- `/_not-found` 之前是 ƒ Dynamic(每条 scanner 都烧 Fluid)→ ○ Static
+- `proxy.ts` 加 bot path 早返 404,scanner 不再进 Fluid
+- `/[locale]/docs` `/events` `/login` 缺 setRequestLocale 导致退回 dynamic →
+ 补上 + generateStaticParams 让 SSG/ISR 真正工作
+
+**撤回的"省 CPU hack"(写完才意识到丢了西瓜)**:
+
+- ~~Sentry tracesSampleRate 0.1 → 0.02~~:保持 10%。observability 不能为这
+ 点 CPU 让步,10% 是行业标准
+- ~~fetchEvents 失败一律返空~~:改成只在 `NEXT_PHASE === "phase-production-build"`
+ 时返空,运行时仍然 throw 让 Sentry 抓到真故障
本次修复在 `next build` 输出层面把 6 条路由从 ƒ 翻成 ● / ○:
@@ -152,16 +170,33 @@ x-vercel-cache: HIT # ← CDN 命中
age: 142
```
-### H3:Sentry tracesSampleRate 0.1(10%)叠在每次调用上
+### H3:~~Sentry tracesSampleRate 0.1 → 0.02~~(撤回)
+
+最初打算改 10% → 2% 省 CPU。CR 后撤回——Sentry trace 在 30 天 CPU 占比远不及
+crawler 流量,2% 省的是芝麻丢的是西瓜(observability)。10% 是行业标准,
+client/server/edge 三处必须一致才能跨 runtime 串联请求链路。
+
+**结论**:保持 0.1,不动 Sentry config。
+
+### H4:dashboard 数据让根因更清晰(SEO 重抓风暴)
+
+补观察后修订:
-**证据**:`sentry.server.config.ts:23` 和 `sentry.edge.config.ts:18` 都写
-`tracesSampleRate: 0.1`。在 368K 月度 invocations 下 = ~37K traces,每条
-trace 含 span 序列化 + 异步上报,叠加在每次 Fluid 调用 + 每次 edge middleware
-上。
+| 日期 | CPU | 主要 deploy |
+| -------------------- | ------------ | -------------------------------------------------------- |
+| 4/14 - 5/5 | 5-15min/day | 普通流量 + 周期扫描 baseline |
+| 5/11 15:01-15:39 UTC | 30 → 50min | PR #341:253 MDX descriptions backfill + 32 新 EN 翻译 |
+| 5/11 16:02-18:41 UTC | 50 → 80min | PR #342:remark heading shift + leetcode dedup |
+| 5/11 19:01-19:27 UTC | **90min 峰** | PR #343 + #340 deps,4 小时内 4 次 deploy ISR cache wipe |
+| 5/12 | ~45min | crawler 余波未消,但日益下降 |
-**修复**:0.1 → 0.02。日均仍能采到几千条 trace 监控 P95 性能趋势。
+`.github/workflows/deploy.yml` 的 `INDEXNOW_API` 让每次 deploy 都**主动告诉**
+Bing / Google "URL 变了,快重抓"。PR #341 一次性改 253 个 MDX + 加 32 个新
+URL,触发的就是这一波重抓。
-**验证**:Sentry dashboard `Performance` tab,Events / 24h 应该下降 5×。
+**结论**:5.11 之后激增是真实流量。本 PR 修的是**不该花的 CPU**(scanner /
+缺 SSG 的路由),让真实流量的边际成本最小化,但解决不了"SEO 太成功"这件事。
+长期路径:上 Pro plan 或 Cloudflare proxy 挡 crawler。
## 还没修但记录在案的问题
diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts
index 4d95d175..7b61cf6c 100644
--- a/sentry.edge.config.ts
+++ b/sentry.edge.config.ts
@@ -15,9 +15,9 @@ const dsn = process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn,
enabled: process.env.NODE_ENV === "production" && !!dsn,
- // 10% → 2%(dev_docs/vercel-cpu-overage-2026-05.md H3)。Edge middleware
- // 每条请求都过这里,10% trace 直接叠在 Fluid CPU 上。降到 2% 显著省 CPU。
- tracesSampleRate: 0.02,
+ // 10% 采样:与 server config 对齐,保证 client/server/edge 三处 trace 比例一致,
+ // 跨 runtime 串联请求链路才完整。省 CPU 的 hack 撤回,observability 优先。
+ tracesSampleRate: 0.1,
debug: false,
beforeSend(event) {
if (event.request?.headers) {
diff --git a/sentry.server.config.ts b/sentry.server.config.ts
index 07530941..67b97b4c 100644
--- a/sentry.server.config.ts
+++ b/sentry.server.config.ts
@@ -20,10 +20,10 @@ const dsn = process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn,
enabled: process.env.NODE_ENV === "production" && !!dsn,
- // 10% → 2%(dev_docs/vercel-cpu-overage-2026-05.md H3)。
- // 10% 在月百万级请求量下产生 10w+ traces,每条 trace 上报又叠加 Fluid CPU。
- // 2% 仍能日采几千条覆盖 P95 性能趋势,trace 配额也更宽裕。
- tracesSampleRate: 0.02,
+ // 10% 采样:行业标准,足以发现 P95/P99 慢请求 + 偶发错误关联的请求链路。
+ // (曾试过 2% 想省 Vercel CPU,但 trace overhead 占比远小于 SEO 重抓带来的
+ // crawler 流量,2% 是省芝麻丢西瓜的 hack —— observability 不能为这点 CPU 让步。)
+ tracesSampleRate: 0.1,
debug: false,
beforeSend(event) {
if (event.request?.headers) {
From c6348211af65a48402856261417ffc93768b8f6a Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 19:51:31 +0000
Subject: [PATCH 3/4] =?UTF-8?q?chore:=20=E7=A0=8D=E5=A4=9A=E4=BD=99?=
=?UTF-8?q?=E6=B3=A8=E9=87=8A=20+=20=E6=96=B0=E5=A2=9E=20CLAUDE.md=20?=
=?UTF-8?q?=E5=9B=BA=E5=8C=96=20review=20=E6=8E=A8=E5=9B=9E=E7=9A=84?=
=?UTF-8?q?=E5=8F=8D=E6=A8=A1=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 砍掉的注释(被推回:"丢西瓜捡芝麻"叙事 / dev_docs 引用 / 历史回顾)
- app/not-found.tsx: 12 行 docstring → 1 行约束说明
- app/[locale]/events: ISR docstring + IS_BUILD 段落 → 单行 each
- app/[locale]/login: 删整段"SSG 化"docstring
- app/[locale]/docs: 删历史叙事,留 force-static 必要性说明
- proxy.ts: 删 dev_docs 引用 + 历史回顾,留 BOT_PATH 维护警告
- sentry.{server,edge}.config: 删"曾试过 2%"叙事,留三处一致约束
引用 dev_docs 文件路径 / PR 编号 / "原版/之前" 叙事都会 rot —— 全删,
真要找上下文用 git blame。
## 新增 CLAUDE.md(不重复 AGENT.md)
固化 5 条被 review 推回过的反模式:
1. 最佳实践 > 微小资源优化(不降 Sentry / 不藏 backend 错误 / 流量真增长就付费)
2. 路由分类必须用 next build 输出验证,不要凭感觉 + next-intl SSG 三条件
3. 注释规则项目特化(默认不写,禁 dev_docs/PR/历史引用)
4. CR 反馈直接改不分级问用户
5. generated/leetcode-slug-map.json 必须 commit 进 git 的特例
build 重跑 6 条路由翻转保持,无 regression。
---
CLAUDE.md | 93 ++++++++++++++++++++++++++++++++++++
app/[locale]/docs/page.tsx | 8 +---
app/[locale]/events/page.tsx | 23 ++-------
app/[locale]/login/page.tsx | 8 ----
app/not-found.tsx | 22 ++-------
proxy.ts | 20 ++------
sentry.edge.config.ts | 3 +-
sentry.server.config.ts | 4 +-
8 files changed, 108 insertions(+), 73 deletions(-)
create mode 100644 CLAUDE.md
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..2b96a396
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,93 @@
+# CLAUDE.md — 给 Claude / AI coding agent 的项目级硬约束
+
+`AGENT.md` 写 workflow 和 coding style。这里只写**已被 review 推回过的反模式**,
+确保下次不再犯。
+
+## 1. 最佳实践优先于"省一点资源"
+
+线上 Vercel CPU 接近 / 超配额时,**第一反应不是降级 observability 或藏起错误**。
+正确的次序:
+
+1. 找真实的 waste(scanner 烧 Fluid、缺 SSG 配置的路由、cache miss 风暴)
+2. 用 best practice 修掉 waste(让 SSG / ISR 真正生效,edge 早返)
+3. 还是过线就升 Pro plan / 上 Cloudflare proxy 挡 crawler
+
+**不要做**("丢西瓜捡芝麻" 反模式,已被推回过):
+
+- ❌ 把 `Sentry tracesSampleRate` 从 0.1 调到 0.02 省 CPU —— 10% 是行业标准,
+ observability 不能为这点 CPU 让步;server/edge/client 三处必须一致才能跨
+ runtime 串 trace
+- ❌ 把后端 fetch 失败一律返空数组 —— 这隐藏 prod 故障,把"backend 挂了"误
+ 显示成"暂无活动",Sentry / 错误页都抓不到。**正确做法**:用
+ `process.env.NEXT_PHASE === "phase-production-build"` guard,只在 build
+ 阶段降级返空(避免 SSG build 挂),运行时仍 throw
+- ❌ 把活跃流量当 bug 优化掉 —— 如果 SEO PR 的 IndexNow ping 让搜索引擎重抓
+ 是 4× 流量增长的原因,那是工作成功的代价,应该付费而不是回滚 SEO
+
+## 2. 路由分类必须用 `next build` 输出验证,不要凭感觉
+
+历史教训:上一轮 SSR 优化(commit `8517332`)声称"首页 SSG 化",但 build 表
+显示**只翻转了 1 条路由**。剩 17 条还是 ƒ Dynamic,没人发现。
+
+**强制流程**:
+
+```bash
+# 修前快照
+pnpm build 2>&1 | tee /tmp/build-before.txt
+
+# 修复后
+pnpm build 2>&1 | tee /tmp/build-after.txt
+
+# 直接 diff,看哪些 ƒ → ● / ○
+diff <(grep -E '^[┌├└] ' /tmp/build-before.txt) \
+ <(grep -E '^[┌├└] ' /tmp/build-after.txt)
+```
+
+**不接受"我加了 force-static 应该就行"这种自证。** 看 build 表,看
+`x-vercel-cache: HIT` header,看 Vercel dashboard 24h 后实测 CPU。
+
+### next-intl SSG 的硬要求
+
+每个 `[locale]/*/page.tsx` 想 SSG / ISR 都必须满足**全部三条**:
+
+1. `params: Promise<{ locale: string }>` 接收 + `await params`
+2. `setRequestLocale(locale)` 调用(必须在任何 `getTranslations` / `getLocale` 之前)
+3. `export function generateStaticParams() { return routing.locales.map(...); }`
+
+缺任一条 → next-intl 退回 `cookies()` 推断 locale → 整页 ƒ Dynamic。
+parent layout 的 setRequestLocale 不传染到子 page。
+
+## 3. 注释规则(CLAUDE.md 顶层约束的项目特化)
+
+**默认不写注释**。只在以下场景写:
+
+- 非显然的工程约束(如 next-intl SSG 三条件、`NEXT_PHASE` guard 的作用域)
+- 维护时容易踩坑的不变量(如 "BOT_PATH_PATTERNS 不要加 admin / login")
+
+**严禁写**(已被推回过的反模式):
+
+- ❌ 引 dev_docs/ 文件路径(doc 改名 / 删除时注释 rot)
+- ❌ 引 PR / commit / issue 编号(提供不了上下文,要看就 `git blame`)
+- ❌ "原版/之前是 X,现在改成 Y" 的历史叙事(PR 描述里写就好)
+- ❌ "修复 XX bug" / "为 YY 任务加" 类引用当前任务的注释
+- ❌ 大段 docstring 描述代码功能 —— 命名清楚就够
+
+## 4. CR 反馈直接改,不分级问用户
+
+Copilot / 人工 review 给的意见**先判真假**:
+
+- 真问题 → 直接 commit 修,挂 `Co-authored-by: copilot-pull-request-reviewer[bot]`
+- 噪音 → 直接 dismiss 并说理由
+
+**不要**列 P0/P1/P2 让用户选 —— 我已经读完 CR 了,用户没读,让他筛选是把
+任务推回给他。
+
+## 5. Build 产物 commit 进 git 的特例
+
+`generated/leetcode-slug-map.json` 是 `pnpm build` prebuild 产物,但**必须
+commit 进 git**——`proxy.ts` 在 edge runtime 静态 import 它,不 commit
+就要把 pinyin-pro 字典塞进 edge bundle(不可行)。
+
+任何改 `content/docs/career/interview-prep/leetcode/` 下题目(新增 / 删除 /
+重命名)的 PR,commit 前**必须**跑一次 `pnpm build` 让 prebuild 同步这个
+JSON,否则下一个 contributor 跑 build 时会被强迫顺手清你的 orphan entry。
diff --git a/app/[locale]/docs/page.tsx b/app/[locale]/docs/page.tsx
index 8ba1353d..3d0c830f 100644
--- a/app/[locale]/docs/page.tsx
+++ b/app/[locale]/docs/page.tsx
@@ -15,14 +15,10 @@ import { ensureSeoDescription } from "@/lib/seo-description";
* 内容交给 ``(root 不传 → 渲染 pageTree 顶层分区)。所有
* 渲染逻辑和 community / career/interview-prep/leetcode 两处共用同一个组
* 件,避免 drift。
- *
- * SSG 化(dev_docs/vercel-cpu-overage-2026-05.md H2):
- * 原版 build 表里是 ƒ Dynamic,即便 setRequestLocale 都加了。原因是 page
- * 没显式 `dynamic = "force-static"`,Next 16 默认让带 await fetch 或 RSC
- * getLocale 的 [locale] page 跑 dynamic。SectionIndex 内部用的 getLocale()
- * 只读静态 pageTree(无 IO),所以加 force-static 不会丢任何东西。
*/
+// force-static 必需:SectionIndex 内部用 getLocale(),Next 16 会按"可能 dynamic"
+// 处理,加这条显式 opt-in 静态化(pageTree 是 build-time 数据,无运行时依赖)。
export const dynamic = "force-static";
interface Props {
diff --git a/app/[locale]/events/page.tsx b/app/[locale]/events/page.tsx
index 1c4c8af2..922a3fba 100644
--- a/app/[locale]/events/page.tsx
+++ b/app/[locale]/events/page.tsx
@@ -9,19 +9,9 @@ import type { EventView } from "./types";
import { sanitizeMediaUrl } from "@/lib/url-safety";
import { routing } from "@/i18n/routing";
-/**
- * /events 列表页。
- *
- * ISR 化(dev_docs/vercel-cpu-overage-2026-05.md H2):
- * 原版 export const revalidate = 300 但 build 输出仍是 ƒ Dynamic —— 因为
- * 没 setRequestLocale,next-intl 退回 cookies() 推断 locale,整页变 dynamic。
- * 每条访问 = 1 Fluid 调用。加 params + setRequestLocale + generateStaticParams
- * 让 revalidate=300 真正生效:build 时各 locale 预渲染一份,5min 内访问
- * 直接命中 CDN,过期时后台静默更新。
- *
- * revalidate: 300 把 Neon 打压力压到每 5min 一次 SSR,和 PR #286 的 profile 策略一致。
- */
-
+// ISR 5min:和 profile/feed 同一节流策略,控后端 QPS。
+// setRequestLocale + generateStaticParams 是 next-intl SSG 的必要条件,
+// 缺任一项会让 next-intl 退回 cookies() 把这条路由钉成 ƒ Dynamic。
export const revalidate = 300;
interface ApiResponse {
@@ -30,12 +20,7 @@ interface ApiResponse {
message?: string;
}
-/**
- * 在 build 阶段才允许"后端不可达就降级返空"。Next 16 用 NEXT_PHASE 标记
- * phase-production-build,build 时返空让 generateStaticParams 能跑完不挂;
- * 运行时仍然 throw,Sentry / 错误页才能感知真故障,不至于把 prod backend
- * 挂了误显示成"暂无活动"。
- */
+// 只在 build 阶段允许 fetch 失败降级(让 SSG 不挂),运行时仍 throw 给 Sentry。
const IS_BUILD = process.env.NEXT_PHASE === "phase-production-build";
async function fetchEvents(): Promise {
diff --git a/app/[locale]/login/page.tsx b/app/[locale]/login/page.tsx
index 55aded28..a22d62d5 100644
--- a/app/[locale]/login/page.tsx
+++ b/app/[locale]/login/page.tsx
@@ -13,14 +13,6 @@ export const metadata: Metadata = {
robots: { index: false, follow: true },
};
-/**
- * /[locale]/login —— SSG 化(dev_docs/vercel-cpu-overage-2026-05.md H2)。
- *
- * 之前没 setRequestLocale,next-intl 退回 cookies() 推断 locale,整页 ƒ
- * Dynamic。login 是纯静态卡片 + 一个 client 按钮,没有理由每请求都 SSR。
- * 加 params + setRequestLocale + generateStaticParams 让两个 locale build 时
- * 预渲染,登录页所有访问都从 CDN 出。
- */
interface Props {
params: Promise<{ locale: string }>;
}
diff --git a/app/not-found.tsx b/app/not-found.tsx
index f87f577e..23a9266a 100644
--- a/app/not-found.tsx
+++ b/app/not-found.tsx
@@ -2,25 +2,9 @@ import Link from "next/link";
import { Button } from "@/app/components/ui/button";
import NotFoundTracker from "./not-found-tracker";
-/**
- * 根 not-found.tsx —— 当 URL 完全不匹配任何 route segment 时 Next 渲染这里。
- *
- * 为什么必须保持静态(修复 Vercel CPU 超额,dev_docs/vercel-cpu-overage-2026-05.md):
- * 之前版本 await getTranslations("notFound") 让这条路由变 ƒ Dynamic。
- * 线上所有 .env / wp- / php / graphql 漏洞扫描都落到这条 404 页面,每条扫描
- * = 1 Fluid 调用 + 1 client-side analytics POST。18:34 单一时间窗就有
- * 30+ 条扫描,月度累积是 CPU 配额的主要消耗源之一。
- *
- * 修复策略:去掉 next-intl 依赖(getTranslations 内部走 cookies(),自动
- * force-dynamic),改成根目录 not-found 用 hardcoded 双语,让 Next 把这条
- * 静态生成。之后所有 scanner 404 直接由 CDN 兜底,0 Fluid CPU。
- *
- * /[locale]/not-found.tsx 由 [locale] segment 各自的 not-found 处理,那里
- * 可以走完整 i18n。根 not-found 只是兜最外层 404,少数情况触发。
- *
- * NotFoundTracker:保留 client 端 umami 埋点。它在浏览器里跑,不影响服务端
- * 静态化判定(client-only useEffect 不会让 RSC 树 dynamic)。
- */
+// 根 not-found 必须保持静态:用 next-intl 的 getTranslations 会触发 cookies()
+// 让这条路由退化成 ƒ Dynamic,每条 404 / scanner 扫描就吃一次 Fluid CPU。
+// 双语并列是 trade-off —— 根级 not-found 拿不到 locale。
export default function NotFound() {
return (
diff --git a/proxy.ts b/proxy.ts
index fa78faf9..606e30ce 100644
--- a/proxy.ts
+++ b/proxy.ts
@@ -29,18 +29,9 @@ const LEETCODE_OLD_PATH_TAIL = "/docs/CommunityShare/Leetcode";
const intlMiddleware = createMiddleware(routing);
-/**
- * Bot / vulnerability scanner path patterns —— 在 edge 早返 404,
- * 不让请求穿透到 Fluid 函数(dev_docs/vercel-cpu-overage-2026-05.md 的 H1b)。
- *
- * 背景:Vercel runtime 日志显示线上有持续的漏洞扫描(.env / wp-admin /
- * php-info / werkzeug / graphql 等)。原来这些路径会被 next-intl 加 /zh 前缀
- * 后落到 [locale]/[...slug] 或根 /_not-found,每条扫描 ≈ 1 Fluid 调用。
- * 整理列表参考 OWASP top-10 + 主流 scanner(nikto / nmap / dirbuster)的指纹。
- *
- * 注意:列表里**不要**放可能跟真实业务路径重名的 segment(不要加 admin、login
- * 这些,admin 路由是有的)。只放 100% 业务用不到的字符串。
- */
+// Bot / vulnerability scanner path patterns —— 在 edge 早返 404,不让进 Fluid。
+// 维护约束:只能放 100% 业务用不到的指纹(不要加 admin / login,业务有真路由)。
+// 参考 OWASP top-10 + nikto / dirbuster 常见探测字串。
const BOT_PATH_PATTERNS = [
// PHP / 老 CMS:本站根本没装 PHP,所有 *.php 都是扫描
/\.php(?:$|[?#/])/i,
@@ -112,11 +103,8 @@ function redirectLeetcodeIfNeeded(req: NextRequest): NextResponse | null {
}
export function proxy(req: NextRequest) {
- // 0. Bot / scanner path 早返 404 —— 拦在 edge,不让进 Fluid 函数。
- // 放在最前面是因为 Leetcode redirect 和 i18n middleware 都会让请求
- // 穿到 [...slug] / not-found,烧 Fluid CPU。这里 0 函数调用直接挡。
+ // Scanner 路径 0 函数调用直接 404,no-store 避免攻击者拿 200 当命中信号。
if (isBotScanPath(req.nextUrl.pathname)) {
- // 显式 404 + no-cache:返回 200 的话攻击者会以为命中漏洞继续扫
return new NextResponse(null, {
status: 404,
headers: { "cache-control": "no-store" },
diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts
index 7b61cf6c..0b163584 100644
--- a/sentry.edge.config.ts
+++ b/sentry.edge.config.ts
@@ -15,8 +15,7 @@ const dsn = process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn,
enabled: process.env.NODE_ENV === "production" && !!dsn,
- // 10% 采样:与 server config 对齐,保证 client/server/edge 三处 trace 比例一致,
- // 跨 runtime 串联请求链路才完整。省 CPU 的 hack 撤回,observability 优先。
+ // 10% 是有意为之,server/edge/client 三处必须一致才能跨 runtime 串 trace。
tracesSampleRate: 0.1,
debug: false,
beforeSend(event) {
diff --git a/sentry.server.config.ts b/sentry.server.config.ts
index 67b97b4c..1602fc37 100644
--- a/sentry.server.config.ts
+++ b/sentry.server.config.ts
@@ -20,9 +20,7 @@ const dsn = process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn,
enabled: process.env.NODE_ENV === "production" && !!dsn,
- // 10% 采样:行业标准,足以发现 P95/P99 慢请求 + 偶发错误关联的请求链路。
- // (曾试过 2% 想省 Vercel CPU,但 trace overhead 占比远小于 SEO 重抓带来的
- // crawler 流量,2% 是省芝麻丢西瓜的 hack —— observability 不能为这点 CPU 让步。)
+ // 10% 是有意为之,server/edge/client 三处必须一致才能跨 runtime 串 trace。
tracesSampleRate: 0.1,
debug: false,
beforeSend(event) {
From e5c858b3bcafc9d83b8f1faa4827dd90dab5cb19 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 19:58:55 +0000
Subject: [PATCH 4/4] =?UTF-8?q?fix(proxy):=20=E5=88=A0=20BOT=5FPATH=20?=
=?UTF-8?q?=E9=87=8C=E7=9A=84=20dot-path=20=E6=AD=BB=E8=A7=84=E5=88=99?=
=?UTF-8?q?=EF=BC=88Copilot=20CR=20PR#346=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Copilot 指出 matcher `.*\\..*` 排除所有含 `.` 的路径,所以 BOT_PATH_PATTERNS
里 .php / .env / .git/ / .(war|jar|sql|bak|key|pem|pfx) 等正则**从来不会被
执行**,是死代码。
实际行为没问题——这些 dot-path scanner 直接走到 Next 默认 404 → 命中
○ Static /_not-found,由 CDN-served,不烧 Fluid。但写在 regex 列表里给人
"已在 edge 早返"的错觉。
删 5 条死规则,留下 4 条无 dot 真正生效的:wp-* / graphql / werkzeug /
phpmyadmin。注释里写明 dot-path 不要再加(已被 static 404 兜底)。
Co-authored-by: copilot-pull-request-reviewer[bot]
---
proxy.ts | 17 ++++++-----------
1 file changed, 6 insertions(+), 11 deletions(-)
diff --git a/proxy.ts b/proxy.ts
index 606e30ce..92f4e153 100644
--- a/proxy.ts
+++ b/proxy.ts
@@ -30,16 +30,15 @@ const LEETCODE_OLD_PATH_TAIL = "/docs/CommunityShare/Leetcode";
const intlMiddleware = createMiddleware(routing);
// Bot / vulnerability scanner path patterns —— 在 edge 早返 404,不让进 Fluid。
-// 维护约束:只能放 100% 业务用不到的指纹(不要加 admin / login,业务有真路由)。
-// 参考 OWASP top-10 + nikto / dirbuster 常见探测字串。
+// 维护约束:
+// 1. 只放 100% 业务用不到的指纹(不要加 admin / login,业务有真路由)
+// 2. **不要放含 `.` 的路径**:下面 matcher 用 `.*\\..*` 排除所有 dot-path,
+// middleware 根本不会被调起。带点的 scanner(.env / .php / .git/ / .war
+// 等)会直接走到 Next 默认 404 → 命中我们的 ○ Static `/_not-found`,
+// 已经是 CDN-served,不烧 Fluid。重复在这里写 dot-path 是死代码。
const BOT_PATH_PATTERNS = [
- // PHP / 老 CMS:本站根本没装 PHP,所有 *.php 都是扫描
- /\.php(?:$|[?#/])/i,
// wp-* 系列:WordPress 扫描
/\/wp-(admin|content|includes|login|json|config)(?:$|[/?#])/i,
- // .env / config 文件直接探测
- /\.env(?:\.[a-z]+)?(?:$|[?#/])/i,
- /\/(config|settings|secrets|credentials)\.(yml|yaml|json|ini|toml|xml)(?:$|[?#])/i,
// GraphQL / GQL endpoint 扫描(本站没 GraphQL)
/\/(graphql|gql|api\/graphql|v\d+\/graphql)(?:$|[/?#])/i,
// Werkzeug / Flask debug console
@@ -47,10 +46,6 @@ const BOT_PATH_PATTERNS = [
// 常见 admin / debug panels 探测路径(本站 admin 在 /[locale]/admin,
// 这些是其他平台特有路径,扫到必是 bot)
/\/(phpmyadmin|adminer|pma|dbadmin|mysqladmin)(?:$|[/?#])/i,
- // git / svn 仓库文件暴露探测
- /\/\.(git|svn|hg|bzr)\/(?:config|HEAD|entries)/i,
- // 已知敏感文件(war/jar/key/pem)扫描
- /\.(war|jar|sql|bak|key|pem|pfx)$/i,
];
function isBotScanPath(pathname: string): boolean {