FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(browser): Emit low cardinality resource span names · getsentry/sentry-javascript@6b1501e · GitHub

Commit 6b1501e

Browse files
committed
feat(browser): Emit low cardinality resource span names
Names `resource.*` spans after the resource domain when span streaming is enabled, falling back to `Resource` when there is none. Also adds the `url.domain` attribute to these spans.
1 parent 06a123b commit 6b1501e

7 files changed

Lines changed: 162 additions & 7 deletions

File tree

‎MIGRATION.md‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -617,20 +617,23 @@ Affected SDKs: All SDKs running in the browser.
617617

618618
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/).
619619

620-
In v11, this only affects `pageload` spans. Further ops will follow in future releases.
620+
In v11, this only affects `pageload` and `resource.*` spans. Further ops will follow in future releases.
621621
If you [opt out of span streaming](#opting-out-of-span-streaming), span names remain unchanged.
622622

623623
The following span names were adjusted:
624624

625-
| Span op | Before | After |
626-
| ---------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
627-
| `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 |
625+
| Span op | Before | After |
626+
| ------------ | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
627+
| `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 |
628+
| `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 |
629+
630+
Resource spans now also carry a `url.domain` attribute holding that domain. The full URL remains available on `url.full`.
628631

629632
Some consequences to be aware of:
630633

631634
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.
632635

633-
`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:
636+
`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:
634637

635638
```js
636639
Sentry.init({
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()],
8+
traceLifecycle: 'stream',
9+
tracesSampleRate: 1,
10+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<img src="https://sentry-test-site.example/path/to/image.svg" />
8+
<script src="https://sentry-test-site.example/path/to/script.js"></script>
9+
<link href="https://sentry-test-site.example/path/to/style.css" type="text/css" rel="stylesheet" />
10+
<span>Rendered</span>
11+
</body>
12+
</html>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { Route } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { shouldSkipTracingTest } from '../../../../utils/helpers';
5+
import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils';
6+
7+
const assetsDir = `${__dirname}/../pageload-resource-spans/assets`;
8+
9+
sentryTest('names streamed resource spans after the resource domain', async ({ getLocalTestUrl, page }) => {
10+
sentryTest.skip(shouldSkipTracingTest());
11+
12+
// Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox).
13+
await page.route('https://sentry-test-site.example/path/to/image.svg', (route: Route) =>
14+
route.fulfill({
15+
path: `${assetsDir}/image.svg`,
16+
headers: {
17+
'Timing-Allow-Origin': '*',
18+
'Content-Type': 'image/svg+xml',
19+
},
20+
}),
21+
);
22+
await page.route('https://sentry-test-site.example/path/to/script.js', (route: Route) =>
23+
route.fulfill({
24+
path: `${assetsDir}/script.js`,
25+
headers: {
26+
'Timing-Allow-Origin': '*',
27+
'Content-Type': 'application/javascript',
28+
},
29+
}),
30+
);
31+
await page.route('https://sentry-test-site.example/path/to/style.css', (route: Route) =>
32+
route.fulfill({
33+
path: `${assetsDir}/style.css`,
34+
headers: {
35+
'Timing-Allow-Origin': '*',
36+
'Content-Type': 'text/css',
37+
},
38+
}),
39+
);
40+
41+
const spanEnvelopePromise = waitForStreamedSpanEnvelope(
42+
page,
43+
env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'resource.img'),
44+
);
45+
46+
const url = await getLocalTestUrl({ testDir: __dirname });
47+
await page.goto(url);
48+
49+
const spans = getSpansFromEnvelope(await spanEnvelopePromise);
50+
51+
const imgSpan = spans.find(s => getSpanOp(s) === 'resource.img');
52+
const linkSpan = spans.find(s => getSpanOp(s) === 'resource.link');
53+
54+
expect(imgSpan?.name).toBe('sentry-test-site.example');
55+
expect(imgSpan?.attributes['url.domain']).toEqual({ type: 'string', value: 'sentry-test-site.example' });
56+
expect(imgSpan?.attributes['url.full']).toEqual({
57+
type: 'string',
58+
value: 'https://sentry-test-site.example/path/to/image.svg',
59+
});
60+
61+
expect(linkSpan?.name).toBe('sentry-test-site.example');
62+
63+
// Same-origin resources used to be named by their origin-relative path, they now carry the test host.
64+
const sameOriginScriptSpan = spans.find(
65+
s => getSpanOp(s) === 'resource.script' && s.name !== 'sentry-test-site.example',
66+
);
67+
expect(sameOriginScriptSpan?.name).toBe('sentry-test.io');
68+
});

‎dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU
9898
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.img',
9999
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
100100
'server.address': 'sentry-test-site.example',
101+
'url.domain': 'sentry-test-site.example',
101102
'url.same_origin': false,
102103
'url.scheme': 'https',
103104
'url.full': 'https://sentry-test-site.example/path/to/image.svg',
@@ -147,6 +148,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU
147148
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.link',
148149
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
149150
'server.address': 'sentry-test-site.example',
151+
'url.domain': 'sentry-test-site.example',
150152
'url.same_origin': false,
151153
'url.scheme': 'https',
152154
'url.full': 'https://sentry-test-site.example/path/to/style.css',
@@ -190,6 +192,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU
190192
'sentry.op': 'resource.script',
191193
'sentry.origin': 'auto.resource.browser.metrics',
192194
'server.address': 'sentry-test-site.example',
195+
'url.domain': 'sentry-test-site.example',
193196
'url.same_origin': false,
194197
'url.scheme': 'https',
195198
'url.full': 'https://sentry-test-site.example/path/to/script.js',

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import {
44
browserPerformanceTimeOrigin,
55
getActiveSpan,
66
parseUrl,
7+
RESOURCE_SPAN_NAME_FALLBACK,
78
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
89
setMeasurement,
910
spanToJSON,
1011
filterCollectedUrl,
1112
} from '@sentry/core';
12-
import { CODE_FILE_PATH, CODE_FUNCTION_NAME, SENTRY_OP, URL_FULL } from '@sentry/conventions/attributes';
13+
import { CODE_FILE_PATH, CODE_FUNCTION_NAME, SENTRY_OP, URL_DOMAIN, URL_FULL } from '@sentry/conventions/attributes';
1314
import { BROWSER_BROWSER_PAINT_SPAN_OP } from '@sentry/conventions/op';
1415
import {
1516
addPerformanceInstrumentationHandler,
@@ -225,6 +226,7 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries
225226
duration,
226227
timeOrigin,
227228
ignoreResourceSpans,
229+
spanStreamingEnabled,
228230
);
229231
break;
230232
}
@@ -366,6 +368,7 @@ export function _addResourceSpans(
366368
duration: number,
367369
timeOrigin: number,
368370
ignoredResourceSpanOps?: Array<string>,
371+
spanStreamingEnabled?: boolean,
369372
): void {
370373
// we already instrument based on fetch and xhr, so we don't need to
371374
// duplicate spans here.
@@ -392,6 +395,13 @@ export function _addResourceSpans(
392395
attributes['server.address'] = parsedUrl.host;
393396
}
394397

398+
// `host` carries the port, which `url.domain` doesn't.
399+
const domain = parsedUrl.host?.replace(/:\d+$/, '');
400+
401+
if (domain) {
402+
attributes[URL_DOMAIN] = domain;
403+
}
404+
395405
attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
396406

397407
attributes[URL_FULL] = filterCollectedUrl(resourceUrl);
@@ -417,7 +427,10 @@ export function _addResourceSpans(
417427
const endTimestamp = startTimestamp + duration;
418428

419429
startAndEndSpan(span, startTimestamp, endTimestamp, {
420-
name: resourceUrl.replace(WINDOW.location.origin, ''),
430+
// With span streaming, span names have to be low cardinality, so we can't fall back to the URL.
431+
name: spanStreamingEnabled
432+
? domain || RESOURCE_SPAN_NAME_FALLBACK
433+
: resourceUrl.replace(WINDOW.location.origin, ''),
421434
op,
422435
attributes: attributesWithResourceTiming,
423436
});

‎packages/browser-utils/test/performance/browserMetrics.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ describe('_addResourceSpans', () => {
249249
['resource.render_blocking_status']: entry.renderBlockingStatus,
250250
['url.scheme']: 'https',
251251
['server.address']: 'example.com',
252+
['url.domain']: 'example.com',
252253
['url.same_origin']: true,
253254
['url.full']: resourceEntryName,
254255
['network.protocol.name']: 'http',
@@ -431,6 +432,7 @@ describe('_addResourceSpans', () => {
431432
['resource.render_blocking_status']: entry.renderBlockingStatus,
432433
['url.scheme']: 'https',
433434
['server.address']: 'example.com',
435+
['url.domain']: 'example.com',
434436
['url.same_origin']: true,
435437
['url.full']: resourceEntryName,
436438
['network.protocol.name']: 'http',
@@ -464,6 +466,7 @@ describe('_addResourceSpans', () => {
464466
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css',
465467
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
466468
'server.address': 'example.com',
469+
'url.domain': 'example.com',
467470
'url.same_origin': true,
468471
'url.scheme': 'https',
469472
'url.full': resourceEntryName,
@@ -515,6 +518,7 @@ describe('_addResourceSpans', () => {
515518
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css',
516519
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
517520
'server.address': 'example.com',
521+
'url.domain': 'example.com',
518522
'url.same_origin': true,
519523
'url.scheme': 'https',
520524
'url.full': resourceEntryName,
@@ -569,6 +573,48 @@ describe('_addResourceSpans', () => {
569573
});
570574
},
571575
);
576+
577+
describe('with span streaming enabled', () => {
578+
it.each([
579+
['https://example.com/assets/to/css', 'example.com'],
580+
['https://cdn.example.org:8443/static/logo.png', 'cdn.example.org'],
581+
])('names the span after the resource domain (%s)', (url, expectedName) => {
582+
const spans: Span[] = [];
583+
584+
getClient()?.on('spanEnd', span => {
585+
spans.push(span);
586+
});
587+
588+
const entry = mockPerformanceResourceTiming({ initiatorType: 'css', nextHopProtocol: 'h2' });
589+
590+
_addResourceSpans(span, entry, url, 100, 23, 345, undefined, true);
591+
592+
expect(spans).toHaveLength(1);
593+
expect(spanToJSON(spans[0]!)).toEqual(
594+
expect.objectContaining({
595+
name: expectedName,
596+
attributes: expect.objectContaining({ 'url.domain': expectedName }),
597+
}),
598+
);
599+
});
600+
601+
it('falls back to a static name when the resource URL has no domain', () => {
602+
const spans: Span[] = [];
603+
604+
getClient()?.on('spanEnd', span => {
605+
spans.push(span);
606+
});
607+
608+
const entry = mockPerformanceResourceTiming({ initiatorType: 'script', nextHopProtocol: 'h2' });
609+
610+
_addResourceSpans(span, entry, 'blob:0f6b3f0a-1e2d-4d1a-9c3f-2a5c1d7b8e90', 100, 23, 345, undefined, true);
611+
612+
expect(spans).toHaveLength(1);
613+
const spanJson = spanToJSON(spans[0]!);
614+
expect(spanJson.name).toBe('Resource');
615+
expect(spanJson.attributes['url.domain']).toBeUndefined();
616+
});
617+
});
572618
});
573619

574620
describe('_addNavigationSpans', () => {

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL