Skip to content

feat(browser): Vue Router and Nuxt integration entries - #1131

Open
JeremyFunk wants to merge 3 commits into
feat/browser-server-nextjsfrom
feat/browser-vue
Open

JeremyFunk wants to merge 3 commits into
feat/browser-server-nextjsfrom
feat/browser-vue

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

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).

Export Purpose
@maple-dev/browser/vue → traceRouter(router) Call right after createRouter: a pageload/navigate span per navigation, named after the route (/projects/:id), ended once Vue rendered it; guard and route-chunk errors reported as vue_router.error
@maple-dev/browser/vue → MapleVue app.use(MapleVue): sets app.config.errorHandler so 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) For Nuxt's vue:error hook, your own errorHandler, or an errorCaptured hook that stops an error
@maple-dev/browser/nuxt → mapleNitroPlugin export default defineNitroPlugin(mapleNitroPlugin): Server-Timing on rendered pages via serverTiming()
@maple-dev/browser/nuxt → nameSsrSpan(router) export default defineNuxtPlugin(() => nameSsrSpan(useRouter())) in a .server.ts plugin: names the request span ssr /projects/:id()

/nuxt depends on @opentelemetry/api only (vue-router is a type import). vue and vue-router are optional peers (*); /vue imports vue at runtime and Vue Router only for types, so MapleVue works without a router. The main entry's bundle is unchanged (eager 37.42 kB, first-party 14.30 kB).

Behavior

  • A redirect (a guard's, or a route record's redirect) stays in the span its original location opened: the origin is tracked by identity through to.redirectedFrom.
  • The span ends on nextTick after afterEach, so what the new page's setup or immediate watchers start nests under it, unless another navigation started since.
  • A navigation replaced by one that ran through the guards ends as interrupted there. One cancelled by a navigation that ran no guards (a link back to the page on screen while it was pending) now ends as interrupted too, instead of staying open until the next click. A guard redirect back to the page on screen (a duplicated failure) ends normally instead of leaking.
  • afterEach and onError only end the span of the navigation that opened it: an older navigation's guard error, or one stopped by a leave guard before beforeEach, doesn't end or rename the span in flight.
  • traceRouter is idempotent per router. onError also logs the error, because registering a handler turns off Vue Router's own logging.
  • MapleVue rethrows from its handler when no earlier handler exists, which sends the error back to Vue's default handling: production logs with console.error, development warns and throws. Errors traced recorded are skipped (shared dedupe).
  • nameSsrSpan leaves a URL no route matches with the request span's own name, where the guide named it after the concrete path. mapleNitroPlugin appends to an existing Server-Timing header, whatever its casing.

Decisions

  • No router option on MapleVue. app.use(MapleVue, { router }) in main.ts would add the guards after the ones the router file already registered, so an auth guard's await (and its requests) would fall outside the navigation span. traceRouter(router) stays in the router file, first.
  • No Nuxt module. Two one-line plugins do the server side. A module would need @nuxt/kit, runtime files importing Nuxt's virtual modules, and still couldn't configure init() (regexes like propagateTraceHeaderCorsUrls don't serialize through runtime config), so the client plugin stays in the app anyway.
  • nameSsrSpan(router), not a defineNuxtPlugin(fn)-ready plugin. A function whose parameter references NuxtApp/App makes Nuxt's generated plugin-injection types circular (TS7022 on export default defineNuxtPlugin(plugin)); taking the router typechecks and also works for other Vue SSR setups.

Tests

  • src/vue/index.browser.test.ts (Chromium, real vue + vue-router, run against both Vue Router 5 and 4): template naming incl. nested routes and the catch-all, setup work 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 with traced), a stale navigation's error, double traceRouter; MapleVue handing 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.

Scenario Old guide code This PR
Page load /, /projects/1, reload pageload /projects/:id → loader → 2 fetches → API identical
Click, param change, query, redirect /old one navigate /projects/:id with loader, fetches and API spans identical
Hash link, query-only push, back/forward navigate /, navigate /projects/:id per step identical
Interrupted (slow chunk / slow loader) navigate INTERRUPTED + navigate /projects/:id; loader outlives its span identical
/projects/missing, /does-not-exist 404 fetch Error only, then navigate /:pathMatch(.*)* identical
Loader / render / click errors, /api/boom one exception each (loader, vue.error with vue.error.info, fetch 500) identical, same console output
Third-party / first-party propagation third party got no traceparent, API did identical
Nuxt page load, reload, SSR loader error, 404 ssr /projects/:id() → SSR loader → fetch → API, pageload its child; header on HTML only; new trace per reload identical
Nuxt client nav, loader and render errors navigate /projects/:id(); vue.error twice for a template error (two renders) identical
Nuxt unmatched URL ssr /does-not-exist GET (no concrete path in the span name)

defineNitroPlugin(mapleNitroPlugin), defineNuxtPlugin(() => nameSsrSpan(useRouter())) and nuxtApp.hook("vue:error", reportVueError) pass nuxt typecheck; the Vue app passes vue-tsc.

Known limits

  • Nuxt route middleware runs in Nuxt's own beforeEach, registered before any plugin's, so it's outside the navigation span (same as the guide).
  • mapleNitroPlugin can't see route rules: pages cached by swr/isr/cache rules or a CDN keep their header. Those apps add it from their own render:response hook with serverTiming() (documented).
  • nameSsrSpan renames the active span; that's the HTTP instrumentation's request span unless the app keeps its own span active around the router.
  • Registering router.onError makes Vue Router stop logging, so traceRouter logs guard errors itself; an app that already logs in its own onError sees them twice.
  • Peer ranges are * (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.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Devin Review

@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 4/5 · likely safe to merge
Guard, redirect, cancellation and error-handoff paths are all exercised by the new browser tests against Vue Router 4 and 5.
quality 100/100 · no findings · tests covered · risk medium · 3/3 new units observable

Adds /vue and /nuxt entries to @maple-dev/browser (router navigation spans, Vue error reporting, Nitro Server-Timing, SSR span naming), with peer deps, exports and knip wiring. Contained to the browser SDK and safe to merge.

  • traceRouter opens one navigation span per Vue Router navigation, named from the route template
  • MapleVue / reportVueError report Vue-caught errors as vue.error, handing them back to Vue
  • nameSsrSpan renames the Nitro request span to ssr <route>
  • mapleNitroPlugin appends a traceparent Server-Timing entry to rendered pages
What was checked
  • traceRouter idempotence and the origin-identity redirect tracking (vue/index.ts:32, :22, tests at vue/index.browser.test.ts:166, :176)
  • MapleVue rethrow matches Vue's callWithErrorHandling(appErrorHandler…) path, so an unhandled error still reaches Vue's default handling
  • nameSsrSpan/mapleNitroPlugin leave an unmatched URL and a response with no active span unchanged (nuxt/index.test.ts:71, :101)
Observability coverage: 3 of 3 changes observable
Change Kind Observable Evidence
traceRouter navigation spans (vue/index.ts) client navigation yes startNavigation/endNavigation on Maple's tracer; guard errors recorded via captureException as vue_router.error
MapleVue / reportVueError (vue/index.ts) client error capture yes captureException(error, { name: "vue.error", attributes: { "vue.error.info": info } })
mapleNitroPlugin / nameSsrSpan (nuxt/index.ts) server render hook yes serverTiming() on render:response; trace.getActiveSpan()?.updateName("ssr <route>")

7be71cb · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 2760e232-1553-46bd-9d81-f22455e959c5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +85 to +86
app.config.errorHandler = (error, instance, info) => {
reportVueError(error, instance, info)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +37 to +43
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)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant