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
20 changes: 13 additions & 7 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,15 +619,17 @@ Affected SDKs: All SDKs.

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 affects `pageload` and `graphql` 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 |
| `graphql` | The graphql phase and, for operations, the operation name (`query GetUser`, `graphql.parse`, `graphql.resolve user.0.name`) | The operation type, or the processing type where there is none (`GraphQL query`, `GraphQL parse`, `GraphQL resolve`) |
| 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 |
| `graphql` | The graphql phase and, for operations, the operation name (`query GetUser`, `graphql.parse`, `graphql.resolve user.0.name`) | The operation type, or the processing type where there is none (`GraphQL query`, `GraphQL parse`, `GraphQL resolve`) |
| `resource.*` | The resource URL, relative to the page origin for same-origin resources (`/assets/app.js`) | The resource domain (`cdn.example.com`), or `Resource` if the SDK has none |

Resource spans now also carry a `url.domain` attribute holding that domain. The full URL remains available on `url.full`.

Some consequences to be aware of:

Expand All @@ -637,9 +639,13 @@ Because a low-cardinality name cannot say which part of request processing a spa

For the same reason, `useOperationNameForRootSpan` no longer renames the enclosing root span (`GET /graphql` stays `GET /graphql`, instead of becoming `GET /graphql (query GetUser)`). The operations are still recorded on that span's `sentry.graphql.operation` attribute, as long as the option stays enabled (the default). Disabling it skips both, as before.

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 service or root 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.

`ignoreSpans` is evaluated when a span **starts**, at which point a span might not yet have its final name. For example, an unresolved pageload span name is named `'Pageload'` and might receive its final, resolved route name later.
`ignoreSpans` filters matching a URL path no longer apply to them.
Another example where filters might need adjustments are `resource.*` spans where their name now only includes the domain the resource was taken from.

`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:
Match on attributes instead:

```js
Sentry.init({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()],
traceLifecycle: 'stream',
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<img src="https://sentry-test-site.example/path/to/image.svg" />
<script src="https://sentry-test-site.example/path/to/script.js"></script>
<link href="https://sentry-test-site.example/path/to/style.css" type="text/css" rel="stylesheet" />
<span>Rendered</span>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Route } from '@playwright/test';
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
import { shouldSkipTracingTest } from '../../../../utils/helpers';
import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils';

const assetsDir = `${__dirname}/../pageload-resource-spans/assets`;

sentryTest('names streamed resource spans after the resource domain', async ({ getLocalTestUrl, page }) => {
sentryTest.skip(shouldSkipTracingTest());

// Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox).
await page.route('https://sentry-test-site.example/path/to/image.svg', (route: Route) =>
route.fulfill({
path: `${assetsDir}/image.svg`,
headers: {
'Timing-Allow-Origin': '*',
'Content-Type': 'image/svg+xml',
},
}),
);
await page.route('https://sentry-test-site.example/path/to/script.js', (route: Route) =>
route.fulfill({
path: `${assetsDir}/script.js`,
headers: {
'Timing-Allow-Origin': '*',
'Content-Type': 'application/javascript',
},
}),
);
await page.route('https://sentry-test-site.example/path/to/style.css', (route: Route) =>
route.fulfill({
path: `${assetsDir}/style.css`,
headers: {
'Timing-Allow-Origin': '*',
'Content-Type': 'text/css',
},
}),
);

const spanEnvelopePromise = waitForStreamedSpanEnvelope(
page,
env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'resource.img'),
);

const url = await getLocalTestUrl({ testDir: __dirname });
await page.goto(url);

const spans = getSpansFromEnvelope(await spanEnvelopePromise);

const imgSpan = spans.find(s => getSpanOp(s) === 'resource.img');
const linkSpan = spans.find(s => getSpanOp(s) === 'resource.link');

expect(imgSpan?.name).toBe('sentry-test-site.example');
expect(imgSpan?.attributes['url.domain']).toEqual({ type: 'string', value: 'sentry-test-site.example' });
expect(imgSpan?.attributes['url.full']).toEqual({
type: 'string',
value: 'https://sentry-test-site.example/path/to/image.svg',
});

expect(linkSpan?.name).toBe('sentry-test-site.example');

// Same-origin resources used to be named by their origin-relative path, they now carry the test host.
const sameOriginScriptSpan = spans.find(
s => getSpanOp(s) === 'resource.script' && s.name !== 'sentry-test-site.example',
);
expect(sameOriginScriptSpan?.name).toBe('sentry-test.io');
});
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.img',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
'server.address': 'sentry-test-site.example',
'url.domain': 'sentry-test-site.example',
'url.same_origin': false,
'url.scheme': 'https',
'url.full': 'https://sentry-test-site.example/path/to/image.svg',
Expand Down Expand Up @@ -147,6 +148,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.link',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
'server.address': 'sentry-test-site.example',
'url.domain': 'sentry-test-site.example',
'url.same_origin': false,
'url.scheme': 'https',
'url.full': 'https://sentry-test-site.example/path/to/style.css',
Expand Down Expand Up @@ -190,6 +192,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU
'sentry.op': 'resource.script',
'sentry.origin': 'auto.resource.browser.metrics',
'server.address': 'sentry-test-site.example',
'url.domain': 'sentry-test-site.example',
'url.same_origin': false,
'url.scheme': 'https',
'url.full': 'https://sentry-test-site.example/path/to/script.js',
Expand Down
24 changes: 20 additions & 4 deletions packages/browser-utils/src/performance/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import {
browserPerformanceTimeOrigin,
getActiveSpan,
parseUrl,
RESOURCE_SPAN_NAME_FALLBACK,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
setMeasurement,
spanToJSON,
filterCollectedUrl,
} from '@sentry/core';
import { CODE_FILE_PATH, CODE_FUNCTION_NAME, SENTRY_OP, URL_FULL } from '@sentry/conventions/attributes';
import { CODE_FILE_PATH, CODE_FUNCTION_NAME, SENTRY_OP, URL_DOMAIN, URL_FULL } from '@sentry/conventions/attributes';
import { BROWSER_PAINT, UI_LONG_ANIMATION_FRAME, UI_LONG_TASK } from '@sentry/conventions/op';
import {
addPerformanceInstrumentationHandler,
Expand Down Expand Up @@ -225,6 +226,7 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries
duration,
timeOrigin,
ignoreResourceSpans,
spanStreamingEnabled,
);
break;
}
Expand Down Expand Up @@ -366,6 +368,7 @@ export function _addResourceSpans(
duration: number,
timeOrigin: number,
ignoredResourceSpanOps?: Array<string>,
spanStreamingEnabled?: boolean,
): void {
// we already instrument based on fetch and xhr, so we don't need to
// duplicate spans here.
Expand All @@ -388,8 +391,18 @@ export function _addResourceSpans(
attributes['url.scheme'] = parsedUrl.protocol.split(':').pop(); // the protocol returned by parseUrl includes a :, but OTEL spec does not, so we remove it.
}

if (parsedUrl.host) {
attributes['server.address'] = parsedUrl.host;
// `host` is the URL authority, so it can carry userinfo, which doesn't belong on either attribute.
const host = parsedUrl.host?.replace(/^.*@/, '');

if (host) {
attributes['server.address'] = host;
}

// Unlike `server.address`, `url.domain` excludes the port.
const domain = host?.replace(/:\d+$/, '');

if (domain) {
attributes[URL_DOMAIN] = domain;
}

attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
Expand Down Expand Up @@ -417,7 +430,10 @@ export function _addResourceSpans(
const endTimestamp = startTimestamp + duration;

startAndEndSpan(span, startTimestamp, endTimestamp, {
name: resourceUrl.replace(WINDOW.location.origin, ''),
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL.
name: spanStreamingEnabled
? domain || RESOURCE_SPAN_NAME_FALLBACK
: resourceUrl.replace(WINDOW.location.origin, ''),
Comment thread
cursor[bot] marked this conversation as resolved.
op,
attributes: attributesWithResourceTiming,
});
Expand Down
50 changes: 50 additions & 0 deletions packages/browser-utils/test/performance/browserMetrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ describe('_addResourceSpans', () => {
['resource.render_blocking_status']: entry.renderBlockingStatus,
['url.scheme']: 'https',
['server.address']: 'example.com',
['url.domain']: 'example.com',
['url.same_origin']: true,
['url.full']: resourceEntryName,
['network.protocol.name']: 'http',
Expand Down Expand Up @@ -431,6 +432,7 @@ describe('_addResourceSpans', () => {
['resource.render_blocking_status']: entry.renderBlockingStatus,
['url.scheme']: 'https',
['server.address']: 'example.com',
['url.domain']: 'example.com',
['url.same_origin']: true,
['url.full']: resourceEntryName,
['network.protocol.name']: 'http',
Expand Down Expand Up @@ -464,6 +466,7 @@ describe('_addResourceSpans', () => {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
'server.address': 'example.com',
'url.domain': 'example.com',
'url.same_origin': true,
'url.scheme': 'https',
'url.full': resourceEntryName,
Expand Down Expand Up @@ -515,6 +518,7 @@ describe('_addResourceSpans', () => {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
'server.address': 'example.com',
'url.domain': 'example.com',
'url.same_origin': true,
'url.scheme': 'https',
'url.full': resourceEntryName,
Expand Down Expand Up @@ -569,6 +573,52 @@ describe('_addResourceSpans', () => {
});
},
);

describe('with span streaming enabled', () => {
it.each([
['https://example.com/assets/to/css', 'example.com', 'example.com'],
['https://cdn.example.org:8443/static/logo.png', 'cdn.example.org', 'cdn.example.org:8443'],
['https://user:pass@example.com:8443/static/logo.png', 'example.com', 'example.com:8443'],
])('names the span after the resource domain (%s)', (url, expectedName, expectedAddress) => {
const spans: Span[] = [];

getClient()?.on('spanEnd', span => {
spans.push(span);
});

const entry = mockPerformanceResourceTiming({ initiatorType: 'css', nextHopProtocol: 'h2' });

_addResourceSpans(span, entry, url, 100, 23, 345, undefined, true);

expect(spans).toHaveLength(1);
expect(spanToJSON(spans[0]!)).toEqual(
expect.objectContaining({
name: expectedName,
attributes: expect.objectContaining({
'url.domain': expectedName,
'server.address': expectedAddress,
}),
}),
);
});

it('falls back to a static name when the resource URL has no domain', () => {
const spans: Span[] = [];

getClient()?.on('spanEnd', span => {
spans.push(span);
});

const entry = mockPerformanceResourceTiming({ initiatorType: 'script', nextHopProtocol: 'h2' });

_addResourceSpans(span, entry, 'blob:0f6b3f0a-1e2d-4d1a-9c3f-2a5c1d7b8e90', 100, 23, 345, undefined, true);

expect(spans).toHaveLength(1);
const spanJson = spanToJSON(spans[0]!);
expect(spanJson.name).toBe('Resource');
expect(spanJson.attributes['url.domain']).toBeUndefined();
});
});
});

describe('_addNavigationSpans', () => {
Expand Down
Loading