Skip to content

[Nuxt 5] Replace the Vue integration’s reliance on app.mixin() #23375

Description

@s1gr1d

Nuxt 5 disables Vue’s Options API by default
Refernce: nuxt/nuxt#35791

For the time being, it can be enabled in Nuxt with:

export default defineNuxtConfig({
  vue: {
    optionsApi: true
  }
})

What app.mixin() captures today

vueInit() in packages/vue/src/integration.ts registers one mixin when tracing is on. That mixin produces every Vue UI span we emit:

Span Op Operation Mixin hooks (start / end) On by default
Application Render ui.render any the root's first hook starts it, a timeout debounce (2000 ms) ends it yes
Vue <Root> ui.mount mount beforeMount / mounted yes
Vue <ComponentName> ui.mount mount beforeMount / mounted no (trackComponents)
Vue <ComponentName> ui.mount activate activated / deactivated no (trackComponents)
Vue <ComponentName> ui.mount create beforeCreate / created no (trackComponents + hooks: ['create'])
Vue <ComponentName> ui.update update beforeUpdate / updated no (trackComponents + hooks: ['update'])
Vue <ComponentName> ui.unmount unmount (Vue 3) beforeUnmount / unmounted no (trackComponents + hooks: ['unmount'])
Vue <ComponentName> ui.unmount destroy (Vue 2) beforeDestroy / destroyed no (trackComponents + hooks: ['destroy'])

Not affected: Pageload and navigation transactions come from browserTracingIntegration and the router. Error capture uses app.config.errorHandler, which Vue does not gate. Pinia and TanStack Router never touch mixins.

The alternative: register Composition API hooks in setup

Composition API lifecycle hooks are not gated by __VUE_OPTIONS_API__. They write to instance.m, instance.u, and so on, and the renderer invokes those arrays unconditionally. The question is only how to reach every component's setup.

Two mechanisms, and the split runs between the root component and everything else.

Runtime, for the root. createApp stores the root component as app._component, a plain writable property, and shallow-copies it first, so mutating app._component.setup in vueInit() does not touch the user's module. Function root components are not copied, so replace the property instead of mutating it. This covers both default-on spans with no plugin and no new install step.

Build time, for trackComponents. A Vite, Rollup, or webpack plugin adds a setup to each .vue module's default export that registers the hooks and then calls the original setup. Wrap by adding to the existing options object rather than returning a new component, so name and __file survive.

Operation Composition API hook Runtime root wrap Build-time plugin
mount (default on) onBeforeMount / onMounted Application Render and Vue <Root>, identical to the mixin every .vue the plugin transforms
activate (default on) onActivated / onDeactivated never fires, the root is not inside <KeepAlive> every .vue the plugin transforms
update (opt-in) onBeforeUpdate / onUpdated root re-renders only every .vue the plugin transforms
unmount (opt-in) onBeforeUnmount / onUnmounted app.unmount() only every .vue the plugin transforms
create (opt-in) none, setup is the create phase approximate, brackets the original setup approximate, same

Component names stay correct. compiler-sfc emits __name in production builds, and formatComponentName already reads it.

What it misses: render function and (non SFC) JSX components, runtime compiled templates, CDN builds, and pre-compiled components in node_modules, including Nuxt's own <NuxtPage> and <NuxtLayout>, unless the plugin also transforms node_modules.

Nuxt can get the plugin through the existing addVitePlugin call in packages/nuxt/src/module.ts. Plain @sentry/vue users need a new install step.

Vue 2 keeps the mixin

We cannot wrap setup at runtime in Vue 2, and Vue 2.7 does not change that.

The runtime wrap needs a handle on the root component's options object before Vue instantiates it. In Vue 3, createApp(App) gives us exactly that as app._component. In Vue 2 the integration receives the Vue constructor, the user calls new Vue({...}) afterwards, and that options object never reaches us. Vue 2.7 backported the Composition API but explicitly did not port createApp, and 2.7 is the last Vue 2 minor.

Warning users on missing mixins

app.mixin() is a silent no-op when __VUE_OPTIONS_API__ is false, and Vue only warns in dev.

We can detect if mixins are disabled and we warn the users: #23568

Metadata

Metadata

Assignees

Labels

javascriptPull requests that update javascript code

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions