Skip to content
Merged
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: 24 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -1053,6 +1054,29 @@ The following span names were adjusted:
| `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` |
| `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` |

#### 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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,
Expand All @@ -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' },
}),
}),
);
Expand Down
72 changes: 51 additions & 21 deletions packages/browser-utils/src/performance/entries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable max-lines */
import type { Span, SpanAttributes } from '@sentry/core';
import {
BROWSER_NAVIGATION_TIMING_SPAN_NAMES,
browserPerformanceTimeOrigin,
getActiveSpan,
parseUrl,
Expand All @@ -11,6 +12,7 @@ import {
filterCollectedUrl,
} from '@sentry/core';
import {
BROWSER_PAINT_TYPE,
CODE_FILE_PATH,
CODE_FUNCTION_NAME,
HTTP_REQUEST_SAME_ORIGIN,
Expand Down Expand Up @@ -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': {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
},
});
}
Expand All @@ -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 =
Expand All @@ -315,7 +328,7 @@ type StartEventName =
| 'domContentLoadedEvent'
| 'loadEvent';

const NAVIGATION_TIMING_SPAN_OPS: Record<StartEventName, string> = {
const NAVIGATION_TIMING_SPAN_OPS = {
secureConnection: BROWSER_TLS_SSL,
fetch: BROWSER_CACHE,
domainLookup: BROWSER_DNS,
Expand All @@ -324,7 +337,7 @@ const NAVIGATION_TIMING_SPAN_OPS: Record<StartEventName, string> = {
connect: BROWSER_CONNECT,
domContentLoadedEvent: BROWSER_DOM_CONTENT_LOADED_EVENT,
loadEvent: BROWSER_LOAD_EVENT,
};
} as const satisfies Record<StartEventName, string>;

type EndEventName =
| 'domainLookupStart'
Expand All @@ -341,18 +354,23 @@ function _addPerformanceNavigationTiming(
entry: PerformanceNavigationTiming,
event: StartEventName,
timeOrigin: number,
spanStreamingEnabled: boolean | undefined,
): void {
const eventEnd = _getEndPropertyNameForNavigationTiming(event) satisfies keyof PerformanceNavigationTiming;
const end = entry[eventEnd];
const start = entry[`${event}Start`];
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 } : {}),
},
});
Expand All @@ -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);
Expand All @@ -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,
},
});
}
Expand Down
Loading
Loading