Sentry Source Map 上传踩坑记:从 0% Adoption 到完整解析
接入 Sentry 错误监控后,Release Adoption 一直是 0%,Source Map 永远不解析。 排查了整整两天,踩了 6 个坑。
背景
📚 本文是 UtlKit 技术系列第 6 篇 — 系列索引 →
- 上一篇:浏览器兼容性 →
- 下一篇:Web Crypto API →
项目是 Next.js 15 静态导出,部署在 Cloudflare Pages。想接入 Sentry 做错误监控,让用户报错时能看到可读的堆栈信息。
看起来简单:安装 SDK → 上传 Source Map → 完工。 实际经历:
0% Adoption → SDK 没初始化 → 换 CDN
Source Map 上传失败 → sentry-cli 崩溃 → 换 HTTP API
Region 不对 → Token 解析失败 → 写解析逻辑
Release 不匹配 → 文件名不对 → unicode escape
坑 1:Release Adoption 永远是 0%
现象
Sentry Dashboard 上:
- Releases 列表里有 release ✅
- 但 Adoption 是 0% ❌
- Events 列表为空 ❌
根因
@sentry/browser 被 SWC tree-shaking 干掉了。
// ❌ 这段代码在 build 后被 SWC 优化掉了
import * as Sentry from '@sentry/browser'
Sentry.init({ dsn: '...', release: process.env.NEXT_PUBLIC_SENTRY_RELEASE })
原因是 @sentry/browser 内部用了大量的 lazy import 和条件导出,SWC 在 minification 阶段认为这些代码是 dead code,直接删掉了。
解决方案:用 Sentry CDN
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<head>
<script
src="https://browser.sentry-cdn.com/7.113.0/bundle.min.js"
crossorigin="anonymous"
/>
<script dangerouslySetInnerHTML={{ __html: `
Sentry.init({
dsn: '${process.env.NEXT_PUBLIC_SENTRY_DSN}',
release: '${process.env.NEXT_PUBLIC_SENTRY_RELEASE}',
environment: 'production'
})
`}} />
</head>
<body>{children}</body>
</html>
)
}
CDN script 在 SWC 处理之外,不会被 tree-shake。
效果: Release Adoption 从 0% 变为 >0% ✅
坑 2:sentry-cli 在 GitHub Actions 上崩溃
现象
# .github/workflows/sentry.yml
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npx @sentry/cli sourcemaps upload out/
运行时报错:
Error: The module '/node_modules/@sentry/cli/bin/sentry-cli'
was compiled against a different Node.js version
根因
sentry-cli 是 Rust 编译的二进制文件,内置的 Node.js FFI 绑定和 Actions 环境的 Node.js 版本不兼容。
解决方案:直接用 HTTP API
// scripts/upload-sourcemaps.js
const fs = require('fs')
const path = require('path')
const https = require('https')
// 从 Auth Token 解析组织 ID 和区域
function parseSentryToken(authToken) {
// Sentry token format: sntrys_<base64_encoded_json>
const payload = authToken.replace('sntrys_', '')
const decoded = JSON.parse(Buffer.from(payload, 'base64').toString())
return {
orgId: decoded.org_slug,
region: decoded.host === 'https://sentry.io' ? 'us' : 'eu'
}
}
// multipart/form-data 上传
function uploadSourceMap(options) {
const { url, token, data } return new Promise((resolve, reject) => {
const boundary = '----FormBoundary' + Math.random().toString(36).slice(2)
let body = ''
for (const [key, value] of Object.entries(data)) {
body += `--${boundary}\r\n`
body += `Content-Disposition: form-data; name="${key}"\r\n\r\n`
body += value + '\r\n'
}
// 添加文件
for (const [key, fileBuffer] of Object.entries(options.files)) {
body += `--${boundary}\r\n`
body += `Content-Disposition: form-data; name="${key}"; filename="${options.filenames[key]}"\r\n`
body += `Content-Type: application/octet-stream\r\n\r\n`
// ... append buffer
}
body += `--${boundary}--\r\n`
const parsedUrl = new URL(url)
const req = https.request({
hostname: parsedUrl.hostname,
path: parsedUrl.pathname,
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': `multipart/form-data; boundary=${boundary}`
}
}, (res) => {
let data = ''
res.on('data', chunk => data += chunk)
res.on('end', () => resolve(JSON.parse(data)))
})
req.write(body)
req.end()
})
}
坑 3:Region 不对
现象
404 Not Found: /api/0/organizations/myorg/releases/
根因
Sentry 有两个区域:
us.sentry.io— 美区eu.sentry.io— 欧区
我的组织在美区,但默认请求了欧区 API。
解决方案
Auth Token 里编码了区域信息:
// Sentry token 是 sntrys_ 前缀 + base64(JSON)
const token = 'sntrys_eyJyZWdpb25FbmRwb2ludCI6ICJodHRwczovL3VzLnNlbnRyeS5pbyJ9'
const payload = Buffer.from(token.slice(7), 'base64').toString()
const info = JSON.parse(payload)
// info.regionEndpoint → "https://us.sentry.io"
坑 4:Release 名称不匹配
现象
Source Map 上传成功,但 Sentry Dashboard 上 Source Map 状态仍然是 "Missing"。
根因
Source Map 的 release name 和 SDK 报告的 release name 不一致。
SDK 报告: "release": "utlkit-20260521-abc1234"
Source Map 上传: "release": "utlkit-abc1234" ← 少了日期
解决方案
用同一个脚本生成 release 名称,SDK 和上传脚本都用这个值:
// scripts/generate-sentry-release.js
const { execSync } = require('child_process')
const commitSha = execSync('git rev-parse --short HEAD').toString().trim()
const date = new Date().toISOString().split('T')[0]
const release = `utlkit-${date}-${commitSha}`
console.log(release)
坑 5:unicode escape 导致文件名错误
现象
Source Map 上传后,Sentry 显示的 source map URL 里路径有乱码。
根因
JavaScript 字符串里的中文路径被序列化成 \uXXXX,Sentry API 无法正确解析。
解决方案
// ❌ JSON.stringify 会把中文转成 \uXXXX
JSON.stringify({ url: 'src/中文/path.js' })
// ✅ 用 ensure_ascii: false 的替代方案
JSON.stringify({ url: 'src/中文/path.js' }, null, 0)
最终工作流
# .github/workflows/sentry.yml
name: Upload Sentry Source Maps
on:
push:
branches: [main]
jobs:
upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npm run build
- run: node scripts/generate-sentry-release.js > .sentry-release
- run: node scripts/upload-sourcemaps.js
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
经验总结
| 坑 | 症状 | 解决方案 |
|---|---|---|
| SWC tree-shake SDK | Adoption 0% | 用 Sentry CDN |
| sentry-cli Node 不兼容 | 二进制崩溃 | 直接 HTTP API |
| Region 不对 | 404 | 从 Token 解析区域 |
| Release 不匹配 | Source Map Missing | 统一生成 release 名 |
| Unicode escape | 文件名乱码 | 正确处理编码 |
| dynamic import 被优化 | SDK 不初始化 | CDN script |
核心经验: Sentry 的官方文档假设你用 Node.js SSR 或 bundler 插件。静态导出 + CDN SDK 的组合需要自己写上传逻辑。
📚 本文是 UtlKit 技术系列第 6 篇 — 系列索引 →
- 上一篇:浏览器兼容性 →
- 下一篇:Web Crypto API →
utlkit.com — 所有格式化工具都是纯前端实现,上述方案在线上稳定运行。