From d7e7d2033fae36f9b3e84c870e7d7797d65c4840 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 21 Sep 2026 17:33:40 +0200 Subject: [PATCH 1/2] docs(skills): Cover span descriptions in the port-span-names skill Porting an op to low-cardinality names silently drops information: Relay re-derives descriptions from a separate, much shorter set of templates than the name rules, and for several ops (`function` has one template and no fallback) it cannot rebuild the name the span gave up. Document that check, and when to set `sentry.description` locally instead. Also widen the enumeration step. Its grep only matched string-literal ops, so it missed every site using the constant from `@sentry/conventions/op` - which is nearly all of them - as well as ops that arrive through a wrapper option, a helper return value or a parameter default. Co-Authored-By: Claude Opus 5 (1M context) --- .agents/skills/port-span-names/SKILL.md | 41 +++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/.agents/skills/port-span-names/SKILL.md b/.agents/skills/port-span-names/SKILL.md index 85ded4b6f3e9..d4b2af4d3faf 100644 --- a/.agents/skills/port-span-names/SKILL.md +++ b/.agents/skills/port-span-names/SKILL.md @@ -29,7 +29,7 @@ These are non-negotiable. Every one of them was arrived at by rejecting the alte 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 changes.** Do not touch `sentry.source`, `url.template`, `http.route`, or any other attribute. They keep describing where the name came from. +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. @@ -41,13 +41,39 @@ Read and find the op. Ea 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 is the common one: 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 -grep -rn "SEMANTIC_ATTRIBUTE_SENTRY_OP\]: ''\|SENTRY_OP\]: ''\|op: ''" packages/*/src +# span starts, via the op constant +grep -rn "SENTRY_OP\]: \|SEMANTIC_ATTRIBUTE_SENTRY_OP\]: \|op: \|spanOp: \|\.op = " packages/*/src +# span starts, via a string literal +grep -rn "SENTRY_OP\]: ''\|SEMANTIC_ATTRIBUTE_SENTRY_OP\]: ''\|op: ''\|= ''" packages/*/src # for browser routing ops, also the dedicated starters grep -rn "startBrowserTracingNavigationSpan\|startBrowserTracingPageLoadSpan" packages/*/src @@ -58,6 +84,14 @@ grep -rn "\.updateName(\|updateSpanName(" packages/*/src 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 "" 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) @@ -151,6 +185,7 @@ Before assuming a failure is yours, baseline it: `git stash`, re-run, `git stash 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 From 19d3f772a599f9ceec231a5dabb31469f779d744 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 22 Sep 2026 13:45:30 +0200 Subject: [PATCH 2/2] Update .agents/skills/port-span-names/SKILL.md --- .agents/skills/port-span-names/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/port-span-names/SKILL.md b/.agents/skills/port-span-names/SKILL.md index d4b2af4d3faf..69850249dbfe 100644 --- a/.agents/skills/port-span-names/SKILL.md +++ b/.agents/skills/port-span-names/SKILL.md @@ -51,7 +51,7 @@ Renaming a span therefore drops information unless you check what the op's descr 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 is the common one: the templates are generic, and most old names were framework-specific strings the conventions cannot express (`serverAction/updateUser`, `Scheduled Cron */5 * * * *`, `Fetcher fetcher-1`). +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.