阅读时长 4 分钟

基于 rsbuild 快速集成 Sentry 流程

目录

本文提供通用解决方案,适用于任何使用 Rsbuild 的前端项目。

技术栈

  • 构建工具: Rsbuild (基于 Rspack)
  • 监控平台: Sentry (支持云版和自部署版本)
  • 包管理工具: npm/pnpm/yarn
  • 核心依赖:
    • @sentry/browser: Sentry 浏览器端 SDK
    • @sentry/webpack-plugin: Sentry SourceMap 上传插件
    • @rspack/core: Rspack 核心(Rsbuild 内置)

开始集成

第一步:安装依赖

npm install @sentry/browser
npm install -D @sentry/webpack-plugin
  • @sentry/browser:Sentry 浏览器端 SDK,用于在浏览器端捕获错误并上报至 Sentry 服务器。

  • @sentry/webpack-plugin:Sentry SourceMap 上传插件,用于在构建时将 SourceMap 上传到 Sentry 服务器。

第二步:创建 Sentry 初始化文件

创建 src/sentry.js,用于初始化 Sentry。

其中:

YOUR_SENTRY_DSN_HERE 是 Sentry 控制台,创建完项目后获取的 DSN。

__PACKAGE_VERSION__ 是项目版本号,用于在 Sentry 中显示版本号。

import * as Sentry from '@sentry/browser'

Sentry.init({
  dsn: 'YOUR_SENTRY_DSN_HERE', // 从 Sentry 控制台获取
  environment: process.env.NODE_ENV,
  release: __PACKAGE_VERSION__,
  tracesSampleRate: 1.0,
})

export default Sentry

第三部:在应用入口引入

修改 src/index.tsx

import './sentry' // ⚠️ 必须在最前面
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')!).render(<App />)

必须保证 Sentry 初始化文件在应用入口之前引入,才能保证捕获所有错误。

除了在 index.tsx中引入,还可以在rsbuild.config.tsentry中引入,代码如下:

export default defineConfig({
  source: {
    entry: {
      index: {
        import: [
          './src/sentry.js', // ⚠️ 必须在最前面
           './src/index.tsx'
        ],
      },
    },
  },
})

第四步:配置 Rsbuild

修改 rsbuild.config.ts

添加 DefinePlugin 插件,注入版本号,这样在 src/sentry.js 中才可以使用 __PACKAGE_VERSION__ 获取版本号。

version 一般为项目根目录下的 package.json 中的 version 字段,可以通过 import 导入。

import { defineConfig } from '@rsbuild/core'
import { DefinePlugin } from '@rspack/core'
import { version } from './package.json'

export default defineConfig({
  tools: {
    rspack: {
      plugins: [
        new DefinePlugin({
          __PACKAGE_VERSION__: JSON.stringify(version), // 注入版本号
        }),
      ],
    },
  },
})

第五步:创建 Rspack 插件配置(可选)

创建 config/sentry.plugins.js 文件,用于配置 Sentry 相关插件:

import { sentryWebpackPlugin } from '@sentry/webpack-plugin'
import { DefinePlugin } from '@rspack/core'
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'

/**
 * 获取 Git 信息
 */
function getGitInfo() {
  try {
    const commitHash = execSync('git rev-parse --short HEAD').toString().trim()
    const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
    return { commitHash, branch }
  } catch (error) {
    console.warn('Failed to get git info:', error)
    return { commitHash: 'unknown', branch: 'unknown' }
  }
}

/**
 * Sentry Webpack Plugin - 上传 SourceMap
 */
export function getSentryPlugin(options = {}) {
  const {
    projectName,
    org,
    authToken,
    release,
    dist,
    outputPath,
    url = 'https://sentry.io', // Sentry 云版地址
  } = options

  if (!authToken) {
    console.warn('⚠️ Sentry authToken not provided, skipping SourceMap upload')
    return null
  }

  const { commitHash, branch } = getGitInfo()
  
  return sentryWebpackPlugin({
    org,
    project: projectName,
    url,
    authToken,
    telemetry: false,
    
    release: {
      name: release,
      // 可选:添加部署信息
      dist: dist || `${commitHash}-${branch}`,
    },
    
    sourcemaps: {
      // 指定要上传的文件路径
      assets: `${outputPath}/**/*.{js,map}`,
      // 上传后删除本地 SourceMap 文件
      filesToDeleteAfterUpload: `${outputPath}/**/*.map`,
    },
  })
}

/**
 * 清理 SourceMap 插件 - 确保不部署 .map 文件
 */
export function getClearSourceMapPlugin(outputPath) {
  return {
    name: 'ClearSourceMapPlugin',
    apply(compiler) {
      compiler.hooks.done.tap('ClearSourceMapPlugin', () => {
        try {
          const files = fs.readdirSync(outputPath, { recursive: true })
          files.forEach(file => {
            const fullPath = path.join(outputPath, file)
            if (fullPath.endsWith('.map') && fs.existsSync(fullPath)) {
              fs.unlinkSync(fullPath)
              console.log(`✓ Deleted: ${file}`)
            }
          })
        } catch (error) {
          console.warn('Failed to delete source maps:', error.message)
        }
      })
    }
  }
}

rsbuild.config.ts 中,开启生成环境的 sourceMap,并配置 Sentry 相关插件,完成最终的集成配置,最终集成代码如下:

import { defineConfig } from '@rsbuild/core'
import { pluginReact } from '@rsbuild/plugin-react'
import { getDefinePlugin, getSentryPlugin, getClearSourceMapPlugin } from './config/sentry.plugins.js'
import { version } from './package.json'

const isProd = process.env.NODE_ENV === 'production'

export default defineConfig({
  // 源码配置
  source: {
    entry: {
      index: {
        import: [
          './src/sentry.js', // Sentry 初始化文件必须在最前面
           './src/index.tsx'
        ],
      },
    },
  },
  
  // 输出配置
  output: {
    // 生产环境生成 SourceMap,这里一定要是完整的 source-map,不要使用 cheap-source-map 或 eval-source-map
    sourceMap: isProd ? {
      js: 'source-map',
    } : false,
  },
  
  // 插件配置
  plugins: [pluginReact()],
  
  // 工具配置
  tools: {
    rspack: {
      // 需要将自定义的插件配置在这里
      // 因为 rsbuild 本质上是基于 Rspack 的
      plugins: [
        // 注入版本号
        getDefinePlugin(version),
        
        // 生产环境启用 Sentry
        isProd && getSentryPlugin({
          projectName: 'your-project-name',
          org: 'your-org-name',
          authToken: process.env.SENTRY_AUTH_TOKEN,
          release: version,
          outputPath: 'dist',
          // 如果使用自部署版本,修改 url
          // url: 'https://sentry.yourcompany.com',
        }),
        
        // 清理 SourceMap
        isProd && getClearSourceMapPlugin('dist'),
      ].filter(Boolean),
    },
  },
})