给 Vite 前端接入 Sentry,并在构建时上传 source map:线上报错的堆栈能还原到
src/cart.js第几行,但.map文件和源码不会公开给访客。附带构建后检查脚本。
先看清成品
本机用 Vite 8.3.5 + @sentry/browser 11.8.0 + @sentry/vite-plugin 11.8.0 实际构建:插件提示没有令牌、不创建 release、不上传;产物中注入了 Debug ID,没有
sourceMappingURL,.map文件在构建结束后被删除,检查脚本通过。没有提供真实令牌上传,也没有在浏览器中触发错误并到 Sentry 查看还原后的堆栈。
注意:Sentry 有免费和付费计划,额度与价格以官方价格页为准。错误数据里可能包含用户个人信息,上线前确认采集范围符合你所在地区的个人信息保护法规(如《个人信息保护法》),并在隐私政策中说明。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
| 前端错误上报 | 带 release、environment |
| source map 上传 | 构建时自动完成 |
| 不公开源码 | hidden source map + 上传后删除 |
| 检查脚本 | Debug ID 已注入、无 sourceMappingURL、无残留 .map |
需要准备哪些环境?
| 项目 | 要求 | 说明 |
|---|---|---|
| Node.js | 当前 LTS(本机 v22) | |
| Sentry 项目 | 平台选 Browser JavaScript | DSN 写进 VITE_SENTRY_DSN |
| Auth Token | 组织令牌,用于上传 | 只放在 CI 的 Secret 里 |
文件结构
25-sentry-B-src/
├── package.json
├── vite.config.js
├── index.html
├── check-maps.mjs
└── src/
├── main.js
└── cart.js
第 1 步:安装
npm install @sentry/browser
npm install -D vite @sentry/vite-plugin
package.json:
{
"name": "shop-web",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"check-maps": "node check-maps.mjs"
},
"dependencies": {
"@sentry/browser": "^11.8.0"
},
"devDependencies": {
"@sentry/vite-plugin": "^11.8.0",
"vite": "^8.3.5"
}
}
第 2 步:前端代码
src/main.js:
import * as Sentry from "@sentry/browser";
import { calcTotal } from "./cart.js";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN, // 为空时不发送
release: __APP_RELEASE__,
environment: import.meta.env.MODE,
sampleRate: 1.0,
});
document.getElementById("pay").addEventListener("click", () => {
// 故意传错数据,制造一个带堆栈的错误
const total = calcTotal([{ price: 10, qty: 2 }, { price: 5 }]);
alert(`合计 ${total} 元`);
});
src/cart.js:
export function calcTotal(items) {
return items.reduce((sum, item) => {
if (!Number.isInteger(item.qty)) {
throw new TypeError(`商品数量无效:${JSON.stringify(item)}`);
}
return sum + item.price * item.qty;
}, 0);
}
index.html:
<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>小店</title></head>
<body>
<button id="pay">去支付</button>
<script type="module" src="/src/main.js"></script>
</body>
</html>
第 3 步:Vite 配置
vite.config.js:
import { defineConfig } from "vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";
const release = process.env.APP_RELEASE || "shop-web@0.1.0";
export default defineConfig({
define: {
__APP_RELEASE__: JSON.stringify(release),
},
build: {
sourcemap: "hidden", // 生成 .map 但不在 JS 末尾写 sourceMappingURL,浏览器看不到
},
plugins: [
sentryVitePlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN, // 没有令牌时插件只注入 Debug ID,不上传
release: { name: release },
sourcemaps: {
filesToDeleteAfterUpload: ["./dist/**/*.map"], // 构建结束删除 .map(本机实测:没有令牌未上传时也会删除)
},
telemetry: false,
}),
],
});
要点:
sourcemap: "hidden":生成.map,但 JS 里不写引用,浏览器开发者工具不会自动加载。- 插件在产物中注入 Debug ID,Sentry 用它把错误和上传的 source map 对应起来,不依赖文件名。
release.name与Sentry.init的 release 一致。
第 4 步:构建并检查
check-maps.mjs:
// 构建后检查:JS 里注入了 Debug ID,且不包含 sourceMappingURL;上传后 dist 中不应再有 .map
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
const dir = "dist/assets";
const files = readdirSync(dir);
const js = files.filter((f) => f.endsWith(".js"));
const maps = files.filter((f) => f.endsWith(".map"));
let ok = true;
for (const f of js) {
const code = readFileSync(join(dir, f), "utf8");
const hasId = /sentry-dbid-|_sentryDebugIds/.test(code);
const hasUrl = /sourceMappingURL=/.test(code);
console.log(`${f}: Debug ID ${hasId ? "✓" : "✗"},sourceMappingURL ${hasUrl ? "✗ 存在" : "✓ 无"}`);
if (!hasId || hasUrl) ok = false;
}
console.log(`.map 文件:${maps.length} 个${maps.length ? "(未上传或未删除,不要部署到线上)" : ""}`);
process.exit(ok ? 0 : 1);
npm run build
npm run check-maps
检查结果(本机实际输出,未设置令牌):
[sentry-vite-plugin] Warning: No auth token provided. Will not create release. ...
[sentry-vite-plugin] Warning: No auth token provided. Will not upload source maps. ...
dist/assets/index-BT0ABWbZ.js 90.74 kB │ gzip: 31.48 kB │ map: 1,002.90 kB
index-BT0ABWbZ.js: Debug ID ✓,sourceMappingURL ✓ 无
.map 文件:0 个
第 5 步:在 CI 中上传(本文未执行)
- run: npm ci && npm run build && npm run check-maps
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: 你的组织 slug
SENTRY_PROJECT: 你的项目 slug
APP_RELEASE: shop-web@${{ github.sha }}
VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
预期结果:构建日志显示上传成功;部署后点“去支付”,Sentry 里 TypeError: 商品数量无效 的堆栈指向 src/cart.js 的 throw 那一行。
调试示例
本机构建时没有设置 SENTRY_AUTH_TOKEN,插件只发出警告、构建照常成功(见第 4 步输出)。这适合本地开发;但在 CI 里同样不会失败,容易“以为上传了其实没有”。建议在 CI 中先检查变量:
test -n "$SENTRY_AUTH_TOKEN" || { echo "缺少 SENTRY_AUTH_TOKEN"; exit 1; }
另外,本机实测 filesToDeleteAfterUpload 在没有上传时也删除了 .map。如果你需要在本地保留 .map 排查问题,把这一项改为只在 CI 中启用。
出错了怎么办?
| 现象 | 原因 | 解决 |
|---|---|---|
| 堆栈仍是压缩后的代码 | 没上传或 Debug ID 不匹配 | 确认 CI 日志有上传记录;上传和部署要用同一次构建产物 |
| 上传报 401 / 403 | 令牌无效或权限不足 | 重新生成组织令牌 |
线上能下载到 .map |
用了 sourcemap: true 且没删除 |
用 hidden,并保留删除配置 |
| 事件没有 release | 前端和插件 release 不一致 | 两处都用 APP_RELEASE |
| 本地开发也上报了 | 设置了 VITE_SENTRY_DSN |
本地不设,或按 MODE 区分 |
模板规则
- source map 只给 Sentry,不给访客。
- 令牌只放 CI Secret,不写进代码或
.env。 - 构建后运行检查脚本再部署。
项目资源
| 用途 | 地址 |
|---|---|
| Vite 上传 source map | https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/vite/ |
| Sentry 浏览器 SDK | https://docs.sentry.io/platforms/javascript/ |
| Auth Token | https://docs.sentry.io/account/auth-tokens/ |
| Releases | https://docs.sentry.io/product/releases/ |
| Vite 构建选项 | https://vite.dev/config/build-options |
前端错误只有能还原到源码才有用。source map 交给 Sentry、不交给访客,既能排查问题,又不会把整份源码公开出去。