From ed0679a2c3bf508f0977aaf616a57b2fe7767323 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 22 Sep 2026 19:09:05 +0200 Subject: [PATCH] feat(browser): Emit low cardinality `browser` request timing span names --- MIGRATION.md | 24 +++ .../astro-4/tests/tracing.dynamic.test.ts | 2 +- .../astro-5/tests/tracing.dynamic.test.ts | 2 +- .../tests/tracing.dynamic.test.ts | 2 +- .../astro-6/tests/tracing.dynamic.test.ts | 2 +- .../tests/tracing.dynamic.test.ts | 2 +- .../astro-7/tests/tracing.dynamic.test.ts | 2 +- .../tests/spans.test.ts | 15 +- .../browser-utils/src/performance/entries.ts | 72 +++++--- .../test/performance/browserMetrics.test.ts | 174 +++++++++++++----- packages/core/src/tracing/spans/spanNames.ts | 34 +++- 11 files changed, 248 insertions(+), 83 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 128947e78e9c..ab36abf504f4 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1027,6 +1027,7 @@ The following span names were adjusted: | `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` | | `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` | | `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` | +| `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` | | `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` | | `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` | | `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: | `db` (supabase) | The query builder call and the table, or `auth ` for auth calls | `select(...) from(users)`, `auth signInWithPassword` | The operation and the table, or the dotted auth method | `select users`, `auth.signInWithPassword` | | `db.query` (redis, ioredis) | The serialized command, with its arguments redacted, or `redis-` 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` | +#### Browser navigation timing spans + +The spans for the phases of a document load were all named after the document URL, which put the raw +URL on ten spans of every pageload. None of these ops has an attribute template in the conventions, so +each phase now gets a static name: + +| Span op | Name | +| ---------------------------------- | ------------------------ | +| `browser.cache` | `Cache lookup` | +| `browser.dns` | `DNS lookup` | +| `browser.connect` | `Connect` | +| `browser.tls_ssl` | `TLS handshake` | +| `browser.redirect` | `Redirect` | +| `browser.request` | `Request` | +| `browser.response` | `Response` | +| `browser.unload_event` | `Unload event` | +| `browser.dom_content_loaded_event` | `DOMContentLoaded event` | +| `browser.load_event` | `Load event` | + +These spans now carry the document URL on `url.full` in both trace lifecycles, subject to the +`dataCollection.urlQueryParams` option. Match on that attribute in `ignoreSpans` and `tracesSampler` +rules that used to match these names. + #### Serverless function spans `function.gcp` spans are named after the function, which the SDK reads from the `FUNCTION_TARGET` or diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts index 88d123e2c8f6..942a1b916bee 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts @@ -74,7 +74,7 @@ test.describe('nested SSR routes (client, server, server request)', () => { /** The user-page route fetches from an endpoint and creates a deeply nested span structure: * pageload — /user-page/[userId] * ├── browser.** — multiple browser spans - * └── browser.request — /user-page/myUsername123 + * └── browser.request — Request * └── http.server — GET /user-page/[userId] (SSR page request) * └── http.client — GET localhost (executing fetch call from SSR page - span) * └── http.server — GET /api/user/myUsername123.json (server request, not parametrized) diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts index dfc5c3b5de16..04618aa999e8 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts @@ -74,7 +74,7 @@ test.describe('nested SSR routes (client, server, server request)', () => { /** The user-page route fetches from an endpoint and creates a deeply nested span structure: * pageload — /user-page/[userId] * ├── browser.** — multiple browser spans - * └── browser.request — /user-page/myUsername123 + * └── browser.request — Request * └── http.server — GET /user-page/[userId] (SSR page request) * └── http.client — GET localhost (executing fetch call from SSR page - span) * └── http.server — GET /api/user/[userId].json (server request) diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts index 51a22ac11f06..c6b2dd13c5b3 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts @@ -73,7 +73,7 @@ test.describe('nested SSR routes (client, server, server request)', () => { /** The user-page route fetches from an endpoint and creates a deeply nested span structure: * pageload — /user-page/[userId] * ├── browser.** — multiple browser spans - * └── browser.request — /user-page/myUsername123 + * └── browser.request — Request * └── http.server — GET /user-page/[userId] (SSR page request) * └── http.client — GET localhost (executing fetch call from SSR page - span) * └── http.server — GET /api/user/[userId].json (server request) diff --git a/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.dynamic.test.ts index 8b272e8c1929..209f9749309f 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-6/tests/tracing.dynamic.test.ts @@ -74,7 +74,7 @@ test.describe('nested SSR routes (client, server, server request)', () => { /** The user-page route fetches from an endpoint and creates a deeply nested span structure: * pageload — /user-page/[userId] * ├── browser.** — multiple browser spans - * └── browser.request — /user-page/myUsername123 + * └── browser.request — Request * └── http.server — GET /user-page/[userId] (SSR page request) * └── http.client — GET localhost (executing fetch call from SSR page - span) * └── http.server — GET /api/user/[userId].json (server request) diff --git a/dev-packages/e2e-tests/test-applications/astro-7-static/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-7-static/tests/tracing.dynamic.test.ts index c33f97788382..ef33638e8999 100644 --- a/dev-packages/e2e-tests/test-applications/astro-7-static/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-7-static/tests/tracing.dynamic.test.ts @@ -128,7 +128,7 @@ test.describe('nested SSR routes (client, server, server request)', () => { /** The user-page route fetches from an endpoint and creates a deeply nested span structure: * pageload — /user-page/myUsername123 * ├── browser.** — multiple browser spans - * └── browser.request — /user-page/myUsername123 + * └── browser.request — Request * └── http.server — GET /user-page/[userId] (SSR page request) * └── http.client — GET /api/user/myUsername123.json (executing fetch call from SSR page - span) * └── http.server — GET /api/user/myUsername123.json (server request) diff --git a/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.dynamic.test.ts index 4733d5e64ff0..aaf573032918 100644 --- a/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-7/tests/tracing.dynamic.test.ts @@ -71,7 +71,7 @@ test.describe('nested SSR routes (client, server, server request)', () => { /** The user-page route fetches from an endpoint and creates a deeply nested span structure: * pageload — /user-page/[userId] * ├── browser.** — multiple browser spans - * └── browser.request — /user-page/myUsername123 + * └── browser.request — Request * └── http.server — GET /user-page/[userId] (SSR page request) * └── http.client — GET localhost (executing fetch call from SSR page - span) * └── http.server — GET /api/user/[userId].json (server request) diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/spans.test.ts index e1b9c218ff43..b5821fbe80b7 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/spans.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/spans.test.ts @@ -6,11 +6,11 @@ import { waitForStreamedSpans, } from '@sentry-internal/test-utils'; -const BROWSER_TIMING_OPS = [ - 'browser.dom_content_loaded_event', - 'browser.connect', - 'browser.request', - 'browser.response', +const BROWSER_TIMING_SPANS: Array<[op: string, name: string]> = [ + ['browser.dom_content_loaded_event', 'DOMContentLoaded event'], + ['browser.connect', 'Connect'], + ['browser.request', 'Request'], + ['browser.response', 'Response'], ]; test('Captures a pageload span', async ({ page }) => { @@ -49,10 +49,10 @@ test('Captures a pageload span', async ({ page }) => { 'url.path': { value: '/', type: 'string' }, }); - for (const op of BROWSER_TIMING_OPS) { + for (const [op, name] of BROWSER_TIMING_SPANS) { expect(spans).toContainEqual( expect.objectContaining({ - name: page.url(), + name, is_segment: false, status: 'ok', parent_span_id: pageloadSpan.span_id, @@ -63,6 +63,7 @@ test('Captures a pageload span', async ({ page }) => { attributes: expect.objectContaining({ 'sentry.origin': { value: 'auto.ui.browser.metrics', type: 'string' }, 'sentry.op': { value: op, type: 'string' }, + 'url.full': { value: page.url(), type: 'string' }, }), }), ); diff --git a/packages/browser-utils/src/performance/entries.ts b/packages/browser-utils/src/performance/entries.ts index 8620b71a22d7..336122567ade 100644 --- a/packages/browser-utils/src/performance/entries.ts +++ b/packages/browser-utils/src/performance/entries.ts @@ -1,6 +1,7 @@ /* eslint-disable max-lines */ import type { Span, SpanAttributes } from '@sentry/core'; import { + BROWSER_NAVIGATION_TIMING_SPAN_NAMES, browserPerformanceTimeOrigin, getActiveSpan, parseUrl, @@ -11,6 +12,7 @@ import { filterCollectedUrl, } from '@sentry/core'; import { + BROWSER_PAINT_TYPE, CODE_FILE_PATH, CODE_FUNCTION_NAME, HTTP_REQUEST_SAME_ORIGIN, @@ -240,7 +242,7 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries switch (entry.entryType) { case 'navigation': { - _addNavigationSpans(span, entry as PerformanceNavigationTiming, timeOrigin); + _addNavigationSpans(span, entry as PerformanceNavigationTiming, timeOrigin, spanStreamingEnabled); break; } case 'paint': { @@ -269,8 +271,11 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries _trackNavigator(span, spanStreamingEnabled); } -/** Create a span for a browser paint performance entry. */ -function _addPaintSpan( +/** + * Create a span for a browser paint performance entry. + * Exported only for tests. + */ +export function _addPaintSpan( span: Span, entry: PerformanceEntry, startTime: number, @@ -280,10 +285,13 @@ function _addPaintSpan( const startTimestamp = timeOrigin + startTime; startAndEndSpan(span, startTimestamp, startTimestamp + duration, { + // The entry name (`first-paint`, `first-contentful-paint`) is already the low-cardinality name + // the conventions ask for, so only the attribute backing it has to be added. name: entry.name, attributes: { [SENTRY_OP]: BROWSER_PAINT, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [BROWSER_PAINT_TYPE]: entry.name, }, }); } @@ -292,17 +300,22 @@ function _addPaintSpan( * Instrument navigation entries * exported only for tests */ -export function _addNavigationSpans(span: Span, entry: PerformanceNavigationTiming, timeOrigin: number): void { - _addPerformanceNavigationTiming(span, entry, 'unloadEvent', timeOrigin); - _addPerformanceNavigationTiming(span, entry, 'redirect', timeOrigin); - _addPerformanceNavigationTiming(span, entry, 'domContentLoadedEvent', timeOrigin); - _addPerformanceNavigationTiming(span, entry, 'loadEvent', timeOrigin); - _addPerformanceNavigationTiming(span, entry, 'connect', timeOrigin); - _addPerformanceNavigationTiming(span, entry, 'secureConnection', timeOrigin); - _addPerformanceNavigationTiming(span, entry, 'fetch', timeOrigin); - _addPerformanceNavigationTiming(span, entry, 'domainLookup', timeOrigin); - - _addRequest(span, entry, timeOrigin); +export function _addNavigationSpans( + span: Span, + entry: PerformanceNavigationTiming, + timeOrigin: number, + spanStreamingEnabled?: boolean, +): void { + _addPerformanceNavigationTiming(span, entry, 'unloadEvent', timeOrigin, spanStreamingEnabled); + _addPerformanceNavigationTiming(span, entry, 'redirect', timeOrigin, spanStreamingEnabled); + _addPerformanceNavigationTiming(span, entry, 'domContentLoadedEvent', timeOrigin, spanStreamingEnabled); + _addPerformanceNavigationTiming(span, entry, 'loadEvent', timeOrigin, spanStreamingEnabled); + _addPerformanceNavigationTiming(span, entry, 'connect', timeOrigin, spanStreamingEnabled); + _addPerformanceNavigationTiming(span, entry, 'secureConnection', timeOrigin, spanStreamingEnabled); + _addPerformanceNavigationTiming(span, entry, 'fetch', timeOrigin, spanStreamingEnabled); + _addPerformanceNavigationTiming(span, entry, 'domainLookup', timeOrigin, spanStreamingEnabled); + + _addRequest(span, entry, timeOrigin, spanStreamingEnabled); } type StartEventName = @@ -315,7 +328,7 @@ type StartEventName = | 'domContentLoadedEvent' | 'loadEvent'; -const NAVIGATION_TIMING_SPAN_OPS: Record = { +const NAVIGATION_TIMING_SPAN_OPS = { secureConnection: BROWSER_TLS_SSL, fetch: BROWSER_CACHE, domainLookup: BROWSER_DNS, @@ -324,7 +337,7 @@ const NAVIGATION_TIMING_SPAN_OPS: Record = { connect: BROWSER_CONNECT, domContentLoadedEvent: BROWSER_DOM_CONTENT_LOADED_EVENT, loadEvent: BROWSER_LOAD_EVENT, -}; +} as const satisfies Record; type EndEventName = | 'domainLookupStart' @@ -341,6 +354,7 @@ function _addPerformanceNavigationTiming( entry: PerformanceNavigationTiming, event: StartEventName, timeOrigin: number, + spanStreamingEnabled: boolean | undefined, ): void { const eventEnd = _getEndPropertyNameForNavigationTiming(event) satisfies keyof PerformanceNavigationTiming; const end = entry[eventEnd]; @@ -348,11 +362,15 @@ function _addPerformanceNavigationTiming( if (!start || !end) { return; } + const op = NAVIGATION_TIMING_SPAN_OPS[event]; startAndEndSpan(span, timeOrigin + msToSec(start), timeOrigin + msToSec(end), { - name: entry.name, + // With span streaming, span names have to be low cardinality, so we can't fall back to the + // document URL. `url.full` keeps it, and is what Relay derives the description from. + name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[op] : entry.name, attributes: { - [SENTRY_OP]: NAVIGATION_TIMING_SPAN_OPS[event], + [SENTRY_OP]: op, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [URL_FULL]: filterCollectedUrl(entry.name), ...(event === 'redirect' && entry.redirectCount != null ? { 'http.redirect_count': entry.redirectCount } : {}), }, }); @@ -369,7 +387,12 @@ function _getEndPropertyNameForNavigationTiming(event: StartEventName): EndEvent } /** Create request and response related spans */ -function _addRequest(span: Span, entry: PerformanceNavigationTiming, timeOrigin: number): void { +function _addRequest( + span: Span, + entry: PerformanceNavigationTiming, + timeOrigin: number, + spanStreamingEnabled: boolean | undefined, +): void { const requestStartTimestamp = timeOrigin + msToSec(entry.requestStart); const responseEndTimestamp = timeOrigin + msToSec(entry.responseEnd); const responseStartTimestamp = timeOrigin + msToSec(entry.responseStart); @@ -378,19 +401,26 @@ function _addRequest(span: Span, entry: PerformanceNavigationTiming, timeOrigin: // In this case, ie. when the document request hasn't finished yet, `entry.responseEnd` will be 0. // In order not to produce faulty spans, where the end timestamp is before the start timestamp, we will only collect // these spans when the responseEnd value is available. The backend (Relay) would drop the entire span if it contained faulty spans. + + // With span streaming, span names have to be low cardinality, so we can't fall back to the + // document URL. `url.full` keeps it, and is what Relay derives the description from. + const url = filterCollectedUrl(entry.name); + startAndEndSpan(span, requestStartTimestamp, responseEndTimestamp, { - name: entry.name, + name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[BROWSER_REQUEST] : entry.name, attributes: { [SENTRY_OP]: BROWSER_REQUEST, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [URL_FULL]: url, }, }); startAndEndSpan(span, responseStartTimestamp, responseEndTimestamp, { - name: entry.name, + name: spanStreamingEnabled ? BROWSER_NAVIGATION_TIMING_SPAN_NAMES[BROWSER_RESPONSE] : entry.name, attributes: { [SENTRY_OP]: BROWSER_RESPONSE, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', + [URL_FULL]: url, }, }); } diff --git a/packages/browser-utils/test/performance/browserMetrics.test.ts b/packages/browser-utils/test/performance/browserMetrics.test.ts index 10555be9cb44..d01d7b3c779a 100644 --- a/packages/browser-utils/test/performance/browserMetrics.test.ts +++ b/packages/browser-utils/test/performance/browserMetrics.test.ts @@ -9,7 +9,12 @@ import { spanToJSON, } from '@sentry/core'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; -import { _addNavigationSpans, _addResourceSpans, _setResourceRequestAttributes } from '../../src/performance/entries'; +import { + _addNavigationSpans, + _addPaintSpan, + _addResourceSpans, + _setResourceRequestAttributes, +} from '../../src/performance/entries'; import { addWebVitalsToSpan, startTrackingWebVitals } from '../../src/web-vitals/tracking'; import { WINDOW } from '../../src/types'; import { getDefaultClientOptions, TestClient } from '../utils/TestClient'; @@ -644,45 +649,46 @@ describe('_addNavigationSpans', () => { client.init(); }); + // entry taken from a real entry via browser dev tools + const entry: PerformanceNavigationTiming = { + name: 'https://santry.com/test?q=secret#frag', + entryType: 'navigation', + startTime: 0, + duration: 546.1000000014901, + initiatorType: 'navigation', + nextHopProtocol: 'h2', + workerStart: 0, + redirectStart: 7.5, + redirectEnd: 20.5, + redirectCount: 2, + fetchStart: 4.9000000059604645, + domainLookupStart: 4.9000000059604645, + domainLookupEnd: 4.9000000059604645, + connectStart: 4.9000000059604645, + secureConnectionStart: 4.9000000059604645, + connectEnd: 4.9000000059604645, + requestStart: 7.9000000059604645, + responseStart: 396.80000000447035, + responseEnd: 416.40000000596046, + transferSize: 14726, + encodedBodySize: 14426, + decodedBodySize: 67232, + responseStatus: 200, + serverTiming: [], + unloadEventStart: 0, + unloadEventEnd: 0, + domInteractive: 473.20000000298023, + domContentLoadedEventStart: 480.1000000014901, + domContentLoadedEventEnd: 480.30000000447035, + domComplete: 546, + loadEventStart: 546, + loadEventEnd: 546.1000000014901, + type: 'navigate', + activationStart: 0, + toJSON: () => ({}), + }; + it('adds navigation spans based on the navigation performance entry', () => { - // entry taken from a real entry via browser dev tools - const entry: PerformanceNavigationTiming = { - name: 'https://santry.com/test', - entryType: 'navigation', - startTime: 0, - duration: 546.1000000014901, - initiatorType: 'navigation', - nextHopProtocol: 'h2', - workerStart: 0, - redirectStart: 7.5, - redirectEnd: 20.5, - redirectCount: 2, - fetchStart: 4.9000000059604645, - domainLookupStart: 4.9000000059604645, - domainLookupEnd: 4.9000000059604645, - connectStart: 4.9000000059604645, - secureConnectionStart: 4.9000000059604645, - connectEnd: 4.9000000059604645, - requestStart: 7.9000000059604645, - responseStart: 396.80000000447035, - responseEnd: 416.40000000596046, - transferSize: 14726, - encodedBodySize: 14426, - decodedBodySize: 67232, - responseStatus: 200, - serverTiming: [], - unloadEventStart: 0, - unloadEventEnd: 0, - domInteractive: 473.20000000298023, - domContentLoadedEventStart: 480.1000000014901, - domContentLoadedEventEnd: 480.30000000447035, - domComplete: 546, - loadEventStart: 546, - loadEventEnd: 546.1000000014901, - type: 'navigate', - activationStart: 0, - toJSON: () => ({}), - }; const spans: Span[] = []; getClient()?.on('spanEnd', span => { @@ -701,8 +707,9 @@ describe('_addNavigationSpans', () => { attributes: { 'sentry.op': 'browser.dom_content_loaded_event', 'sentry.origin': 'auto.ui.browser.metrics', + 'url.full': 'https://santry.com/test?q=secret#frag', }, - name: 'https://santry.com/test', + name: 'https://santry.com/test?q=secret#frag', parent_span_id, trace_id, }), @@ -710,8 +717,9 @@ describe('_addNavigationSpans', () => { attributes: { 'sentry.op': 'browser.load_event', 'sentry.origin': 'auto.ui.browser.metrics', + 'url.full': 'https://santry.com/test?q=secret#frag', }, - name: 'https://santry.com/test', + name: 'https://santry.com/test?q=secret#frag', parent_span_id, trace_id, }), @@ -719,8 +727,9 @@ describe('_addNavigationSpans', () => { attributes: { 'sentry.op': 'browser.connect', 'sentry.origin': 'auto.ui.browser.metrics', + 'url.full': 'https://santry.com/test?q=secret#frag', }, - name: 'https://santry.com/test', + name: 'https://santry.com/test?q=secret#frag', parent_span_id, trace_id, }), @@ -728,8 +737,9 @@ describe('_addNavigationSpans', () => { attributes: { 'sentry.op': 'browser.tls_ssl', 'sentry.origin': 'auto.ui.browser.metrics', + 'url.full': 'https://santry.com/test?q=secret#frag', }, - name: 'https://santry.com/test', + name: 'https://santry.com/test?q=secret#frag', parent_span_id, trace_id, }), @@ -737,8 +747,9 @@ describe('_addNavigationSpans', () => { attributes: { 'sentry.op': 'browser.cache', 'sentry.origin': 'auto.ui.browser.metrics', + 'url.full': 'https://santry.com/test?q=secret#frag', }, - name: 'https://santry.com/test', + name: 'https://santry.com/test?q=secret#frag', parent_span_id, trace_id, }), @@ -746,8 +757,9 @@ describe('_addNavigationSpans', () => { attributes: { 'sentry.op': 'browser.dns', 'sentry.origin': 'auto.ui.browser.metrics', + 'url.full': 'https://santry.com/test?q=secret#frag', }, - name: 'https://santry.com/test', + name: 'https://santry.com/test?q=secret#frag', parent_span_id, trace_id, }), @@ -755,8 +767,9 @@ describe('_addNavigationSpans', () => { attributes: { 'sentry.op': 'browser.request', 'sentry.origin': 'auto.ui.browser.metrics', + 'url.full': 'https://santry.com/test?q=secret#frag', }, - name: 'https://santry.com/test', + name: 'https://santry.com/test?q=secret#frag', parent_span_id, trace_id, }), @@ -764,8 +777,9 @@ describe('_addNavigationSpans', () => { attributes: { 'sentry.op': 'browser.response', 'sentry.origin': 'auto.ui.browser.metrics', + 'url.full': 'https://santry.com/test?q=secret#frag', }, - name: 'https://santry.com/test', + name: 'https://santry.com/test?q=secret#frag', parent_span_id, trace_id, }), @@ -774,14 +788,78 @@ describe('_addNavigationSpans', () => { 'http.redirect_count': 2, 'sentry.op': 'browser.redirect', 'sentry.origin': 'auto.ui.browser.metrics', + 'url.full': 'https://santry.com/test?q=secret#frag', }, - name: 'https://santry.com/test', + name: 'https://santry.com/test?q=secret#frag', parent_span_id, trace_id, }), ]), ); }); + + describe('with span streaming enabled', () => { + it.each([ + ['browser.unload_event', 'Unload event'], + ['browser.redirect', 'Redirect'], + ['browser.dom_content_loaded_event', 'DOMContentLoaded event'], + ['browser.load_event', 'Load event'], + ['browser.connect', 'Connect'], + ['browser.tls_ssl', 'TLS handshake'], + ['browser.cache', 'Cache lookup'], + ['browser.dns', 'DNS lookup'], + ['browser.request', 'Request'], + ['browser.response', 'Response'], + ])('names the %s span %j and keeps the document URL in url.full', (op, expectedName) => { + const spans: Span[] = []; + + getClient()?.on('spanEnd', span => { + spans.push(span); + }); + + // `unloadEventStart`/`End` are 0 in the shared entry, so that span is never created. + _addNavigationSpans(pageloadSpan, { ...entry, unloadEventStart: 1, unloadEventEnd: 2 }, 999, true); + + const spanJson = spans.map(spanToJSON).find(span => span.attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] === op); + + expect(spanJson?.name).toBe(expectedName); + expect(spanJson?.attributes['url.full']).toBe('https://santry.com/test?q=secret#frag'); + }); + }); +}); + +describe('_addPaintSpan', () => { + const pageloadSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); + + beforeEach(() => { + getMainCarrier().__SENTRY__ = undefined; + + const client = new TestClient(getDefaultClientOptions({ tracesSampleRate: 1 })); + setCurrentClient(client); + client.init(); + }); + + it('names the span after the paint type and keeps it on browser.paint.type', () => { + const spans: Span[] = []; + + getClient()?.on('spanEnd', span => { + spans.push(span); + }); + + const entry = { + entryType: 'paint', + name: 'first-contentful-paint', + startTime: 12, + duration: 0, + } as PerformanceEntry; + + _addPaintSpan(pageloadSpan, entry, 12, 0, 999); + + expect(spans).toHaveLength(1); + const spanJson = spanToJSON(spans[0]!); + expect(spanJson.name).toBe('first-contentful-paint'); + expect(spanJson.attributes['browser.paint.type']).toBe('first-contentful-paint'); + }); }); describe('_setResourceRequestAttributes', () => { diff --git a/packages/core/src/tracing/spans/spanNames.ts b/packages/core/src/tracing/spans/spanNames.ts index 79ec92cf4e22..4de11df7674e 100644 --- a/packages/core/src/tracing/spans/spanNames.ts +++ b/packages/core/src/tracing/spans/spanNames.ts @@ -1,4 +1,18 @@ -import { CACHE_GET, CACHE_PUT, CACHE_REMOVE } from '@sentry/conventions/op'; +import { + BROWSER_CACHE, + BROWSER_CONNECT, + BROWSER_DNS, + BROWSER_DOM_CONTENT_LOADED_EVENT, + BROWSER_LOAD_EVENT, + BROWSER_REDIRECT, + BROWSER_REQUEST, + BROWSER_RESPONSE, + BROWSER_TLS_SSL, + BROWSER_UNLOAD_EVENT, + CACHE_GET, + CACHE_PUT, + CACHE_REMOVE, +} from '@sentry/conventions/op'; // This file contains constants for low-cardinality span names: fallback names to be used when no // better-suited span name is available, as well as the building blocks for derived names. @@ -104,3 +118,21 @@ export const CACHE_OPERATION_NAMES = { [CACHE_PUT]: 'put', [CACHE_REMOVE]: 'remove', } as const; + +/** + * Span names for the browser navigation timing ops, keyed by op. None of these ops has an attribute + * template, so the static name is the only name they can get. + * @see https://getsentry.github.io/sentry-conventions/names/#browser-navigation-timing + */ +export const BROWSER_NAVIGATION_TIMING_SPAN_NAMES = { + [BROWSER_CACHE]: 'Cache lookup', + [BROWSER_DNS]: 'DNS lookup', + [BROWSER_CONNECT]: 'Connect', + [BROWSER_TLS_SSL]: 'TLS handshake', + [BROWSER_REDIRECT]: 'Redirect', + [BROWSER_REQUEST]: 'Request', + [BROWSER_RESPONSE]: 'Response', + [BROWSER_UNLOAD_EVENT]: 'Unload event', + [BROWSER_DOM_CONTENT_LOADED_EVENT]: 'DOMContentLoaded event', + [BROWSER_LOAD_EVENT]: 'Load event', +} as const;