feat(observability): group /v1 API spans by domain and endpoint#888
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #888 +/- ##
========================================
+ Coverage 64.2% 66.0% +1.9%
========================================
Files 350 337 -13
Lines 41492 40010 -1482
Branches 2010 2025 +15
========================================
- Hits 26602 26376 -226
+ Misses 14847 13592 -1255
+ Partials 43 42 -1
🚀 New features to boost your workflow:
|
Client spans only carry the full URL in span.description, so dashboards
can't group them. Derive low-cardinality api.domain and api.endpoint tags
in a beforeSendSpan hook across all three runtimes; redact dynamic
segments to {id} so no id or PII reaches a tag value.
Open results/facets spans in useLayoutEffect and close after paint via requestAnimationFrame, instead of module-level slots. Opt-in via measureRenderSpan so the shared result list doesn't emit search spans off the search page.
aaccb7e to
26ce4dd
Compare
There was a problem hiding this comment.
Pull request overview
This PR improves observability by normalizing Sentry spans for /v1/* API calls (domain + redacted endpoint, no query/fragment) across client/server/edge, and by making search render spans opt-in and measured closer to paint time to avoid emitting search render spans from shared result lists on non-search pages.
Changes:
- Add
beforeSendSpanhook (beforeSendApiSpan) to tag/v1/*http.clientspans with low-cardinalityapi.domain+api.endpoint, including dynamic-segment redaction. - Replace “open in Telemetry / close after paint” render-span flow with a hook-driven paint-timed approach gated behind
measureRenderSpan. - Add Vitest coverage for API span normalization behavior.
Risk summary
Medium risk overall: touches core telemetry and Sentry initialization across runtimes; one confirmed TypeScript compile-time blocker exists.
Findings (priority order)
blocker
- TypeScript compile error from
import type { spanToJSON }used withtypeof spanToJSON- Impact: build fails (new
normalizeApiSpan.tswon’t typecheck). - Location:
src/lib/normalizeApiSpan.ts:1-4 - Minimal fix: import
spanToJSONas a value (or use another exported JSON type). - Confidence: high
- Impact: build fails (new
medium
-
useLayoutEffectwill trigger SSR warning for server-renderedSimpleResultList- Impact: noisy SSR warnings; can hide real SSR issues during debugging.
- Location:
src/lib/useRenderSpan.ts:19-23 - Minimal fix: use an isomorphic layout effect (layout in browser, effect on server).
- Confidence: high
-
Spans can leak if
enabledtoggles fromtrue→falsewhile mounted- Impact: long-running/never-ended spans skew render timing data.
- Location:
src/lib/useRenderSpan.ts:39-42 - Minimal fix: when disabled, cancel pending rAF and end/null any open spans.
- Confidence: high
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/providers.tsx | Removes render-span opening from Telemetry to avoid shared-list emissions. |
| src/pages/search/index.tsx | Enables render-span measurement only on the search results page. |
| src/lib/useRenderSpan.ts | Implements paint-timed render spans with opt-in gating. |
| src/lib/performance.ts | Removes module-level slots for results/facets render spans. |
| src/lib/normalizeApiSpan.ts | Adds span normalization/tagging for /v1/* http.client spans. |
| src/lib/normalizeApiSpan.test.ts | Adds unit tests for domain/endpoint parsing and redaction. |
| src/components/ResultList/SimpleResultList.tsx | Adds measureRenderSpan prop to gate render-span emission. |
| sentry.server.config.ts | Registers beforeSendSpan normalization hook on server runtime. |
| sentry.edge.config.ts | Registers beforeSendSpan normalization hook on edge runtime. |
| sentry.client.config.ts | Registers beforeSendSpan normalization hook on client runtime. |
| import type { spanToJSON } from '@sentry/nextjs'; | ||
|
|
||
| // @sentry/nextjs doesn't re-export SpanJSON, so derive it from spanToJSON. | ||
| export type SpanJSON = ReturnType<typeof spanToJSON>; |
| // Open after DOM commit, before the browser paints. | ||
| useLayoutEffect(() => { | ||
| if (!enabled) { | ||
| return; | ||
| } |
| useEffect(() => { | ||
| if (docs.length === 0) { | ||
| if (!enabled) { | ||
| return; | ||
| } |
… span leak on disable
Sentry couldn't tell our API calls apart. Client http.client spans only carry the full URL in span.description, so everything grouped on raw descriptions with query strings. The
search.results.render / search.facets.render spans also opened from the shared result list, so they fired on non-search pages too.