Skip to content

Commit 51c5eda

Browse files
Lms24claude
andauthored
feat: Emit low-cardinality pageload span names when streaming spans (#23424)
This PR adjusts all `sentry.op: 'pageload'` spans to only emit low cardinality span names if span streaming is enabled. Concretely, for pageload spans, this means that we want to never set a raw URL as a span name, which was the current fallback for transaction-based span names. Instead, we now fall back to a static `"Pageload"` span name. The new low-cardinality pageload span names now adhere to the rules set in [conventions](https://getsentry.github.io/sentry-conventions/names/#browser-pageload). ref #22350 --------- Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
1 parent 897f5a2 commit 51c5eda

38 files changed

Lines changed: 291 additions & 70 deletions

File tree

‎MIGRATION.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,37 @@ These changes are not caught by TypeScript. If you filter, group, or alert on sp
610610
| `browser.TLS/SSL` | `browser.tls_ssl` |
611611
| `browser.DNS` | `browser.dns` |
612612

613+
### Span name changes
614+
615+
Affected SDKs: All SDKs running in the browser.
616+
617+
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/).
618+
619+
In v11, this only affects `pageload` spans. Further ops will follow in future releases.
620+
If you [opt out of span streaming](#opting-out-of-span-streaming), span names remain unchanged.
621+
622+
The following span names were adjusted:
623+
624+
| Span op | Before | After |
625+
| ---------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
626+
| `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 |
627+
628+
Some consequences to be aware of:
629+
630+
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.
631+
632+
`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:
633+
634+
```js
635+
Sentry.init({
636+
// Before
637+
ignoreSpans: ['/health'],
638+
639+
// After
640+
ignoreSpans: [{ name: 'Pageload', attributes: { 'sentry.op': 'pageload', 'url.path': '/health' } }],
641+
});
642+
```
643+
613644
### LangGraph no longer emits `create_agent` spans
614645

615646
Affected SDKs: All server-side SDKs.

‎dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions-streamed/test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
9999
},
100100
[SENTRY_SEGMENT_NAME]: {
101101
type: 'string',
102-
value: '/index.html',
102+
value: 'Pageload',
103103
},
104104
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: {
105105
type: 'string',
@@ -116,7 +116,8 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
116116
},
117117
end_timestamp: expect.any(Number),
118118
is_segment: true,
119-
name: '/index.html',
119+
// Interaction spans are named after the current route, which is the pageload span's name.
120+
name: 'Pageload',
120121
span_id: interactionSegmentSpan!.span_id,
121122
start_timestamp: expect.any(Number),
122123
status: 'ok',
@@ -155,7 +156,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser
155156
},
156157
[SENTRY_SEGMENT_NAME]: {
157158
type: 'string',
158-
value: '/index.html',
159+
value: 'Pageload',
159160
},
160161
[SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: {
161162
type: 'string',

‎dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/navigation-streamed/test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ sentryTest('starts a streamed navigation span on page navigation', async ({ brow
6868
expect(navigationTraceId).toBeDefined();
6969
expect(pageloadTraceId).not.toEqual(navigationTraceId);
7070

71-
expect(pageloadSpan.name).toEqual('/index.html');
71+
expect(pageloadSpan.name).toEqual('Pageload');
7272

7373
expect(navigationSpan).toEqual({
7474
attributes: {

‎dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/pageload-streamed/test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ sentryTest(
159159
},
160160
[SENTRY_SEGMENT_NAME]: {
161161
type: 'string',
162-
value: '/index.html',
162+
value: 'Pageload',
163163
},
164164
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: {
165165
type: 'string',
@@ -180,7 +180,9 @@ sentryTest(
180180
},
181181
end_timestamp: expect.any(Number),
182182
is_segment: true,
183-
name: '/index.html',
183+
// The raw URL stays in `url.path`/`url.full`: with span streaming, a pageload span name is
184+
// low cardinality and falls back to 'Pageload' when there is no parameterized route.
185+
name: 'Pageload',
184186
span_id: expect.stringMatching(/^[\da-f]{16}$/),
185187
start_timestamp: expect.any(Number),
186188
status: 'ok',

‎dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ sentryTest('captures CLS as a streamed span with source attributes', async ({ ge
3939
expect(clsSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome'));
4040

4141
// Check the CLS span carries the segment name it belongs to
42-
expect(clsSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: '/index.html' });
42+
expect(clsSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' });
4343

4444
// Check browser.web_vital.cls.source attributes
4545
expect(clsSpan.attributes['browser.web_vital.cls.source.1']?.value).toEqual(

‎dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ sentryTest('captures INP click as a streamed span', async ({ getLocalTestUrl, pa
3232
expect(inpSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome'));
3333

3434
// Check the INP span carries the transaction/segment name it belongs to
35-
expect(inpSpan.attributes['sentry.transaction']).toEqual({ type: 'string', value: '/index.html' });
36-
expect(inpSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: '/index.html' });
35+
expect(inpSpan.attributes['sentry.transaction']).toEqual({ type: 'string', value: 'Pageload' });
36+
expect(inpSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' });
3737

3838
const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number;
3939
expect(inpValue).toBeGreaterThan(0);

‎dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ sentryTest('captures LCP as a streamed span with element attributes', async ({ g
4141
expect(lcpSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome'));
4242

4343
// Check the LCP span carries the segment name it belongs to
44-
expect(lcpSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: '/index.html' });
44+
expect(lcpSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' });
4545

4646
// Check browser.web_vital.lcp.* attributes
4747
expect(lcpSpan.attributes['browser.web_vital.lcp.element']?.value).toEqual(expect.stringContaining('body > img'));

‎dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheComponents.spec.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ test('Prerendered shell does not stitch the pageload onto a stale trace', async
7171
});
7272

7373
const pageloadSpanPromise = waitForStreamedSpan('nextjs-16-streaming-cacheComponents', span => {
74-
return span.name === '/pageload-tracing' && getSpanOp(span) === 'pageload' && span.is_segment;
74+
return span.name === 'Pageload' && getSpanOp(span) === 'pageload' && span.is_segment;
7575
});
7676

7777
await page.goto('/pageload-tracing');
@@ -80,6 +80,10 @@ test('Prerendered shell does not stitch the pageload onto a stale trace', async
8080

8181
const [serverSpan, pageloadSpan] = await Promise.all([serverSpanPromise, pageloadSpanPromise]);
8282

83+
expect(pageloadSpan.attributes).toMatchObject({
84+
['sentry.segment.name.source']: { value: 'url', type: 'string' },
85+
['url.path']: { value: '/pageload-tracing', type: 'string' },
86+
});
8387
// Under Cache Components the can be prerendered and rendered in a context detached from the
8488
// runtime server request, so a `sentry-trace` meta tag would carry a stale/unrelated trace. The
8589
// SDK therefore does not enable the trace meta tags, and the browser pageload starts a fresh trace

‎dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/pageload-tracing.test.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ test('Server and client pageload spans should share the same trace', async ({ pa
77
});
88

99
const pageloadSpanPromise = waitForStreamedSpan('nextjs-16-streaming', span => {
10-
return span.name === '/pageload-tracing' && getSpanOp(span) === 'pageload' && span.is_segment;
10+
return span.name === 'Pageload' && getSpanOp(span) === 'pageload' && span.is_segment;
1111
});
1212

1313
await page.goto(`/pageload-tracing`);
@@ -16,4 +16,8 @@ test('Server and client pageload spans should share the same trace', async ({ pa
1616

1717
expect(pageloadSpan.trace_id).toBeTruthy();
1818
expect(serverSpan.trace_id).toBe(pageloadSpan.trace_id);
19+
expect(pageloadSpan.attributes).toMatchObject({
20+
['sentry.segment.name.source']: { value: 'url', type: 'string' },
21+
['url.path']: { value: '/pageload-tracing', type: 'string' },
22+
});
1923
});

‎dev-packages/e2e-tests/test-applications/nextjs-16-streaming/tests/parameterized-routes.test.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,19 @@ test('should create a static streamed span when the `app` directory is used and
1919
page,
2020
}) => {
2121
const spanPromise = waitForStreamedSpan('nextjs-16-streaming', span => {
22-
return span.name === '/parameterized/static' && getSpanOp(span) === 'pageload' && span.is_segment;
22+
return span.name === 'Pageload' && getSpanOp(span) === 'pageload' && span.is_segment;
2323
});
2424

2525
await page.goto(`/parameterized/static`);
2626

2727
const span = await spanPromise;
2828

29-
expect(span.name).toBe('/parameterized/static');
29+
expect(span.name).toBe('Pageload');
3030
expect(span.trace_id).toMatch(/[a-f0-9]{32}/);
31-
expect(span.attributes['sentry.source']?.value).toBe('url');
31+
expect(span.attributes).toMatchObject({
32+
['sentry.segment.name.source']: { value: 'url', type: 'string' },
33+
['url.path']: { value: '/parameterized/static', type: 'string' },
34+
});
3235
});
3336

3437
test('should create a partially parameterized streamed span when the `app` directory is used', async ({ page }) => {

0 commit comments

Comments
 (0)