Skip to content
Open
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
24 changes: 18 additions & 6 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -613,24 +613,36 @@ These changes are not caught by TypeScript. If you filter, group, or alert on sp

### Span name changes

Affected SDKs: All SDKs running in the browser.
Affected SDKs: All SDKs running in the browser, plus the Express, Koa and Hapi integrations on the server.

With [span streaming](#span-streaming-is-now-the-default) enabled(the default), span names are now **low cardinality**, following the [Sentry span name conventions](https://getsentry.github.io/sentry-conventions/names/).

In v11, this only affects `pageload` spans. Further ops will follow in future releases.
In v11, this affects `pageload` and `router` spans. Further ops will follow in future releases.
If you [opt out of span streaming](#opting-out-of-span-streaming), span names remain unchanged.

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 |
| 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 |
| `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 |

Per framework, `router` spans are named:

| SDK | Before | After |
| --------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Express | The router's mount path, or the raw matched URL segment on the orchestrion-based integration (`/users/123`) | The request's `http.route` (`/users/:id/detail`), or `Router` |
| Koa | The layer's path (`/users/:id`) | Unchanged; `Router` when the layer has no path |
| Hapi | `GET /users/{id}` | `/users/{id}` — the method prefix is dropped |
| Angular | The raw navigation URL (`/users/123`) | `Router` — the parameterized route is only resolved after this span starts |
| Ember | `route:index -> route:posts` | `Router` |
| SvelteKit | `SvelteKit Route Change` | `Router` |

Some consequences to be aware of:

Child spans of a pageload 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.

`ignoreSpans` is evaluated when a span **starts**, at which point a pageload span without a resolved route is already named `'Pageload'`, so filters matching a URL path no longer apply to it. Match on attributes instead:
`ignoreSpans` is evaluated when a span **starts**, at which point a pageload span without a resolved route is already named `'Pageload'` and a router span without one is already named `'Router'`, so filters matching a URL path no longer apply to them. Match on attributes instead:

```js
Sentry.init({
Expand Down
6 changes: 5 additions & 1 deletion packages/angular/src/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ import { FUNCTION } from '@sentry/conventions/op';
import type { Integration, Span } from '@sentry/core';
import {
debug,
hasSpanStreamingEnabled,
parseStringToURLObject,
ROUTER_SPAN_NAME_FALLBACK,
stripUrlQueryAndFragment,
timestampInSeconds,
filterCollectedUrl,
Expand Down Expand Up @@ -136,7 +138,9 @@ export class TraceService implements OnDestroy {
this._routingSpan =
runOutsideAngular(() =>
startInactiveSpan({
name: `${navigationEvent.url}`,
// With span streaming, span names have to be low cardinality. The parameterized route is only
// known at `ResolveEnd`, well after this span starts, so there is nothing but the fallback.
name: hasSpanStreamingEnabled(client) ? ROUTER_SPAN_NAME_FALLBACK : `${navigationEvent.url}`,
attributes: {
// TODO(conventions): Replace `'router'` with the `router` span op constant once it is released in `@sentry/conventions`.
[SENTRY_OP]: 'router',
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/integrations/express/patch-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { MIDDLEWARE } from '@sentry/conventions/op';
import { DEBUG_BUILD } from '../../debug-build';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes';
import { SPAN_STATUS_ERROR, withActiveSpan } from '../../tracing';
import { hasSpanStreamingEnabled } from '../../tracing/spans/hasSpanStreamingEnabled';
import { ROUTER_SPAN_NAME_FALLBACK } from '../../tracing/spans/spanNames';
import { startSpanManual } from '../../tracing/trace';
import { debug } from '../../utils/debug-logger';
import type { SpanAttributes } from '../../types/span';
Expand All @@ -56,7 +58,7 @@ import {
getLayerMetadata,
isLayerIgnored,
} from './utils';
import { getIsolationScope } from '../../currentScopes';
import { getClient, getIsolationScope } from '../../currentScopes';
import { getDefaultIsolationScope } from '../../defaultScopes';
import { getOriginalFunction, markFunctionWrapped } from '../../utils/object';
import { setSDKProcessingMetadata } from './set-sdk-processing-metadata';
Expand Down Expand Up @@ -165,7 +167,13 @@ export function patchLayer(
DEBUG_BUILD && debug.warn('Isolation scope is still default isolation scope - skipping setting transactionName');
}

return startSpanManual({ name, attributes }, span => {
const client = getClient();
// With span streaming, span names have to be low cardinality, so router spans are named after their route.
const isStreamedRouterSpan = type === ExpressLayerType_ROUTER && !!client && hasSpanStreamingEnabled(client);

const spanName = isStreamedRouterSpan ? actualMatchedRoute || ROUTER_SPAN_NAME_FALLBACK : name;

return startSpanManual({ name: spanName, attributes }, span => {
let spanHasEnded = false;
// TODO: Fix router spans (getRouterPath does not work properly) to
// have useful names before removing this branch
Expand Down
74 changes: 74 additions & 0 deletions packages/core/test/lib/integrations/express/patch-layer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,15 @@ const defaultIsolationScope = {
this._scopeData.sdkProcessingMetadata = data;
},
};
let spanStreamingEnabled = false;
beforeEach(() => (spanStreamingEnabled = false));
vi.mock('../../../../src/currentScopes', () => ({
getIsolationScope() {
return inDefaultIsolationScope ? defaultIsolationScope : notDefaultIsolationScope;
},
getClient() {
return { getOptions: () => ({ traceLifecycle: spanStreamingEnabled ? 'stream' : 'static' }) };
},
}));
vi.mock('../../../../src/defaultScopes', () => ({
getDefaultIsolationScope() {
Expand Down Expand Up @@ -468,6 +473,75 @@ describe('patchLayer', () => {
checkSpans([]);
});

it('names router spans after their route when span streaming is enabled', () => {
spanStreamingEnabled = true;
const options: ExpressPatchLayerOptions = {};
const req = Object.assign(new EventEmitter(), {
originalUrl: '/a/b/c',
}) as unknown as ExpressRequest;

const layer = {
name: 'router',
handle: vi.fn(),
} as unknown as ExpressLayer;

const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse;

storeLayer(req, '/a');
storeLayer(req, '/b');

patchLayer(() => options, layer, '/c');
layer.handle(req, res);

checkSpans([
{
status: { code: 0, message: 'OK' },
data: {
'express.name': '/c',
'express.type': 'router',
'http.route': '/a/b/c',
'sentry.op': 'router',
'sentry.origin': 'auto.http.express',
},
description: '/a/b/c',
},
]);
});

it('falls back to a static router span name when the route is unknown', () => {
spanStreamingEnabled = true;
const options: ExpressPatchLayerOptions = {};
const req = Object.assign(new EventEmitter(), {
originalUrl: '/abcdef',
}) as unknown as ExpressRequest;

const layer = {
name: 'router',
handle: vi.fn(),
} as unknown as ExpressLayer;

const res = Object.assign(new EventEmitter(), {}) as unknown as ExpressResponse;

storeLayer(req, '/a');
storeLayer(req, '/b');

patchLayer(() => options, layer, '/c');
layer.handle(req, res);

checkSpans([
{
status: { code: 0, message: 'OK' },
data: {
'express.name': '/c',
'express.type': 'router',
'sentry.op': 'router',
'sentry.origin': 'auto.http.express',
},
description: 'Router',
},
]);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing integration or E2E tests

Low Severity

Flagged because it was mentioned in the review rules file. This is a feat PR, and the new coverage is package-level unit tests (Express patch-layer, Hapi utils, SvelteKit). The guidelines ask for at least one integration or E2E test so the streamed router naming is exercised against a real framework request or navigation path.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit be3a06a. Configure here.


it('handles case when route does not match url', () => {
const onRouteResolved = vi.fn();
const options: ExpressPatchLayerOptions = { onRouteResolved };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getCurrentScope,
hasSpanStreamingEnabled,
PAGELOAD_SPAN_NAME_FALLBACK,
ROUTER_SPAN_NAME_FALLBACK,
spanToJSON,
type Client,
type Span,
Expand Down Expand Up @@ -148,7 +149,9 @@ export function instrumentEmberAppInstanceForPerformance(
[SENTRY_OP]: 'router',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.ember',
},
name: `route:${fromRoute} -> route:${toRoute}`,
// With span streaming, span names have to be low cardinality, and Ember gives us no route
// template for the transition itself, so it's the fallback.
name: hasSpanStreamingEnabled(client) ? ROUTER_SPAN_NAME_FALLBACK : `route:${fromRoute} -> route:${toRoute}`,
onlyIfParent: true,
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import type { Span } from '@sentry/core';
import {
debug,
getActiveSpan,
getClient,
getDefaultIsolationScope,
getIsolationScope,
hasSpanStreamingEnabled,
ROUTER_SPAN_NAME_FALLBACK,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startInactiveSpan,
stringMatchesSomePattern,
Expand Down Expand Up @@ -224,8 +227,12 @@ function getSpanForLayer(data: HandleChannelContext, options: ExpressIntegration
return undefined;
}

const client = getClient();
// With span streaming, span names have to be low cardinality, so router spans are named after their route.
const isStreamedRouterSpan = type === 'router' && !!client && hasSpanStreamingEnabled(client);

const span = startInactiveSpan({
name,
name: isStreamedRouterSpan ? matchedRoute || ROUTER_SPAN_NAME_FALLBACK : name,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
[SENTRY_OP]: EXPRESS_TYPE_TO_SPAN_OP[type],
Expand Down
21 changes: 19 additions & 2 deletions packages/server-utils/src/integrations/hapi-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@
* is replaced with `getActiveSpan()`.
*/

import { getActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';
import {
getActiveSpan,
getClient,
hasSpanStreamingEnabled,
ROUTER_SPAN_NAME_FALLBACK,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startSpan,
} from '@sentry/core';
import { SENTRY_OP } from '@sentry/conventions/attributes';
import { MIDDLEWARE } from '@sentry/conventions/op';
import type {
Expand Down Expand Up @@ -74,7 +81,17 @@ export const getRouteMetadata = (route: ServerRoute, pluginName?: string): SpanM
attributes[AttributeNames.HAPI_TYPE] = HapiLayerType.ROUTER;
}

return { attributes, name: `${route.method.toUpperCase()} ${route.path}` };
const client = getClient();
// With span streaming, span names have to be low cardinality, so router spans are named after their
// route alone, without the method prefix.
const isStreamedRouterSpan = !pluginName && !!client && hasSpanStreamingEnabled(client);

return {
attributes,
name: isStreamedRouterSpan
? route.path || ROUTER_SPAN_NAME_FALLBACK
: `${route.method.toUpperCase()} ${route.path}`,
};
};

/** Build the span name and attributes for a Hapi server extension. */
Expand Down
10 changes: 9 additions & 1 deletion packages/server-utils/src/integrations/koa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import {
debug,
defineIntegration,
getActiveSpan,
getClient,
getDefaultIsolationScope,
getIsolationScope,
hasSpanStreamingEnabled,
ROUTER_SPAN_NAME_FALLBACK,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startSpan,
} from '@sentry/core';
Expand Down Expand Up @@ -173,7 +176,12 @@ function patchLayer(
const koaName = metadata.attributes[KOA_NAME];
// Somehow, name is sometimes `''` for middleware spans.
// See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220
const name = typeof koaName === 'string' ? koaName || '< unknown >' : metadata.name;
const staticName = typeof koaName === 'string' ? koaName || '< unknown >' : metadata.name;

const client = getClient();
// With span streaming, span names have to be low cardinality, so router spans are named after their route.
const isStreamedRouterSpan = layerType === LAYER_TYPE.ROUTER && !!client && hasSpanStreamingEnabled(client);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Avoidable getClient calls

Low Severity

Flagged because it was mentioned in the review rules file — this is more an "is this necessary" check than a hard violation. These sites newly call getClient() to gate span streaming, while the Koa, Express, and Hapi integrations already receive a client in setup. Prefering that existing reference would avoid relying on ambient current-client state in multi-client setups.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit be3a06a. Configure here.

const name = isStreamedRouterSpan ? metadata.attributes[HTTP_ROUTE] || ROUTER_SPAN_NAME_FALLBACK : staticName;

return startSpan(
{
Expand Down
22 changes: 21 additions & 1 deletion packages/server-utils/test/integrations/hapi-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { describe, expect, it } from 'vitest';
import { setCurrentClient } from '@sentry/core';
import { afterEach, describe, expect, it } from 'vitest';
import { getExtMetadata, getRouteMetadata } from '../../src/integrations/hapi-utils';
import { getDefaultTestClientOptions, TestClient } from '../mocks/client';

describe('getRouteMetadata', () => {
const route = { path: '/users/{id}', method: 'get' } as any;

afterEach(() => {
setCurrentClient(undefined as unknown as TestClient);
});

it('describes a directly-registered route as a router layer', () => {
expect(getRouteMetadata(route)).toEqual({
name: 'GET /users/{id}',
Expand All @@ -26,6 +32,20 @@ describe('getRouteMetadata', () => {
},
});
});

it('drops the method from the router span name when span streaming is enabled', () => {
const client = new TestClient(getDefaultTestClientOptions({ traceLifecycle: 'stream' }));
setCurrentClient(client);

expect(getRouteMetadata(route).name).toBe('/users/{id}');
});

it('keeps the plugin span name when span streaming is enabled', () => {
const client = new TestClient(getDefaultTestClientOptions({ traceLifecycle: 'stream' }));
setCurrentClient(client);

expect(getRouteMetadata(route, 'my-plugin').name).toBe('GET /users/{id}');
});
});

describe('getExtMetadata', () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/sveltekit/src/client/svelte4BrowserTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Client, Span } from '@sentry/core';
import {
hasSpanStreamingEnabled,
PAGELOAD_SPAN_NAME_FALLBACK,
ROUTER_SPAN_NAME_FALLBACK,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/core';
Expand Down Expand Up @@ -132,7 +133,9 @@ function _instrumentNavigations(client: Client, navigatingStore: Readable<Naviga
);

routingSpan = startInactiveSpan({
name: 'SvelteKit Route Change',
// With span streaming, span names have to be low cardinality, and this span carries no route
// of its own, so it's the fallback.
name: hasSpanStreamingEnabled(client) ? ROUTER_SPAN_NAME_FALLBACK : 'SvelteKit Route Change',
attributes: {
// TODO(conventions): Replace `'router'` with the `router` span op constant once it is released in `@sentry/conventions`.
[SENTRY_OP]: 'router',
Expand Down
5 changes: 4 additions & 1 deletion packages/sveltekit/src/client/svelte5BrowserTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Client, Span } from '@sentry/core';
import {
hasSpanStreamingEnabled,
PAGELOAD_SPAN_NAME_FALLBACK,
ROUTER_SPAN_NAME_FALLBACK,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
} from '@sentry/core';
Expand Down Expand Up @@ -112,7 +113,9 @@ function _instrumentNavigations(client: Client): void {
);

routingSpan = startInactiveSpan({
name: 'SvelteKit Route Change',
// With span streaming, span names have to be low cardinality, and this span carries no route
// of its own, so it's the fallback.
name: hasSpanStreamingEnabled(client) ? ROUTER_SPAN_NAME_FALLBACK : 'SvelteKit Route Change',
attributes: {
// TODO(conventions): Replace `'router'` with the `router` span op constant once it is released in `@sentry/conventions`.
[SENTRY_OP]: 'router',
Expand Down
Loading
Loading