初探 Sentry 監控工具,心得筆記

Lastor
Code 隨筆放置場
14 min readMar 5, 2023

Overview

Sentry 是一套監控工具,可以蒐集 App 的錯誤訊息,在 Sentry 的平台上呈現,方便專案交付之後的維護與 debug。

如果專案本身是在自家公司運作的系統,或許就不太需要用這個。比較適合接外包、委託案,實際使用者是其他公司的團隊時,才比較需要這樣的監控工具,不然要修 bug 時,礙於公司資料要保密,可能只能通靈 debug。

當前 2023 年 3 月的版本,Sentry 提供新註冊用戶 14 天完整試用所有功能,之後就會轉為免費方案,只能使用基礎功能。使用方式類似 GA,強迫申請帳號,之後會有一個屬於自己 or 組織的 Sentry Web 後台系統,可在裡面建立多個 Project,個別進行監控。

每個 Project 都有自己的 DSN (Client Key),類似 token key 之類的玩意,用來跟 App 對接的。需要 npm 安裝它的 SDK 套件,在專案裡初始化,然後把 key 填上。

主要的功能有:

  1. Issues,錯誤紀錄的最小單位
  2. Replay,可記錄發生錯誤前後 user 的操作與畫面,進行回放
  3. Alert,錯誤發生後用 email、slack 等方式進行通知
  4. GitHub / GitLab 聯動,可使錯誤紀錄追加 git commit 情報,快速聚焦
  5. Other… (還有別的功能,沒研究太深)

Issues 是主要的面板,會把 App 發生的所有報錯都記錄在這,Sentry 會自動智能分類,同樣的錯誤會 group 到同一個條目顯示。可以看到是哪個瀏覽器、作業系統、版本、錯誤訊息、錯誤發生的行數、前後的 console 等等。

但是錯誤的具體 stack 不會全部顯示,預設只會顯示 error.message。如果希望更具體的知道是哪支檔案的第幾行出錯,必須要上傳 sourcemap 給 Sentry 才能被分析。

Replay 不是直接 video 錄影,而是記錄操作歷程然後重現。紀錄會存在瀏覽器的 localStorge。估計這應該會比較吃效能,沒研究太深,畫面上的具體文字會幫你馬賽克掉,Sentry 保證不會上傳記錄敏感資料。

Alert 就是通知系統,預設所有錯誤都會發 email 通知。可以進一步的用 if else 的概念在控制面板上設定怎樣的錯誤才發通知,要通知給誰。這個我沒摸太深,還不太確定怎麼用。

GitHub 聯動,可以確切知道發生錯誤的那行是哪個 commit,方便抓戰犯 (笑)。好像還能與 GitHub Action 自動部屬聯動,去自動記錄版本號之類的,我沒 try 過。

基本使用

Sentry 支持各種語言、框架,例如前端就有多種 SDK 套件,無論是用原生、Vue 還是 React,都可以對應。

初次申請帳號會有個 Quick Start 教學,一步一步教你基礎功能怎麼用,跟著跑一次就差不多了,有一些功能教學可以跳過,例如他會教你怎麼邀請 team member。參考前面的圖,左邊 menu 有個 Quick Start,可以在那邊確認、回顧。

申請帳號後,生成的後台會有自己的 domain,名稱會是一開始填的 Organization Name。個人用戶可以填自己的名子,後面可以再改。

https://username.sentry.io/issues/

教學最開始就會要你選語言、框架,基本上跟著走就好。不同語言跟框架都會有不同的 SDK 套件可以用,以 React 為例:

// main.tsx
import * as Sentry from '@sentry/react'
import { BrowserTracing } from '@sentry/tracing'

// 產品模式才啟用
if (import.meta.env.PROD) {
Sentry.init({
// 跟 Sentry 對接的 main key
dsn: import.meta.env.VITE_SENTRY_DSN,
// 標記用, 非必要, production | development | other...
environment: import.meta.env.MODE,
// 要啟用哪些功能
integrations: [
new BrowserTracing(),
new Sentry.Replay(),
],
// 對應 BrowserTracing 的採樣率
tracesSampleRate: 1.0,
// 對應 Replay 的採樣率
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
})
}

在進入點初始化之後,如果只要基礎功能,那把 BrowserTracing 加上去就可以用了。

發生錯誤時 Sentry 會自動蒐集上傳到後台。但如果有特別去 catch error 的話,就不會被 Sentry 蒐集到,需要手動發送。

try {
const res = await axios.get('...')
// ...
} catch (err) {
// 直接整包 Error 物件放進去
Sentry.captureException(err)

// 也可以只傳 Message,但平台上就只會有 msg 情報
if (isAxiosError<{ message: string }>(err)) {
const serverMsg = err.response?.data.message
Sentry.captureMessage(serverMsg || err.message)
} else if (err instanceof Error) {
Sentry.captureMessage(err.message)
}
}

上傳 Sourcemap

用 Vite or Webpack 進行打包都會做 minify,所以要上傳 sourcemap 給 Sentry,才能分析出具體是出錯的原始碼在哪。否則在後台上就只能看到 error.message 基本沒啥幫助。

Sentry 有針對不同的編譯、打包工具提供專門的整合工具,來方便上傳 sourcemap。如果是用原生寫,就得另外裝 Sentry CLI。

以 Vite 為例,官方文件給這些,也都有寫註解,相當詳細。這邊我把註解略掉。

import { defineConfig, loadEnv } from "vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";

export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd(), '')

return {
build: {
sourcemap: true,
},
plugins: [
sentryVitePlugin({
org: "example-org",
project: "example-project",
include: "./dist",
authToken: env.VITE_SENTRY_AUTH_TOKEN,
// release: env.RELEASE,
}),
],
}
});

雖然很詳細,但其實有坑……

org、project、authToken 這三個是連線用的 key。org 是 Organization id,也就是一開始輸入的組織名,在左上方帳戶下拉選單的 Organization Setting 裡面可以看,也可以直接看 domain。

project 就是在後台上建立的專案名,authToken 在帳戶的下拉選單,有個 API keys 點進去就是,需要自己新建,概念類似 GitHub 的 Personal Access Token。可以賦予每個 authToken 不同的權限。

由於一個專案可能會同時對接多個 Project,需要對正式站、測試站等等做區分,所以這些玩意用 env 環境變數來給會比較方便,也能避免被記到 git。

這三個 key 只有 Vite build 時會用到,在 Node.js 端執行,所以不用暴露給 Client 端,但官網的範例卻用了 VITE_ 開頭的環境變數,這其實是不需要的。

另外要注意 release 這屬性,這是填版本號。要上傳 sourcemap 就必須要給上 release 讓 Sentry 跟該版本的 sourcemap 最配對。這玩意其實是可以隨便填的,只是標記用。

sentryVitePlugin 填的 release 必須要跟 Sentry.init() 一致才可以。

// main.tsx
Sentry.init({
// ...
release: '...',
})

官方文件有說明,Vite plugin 不給 release 就會去抓 git commit 的 SHA 自動產生。如果要自動產生,那 sentryVitePlugin 以及進入點的 init,都不要填個屬性才行。

但是!! Vite 這邊有 bug,自動生成 release 其實是不 work 的。可參考這篇:

由於 Vite 的核心進入點其實是 index.html,而不是 main.js,所以 Sentry 的 vite 插件無法成功的自動注入 release。需要多填一個設定,告訴 Sentry js 的進入點是誰。

sentryVitePlugin({
// ...
releaseInjectionTargets: path.join(__dirname, './src/main.tsx'),
}),

基本的 init 設定跟 sourcemap 上傳搞定之後,應該就堪用了。剩下要如何更好的進行客製,還有哪些功能是必要的,我也還沒實戰經驗。

各種坑點

最後來說一下我碰到的各種坑。前兩個上面已經提過,不再重複:

  • 被 catch 的 error 需要手動發送給 Sentry
  • vite 插件的自動 release 生成不 work,需要改設定

本機 run dev 時的報錯不需要傳給 Sentry

要另外加 if 判定是否初始化。dsn 屬性不給也能有不啟動的效果,後續手動發送的 code 並不會壞掉。sentryVitePlugin 也可以加上這個判定。

一定要 build 後實測 Sentry 的行為

要確認 Sentry 的行為與記錄方式,要在 build 之後以產品模式實測才準確。本機 run dev 去測試會有很多盲點。

例如 React devtools、React Query (TanStack Query) 都僅會在 dev 模式去 echo 錯誤訊息到 console,並且會被 Sentry 記錄下來。但是正式產品環境,這些錯誤訊息是不會被 console 出來的。

如果你想說,靠這些 console 給 Sentry 記錄就夠了,不需要上傳 sourcemap 那就會很尷尬。到時候產品環境就只能看到 error.message,其他啥都看不到。

Sentry 會發一個 OPTIONS 預檢

Sentry 初始化後,所有的 Request 發送前,都會自動發一個 OPTIONS 且 Header 為 sentry-trace 的預檢,來確認是否 CORS 有被允許。

這個比較偏後端的範疇了,如果不知道有這行為,可能會一直被擋 CORS 不明所以。需要後端對這個 OPTIONS 請求回一個 200,並允許相關 origin 以及 headers。

// Express.js
app.options('*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers'])
res.status(200).send('ok')
})

Access-Control-Allow-Headers 需要開通 sentry-trace 等標頭。我這邊是偷懶,Request 來甚麼就開甚麼,視同給 * 全開。如果想要確認具體要允許哪些,可以把 Allow-Headers 這行拿掉,看瀏覽器報的 CORS 錯誤,就能知道需要開哪些。

如果有使用 Express CORS 之類的中間件,處理起來會簡單些。

Sourcemap 需要手動刪除

不特別處理的話,會被上傳到正式站,源碼會更輕鬆被別人看光。

Sentry 的 vite / webpack 集成工具,是依賴打包工具自己生成 sourcemap,它只做上傳動作而已。被生成的 sourcemap 會遺留在 dist 裡面,不會自動刪除。所以不做處理的話,就會一起被部署到正式機。

得自己寫個腳本在 build 之後去清掉 sourcemap。會有兩個地方需要清,一個是 *.js.map ,另一個是編譯後的 .js 屁股會帶有這樣的參照用註解:

//# sourceMappingURL=index-xxxx.js

理論上只要刪掉 *.js.map 就好,參照註解可刪可不刪,如果不刪的話是不會有問題,只是瀏覽器有可能會報個警告跟你說找不到 sourcemap。對於有強迫症的人會很難受。

只想刪 map 檔的話,可以單純用 cmd 指令 rm -rf 處理就好。

// package.json
"scripts": {
"build": "vite build && rm -rf dist/**/*.js.map"
}

如果參照註解也想刪的話,就得另外用 Node.js 寫腳本自動化處理。

// package.json
"scripts": {
"build": "vite build && pnpm postbuild",
"postbuild": "node postbuild.js"
}
// 這是 Node.js v18
// postbuild.js
import fs from 'node:fs/promises'
import path from 'node:path'

// 要清除的 sourcemap 的目標資料夾, vite 會放在 "dist/assets"
const rootDir = path.join('./dist', 'assets')

try {
// 取得目標資料夾的 files
const files = await fs.readdir(rootDir)

// 遍歷 files
files.forEach(async (file) => {
const filePath = path.join(rootDir, file)

// 移除 sourcemap
if (path.extname(file) === '.map') {
await fs.unlink(filePath)
}

// 移除 sourcemap 參照註解
if (path.extname(file) === '.js') {
const content = await fs.readFile(filePath, 'utf8')
const removed = content.replace(/(\r\n|\n)\/\/# sourceMappingURL=\S+/g, '')
await fs.writeFile(filePath, removed)
}
})

console.log('[PostBuild] sourcemap is already removed.\n')
} catch (err) {
console.log('[PostBuild]', err, '\n')
}

可以參考這篇討論,不過這討論有拉一些套件來做,我不想為了這個拉套件,就用純原生來寫。

以上,終わり。

--

--

Lastor
Code 隨筆放置場

Web Frontend / 3D Modeling / Game and Animation. 設計本科生,前遊戲業 3D Artist,專擅日本動畫與遊戲相關領域。現在轉職為前端工程師,以專業遊戲美術的角度涉足 Web 前端開發。