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
15 changes: 9 additions & 6 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,20 +617,23 @@ Affected SDKs: All SDKs running in the browser.

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 only affects `pageload` and `navigation` 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 |
| `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 |

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

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.
Child spans of a pageload or navigation 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 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 or navigation span without a resolved route is already named `'Pageload'`/`'Navigation'`, so filters matching a URL path no longer apply to it. Match on attributes instead:

```js
Sentry.init({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow
},
'sentry.segment.name': {
type: 'string',
value: '/index.html',
value: 'Navigation',
},
'sentry.source': {
type: 'string',
Expand Down Expand Up @@ -182,7 +182,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 @@ -198,11 +200,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 @@ -212,9 +215,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 @@ -237,9 +244,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
6 changes: 5 additions & 1 deletion packages/angular/src/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { FUNCTION } from '@sentry/conventions/op';
import type { Integration, Span } from '@sentry/core';
import {
debug,
hasSpanStreamingEnabled,
NAVIGATION_SPAN_NAME_FALLBACK,
parseStringToURLObject,
stripUrlQueryAndFragment,
timestampInSeconds,
Expand Down Expand Up @@ -115,7 +117,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',
[SEMANTIC_ATTRIBUTE_SENTRY_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 @@ -633,7 +634,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: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.browser',
Expand Down Expand Up @@ -714,7 +719,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 @@ -293,7 +293,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 @@ -336,7 +338,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 @@ -425,7 +427,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 @@ -990,6 +994,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 @@ -115,15 +116,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}`,
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url',
Expand All @@ -135,7 +139,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 @@ -152,8 +156,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(
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
parameterizedPathname ? 'route' : 'url',
Expand All @@ -166,7 +173,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',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedPathname ? 'route' : 'url',
Expand Down Expand Up @@ -270,10 +277,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,
[SEMANTIC_ATTRIBUTE_SENTRY_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 @@ -165,7 +166,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
14 changes: 11 additions & 3 deletions packages/react-router/src/client/createClientInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
getClient,
getRootSpan,
GLOBAL_OBJ,
hasSpanStreamingEnabled,
NAVIGATION_SPAN_NAME_FALLBACK,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
Expand Down Expand Up @@ -106,7 +108,9 @@ export function createSentryClientInstrumentation(
startBrowserTracingNavigationSpan(
client,
{
name: pathname,
// With span streaming, span names have to be low cardinality, so we can't fall back to
// the URL. The route hooks parameterize the span once they resolve.
name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : pathname,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
Expand Down Expand Up @@ -149,7 +153,9 @@ export function createSentryClientInstrumentation(
navigationSpan = startBrowserTracingNavigationSpan(
client,
{
name: currentPathname,
// With span streaming, span names have to be low cardinality, so we can't fall back
// to the URL. The route is resolved once the navigation settles.
name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : currentPathname,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
Expand Down Expand Up @@ -194,7 +200,9 @@ export function createSentryClientInstrumentation(
navigationSpan = startBrowserTracingNavigationSpan(
client,
{
name: toPath,
// With span streaming, span names have to be low cardinality, so we can't fall back to
// the URL. The route hooks parameterize the span once they resolve.
name: hasSpanStreamingEnabled(client) ? NAVIGATION_SPAN_NAME_FALLBACK : toPath,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
Expand Down
6 changes: 5 additions & 1 deletion packages/react-router/src/client/hydratedRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
getClient,
getRootSpan,
GLOBAL_OBJ,
hasSpanStreamingEnabled,
isThenable,
NAVIGATION_SPAN_NAME_FALLBACK,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
Expand Down Expand Up @@ -190,7 +192,9 @@ function maybeCreateNavigationTransaction(name: string, url: string, source: 'ur
return startBrowserTracingNavigationSpan(
client,
{
name,
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL.
// The route is resolved once the router settles, which updates the span name then.
name: source === 'route' || !hasSpanStreamingEnabled(client) ? name : NAVIGATION_SPAN_NAME_FALLBACK,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation',
Expand Down
Loading
Loading