-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Export @sentry/vue errorHandler #11471
Comments
Well upon further testing, the second solution that lets the event propogate doesn't work - it seemingly attaches to the top level scope (which, I guess I expected) and thus locks all future errors to that appName |
To be able to ship as we need error reporting yesterday I've ended up vendoring a modified version of https://github.com/getsentry/sentry-javascript/blob/develop/packages/vue/src/errorhandler.ts#L12-L56 Using an ErrorBoundary.vue component looking like thisimport { type SlotsType, defineComponent, onErrorCaptured } from 'vue'
import { sentryErrorHandler } from '../sentry'
import { type ViewModel } from '@sentry/vue/types/types'
/**
* This is a transparent component that captures errors propagating from
* descendant components and captures it with sentry with extra information
* about the microfrontend to ensure it's routed to the correct sentry project.
*
* It renders the default slot with props passed through transparently.
*/
export default defineComponent({
name: 'ErrorBoundary',
slots: Object as SlotsType<{ default: Record<string, unknown> }>,
setup (props, { slots, attrs }) {
onErrorCaptured((err, vm, info) => {
sentryErrorHandler(err, vm as ViewModel, info, __APP_NAME__)
return false
})
return () => slots.default(props)
}
}) And sentry.ts looking like this/**
* Source: https://github.com/getsentry/sentry-javascript/blob/83f7ccec7298d77c862b83fce4ef6c1439c31a7e/packages/vue/src/errorhandler.ts
* @license MIT - Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors. All rights reserved.
*/
export function sentryErrorHandler (error: Error, vm: ViewModel, lifecycleHook: string, appName: string): void {
const componentName = formatComponentName(vm, false)
const trace = vm ? generateComponentTrace(vm) : ''
const metadata: Record<string, unknown> = {
componentName,
lifecycleHook,
trace
}
if (vm) {
// Vue2 - $options.propsData
// Vue3 - $props
if (vm.$options?.propsData) {
metadata.propsData = vm.$options.propsData
} else if (vm.$props) {
metadata.propsData = vm.$props
}
}
// Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time.
setTimeout(() => {
withScope(s => {
s.setTag('appName', appName)
s.captureException(error, {
captureContext: { contexts: { vue: metadata } },
mechanism: { handled: false }
})
})
})
const hasConsole = typeof console !== 'undefined'
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
const message = `Error in ${lifecycleHook}: "${error && error.toString()}"`
if (hasConsole) {
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.error(`[Vue warn]: ${message}${trace}`)
})
}
}
/**
* Vendored from https://github.com/vuejs/vue/blob/612fb89547711cacb030a3893a0065b785802860/src/core/util/debug.js
* with types only changes.
*
* @license MIT - Copyright (c) 2013-present, Yuxi (Evan) You
* @license MIT - Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors. All rights reserved.
*/
const classifyRE = /(?:^|[-_])(\w)/g
const classify = (str: string): string => str.replace(classifyRE, c => c.toUpperCase()).replace(/[-_]/g, '')
const ROOT_COMPONENT_NAME = '<Root>'
const ANONYMOUS_COMPONENT_NAME = '<Anonymous>'
const repeat = (str: string, n: number): string => {
return str.repeat(n)
}
export const formatComponentName = (vm?: ViewModel, includeFile?: boolean): string => {
if (!vm) {
return ANONYMOUS_COMPONENT_NAME
}
if (vm.$root === vm) {
return ROOT_COMPONENT_NAME
}
// https://github.com/getsentry/sentry-javascript/issues/5204 $options can be undefined
if (!vm.$options) {
return ANONYMOUS_COMPONENT_NAME
}
const options = vm.$options
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let name = options.name || options._componentTag
const file = options.__file
if (!name && file) {
const match = file.match(/([^/\\]+)\.vue$/)
if (match) {
name = match[1]
}
}
return (
(name ? `<${classify(name)}>` : ANONYMOUS_COMPONENT_NAME) + (file && includeFile !== false ? ` at ${file}` : '')
)
}
export const generateComponentTrace = (vm?: ViewModel): string => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (vm && (vm._isVue || vm.__isVue) && vm.$parent) {
const tree = []
let currentRecursiveSequence = 0
while (vm) {
if (tree.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const last = tree[tree.length - 1] as any
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (last.constructor === vm.constructor) {
currentRecursiveSequence++
vm = vm.$parent // eslint-disable-line no-param-reassign
continue
} else if (currentRecursiveSequence > 0) {
tree[tree.length - 1] = [last, currentRecursiveSequence]
currentRecursiveSequence = 0
}
}
tree.push(vm)
vm = vm.$parent // eslint-disable-line no-param-reassign
}
const formattedTree = tree
.map(
(vm, i) =>
`${
(i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) +
(Array.isArray(vm)
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
? `${formatComponentName(vm[0])}... (${vm[1]} recursive calls)`
: formatComponentName(vm))
}`
)
.join('\n')
return `\n\nfound in\n\n${formattedTree}`
}
return `\n\n(found in ${formatComponentName(vm)})`
} the main change to the latter is that I hardcoded our settings, and added a |
Hey @swantzter apologies for the late reply! Are you fine with the custom code you wrote for this or do you still need a specific export from the SDK? We could generally think about exporting the errorHandler but we're a bit swamped with tasks at the moment, so I'm just trying to prioritize. As for the ask around the scopes and propagation context: I believe I answered this (at least partially) here. Short version: There's very little we can do to retain scope integrity for MFE setups globally as long as there is no async context handling strategy for browsers. |
@Lms24 no worries. The custom code route I went with works well for now, but it feels a little risky to keep in sync in case you change the error handler upstream. |
Problem Statement
I'm trying to write an ErrorBoundary component to scope components to the right vue app in our nested-vue-app microfrontends setup in order to capture them with specific tags to make our multiplexed transport route them correctly (as described on https://docs.sentry.io/platforms/javascript/guides/vue/best-practices/micro-frontends/#manually-route-errors-to-different-projects)
I have a component looking like this where I'd like to add some tags to the scope, then have it processed with Sentry's error handler, without having it propagate higher up to other ErrorBoundaries
Solution Brainstorm
Either, it'd be nice to export https://github.com/getsentry/sentry-javascript/blob/develop/packages/vue/src/errorhandler.ts#L12-L56 so you can create a "custom" instance of that error handler
Alternatively, is there maybe a way to set these tags at the current scope when it gets to the error handler, keep propagating it, and skip setting those tags if they're already set at higher error boundary functions?
The documentation recommends against it, but could something like this work perhaps?
The text was updated successfully, but these errors were encountered: