Skip to content

在 Nuxt3 接入 Sentry 实现错误监控及自动上报

Posted on:2024-01-26 at 10:32

记录一下 Nuxt3 接入 Sentry 的过程和踩过的坑

目录

Sentry for Nuxt 的官方包 @nuxtjs/sentry 目前只支持 Nuxt2,它的开发者表示没有动力为 Nuxt3 开发这个模块

在我自己为 Nuxt3 接入 Sentry 时还是踩了不少坑的,前后花的时间至少有 5 个小时,这里直接贴出最后可用的代码。

Sentry Module

我把 sentry 相关的代码都封装到了 Nuxt Module 中,不过没有花心思设计配置项,都是直接读取 public runtime config,所以有些地方需要按自己的情况去修改,需要修改的地方都已标注。

modules/sentry/index.ts
import {
defineNuxtModule,
createResolver,
addPlugin,
addServerPlugin,
addComponentsDir,
addImports,
addTypeTemplate,
} from "@nuxt/kit";
export default defineNuxtModule({
setup(options, nuxt) {
const { resolve } = createResolver(import.meta.url);
addPlugin({
mode: "client",
src: resolve("./runtime/plugins/nuxt"),
});
addServerPlugin(resolve("./runtime/plugins/nitro"));
addComponentsDir({
path: resolve("runtime/components"),
});
addImports({
name: "useSentry",
from: resolve("runtime/composables"),
});
addTypeTemplate({
filename: "types/sentry-module.d.ts",
getContents: () => `// Generated by sentry-module
import * as SentryVue from '@sentry/vue';
import * as SentryNode from '@sentry/node';
declare module '#app' {
interface NuxtApp {
$sentry?: Pick<typeof SentryVue, 'captureException' | 'captureMessage' | 'withScope'>;
}
}
declare module 'vue' {
interface ComponentCustomProperties {
$sentry?: Pick<typeof SentryVue, 'captureException' | 'captureMessage' | 'withScope'>;
}
}
declare module 'h3' {
interface H3EventContext {
$sentry?: typeof SentryNode;
}
}
export {};`,
});
},
});
modules/sentry/runtime/plugins/nuxt.ts
import * as Sentry from "@sentry/vue";
export default defineNuxtPlugin({
name: "sentry",
setup(nuxtApp) {
// TODO: 需要改为你自己的配置项
const { public: publicConfig } = useRuntimeConfig();
if (!publicConfig.sentry.dsn) {
return;
}
Sentry.init({
app: nuxtApp.vueApp,
// TODO: 需要改为你自己的配置项
dsn: publicConfig.sentry.dsn,
release: publicConfig.appVersion,
environment: publicConfig.env,
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(useRouter()),
}),
],
// Sample rate for performance monitoring.
tracesSampleRate: 0.1,
// Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled
// TODO: 需要改为你自己的配置项
tracePropagationTargets: ["localhost", /^https:\/\/example\.com\/api/],
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0.5,
beforeSend: (event, hint) => {
// TODO: 需要改为你自己的配置项
// 上报用户信息
const user = useAuthUser();
if (user.value) {
event.user = event.user || {};
event.user.id = user.value.id;
}
return event;
},
});
// 只导出部分接口,可以减小 entry.js 体积
nuxtApp.provide("sentry", {
captureException: Sentry.captureException,
captureMessage: Sentry.captureMessage,
withScope: Sentry.withScope,
});
},
});
modules/sentry/runtime/plugins/nitro.ts
import * as Sentry from "@sentry/node";
import { H3Error } from "h3";
export default defineNitroPlugin(nitroApp => {
// TODO: 需要改为你自己的配置项
const { public: publicConfig } = useRuntimeConfig();
if (!publicConfig.sentry.dsn) {
return;
}
Sentry.init({
// TODO: 需要改为你自己的配置项
dsn: publicConfig.sentry.dsn,
release: publicConfig.appVersion,
environment: publicConfig.env,
// Sample rate for performance monitoring.
tracesSampleRate: 0.1,
// Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled
// TODO: 需要改为你自己的配置项
tracePropagationTargets: ["localhost", /^https:\/\/example\.com\/api/],
});
nitroApp.hooks.hook("request", event => {
event.context.$sentry = Sentry;
});
nitroApp.hooks.hook("error", async err => {
// Do not handle 404s
if (err instanceof H3Error) {
if (err.statusCode === 404) {
return;
}
}
Sentry.captureException(err);
});
nitroApp.hooks.hookOnce("close", async () => {
await Sentry.close(2000);
});
});

然后在 Nuxt 中添加 sentry 配置,启动服务时设置环境变量 SENTRY_DSNNUXT_PUBLIC_SENTRY_DSN 即可。

nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
sentry: {
dsn: process.env.SENTRY_DSN || "",
},
},
},
});

其他工具类:

modules/sentry/runtime/composables.ts
type Sentry = Exclude<ReturnType<typeof useNuxtApp>["$sentry"], undefined>;
export const useSentry = (scope: (sentry: Sentry) => void) => {
const { $sentry } = useNuxtApp();
if ($sentry) {
scope($sentry);
}
};

Sentry Replay

懒加载 Sentry Replay 插件

modules/sentry/runtime/components/Sentry.vue
<script setup lang="ts">
onMounted(async () => {
try {
// lazy load sentry replay
const { addIntegration, Replay } = await import("@sentry/vue");
addIntegration(
new Replay({
beforeErrorSampling: event => {
// 这里加了一个自定义 tag,来过滤有些错误不需要记录 replay
if (event.tags?.["no-sentry-replay"]) {
return false;
}
return true;
},
})
);
} catch {
// 重复加载 sentry replay 插件会抛出异常
// 正常情况下不会出现这个问题,在开发环境热加载有可能出现
// 这个集成并不是很重要,只是作为 debug 的辅助手段,所以加载失败也无所谓
}
});
</script>
<template></template>

这里单独写一个 Sentry.vue 组件来懒加载 replay 插件是处于性能考虑,不用 hooks 实现的原因是我试过了 app:mounted 等 hook 懒加载时,控制台会出现大量警告。

app.vue 中引入这个组件:

app.vue
<template>
<ClientOnly>
<Sentry />
</ClientOnly>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

手动上报 Sentry

Sentry 会默认监控 vue 组件异常并上报,手动上报时只需要用 useSentry 这个 composable

<script setup lang="ts">
onMounted(() => {
useSentry(sentry => sentry.captureException(new Error("Something went wrong")));
});
</script>

Nitro Server 的报错也会自动上报,手动上报的话可以用 event.context.$sentry

server/routes/test.ts
export default defineEventHandler(async event => {
try {
throw new Error("Something went wrong");
} catch (err) {
event.context.$sentry?.captureException(err);
}
});