sveltejs / sveltejs/kit

tracing: emit a span for `+server` route handlers

Open
#17,099 0 comments 0 reactions 0 assignees View on GitHub

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:

  1. A span for the endpoint handler.
  2. recordException and OTel status ERROR on 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:

  • Error or HttpError with status >= 500: recordException and ERROR
  • HttpError < 500: attributes only
  • Redirect: attributes only
  • non-Error throw: ERROR without recordException

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-Error throw (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.server JSDoc, and RequestEvent.tracing.current to include +server. All three currently list only handle, load, actions, and remotes.
Alternatives considered
  • Per-handler wrapper (Sentry's wrapServerRouteWithSentry): works, but every +server file 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 throw or error() outside that code. I would rather rely on this upstream fix than a per-endpoint wrapper.
  • Treat sveltekit.resolve's http.response.status_code as the error signal: the status is there, but the span stays UNSET with no recordException, 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 ERROR status for thrown +server failures.
Importance

nice to have

Additional Information

Related Issues:

  • #13900 added spans for load, actions, and handle/resolve. Lms24's review asked for a +server handler span as well. It was not implemented in that PR:

    I think for additional spans, it would be great to also capture spans for +server routes (i.e. the GET/POST etc 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.server on the sveltekit() Vite plugin) and included instrumentation.server.js when present. No +server span was added.

  • Sentry hit the same problem: getsentry/sentry-javascript#13224 ("in +server.ts routes, we don't catch errors thrown via error() … 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 (GETGET /route) and/or exposing it. event.tracing.root is only sveltekit.handle.root, so apps cannot rename the HTTP span themselves. This request is a missing Kit child span inside render_endpoint (sveltekit.endpoint), plus recordException/ERROR when the handler throws. Renaming GET does not wrap the handler or set span status; adding sveltekit.endpoint does not rename the HTTP root.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.