Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -793,13 +793,16 @@ The following span names were adjusted:
| Span op | Before | After |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `pageload` | The parameterized route, or the raw URL path if the SDK couldn't resolve one (`/users/123`) | The parameterized route, or `Pageload` if the SDK has none |
| `navigation` | The parameterized route, or the raw URL path if the SDK couldn't resolve one (`/users/123`) | The parameterized route, or `Navigation` if the SDK has none |
| `http.server` | The request method and route, or the raw URL path if the SDK couldn't resolve one (`GET /users/123`) | `GET /users/:id` when a route is known, otherwise just the request method (`GET`) |
| `router` | Framework-specific, sometimes containing the raw URL (`/users/123`, `SvelteKit Route Change`) | The span's `http.route`, or `Router` if the SDK has none |
| `graphql` | The graphql phase and, for operations, the operation name (`query GetUser`, `graphql.parse`, `graphql.resolve user.0.name`) | The operation type, or the processing type where there is none (`GraphQL query`, `GraphQL parse`, `GraphQL resolve`) |
| `resource.*` | The resource URL, relative to the page origin for same-origin resources (`/assets/app.js`) | The resource domain (`cdn.example.com`), or `Resource` if the SDK has none |
| `mcp.server` | The method and its target, including the resource URI (`resources/read file:///docs/api.md`) | The method alone for resource methods (`resources/read`). Tool and prompt names are unchanged (`tools/call get-weather`) |
| `mcp.notification.client_to_server`, `mcp.notification.server_to_client` | The notification method name (`notifications/tools/list_changed`) | The notification method name, or `MCP notification` if the message carries none |

`navigation.redirect` spans are started through the same code path as navigation spans, so they get the same names.

Resource spans now also carry a `url.domain` attribute holding that domain. The full URL remains available on `url.full`.

`http.server` requests that resolve to a route are **unchanged** — those names were already low cardinality. Only requests the SDK cannot parameterize are affected.
Expand All @@ -816,9 +819,9 @@ Resource URIs are unbounded, so they are no longer part of an `mcp.server` span

Only the Express, Koa and Hapi integrations resolve a route template for `router` spans. Angular, Ember and SvelteKit have none when the span starts, so their router spans are named `Router`.

Child spans of a service or root span carry its name in their `sentry.segment.name` attribute, so that changes with it. If you group or filter spans by segment name in dashboards or alerts, update those references.
Child spans of a service or root span carry its name in their `sentry.segment.name` attribute, so that changes with it. If you group or filter spans by segment name in dashboards or alerts, update those references. The same applies to `ui.action.click` spans, which are named after the current route.

`ignoreSpans` is evaluated when a span **starts**, at which point a span might not yet have its final name. For example, an unresolved pageload span name is named `'Pageload'` and might receive its final, resolved route name later.
`ignoreSpans` is evaluated when a span **starts**, at which point a span might not yet have its final name. For example, an unresolved pageload or navigation span is named `'Pageload'`/`'Navigation'` and might receive its final, resolved route name later.
`ignoreSpans` filters matching a URL path no longer apply to them.
Another example where filters might need adjustments are `resource.*` spans where their name now only includes the domain the resource was taken from.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow
},
'sentry.segment.name': {
type: 'string',
value: '/index.html',
value: 'Navigation',
},
'sentry.segment.name.source': {
type: 'string',
Expand All @@ -177,7 +177,9 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow
trace_id: pageloadTraceId,
},
],
name: '/index.html',
// The raw URL stays in `url.path`/`url.full`: with span streaming, a navigation span name is
// low cardinality and falls back to 'Navigation' when there is no parameterized route.
name: 'Navigation',
span_id: navigationSpan.span_id,
start_timestamp: expect.any(Number),
status: 'ok',
Expand All @@ -193,11 +195,12 @@ sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page })
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
const navigationSpan1Promise = waitForStreamedSpan(
page,
span => getSpanOp(span) === 'navigation' && span.name === '/sub-page',
// Matched on `url.path` rather than the span name, which is low cardinality.
span => getSpanOp(span) === 'navigation' && span.attributes?.[URL_PATH]?.value === '/sub-page',
);
const navigationSpan2Promise = waitForStreamedSpan(
page,
span => getSpanOp(span) === 'navigation' && span.name === '/sub-page-2',
span => getSpanOp(span) === 'navigation' && span.attributes?.[URL_PATH]?.value === '/sub-page-2',
);

await page.goto(url);
Expand All @@ -207,9 +210,13 @@ sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page })

const navigationSpan1 = await navigationSpan1Promise;

expect(navigationSpan1.name).toEqual('/sub-page');
expect(navigationSpan1.name).toEqual('Navigation');

expect(navigationSpan1.attributes).toMatchObject({
[URL_PATH]: {
type: 'string',
value: '/sub-page',
},
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: {
type: 'string',
value: 'auto.navigation.browser',
Expand All @@ -232,9 +239,13 @@ sentryTest('handles pushState with full URL', async ({ getLocalTestUrl, page })

const navigationSpan2 = await navigationSpan2Promise;

expect(navigationSpan2.name).toEqual('/sub-page-2');
expect(navigationSpan2.name).toEqual('Navigation');

expect(navigationSpan2.attributes).toMatchObject({
[URL_PATH]: {
type: 'string',
value: '/sub-page-2',
},
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: {
type: 'string',
value: 'auto.navigation.browser',
Expand Down
5 changes: 4 additions & 1 deletion packages/angular/src/tracing.ts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no action required, just comment: Angular is a pretty bad offender in terms of getting early route parameterization :( Maybe something to revisit some day, though their closed approach to the build system (by default) probably makes a manifest injection like in other frameworks less practical.

Not pretty but I think this is the best we can do here.

Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import type { Integration, Span } from '@sentry/core';
import {
debug,
hasSpanStreamingEnabled,
NAVIGATION_SPAN_NAME_FALLBACK,
parseStringToURLObject,
ROUTER_SPAN_NAME_FALLBACK,
stripUrlQueryAndFragment,
Expand Down Expand Up @@ -123,7 +124,9 @@ export class TraceService implements OnDestroy {
startBrowserTracingNavigationSpan(
client,
{
name: strippedUrl,
// With span streaming, span names have to be low cardinality. The parameterized route
// is only known on `ResolveEnd`, which updates the span name then.
name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : strippedUrl,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.angular',
[SENTRY_SEGMENT_NAME_SOURCE]: 'url',
Expand Down
15 changes: 13 additions & 2 deletions packages/browser/src/tracing/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
GLOBAL_OBJ,
hasSpansEnabled,
hasSpanStreamingEnabled,
NAVIGATION_SPAN_NAME_FALLBACK,
PAGELOAD_SPAN_NAME_FALLBACK,
isURLObjectRelative,
parseStringToURLObject,
Expand Down Expand Up @@ -632,7 +633,11 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
startBrowserTracingNavigationSpan(
client,
{
name: parsed?.pathname || WINDOW.location.pathname,
// With span streaming, span names have to be low cardinality, and there is no route
// information available here.
name: hasSpanStreamingEnabled(client)
? NAVIGATION_SPAN_NAME_FALLBACK
: parsed?.pathname || WINDOW.location.pathname,
attributes: {
[SENTRY_SEGMENT_NAME_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser',
Expand Down Expand Up @@ -713,7 +718,13 @@ export function startBrowserTracingNavigationSpan(
client.emit('startNavigationSpan', spanOptions, { isRedirect, url });

const scope = getCurrentScope();
scope.setTransactionName(spanOptions.name);
// `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. `url` is the
// destination, while `location` still points at the previous page during a `pushState`.
const isFallbackSpanName = spanOptions.name === NAVIGATION_SPAN_NAME_FALLBACK;
scope.setTransactionName(
isFallbackSpanName ? (url && parseStringToURLObject(url)?.pathname) || WINDOW.location?.pathname : spanOptions.name,
);

// We store the normalized request data on the scope, so we get the request data at time of span creation
// otherwise, the URL etc. may already be of the following navigation, and we'd report the wrong URL
Expand Down
28 changes: 25 additions & 3 deletions packages/browser/test/tracing/browserTracingIntegration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,9 @@ describe('browserTracingIntegration', () => {
expect(spanIsSampled(span2)).toBe(true);
expect(span2.isRecording()).toBe(true);
expect(spanToJSON(span2)).toEqual({
name: '/test',
// The raw URL stays in `url.path`/`url.full`: with span streaming, a navigation span name is
// low cardinality and falls back to 'Navigation' when there is no parameterized route.
name: 'Navigation',
status: 'ok',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
Expand Down Expand Up @@ -335,7 +337,7 @@ describe('browserTracingIntegration', () => {
expect(spanIsSampled(span3)).toBe(true);
expect(span3.isRecording()).toBe(true);
expect(spanToJSON(span3)).toEqual({
name: '/test2',
name: 'Navigation',
status: 'ok',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
Expand Down Expand Up @@ -423,7 +425,9 @@ describe('browserTracingIntegration', () => {
[URL_FULL]: 'https://example.com/test',
[URL_PATH]: '/test',
},
name: '/test',
// Redirect spans are started through the same path as navigation spans, so they get the
// low-cardinality fallback name too.
name: 'Navigation',
parent_span_id: span.spanContext().spanId,
}),
);
Expand Down Expand Up @@ -988,6 +992,24 @@ describe('browserTracingIntegration', () => {
expect(getCurrentScope().getScopeData().transactionName).toBe('test navigation span');
});

it("never sets the low-cardinality 'Navigation' span name on `scope.transactionName`", () => {
const client = new BrowserClient(
getDefaultBrowserClientOptions({
tracesSampleRate: 1,
integrations: [browserTracingIntegration()],
}),
);
setCurrentClient(client);
client.init();

startBrowserTracingNavigationSpan(client, { name: 'Navigation' }, { url: 'https://example.com/users/123?q=1' });

// The span name is low cardinality with span streaming enabled, but errors have to stay
// grouped by the actual page, so the scope keeps the destination path.
expect(spanToJSON(getActiveSpan()!).name).toBe('Navigation');
expect(getCurrentScope().getScopeData().transactionName).toBe('/users/123');
});

it("updates the scopes' propagationContexts on a navigation", () => {
const client = new BrowserClient(
getDefaultBrowserClientOptions({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Client, Span } from '@sentry/core';
import {
GLOBAL_OBJ,
hasSpanStreamingEnabled,
NAVIGATION_SPAN_NAME_FALLBACK,
PAGELOAD_SPAN_NAME_FALLBACK,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
Expand Down Expand Up @@ -114,15 +115,18 @@ export function appRouterInstrumentNavigation(client: Client): void {
const normalizedHref = basePath && !href.startsWith(basePath) ? `${basePath}${href}` : href;
const unparameterizedPathname = stripTrailingSlash(new URL(normalizedHref, WINDOW.location.href).pathname);
const parameterizedPathname = maybeParameterizeRoute(unparameterizedPathname);
const pathname = parameterizedPathname ?? unparameterizedPathname;
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL.
const spanName =
parameterizedPathname ??
(hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : unparameterizedPathname);

if (navigationRoutingMode === 'router-patch') {
navigationRoutingMode = 'transition-start-hook';
}

const currentNavigationSpan = currentRouterPatchingNavigationSpanRef.current;
if (currentNavigationSpan) {
currentNavigationSpan.updateName(pathname);
currentNavigationSpan.updateName(spanName);
currentNavigationSpan.setAttributes({
'navigation.type': `router.${navigationType}`,
[SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? 'route' : 'url',
Expand All @@ -134,7 +138,7 @@ export function appRouterInstrumentNavigation(client: Client): void {
startBrowserTracingNavigationSpan(
client,
{
name: pathname,
name: spanName,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation',
Expand All @@ -151,8 +155,11 @@ export function appRouterInstrumentNavigation(client: Client): void {
WINDOW.addEventListener('popstate', () => {
const pathname = stripTrailingSlash(WINDOW.location.pathname);
const parameterizedPathname = maybeParameterizeRoute(pathname);
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL.
const spanName =
parameterizedPathname ?? (hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : pathname);
if (currentRouterPatchingNavigationSpanRef.current?.isRecording()) {
currentRouterPatchingNavigationSpanRef.current.updateName(parameterizedPathname ?? pathname);
currentRouterPatchingNavigationSpanRef.current.updateName(spanName);
currentRouterPatchingNavigationSpanRef.current.setAttribute(
SENTRY_SEGMENT_NAME_SOURCE,
parameterizedPathname ? 'route' : 'url',
Expand All @@ -165,7 +172,7 @@ export function appRouterInstrumentNavigation(client: Client): void {
currentRouterPatchingNavigationSpanRef.current = startBrowserTracingNavigationSpan(
client,
{
name: parameterizedPathname ?? pathname,
name: spanName,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.app_router_instrumentation',
[SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? 'route' : 'url',
Expand Down Expand Up @@ -269,10 +276,19 @@ function patchRouter(client: Client, router: NextRouter, currentNavigationSpanRe
? undefined
: getAbsoluteUrl(normalizedHref);

// The incomplete-instrumentation placeholder is a static name, so it is low cardinality
// already, and keeping it is what makes the `ignoreSpans` entry filtering those spans match.
const isPlaceholderName = transactionName === INCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME;

currentNavigationSpanRef.current = startBrowserTracingNavigationSpan(
client,
{
name: parameterizedPathname ?? transactionName,
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL.
name:
parameterizedPathname ??
(isPlaceholderName || !hasSpanStreamingEnabled(client)
? transactionName
: NAVIGATION_SPAN_NAME_FALLBACK),
attributes: {
...transactionAttributes,
[SENTRY_SEGMENT_NAME_SOURCE]: parameterizedPathname ? 'route' : 'url',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Client, TransactionSource } from '@sentry/core';
import {
debug,
hasSpanStreamingEnabled,
NAVIGATION_SPAN_NAME_FALLBACK,
PAGELOAD_SPAN_NAME_FALLBACK,
parseBaggageHeader,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
Expand Down Expand Up @@ -164,7 +165,8 @@ export function pagesRouterInstrumentNavigation(client: Client): void {
startBrowserTracingNavigationSpan(
client,
{
name: newLocation,
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL.
name: spanSource === 'route' || !hasSpanStreamingEnabled(client) ? newLocation : NAVIGATION_SPAN_NAME_FALLBACK,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.nextjs.pages_router_instrumentation',
Expand Down
Loading
Loading