Files
BYW-VoiceHub/app/plugins/sentry.client.ts
T

154 lines
4.4 KiB
TypeScript

import * as Sentry from '@sentry/vue'
import type { Event, EventHint } from '@sentry/vue'
import {
getSentryEventSearchText,
isExpectedUpstreamMusicError
} from '~/utils/sentryUpstreamMusicErrors'
let sentryClientInitialized = false
let sentryClientInitializing = false
let instanceId = ''
const TELEMETRY_STORAGE_KEY = 'voicehub.telemetryEnabled'
const isTelemetryDisabledLocally = (): boolean => {
try {
return typeof localStorage !== 'undefined' &&
localStorage.getItem(TELEMETRY_STORAGE_KEY) === 'false'
} catch {
return false
}
}
const setTelemetryStorageValue = (value: string) => {
try {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(TELEMETRY_STORAGE_KEY, value)
}
} catch {
// 隐私模式可能禁用 localStorage,遥测开关以服务端返回为准。
}
}
const shouldDropClientEvent = (event: Event, hint?: EventHint): boolean => {
if (isTelemetryDisabledLocally()) {
return true
}
const originalException = hint?.originalException
if (originalException instanceof DOMException && originalException.name === 'AbortError') {
return true
}
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
return true
}
// 外部音源接口波动属于预期失败,本地交互提示即可。
if (isExpectedUpstreamMusicError(getSentryEventSearchText(event, hint))) {
return true
}
const message = (event.message || '').toLowerCase()
if (message.includes('aborterror') || message.includes('networkerror')) {
return true
}
const frames = event.exception?.values?.flatMap(value => value.stacktrace?.frames || []) || []
if (frames.some(frame => {
const filename = frame.filename || ''
return filename.startsWith('chrome-extension://') || filename.startsWith('moz-extension://')
})) {
return true
}
return false
}
export default defineNuxtPlugin((nuxtApp) => {
if (!import.meta.client || sentryClientInitialized || sentryClientInitializing) {
return
}
const config = useRuntimeConfig()
const sentryConfig = config.public?.sentry
if (!sentryConfig?.enabled || !sentryConfig.dsn) {
return
}
sentryClientInitializing = true
void (async () => {
try {
const response = await $fetch<{
success?: boolean
data?: { instanceId?: string; telemetryEnabled?: boolean }
}>('/api/system/instance')
if (!response?.data?.telemetryEnabled) {
setTelemetryStorageValue('false')
return
}
setTelemetryStorageValue('true')
instanceId = response.data.instanceId || ''
console.log(`[VoiceHub] 遥测已开启,实例 ID: ${instanceId}(提交 Bug 时可提供此 ID 以便开发者定位)`)
const deploymentTarget = config.public?.isNetlify
? 'netlify'
: import.meta.env.VERCEL
? 'vercel'
: 'self-hosted-node'
const sentryRouter = nuxtApp.$router as NonNullable<
Parameters<typeof Sentry.browserTracingIntegration>[0]
>['router']
const integrations = [Sentry.browserTracingIntegration({ router: sentryRouter })]
integrations.push(
Sentry.consoleLoggingIntegration({
levels: import.meta.dev ? ['log', 'warn', 'error'] : ['error']
})
)
if (
sentryConfig.replaysSessionSampleRate > 0 ||
sentryConfig.replaysOnErrorSampleRate > 0
) {
integrations.push(Sentry.replayIntegration())
}
Sentry.init({
app: nuxtApp.vueApp,
dsn: sentryConfig.dsn,
environment: sentryConfig.environment,
release: sentryConfig.release || undefined,
integrations,
enableLogs: true,
tracesSampleRate: sentryConfig.tracesSampleRate,
replaysSessionSampleRate: sentryConfig.replaysSessionSampleRate,
replaysOnErrorSampleRate: sentryConfig.replaysOnErrorSampleRate,
beforeSend(event, hint) {
if (shouldDropClientEvent(event, hint)) {
return null
}
return event
}
})
sentryClientInitialized = true
Sentry.setTag('runtime', 'vue')
Sentry.setTag('deployment_target', deploymentTarget)
if (instanceId) {
Sentry.setTag('instance_id', instanceId)
Sentry.setContext('instance', { instanceId })
}
} catch (error) {
setTelemetryStorageValue('false')
console.warn('[Sentry] Failed to initialize client telemetry:', error)
} finally {
sentryClientInitializing = false
}
})()
})