Skip to main content

port-span-names

Port a span op to low-cardinality span names, applied only when span streaming is enabled. Use when asked to make a span op's names low cardinality, remove raw URLs/IDs from span names, apply the Sentry span name conventions to an op, or "port <op> spans to low cardinality names". Trigger phrases include "low cardinality span name", "span name conventions", "span name fallback", "Pageload/Navigation span name".

Datos de origen

Repositorio
getsentry/sentry-javascript
Última actividad en el origen
22 de septiembre de 2026 a las 15:01
Idioma detectado de SKILL.md
inglés
Estrellas
8756
Forks
1860

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
port-span-names
description
Port a span op to low-cardinality span names, applied only when span streaming is enabled. Use when asked to make a span op's names low cardinality, remove raw URLs/IDs from span names, apply the Sentry span name conventions to an op, or "port <op> spans to low cardinality names". Trigger phrases include "low cardinality span name", "span name conventions", "span name fallback", "Pageload/Navigation span name".
argument-hint
<span-op> [constraints] # e.g. navigation, or "db.query --only packages/node"
# Port a span op to low-cardinality span names Span names must be low cardinality, per the [Sentry span name conventions](https://getsentry.github.io/sentry-conventions/names/): a raw URL, id, or query must never end up in a span name. Where the SDK has no low-cardinality value for a span, it uses a **static fallback name** for that op instead. This only applies when span streaming is enabled. With `traceLifecycle: 'static'` every span name must stay byte-identical to before. `pageload` was ported first. Read it as the reference implementation before starting: `packages/core/src/tracing/spans/spanNames.ts` (the constant), `packages/browser/src/tracing/browserTracingIntegration.ts` (a start site plus the scope guard in `startBrowserTracingPageLoadSpan`), and `grep -rn PAGELOAD_SPAN_NAME_FALLBACK packages/*/src` for the full set of call sites. ## Inputs - **`<span-op>`** (required): the op to port, e.g. `navigation`, `db.query`, `http.client`. - **`[constraints]`** (optional): free-form narrowing, e.g. `--only packages/node`, `just the vue router instrumentation`, `skip tanstack`. Honor them literally: touch nothing outside the named scope, and say in your summary which sites you deliberately left for a follow-up. If the op is missing, ask for it. Do not guess. ## Rules These are non-negotiable. Every one of them was arrived at by rejecting the alternative. 1. **Gate on span streaming at each site.** `hasSpanStreamingEnabled(client)` (from `@sentry/core`) must appear inline where the name is chosen, so a reader can see the gate without following a call chain. Never gate centrally. 2. **Set the low-cardinality name when the span _starts_** — and at every later site that could write a high-cardinality name onto it. Never rewrite names retroactively (not in `captureSpan`, not in `spanToJSON`, not in a `processSpan`/`preprocessSpan` hook). A span must never carry a raw URL. 3. **Check span name updates** to ensure every name update is low-cardinality if span streaming is enabled. 4. **Only the name and its description change.** Do not touch `sentry.source`, `url.template`, `http.route`, or any other attribute — they keep describing where the name came from. The two exceptions are `sentry.description` and the single attribute the op's description template reads (see step 1); both exist to preserve what the name gave up. 5. **Do not derive the name from attributes in code.** The conventions describe names as attribute templates, but you implement them by reusing the value the site _already_ has for `url.template` / `http.route`. No attribute lookups, no generic template resolver. 6. **No helpers, no abstraction.** An inline ternary at each site. A shared `const` for the fallback string is fine (and required, see rule 6); a function that sets names or attributes is not. 7. **The fallback must never reach `scope.setTransactionName`.** The scope's transaction name is what error events are grouped by, so it keeps the raw URL or the parameterized route — never `Pageload`/`Navigation`/etc. Export the fallback as a constant from `packages/core/src/tracing/spans/spanNames.ts` so the guard cannot drift. 8. **`sentry.segment.name` must never diverge from the segment span's name.** Any code that stamps it on a child span has to read it off the segment span, not off the scope. ## 1. Look up the convention Read <https://getsentry.github.io/sentry-conventions/names/> and find the op. Each op lists attribute templates in priority order, ending in a static fallback — that fallback is your name. Examples: `pageload` → `Pageload`, `navigation` → `Navigation`, database ops → `Database operation`. Add it next to `PAGELOAD_SPAN_NAME_FALLBACK` in `packages/core/src/tracing/spans/spanNames.ts` and export it from `shared-exports.ts`. Every package imports it from `@sentry/core` directly — no re-export from `@sentry/browser` is needed. ### Then look up the description rules The name is only half the contract. Relay re-derives a span's **description** from its attributes using the [span description rules](https://getsentry.github.io/sentry-conventions/descriptions/) — a separate, much shorter set of templates from the name rules. `function`, for example, has exactly one (`{{code.function.name}}`) and no static fallback, so a `function` span with no `code.function.name` gets no inferred description at all. Renaming a span therefore drops information unless you check what the op's description template can still produce. For each site, in this order: 1. **The template rebuilds the old name from attributes the site already sets** — do nothing, Relay infers it. 2. **It rebuilds it from an attribute the site could legitimately set** — set that attribute. Only when the value genuinely describes the span, never to smuggle the old name into an attribute that means something else. 3. **It cannot** — set `sentry.description` (`SENTRY_DESCRIPTION` from `@sentry/conventions/attributes`) to the name the span had before. Case 3 must only be applied if case 1 or 2 do not work for the span: the templates are generic, and most old names were framework-specific strings the conventions cannot express (`serverAction/updateUser`, `Scheduled Cron */5 * * * *`, `Fetcher fetcher-1`). ```ts // Relay infers a `function` span's description from `code.function.name` alone, which drops the route. ...(hasSpanStreaming && { [SENTRY_DESCRIPTION]: routePattern }), ``` Two constraints on the description: - **Set it on the streaming branch only**, gated by the same `hasSpanStreamingEnabled` check as the name. In transaction mode the description already equals the name, and writing it changes nothing. - **Descriptions may be high cardinality** — the `http.server` templates end in `url.full`. The raw URL, route or id that the name gave up belongs here. This is the one place the cardinality rule does not apply. ## 2. Find every site that names a span with this op Be exhaustive; a missed site is a raw URL in production. Most sites set the op to the constant from `@sentry/conventions/op` (`FUNCTION`, `HTTP_SERVER`, ...), not to a string literal, so grep for both spellings: ```bash # span starts, via the op constant grep -rn "SENTRY_OP\]: <OP_CONST>\|SEMANTIC_ATTRIBUTE_SENTRY_OP\]: <OP_CONST>\|op: <OP_CONST>\|spanOp: <OP_CONST>\|\.op = <OP_CONST>" packages/*/src # span starts, via a string literal grep -rn "SENTRY_OP\]: '<op>'\|SEMANTIC_ATTRIBUTE_SENTRY_OP\]: '<op>'\|op: '<op>'\|= '<op>'" packages/*/src # for browser routing ops, also the dedicated starters grep -rn "startBrowserTracingNavigationSpan\|startBrowserTracingPageLoadSpan" packages/*/src # later name writes grep -rn "\.updateName(\|updateSpanName(" packages/*/src # readers that compare a span name against a URL or route (these break, see step 4) grep -rn "spanToJSON(.*)\.name" packages/*/src ``` Neither grep finds an op that reaches the span through a variable, and those are the sites that get missed. Enumerate them by hand — list every file importing the op constant (`grep -rln "<OP_CONST>" packages/*/src`) and follow the ones that never spell out a span start: - **shared wrappers** taking the op as an option — `spanOp: FUNCTION` handed to cloudflare's `wrapMethodWithSentry` - **helpers computing the op** — `getGenAiSpanOp(operation)` returns `function` for any unrecognized gen-AI operation - **defaults on public APIs** — `SentryTraced(op: string = 'function')` in `@sentry/nestjs` Cross-check the total against the renames too: a `updateSpanName` in a different file can rewrite a span whose op is set elsewhere (`tanstackstart-react`'s global function middleware renames the span that `wrapFetchWithSentry` starts), and it will not appear in any op grep. Classify each write site: does it set a low cardinality value? Low cardinality values are for example: - a parameterized route (i.e. one without dynamic parameters) - an http origin without url path, query or fragment parameters - a component name - a static string In cases, where we already set a low-cardinality value, likely only the fallback branch changes. For example, when a route is known the name is identical in both lifecycles. But when it's unknown, transaction mode falls back to the raw URL. Span streaming mode falls back to a static "Pageload" string. Note: For a lot of route/URL cases, the `sentry.source` attribute having the value `"url"` strongly hints that the span name is high-cardinality. Use this as a strong indicator but not as definitive proof. Sometimes this attribute is incorrectly set. If you find such a case, flag it rather than following the rule. ## 3. Apply the fallback The whole change per site is the else-branch of a ternary: ```ts // With span streaming, span names have to be low cardinality, so we can't fall back to the URL. name: parameterizedRoute ?? (hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : pathname), ``` Where a site computes `[name, source]` together, gate on the source (but see the note above!): ```ts name: source === 'route' || !hasSpanStreamingEnabled(client) ? name : NAVIGATION_SPAN_NAME_FALLBACK, ``` At update sites, keep the surrounding `setAttribute` calls exactly as they were: ```ts const client = getClient(); const isUnparameterizedStreamedSpan = source !== 'route' && !!client && hasSpanStreamingEnabled(client); span.updateName(isUnparameterizedStreamedSpan ? NAVIGATION_SPAN_NAME_FALLBACK : name); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); // unchanged ``` `hasSpanStreamingEnabled` takes a non-optional `Client`, so guard `getClient()` with `!!client` where no client is in scope. ## 4. Fix readers, never work around them Some instrumentation reads a span's name back and compares it to a pathname or route to decide whether an event belongs to that span. Those silently stop matching once the name is a fallback — the span then never gets parameterized at all, with no error. Re-point them at the attribute that holds the real value (`url.path`, `url.full`, `url.template`). Reading an attribute here is correct; only _writing_ attributes is out of scope. ```ts // Matched against `url.path` rather than the span name, which is low cardinality. const spanPath = spanToJSON(span).attributes[URL_PATH]; ``` ## 5. Guard the scope transaction name Find every `setTransactionName` reachable from a span you renamed: ```bash grep -rn "setTransactionName" packages/*/src ``` Most calls pass a route and are fine. The dangerous ones pass a span name through. Guard those: ```ts // `Navigation` is a low-cardinality span name, not a description of the page. The scope's // transaction name is what error events are grouped by, so it keeps the URL instead. const isFallbackSpanName = spanOptions.name === NAVIGATION_SPAN_NAME_FALLBACK; getCurrentScope().setTransactionName(isFallbackSpanName ? WINDOW.location?.pathname : spanOptions.name); ``` Pin this with a test asserting the span name is the fallback **and** `scope.transactionName` is not. ## 6. Keep `sentry.segment.name` consistent `captureSpan` derives `sentry.segment.name` from the real segment span, but any code that pre-sets the attribute wins over it (`safeSetSpanJSONAttributes` does not overwrite). If a child-span emitter copies the name from the scope, it will diverge as soon as the segment span is renamed. Read it off the segment span instead: ```ts const segmentSpan = parentSpan && getRootSpan(parentSpan); const segmentName = segmentSpan ? spanToJSON(segmentSpan).name : getCurrentScope().getScopeData().transactionName; ``` The scope is only a valid fallback for standalone spans, which are sent without their segment span. `packages/browser-utils/src/web-vitals/spans.ts` is the worked example. ## 7. Verify **Build first.** Framework packages and the Playwright suites resolve `@sentry/core` from `build/`, not `src/`. A stale build produces failures that look like your bug but aren't. ```bash yarn build:dev:filter @sentry/browser # plus each package you touched npx vitest run --root packages/<pkg> --coverage.enabled=false cd dev-packages/browser-integration-tests && npx playwright test --project=chromium suites/tracing ``` Before assuming a failure is yours, baseline it: `git stash`, re-run, `git stash pop`. Commonly pre-existing in a dev checkout: `packages/core` `test/types/typedef.test.ts` (needs a full prod build), the `@sentry/ember` build, and `packages/nuxt` lint errors. Expect to update, and read each one to confirm the new value is _correct_ rather than just green: - unit assertions on the span name and on `scope.transactionName` - the span's `sentry.description` in **both** lifecycles: streamed spans carry the previous name, static ones must not gain the attribute - `dev-packages/browser-integration-tests` suites whose `init.js` does **not** set `traceLifecycle: 'static'` (the static ones must not change — that is your regression check) - `sentry.segment.name` / `sentry.transaction` on child spans of a renamed segment span - span mocks missing `spanContext`/`setAttribute` once code paths shift Finish with `yarn format` and `yarn lint`. ## 8. Document it in MIGRATION.md Extend the existing **"Span name changes"** section under `## 2. Behaviour Changes` — add a row to its table rather than starting a new section: | Span op | Before | After | | ------- | ------------------------------------------ | ---------------------------------------------- | | `<op>` | what the name was, with a concrete example | the route, or `<Fallback>` if the SDK has none | Also note, if they apply: that `ignoreSpans` is evaluated at span **start** (so filters matching a URL no longer match a fallback-named span, and users should match on attributes instead), any child-span attribute that follows the new name, and any span of a _different_ op that inherits the name (e.g. `ui.action.click` spans are named after the current route). ## Rejected approaches Do not propose these; they were each tried and rejected in the `pageload` port. - **Rewriting names at capture/serialization time.** Correct output, but the span carries a raw URL for its whole lifetime, and anything reading the name mid-flight sees it. - **A `updateRouteSpanName(span, name, source, attributes)` helper.** It set attributes as a side effect, which is out of scope, and hid the streaming check. - **Deriving the name from `url.template` via a generic convention resolver.** Over-abstracted; the call site already has the value. - **Tracking a second `segmentName` on the scope.** Looks like it fixes `sentry.segment.name`, but it diverges the moment a routing instrumentation renames the span — unless every rename site also updates the scope, which is the same duplicated-truth bug moved elsewhere.
Ver en GitHub