Sentry Source Map 上传踩坑记

17 阅读4分钟

Sentry Source Map 上传踩坑记:从 0% Adoption 到完整解析

接入 Sentry 错误监控后,Release Adoption 一直是 0%,Source Map 永远不解析。 排查了整整两天,踩了 6 个坑。

背景

📚 本文是 UtlKit 技术系列第 6 篇系列索引 →

项目是 Next.js 15 静态导出,部署在 Cloudflare Pages。想接入 Sentry 做错误监控,让用户报错时能看到可读的堆栈信息。

看起来简单:安装 SDK → 上传 Source Map → 完工。 实际经历:

0% AdoptionSDK 没初始化 → 换 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 SDKAdoption 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 篇系列索引 →


utlkit.com — 所有格式化工具都是纯前端实现,上述方案在线上稳定运行。