Repository navigation
feat(browser): Vue Router and Nuxt integration entries - #1131
JeremyFunk wants to merge 3 commits into
Conversation
Maple reviewConfidence 4/5 · likely safe to merge Adds
What was checked
Observability coverage: 3 of 3 changes observable
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Devin Review found 2 potential issues.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| app.config.errorHandler = (error, instance, info) => { | ||
| reportVueError(error, instance, info) |
There was a problem hiding this comment.
🔴 Vue errors escape disabled tracing
With tracing disabled or consent absent, MapleVue still reports component errors when a host OpenTelemetry provider exists. captureException falls back to that provider, so disabled capture creates exported error spans.
Learn more
The Vue plugin runs for component errors even when Maple has not started tracing. captureException calls mapleTracer, which falls back to the global tracer when Maple has no live provider. A host application can register that global provider independently of Maple. Thus an error reaches host telemetry despite Maple tracing being disabled or consent not being granted.
Example: An app registers its own WebTracerProvider and initializes Maple with tracing.enabled: false. A Vue render error passes through MapleVue and produces a vue.error span in the app's provider, although Maple error capture was disabled.
Recommended fix: Gate automatic Vue error capture on Maple's active tracing and consent state before calling captureException, while preserving the previous handler or Vue's default error handling. Verify both disabled tracing and denied consent with an independently registered global provider.
Was this helpful? React with 👍 or 👎 to provide feedback.
| router.beforeEach((to) => { | ||
| // A redirect stays in the span its original location opened | ||
| const origin = originOf(to) | ||
| if (origin === current) return | ||
| current = origin | ||
| startNavigation(to.path) | ||
| }) |
There was a problem hiding this comment.
🟡 Leave-guard requests lack navigation parent
When beforeRouteLeave starts requests, traceRouter opens the navigation span only after those requests start. Vue Router runs leave guards before global beforeEach, so their requests cannot nest under that navigation.
Learn more
Vue Router runs a component's beforeRouteLeave guard before its global beforeEach hooks. This integration starts the navigation span only from its global hook. Requests initiated synchronously inside a leave guard therefore start before the navigation span and lose their parent; an async leave guard can do substantial loading before this hook runs. The repository's leave-guard test also relies on this ordering.
Example: A page's beforeRouteLeave sends fetch('/api/save') before allowing /projects/1. The fetch span has no navigation parent, although the later route navigation is spanned.
Recommended fix: Hook the earliest supported navigation-start point if leave-guard work must be included, or document that component leave guards precede tracing and require explicit instrumentation. Keep the existing cancellation handling for navigations stopped before beforeEach.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Vue Router and Nuxt entries for
@maple-dev/browser, replacing the router hooks, error handler and Nuxt plugins the Vue frontend guide has customers copy. Builds on #1128 (/server, the navigation spans).@maple-dev/browser/vue→traceRouter(router)createRouter: apageload/navigatespan per navigation, named after the route (/projects/:id), ended once Vue rendered it; guard and route-chunk errors reported asvue_router.error@maple-dev/browser/vue→MapleVueapp.use(MapleVue): setsapp.config.errorHandlerso component errors reach Maple (vue.error,vue.error.info), then hands them back to Vue's own handling or the handler set before it@maple-dev/browser/vue→reportVueError(error, instance, info)vue:errorhook, your ownerrorHandler, or anerrorCapturedhook that stops an error@maple-dev/browser/nuxt→mapleNitroPluginexport default defineNitroPlugin(mapleNitroPlugin):Server-Timingon rendered pages viaserverTiming()@maple-dev/browser/nuxt→nameSsrSpan(router)export default defineNuxtPlugin(() => nameSsrSpan(useRouter()))in a.server.tsplugin: names the request spanssr /projects/:id()/nuxtdepends on@opentelemetry/apionly (vue-router is a type import).vueandvue-routerare optional peers (*);/vueimportsvueat runtime and Vue Router only for types, soMapleVueworks without a router. The main entry's bundle is unchanged (eager 37.42 kB, first-party 14.30 kB).Behavior
redirect) stays in the span its original location opened: the origin is tracked by identity throughto.redirectedFrom.nextTickafterafterEach, so what the new page'ssetupor immediate watchers start nests under it, unless another navigation started since.afterEachandonErroronly end the span of the navigation that opened it: an older navigation's guard error, or one stopped by a leave guard beforebeforeEach, doesn't end or rename the span in flight.traceRouteris idempotent per router.onErroralso logs the error, because registering a handler turns off Vue Router's own logging.MapleVuerethrows from its handler when no earlier handler exists, which sends the error back to Vue's default handling: production logs withconsole.error, development warns and throws. Errorstracedrecorded are skipped (shared dedupe).nameSsrSpanleaves a URL no route matches with the request span's own name, where the guide named it after the concrete path.mapleNitroPluginappends to an existingServer-Timingheader, whatever its casing.Decisions
MapleVue.app.use(MapleVue, { router })inmain.tswould add the guards after the ones the router file already registered, so an auth guard'sawait(and its requests) would fall outside the navigation span.traceRouter(router)stays in the router file, first.@nuxt/kit, runtime files importing Nuxt's virtual modules, and still couldn't configureinit()(regexes likepropagateTraceHeaderCorsUrlsdon't serialize through runtime config), so the client plugin stays in the app anyway.nameSsrSpan(router), not adefineNuxtPlugin(fn)-ready plugin. A function whose parameter referencesNuxtApp/Appmakes Nuxt's generated plugin-injection types circular (TS7022onexport default defineNuxtPlugin(plugin)); taking the router typechecks and also works for other Vue SSR setups.Tests
src/vue/index.browser.test.ts(Chromium, realvue+vue-router, run against both Vue Router 5 and 4): template naming incl. nested routes and the catch-all,setupwork nesting under the span, record and guard redirects, redirect back to the page on screen, aborted, replaced, cancelled-without-guards, cancelled by a leave-guard abort, duplicated, query/hash/back, guard errors (logged, deduped withtraced), a stale navigation's error, doubletraceRouter;MapleVuehanding errors back to Vue, chaining an earlier handler, dedupe.src/nuxt/index.test.ts(node, AsyncLocalStorage context): header added/appended (case-insensitive)/untouched without a span; SSR span renamed, unmatched URL left alone.packages/browser: typecheck, 184 tests, build, size budget.End to end
The Vue 3.5 + Vue Router 5.3 SPA and the Nuxt 4.5 app instrumented with the old guide code were each run (production build) and then switched to the packed tarball, and driven with the same Playwright scenarios against an OTLP sink and traced test APIs. Span trees (names, parents, status,
url.path, interrupted flag, exception events) and console errors were compared with ids and timings stripped./,/projects/1, reloadpageload /projects/:id→ loader → 2 fetches → API/oldnavigate /projects/:idwith loader, fetches and API spansnavigate /,navigate /projects/:idper stepnavigateINTERRUPTED +navigate /projects/:id; loader outlives its span/projects/missing,/does-not-existnavigate /:pathMatch(.*)*/api/boomloader,vue.errorwithvue.error.info, fetch 500)traceparent, API didssr /projects/:id()→ SSR loader → fetch → API,pageloadits child; header on HTML only; new trace per reloadnavigate /projects/:id();vue.errortwice for a template error (two renders)ssr /does-not-existGET(no concrete path in the span name)defineNitroPlugin(mapleNitroPlugin),defineNuxtPlugin(() => nameSsrSpan(useRouter()))andnuxtApp.hook("vue:error", reportVueError)passnuxt typecheck; the Vue app passesvue-tsc.Known limits
beforeEach, registered before any plugin's, so it's outside the navigation span (same as the guide).mapleNitroPlugincan't see route rules: pages cached byswr/isr/cacherules or a CDN keep their header. Those apps add it from their ownrender:responsehook withserverTiming()(documented).nameSsrSpanrenames the active span; that's the HTTP instrumentation's request span unless the app keeps its own span active around the router.router.onErrormakes Vue Router stop logging, sotraceRouterlogs guard errors itself; an app that already logs in its ownonErrorsees them twice.*(as in feat(browser): server-side traced, /server and Next.js entries #1128); the entries are for Vue 3 with Vue Router 4 or 5.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.