tracing: emit a span for `+server` route handlers
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 20.8k
- Forks
- 2.3k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 156
Description
Describe the problem
With tracing.server enabled, SvelteKit wraps handle/resolve, load, form actions, and remote functions in record_span. When one of those throws an Error or an HttpError with status >= 500, the handler span gets recordException and SpanStatusCode.ERROR.
+server.js handlers (GET, POST, QUERY, fallback, and so on) are not wrapped. render_endpoint resolves mod[method] || mod.fallback (plus HEAD to GET) and calls the handler without a record_span.
So a +server request produces sveltekit.handle.root, optional sveltekit.handle.sequenced.*, and sveltekit.resolve, but no span for the handler itself:
sveltekit.handle.root
└─ sveltekit.handle.sequenced.* (only if you use sequence())
└─ sveltekit.resolve
└─ render_endpoint() → GET/POST/… ← no extra span
If the handler throws, the try/catch in resolve calls handle_fatal_error(...) and returns a Response instead of rethrowing, so the wrapper spans never see the exception. sveltekit.resolve just records http.response.status_code (e.g. 500) from that Response. SvelteKit already flags this:
// respond.js — resolve() catch
// HttpError from endpoint can end up here - TODO should it be handled there instead?
return await handle_fatal_error(event, state, e);
Two things are missing:
- A span for the endpoint handler.
recordExceptionand OTel statusERRORon some span when the handler throws.
load, form actions, sveltekit.remote.call, and sveltekit.remote.form.post all get a handler span. Endpoints are the one remaining first-class HTTP entry point where a thrown failure leaves no ERROR and no recordException. Trace-based debugging misses it, and so does error-biased sampling that keys off span status.
event.tracing.current does exist inside a +server handler, but resolve runs as merge_tracing(event, resolve_span), so current points at the resolve span, not a handler span. The docs for RequestEvent.tracing.current describe it as the span for handle, load, or a form action (the observability docs also mention remotes). Neither mentions +server.
The observability docs and the tracing.server JSDoc both list handle, load, form actions, and remotes, but not +server. The tracing tests in packages/kit/test/apps/basics/test/vitest/server.spec.js only cover page load.
Describe the proposed solution
Wrap the resolved +server handler in record_span, the same way OpenTelemetry's Express instrumentation records a span per route handler.
In render_endpoint, wrap only the resolved handler (mod[method] || mod.fallback, including HEAD to GET). Leave the prerender and method_not_allowed paths alone, and leave the outer Redirect to Response conversion alone. Use the same steps as load and actions:
record_span, then merge_tracing, then with_request_store({ event: traced, state }, …)
That way event.tracing.current and getRequestEvent().tracing.current both point at the handler span. If the store is left on the pre-merge event, current stays the resolve span.
Suggested span name: sveltekit.endpoint, matching the sveltekit.<noun> pattern of sveltekit.load, sveltekit.form_action, and sveltekit.remote.call.
Suggested attributes: http.route, http.method, and whether fallback or HEAD to GET was used.
On throw, the existing record_span rules apply:
ErrororHttpErrorwith status >= 500:recordExceptionandERRORHttpError< 500: attributes onlyRedirect: attributes only- non-
Errorthrow:ERRORwithoutrecordException
Right now render_endpoint converts Redirect to a Response before any record_span, so endpoint redirects also leave no span signal. Wrapping the handler call inside the existing try, before that conversion, would record the redirect attributes and then rethrow into the current conversion.
The following things should be also implemented:
- Add cases alongside the existing tracing suite: throw
Error,error(500),error(404),redirect(...), a non-Errorthrow (the load suite already covers that last one), and a 200 so the span exists on the happy path. - Update the observability docs, the
tracing.serverJSDoc, andRequestEvent.tracing.currentto include+server. All three currently list only handle, load, actions, and remotes.
Alternatives considered
- Per-handler wrapper (Sentry's
wrapServerRouteWithSentry): works, but every+serverfile has to remember to use it, and it still does not give Kit a first-party handler span. - App-level business spans (my current approach): I record failures inside the server logic by using Effect, so most endpoint errors are traced. The gap is a raw
throworerror()outside that code. I would rather rely on this upstream fix than a per-endpoint wrapper. - Treat
sveltekit.resolve'shttp.response.status_codeas the error signal: the status is there, but the span staysUNSETwith norecordException, so error-biased sampling still misses it. - HTTP auto-instrumentation root span (#14333): a separate gap about renaming the incoming HTTP span. It does not add a Kit handler span or an
ERRORstatus for thrown+serverfailures.
Importance
nice to have
Additional Information
Related Issues:
-
#13900 added spans for
load, actions, andhandle/resolve. Lms24's review asked for a+serverhandler span as well. It was not implemented in that PR:I think for additional spans, it would be great to also capture spans for
+serverroutes (i.e. theGET/POSTetc functions). Technically, the handle spans will already tell us by the route id that a +server route was accessed but I'd see a span for the specfic GET/POST/etc handler as quite valuable, similarly to how OTel express instrumentation would record spans for individual request handlers. -
#13899: base tracing work.
-
#16260: moved tracing out of
experimental(tracing.serveron thesveltekit()Vite plugin) and includedinstrumentation.server.jswhen present. No+serverspan was added. -
Sentry hit the same problem: getsentry/sentry-javascript#13224 ("in
+server.tsroutes, we don't catch errors thrown viaerror()… SvelteKit internally already caught and processed the error"), fixed with a manual wrapper (PR #13247). -
Not a duplicate of #14333. That issue is about renaming the incoming HTTP auto-instrumentation root span (
GET→GET /route) and/or exposing it.event.tracing.rootis onlysveltekit.handle.root, so apps cannot rename the HTTP span themselves. This request is a missing Kit child span insiderender_endpoint(sveltekit.endpoint), plusrecordException/ERRORwhen the handler throws. RenamingGETdoes not wrap the handler or set span status; addingsveltekit.endpointdoes not rename the HTTP root.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in render_endpoint in respond.js, where the resolved +server handler is called, and compare the tracing flow with load and form actions. Add the endpoint tracing coverage in packages/kit/test/apps/basics/test/vitest/server.spec.js for successful and failing handlers, then update the observability docs, tracing.server JSDoc, and RequestEvent.tracing.current documentation so the documented behavior matches the tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- backend, observability
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100