Skip to content
Open
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
13 changes: 8 additions & 5 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,20 +617,23 @@ Affected SDKs: All SDKs running in the browser.

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 only affects `pageload` spans. Further ops will follow in future releases.
In v11, this only affects `pageload` and `resource.*` 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 |
| 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 |
| `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:

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.

`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:
`ignoreSpans` is evaluated when a span **starts**, at which point a pageload span without a resolved route is already named `'Pageload'` and a resource span is already named after its domain, so filters matching a URL path no longer apply to them. 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 } 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