Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
bump: patch
type: fix
---

Handle null instances and fall back to the SFC filename in the Vue error handler.

Vue 3 invokes `app.config.errorHandler` with a null instance for errors raised outside a component context; the handler now reports those as `[unknown Vue component]` instead of throwing away the original error with a `TypeError` of its own. For components without an explicit `name` or `<script setup>` `__name`, the handler now falls back to the basename of `$options.__file` (set by vue-loader; in production builds only when its `exposeFilename` option is enabled), so errors group per component instead of all under `[unknown Vue component]`.
72 changes: 72 additions & 0 deletions packages/vue/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,76 @@ describe("Vue errorHandler", () => {
version: version
})
})

it("falls back to the basename of the __file option in Vue 3", () => {
const err = new Error("test")
const version = "v3.0.0"

const vue3Mock: any = {
$options: {
__file: "src/components/TestComponent.vue"
}
}

errorHandler(appsignal, { version } as VueApp)(err, vue3Mock, "INFO")

expect(mock.setAction).toBeCalledWith("TestComponent")
})

it("reads a basename-only __file option as exposed in production builds", () => {
const err = new Error("test")
const version = "v3.0.0"

const vue3Mock: any = {
$options: {
__file: "TestComponent.vue"
}
}

errorHandler(appsignal, { version } as VueApp)(err, vue3Mock, "INFO")

expect(mock.setAction).toBeCalledWith("TestComponent")
})

it("prefers an explicit name over the __file option", () => {
const err = new Error("test")
const version = "v3.0.0"

const vue3Mock: any = {
$options: {
name: "ExplicitName",
__file: "src/components/TestComponent.vue"
}
}

errorHandler(appsignal, { version } as VueApp)(err, vue3Mock, "INFO")

expect(mock.setAction).toBeCalledWith("ExplicitName")
})

it("reports an unknown component when Vue 3 passes a null instance", () => {
const err = new Error("test")
const version = "v3.0.0"

errorHandler(appsignal, { version } as VueApp)(err, null, "INFO")

expect(mock.setAction).toBeCalledWith("[unknown Vue component]")

expect(mock.setError).toBeCalledWith(err)

expect(appsignal.send).toBeCalled()
})

it("reports an unknown component when no name source is present", () => {
const err = new Error("test")
const version = "v3.0.0"

const vue3Mock: any = {
$options: {}
}

errorHandler(appsignal, { version } as VueApp)(err, vue3Mock, "INFO")

expect(mock.setAction).toBeCalledWith("[unknown Vue component]")
})
})
25 changes: 21 additions & 4 deletions packages/vue/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
import { VueApp } from "./types"
import type Appsignal from "@appsignal/javascript"

function componentName(vm: any): string | undefined {
if (vm?.$vnode) {
return vm.$vnode.componentOptions.tag // Vue 2
}

// `__file` comes from vue-loader: always in development, in production
// only as a basename and only when the app enables its `exposeFilename` option.
const options = vm?.$options
if (!options) return undefined

const file = options.__file
?.split("/")
.pop()
?.replace(/\.vue$/, "")

return options.name || options.__name || file
}

export function errorHandler(appsignal: Appsignal, app?: VueApp) {
const version = app?.version ?? ""

// Vue 3 invokes the handler with a null `vm` for errors raised outside a
// component context, so nothing here may assume a component instance.
return function (error: any, vm: any, info: string) {
const componentName = vm.$vnode
? vm.$vnode.componentOptions.tag // Vue 2
: vm.$options.name || vm.$options.__name // Vue 3
const span = appsignal.createSpan()

span
.setAction(componentName || "[unknown Vue component]")
.setAction(componentName(vm) || "[unknown Vue component]")
.setTags({ framework: "Vue", info, version })
.setError(error)

Expand Down