Skip to content

Commit ed0679a

Browse files
committed
feat(browser): Emit low cardinality browser request timing span names
1 parent 5b691f0 commit ed0679a

11 files changed

Lines changed: 248 additions & 83 deletions

File tree

‎MIGRATION.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,7 @@ The following span names were adjusted:
10271027
| `pageload` | The parameterized route, or the raw URL path if the SDK couldn't resolve one | `/users/:id`, `/users/123` | The parameterized route, or `Pageload` if the SDK has none | `/users/:id`, `Pageload` |
10281028
| `navigation`, `navigation.redirect` | The parameterized route, or the raw URL path if the SDK couldn't resolve one | `/users/:id`, `/users/123` | The parameterized route, or `Navigation` if the SDK has none | `/users/:id`, `Navigation` |
10291029
| `resource.*` | The resource URL, relative to the page origin for same-origin resources | `/assets/app.js` | The resource domain, or `Resource` if the SDK has none | `cdn.example.com`, `Resource` |
1030+
| `browser.*` (navigation timing) | The document URL | `https://example.com/users/123?ref=x` | A static name per timing phase. The URL stays on `url.full` | `DNS lookup`, `Request`, `Load event` |
10301031
| `http.server` | The request method and route, or the raw URL path if the SDK couldn't resolve one | `GET /users/:id`, `GET /users/123` | The request method and route when one is known, otherwise just the method | `GET /users/:id`, `GET` |
10311032
| `http.client`, `http.client.stream` | The request method and sanitized URL | `GET https://api.example.com/users/123` | The request method and the domain, or just the method if there is no domain | `GET api.example.com`, `GET` |
10321033
| `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 | `/users/:id`, `Router` |
@@ -1053,6 +1054,29 @@ The following span names were adjusted:
10531054
| `db` (supabase) | The query builder call and the table, or `auth <method>` for auth calls | `select(...) from(users)`, `auth signInWithPassword` | The operation and the table, or the dotted auth method | `select users`, `auth.signInWithPassword` |
10541055
| `db.query` (redis, ioredis) | The serialized command, with its arguments redacted, or `redis-<command>` on the diagnostics-channel path | `set test-key [1 other arguments]`, `redis-SET` | The operation and the connection, the operation and the redis function for `FCALL`/`FCALL_RO`, or `redis` when the SDK knows neither | `SET localhost:6379`, `fcall my_func`, `redis` |
10551056

1057+
#### Browser navigation timing spans
1058+
1059+
The spans for the phases of a document load were all named after the document URL, which put the raw
1060+
URL on ten spans of every pageload. None of these ops has an attribute template in the conventions, so
1061+
each phase now gets a static name:
1062+
1063+
| Span op | Name |
1064+
| ---------------------------------- | ------------------------ |
1065+
| `browser.cache` | `Cache lookup` |
1066+
| `browser.dns` | `DNS lookup` |
1067+
| `browser.connect` | `Connect` |
1068+
| `browser.tls_ssl` | `TLS handshake` |
1069+
| `browser.redirect` | `Redirect` |
1070+
| `browser.request` | `Request` |
1071+
| `browser.response` | `Response` |
1072+
| `browser.unload_event` | `Unload event` |
1073+
| `browser.dom_content_loaded_event` | `DOMContentLoaded event` |
1074+
| `browser.load_event` | `Load event` |
1075+
1076+
These spans now carry the document URL on `url.full` in both trace lifecycles, subject to the
1077+
`dataCollection.urlQueryParams` option. Match on that attribute in `ignoreSpans` and `tracesSampler`
1078+
rules that used to match these names.
1079+
10561080
#### Serverless function spans
10571081

10581082
`function.gcp` spans are named after the function, which the SDK reads from the `FUNCTION_TARGET` or

‎dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ test.describe('nested SSR routes (client, server, server request)', () => {
7474
/** The user-page route fetches from an endpoint and creates a deeply nested span structure:
7575
* pageload — /user-page/[userId]
7676
* ├── browser.** — multiple browser spans
77-
* └── browser.request — /user-page/myUsername123
77+
* └── browser.request — Request
7878
* └── http.server — GET /user-page/[userId] (SSR page request)
7979
* └── http.client — GET localhost (executing fetch call from SSR page - span)
8080
* └── http.server — GET /api/user/myUsername123.json (server request, not parametrized)

‎dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ test.describe('nested SSR routes (client, server, server request)', () => {
7474
/** The user-page route fetches from an endpoint and creates a deeply nested span structure:
7575
* pageload — /user-page/[userId]
7676
* ├── browser.** — multiple browser spans
77-
* └── browser.request — /user-page/myUsername123
77+
* └── browser.request — Request
7878
* └── http.server — GET /user-page/[userId] (SSR page request)
7979
* └── http.client — GET localhost (executing fetch call from SSR page - span)
8080
* └── http.server — GET /api/user/[userId].json (server request)

‎dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ test.describe('nested SSR routes (client, server, server request)', () => {
7373
/** The user-page route fetches from an endpoint and creates a deeply nested span structure:
7474
* pageload — /user-page/[userId]
7575
* ├── browser.** — multiple browser spans
76-
* └── browser.request — /user-page/myUsername123
76+
* └── browser.request — Request
7777
* └── http.server — GET /user-page/[userId] (SSR page request)
7878
* └── http.client — GET localhost (executing fetch call from SSR page - span)
7979
* └── http.server — GET /api/user/[userId].json (server request)

‎dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.dynamic.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ test.describe('nested SSR routes (client, server, server request)', () => {
7474
/** The user-page route fetches from an endpoint and creates a deeply nested span structure:
7575
* pageload — /user-page/[userId]
7676
* ├── browser.** — multiple browser spans
77-
* └── browser.request — /user-page/myUsername123
77+
* └── browser.request — Request
7878
* └── http.server — GET /user-page/[userId] (SSR page request)
7979
* └── http.client — GET localhost (executing fetch call from SSR page - span)
8080
* └── http.server — GET /api/user/[userId].json (server request)

‎dev-packages/e2e-tests/test-applications/astro-7-static/tests/tracing.dynamic.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ test.describe('nested SSR routes (client, server, server request)', () => {
128128
/** The user-page route fetches from an endpoint and creates a deeply nested span structure:
129129
* pageload — /user-page/myUsername123
130130
* ├── browser.** — multiple browser spans
131-
* └── browser.request — /user-page/myUsername123
131+
* └── browser.request — Request
132132
* └── http.server — GET /user-page/[userId] (SSR page request)
133133
* └── http.client — GET /api/user/myUsername123.json (executing fetch call from SSR page - span)
134134
* └── http.server — GET /api/user/myUsername123.json (server request)

‎dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.dynamic.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ test.describe('nested SSR routes (client, server, server request)', () => {
7171
/** The user-page route fetches from an endpoint and creates a deeply nested span structure:
7272
* pageload — /user-page/[userId]
7373
* ├── browser.** — multiple browser spans
74-
* └── browser.request — /user-page/myUsername123
74+
* └── browser.request — Request
7575
* └── http.server — GET /user-page/[userId] (SSR page request)
7676
* └── http.client — GET localhost (executing fetch call from SSR page - span)
7777
* └── http.server — GET /api/user/[userId].json (server request)

‎dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/spans.test.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import {
66
waitForStreamedSpans,
77
} from '@sentry-internal/test-utils';
88

9-
const BROWSER_TIMING_OPS = [
10-
'browser.dom_content_loaded_event',
11-
'browser.connect',
12-
'browser.request',
13-
'browser.response',
9+
const BROWSER_TIMING_SPANS: Array<[op: string, name: string]> = [
10+
['browser.dom_content_loaded_event', 'DOMContentLoaded event'],
11+
['browser.connect', 'Connect'],
12+
['browser.request', 'Request'],
13+
['browser.response', 'Response'],
1414
];
1515

1616
test('Captures a pageload span', async ({ page }) => {
@@ -49,10 +49,10 @@ test('Captures a pageload span', async ({ page }) => {
4949
'url.path': { value: '/', type: 'string' },
5050
});
5151

52-
for (const op of BROWSER_TIMING_OPS) {
52+
for (const [op, name] of BROWSER_TIMING_SPANS) {
5353
expect(spans).toContainEqual(
5454
expect.objectContaining({
55-
name: page.url(),
55+
name,
5656
is_segment: false,
5757
status: 'ok',
5858
parent_span_id: pageloadSpan.span_id,
@@ -63,6 +63,7 @@ test('Captures a pageload span', async ({ page }) => {
6363
attributes: expect.objectContaining({
6464
'sentry.origin': { value: 'auto.ui.browser.metrics', type: 'string' },
6565
'sentry.op': { value: op, type: 'string' },
66+
'url.full': { value: page.url(), type: 'string' },
6667
}),
6768
}),
6869
);

‎packages/browser-utils/src/performance/entries.ts‎

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable max-lines */
22
import type { Span, SpanAttributes } from '@sentry/core';
33
import {
4+
BROWSER_NAVIGATION_TIMING_SPAN_NAMES,
45
browserPerformanceTimeOrigin,
56
getActiveSpan,
67
parseUrl,
@@ -11,6 +12,7 @@ import {
1112
filterCollectedUrl,
1213
} from '@sentry/core';
1314
import {
15+
BROWSER_PAINT_TYPE,
1416
CODE_FILE_PATH,
1517
CODE_FUNCTION_NAME,
1618
HTTP_REQUEST_SAME_ORIGIN,
@@ -240,7 +242,7 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries
240242

241243
switch (entry.entryType) {
242244
case 'navigation': {
243-
_addNavigationSpans(span, entry as PerformanceNavigationTiming, timeOrigin);
245+
_addNavigationSpans(span, entry as PerformanceNavigationTiming, timeOrigin, spanStreamingEnabled);
244246
break;
245247
}
246248
case 'paint': {
@@ -269,8 +271,11 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries
269271
_trackNavigator(span, spanStreamingEnabled);
270272
}
271273

272-
/** Create a span for a browser paint performance entry. */
273-
function _addPaintSpan(
274+
/**
275+
* Create a span for a browser paint performance entry.
276+
* Exported only for tests.
277+
*/
278+
export function _addPaintSpan(
274279
span: Span,
275280
entry: PerformanceEntry,
276281
startTime: number,
@@ -280,10 +285,13 @@ function _addPaintSpan(
280285
const startTimestamp = timeOrigin + startTime;
281286

282287
startAndEndSpan(span, startTimestamp, startTimestamp + duration, {
288+
// The entry name (`first-paint`, `first-contentful-paint`) is already the low-cardinality name
289+
// the conventions ask for, so only the attribute backing it has to be added.
283290
name: entry.name,
284291
attributes: {
285292
[SENTRY_OP]: BROWSER_PAINT,
286293
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
294+
[BROWSER_PAINT_TYPE]: entry.name,
287295
},
288296
});
289297
}
@@ -292,17 +300,22 @@ function _addPaintSpan(
292300
* Instrument navigation entries
293301
* exported only for tests
294302
*/
295-
export function _addNavigationSpans(span: Span, entry: PerformanceNavigationTiming, timeOrigin: number): void {
296-
_addPerformanceNavigationTiming(span, entry, 'unloadEvent', timeOrigin);
297-
_addPerformanceNavigationTiming(span, entry, 'redirect', timeOrigin);
298-
_addPerformanceNavigationTiming(span, entry, 'domContentLoadedEvent', timeOrigin);
299-
_addPerformanceNavigationTiming(span, entry, 'loadEvent', timeOrigin);
300-
_addPerformanceNavigationTiming(span, entry, 'connect', timeOrigin);
301-
_addPerformanceNavigationTiming(span, entry, 'secureConnection', timeOrigin);
302-
_addPerformanceNavigationTiming(span, entry, 'fetch', timeOrigin);
303-
_addPerformanceNavigationTiming(span, entry, 'domainLookup', timeOrigin);
304-
305-
_addRequest(span, entry, timeOrigin);
303+
export function _addNavigationSpans(
304+
span: Span,
305+
entry: PerformanceNavigationTiming,
306+
timeOrigin: number,
307+
spanStreamingEnabled?: boolean,
308+
): void {
309+
_addPerformanceNavigationTiming(span, entry, 'unloadEvent', timeOrigin, spanStreamingEnabled);
310+
_addPerformanceNavigationTiming(span, entry, 'redirect', timeOrigin, spanStreamingEnabled);
311+
_addPerformanceNavigationTiming(span, entry, 'domContentLoadedEvent', timeOrigin, spanStreamingEnabled);
312+
_addPerformanceNavigationTiming(span, entry, 'loadEvent', timeOrigin, spanStreamingEnabled);
313+
_addPerformanceNavigationTiming(span, entry, 'connect', timeOrigin, spanStreamingEnabled);
314+
_addPerformanceNavigationTiming(span, entry, 'secureConnection', timeOrigin, spanStreamingEnabled);
315+
_addPerformanceNavigationTiming(span, entry, 'fetch', timeOrigin, spanStreamingEnabled);
316+
_addPerformanceNavigationTiming(span, entry, 'domainLookup', timeOrigin, spanStreamingEnabled);
317+
318+
_addRequest(span, entry, timeOrigin, spanStreamingEnabled);
306319
}
307320

308321
type StartEventName =
@@ -315,7 +328,7 @@ type StartEventName =
315328
| 'domContentLoadedEvent'
316329
| 'loadEvent';
317330

318-
const NAVIGATION_TIMING_SPAN_OPS: Record<StartEventName, string> = {
331+
const NAVIGATION_TIMING_SPAN_OPS = {
319332
secureConnection: BROWSER_TLS_SSL,
320333
fetch: BROWSER_CACHE,
321334
domainLookup: BROWSER_DNS,
@@ -324,7 +337,7 @@ const NAVIGATION_TIMING_SPAN_OPS: Record<StartEventName, string> = {
324337
connect: BROWSER_CONNECT,
325338
domContentLoadedEvent: BROWSER_DOM_CONTENT_LOADED_EVENT,
326339
loadEvent: BROWSER_LOAD_EVENT,
327-
};
340+
} as const satisfies Record<StartEventName, string>;
328341

329342
type EndEventName =
330343
| 'domainLookupStart'
@@ -341,18 +354,23 @@ function _addPerformanceNavigationTiming(
341354
entry: PerformanceNavigationTiming,
342355
event: StartEventName,
343356
timeOrigin: number,
357+
spanStreamingEnabled: boolean | undefined,
344358
): void {
345359
const eventEnd = _getEndPropertyNameForNavigationTiming(event) satisfies keyof PerformanceNavigationTiming;
346360
const end = entry[eventEnd];
347361
const start = entry[`${event}Start`];
348362
if (!start || !end) {
349363
return;
350364
}
365+
const op = NAVIGATION_TIMING_SPAN_OPS[event];
351366
startAndEndSpan(span, timeOrigin + msToSec(start), timeOrigin + msToSec(end), {
352-
name: entry.name,
367+
// With span streaming, span names have to be low cardinality, so we can't fall back to the
368+
// document URL. `url.full` keeps it, and is what Relay derives the description from.
369+
name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[op] : entry.name,
353370
attributes: {
354-
[SENTRY_OP]: NAVIGATION_TIMING_SPAN_OPS[event],
371+
[SENTRY_OP]: op,
355372
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
373+
[URL_FULL]: filterCollectedUrl(entry.name),
356374
...(event === 'redirect' && entry.redirectCount != null ? { 'http.redirect_count': entry.redirectCount } : {}),
357375
},
358376
});
@@ -369,7 +387,12 @@ function _getEndPropertyNameForNavigationTiming(event: StartEventName): EndEvent
369387
}
370388

371389
/** Create request and response related spans */
372-
function _addRequest(span: Span, entry: PerformanceNavigationTiming, timeOrigin: number): void {
390+
function _addRequest(
391+
span: Span,
392+
entry: PerformanceNavigationTiming,
393+
timeOrigin: number,
394+
spanStreamingEnabled: boolean | undefined,
395+
): void {
373396
const requestStartTimestamp = timeOrigin + msToSec(entry.requestStart);
374397
const responseEndTimestamp = timeOrigin + msToSec(entry.responseEnd);
375398
const responseStartTimestamp = timeOrigin + msToSec(entry.responseStart);
@@ -378,19 +401,26 @@ function _addRequest(span: Span, entry: PerformanceNavigationTiming, timeOrigin:
378401
// In this case, ie. when the document request hasn't finished yet, `entry.responseEnd` will be 0.
379402
// In order not to produce faulty spans, where the end timestamp is before the start timestamp, we will only collect
380403
// these spans when the responseEnd value is available. The backend (Relay) would drop the entire span if it contained faulty spans.
404+
405+
// With span streaming, span names have to be low cardinality, so we can't fall back to the
406+
// document URL. `url.full` keeps it, and is what Relay derives the description from.
407+
const url = filterCollectedUrl(entry.name);
408+
381409
startAndEndSpan(span, requestStartTimestamp, responseEndTimestamp, {
382-
name: entry.name,
410+
name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[BROWSER_REQUEST] : entry.name,
383411
attributes: {
384412
[SENTRY_OP]: BROWSER_REQUEST,
385413
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
414+
[URL_FULL]: url,
386415
},
387416
});
388417

389418
startAndEndSpan(span, responseStartTimestamp, responseEndTimestamp, {
390-
name: entry.name,
419+
name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[BROWSER_RESPONSE] : entry.name,
391420
attributes: {
392421
[SENTRY_OP]: BROWSER_RESPONSE,
393422
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
423+
[URL_FULL]: url,
394424
},
395425
});
396426
}

0 commit comments

Comments
 (0)