Skip to content

Commit 6152ef5

Browse files
Lms24Zen-cronic
andauthored
ref(v8/browser): Add protocol attributes to resource spans (#15224)
Backport of #15161 --------- Signed-off-by: Kaung Zin Hein <[email protected]> Co-authored-by: Kaung Zin Hein <[email protected]>
1 parent a0470da commit 6152ef5

File tree

9 files changed

+222
-99
lines changed

9 files changed

+222
-99
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
1212

13+
Work in this release was contributed by @Zen-cronic. Thank you for your contribution!
14+
1315
## 8.52.0
1416

1517
### Important Changes
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,44 @@
11
import type { Route } from '@playwright/test';
22
import { expect } from '@playwright/test';
3-
import type { Event } from '@sentry/core';
3+
import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
44

55
import { sentryTest } from '../../../../utils/fixtures';
66
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
77

8-
sentryTest('should add resource spans to pageload transaction', async ({ getLocalTestUrl, page }) => {
8+
sentryTest('should add resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => {
99
if (shouldSkipTracingTest()) {
1010
sentryTest.skip();
1111
}
1212

13+
const isWebkitRun = browserName === 'webkit';
14+
1315
// Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox).
1416
await page.route('https://example.com/path/to/image.svg', (route: Route) =>
15-
route.fulfill({ path: `${__dirname}/assets/image.svg` }),
17+
route.fulfill({
18+
path: `${__dirname}/assets/image.svg`,
19+
headers: {
20+
'Timing-Allow-Origin': '*',
21+
'Content-Type': 'image/svg+xml',
22+
},
23+
}),
1624
);
1725
await page.route('https://example.com/path/to/script.js', (route: Route) =>
18-
route.fulfill({ path: `${__dirname}/assets/script.js` }),
26+
route.fulfill({
27+
path: `${__dirname}/assets/script.js`,
28+
headers: {
29+
'Timing-Allow-Origin': '*',
30+
'Content-Type': 'application/javascript',
31+
},
32+
}),
1933
);
2034
await page.route('https://example.com/path/to/style.css', (route: Route) =>
21-
route.fulfill({ path: `${__dirname}/assets/style.css` }),
35+
route.fulfill({
36+
path: `${__dirname}/assets/style.css`,
37+
headers: {
38+
'Timing-Allow-Origin': '*',
39+
'Content-Type': 'text/css',
40+
},
41+
}),
2242
);
2343

2444
const url = await getLocalTestUrl({ testDir: __dirname });
@@ -27,11 +47,14 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca
2747
const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource'));
2848

2949
const scriptSpans = resourceSpans?.filter(({ op }) => op === 'resource.script');
30-
const linkSpans = resourceSpans?.filter(({ op }) => op === 'resource.link');
31-
const imgSpans = resourceSpans?.filter(({ op }) => op === 'resource.img');
50+
const linkSpan = resourceSpans?.filter(({ op }) => op === 'resource.link')[0];
51+
const imgSpan = resourceSpans?.filter(({ op }) => op === 'resource.img')[0];
52+
53+
const spanId = eventData.contexts?.trace?.span_id;
54+
const traceId = eventData.contexts?.trace?.trace_id;
3255

33-
expect(imgSpans).toHaveLength(1);
34-
expect(linkSpans).toHaveLength(1);
56+
expect(spanId).toBeDefined();
57+
expect(traceId).toBeDefined();
3558

3659
const hasCdnBundle = (process.env.PW_BUNDLE || '').startsWith('bundle');
3760

@@ -41,11 +64,90 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca
4164
}
4265

4366
expect(scriptSpans?.map(({ description }) => description).sort()).toEqual(expectedScripts);
67+
expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId));
4468

45-
const spanId = eventData.contexts?.trace?.span_id;
69+
const customScriptSpan = scriptSpans?.find(
70+
({ description }) => description === 'https://example.com/path/to/script.js',
71+
);
4672

47-
expect(spanId).toBeDefined();
48-
expect(imgSpans?.[0].parent_span_id).toBe(spanId);
49-
expect(linkSpans?.[0].parent_span_id).toBe(spanId);
50-
expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId));
73+
expect(imgSpan).toEqual({
74+
data: {
75+
'http.decoded_response_content_length': expect.any(Number),
76+
'http.response_content_length': expect.any(Number),
77+
'http.response_transfer_size': expect.any(Number),
78+
'network.protocol.name': '',
79+
'network.protocol.version': 'unknown',
80+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.img',
81+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
82+
'server.address': 'example.com',
83+
'url.same_origin': false,
84+
'url.scheme': 'https',
85+
...(!isWebkitRun && {
86+
'resource.render_blocking_status': 'non-blocking',
87+
'http.response_delivery_type': '',
88+
}),
89+
},
90+
description: 'https://example.com/path/to/image.svg',
91+
op: 'resource.img',
92+
origin: 'auto.resource.browser.metrics',
93+
parent_span_id: spanId,
94+
span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
95+
start_timestamp: expect.any(Number),
96+
timestamp: expect.any(Number),
97+
trace_id: traceId,
98+
});
99+
100+
expect(linkSpan).toEqual({
101+
data: {
102+
'http.decoded_response_content_length': expect.any(Number),
103+
'http.response_content_length': expect.any(Number),
104+
'http.response_transfer_size': expect.any(Number),
105+
'network.protocol.name': '',
106+
'network.protocol.version': 'unknown',
107+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.link',
108+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
109+
'server.address': 'example.com',
110+
'url.same_origin': false,
111+
'url.scheme': 'https',
112+
...(!isWebkitRun && {
113+
'resource.render_blocking_status': 'non-blocking',
114+
'http.response_delivery_type': '',
115+
}),
116+
},
117+
description: 'https://example.com/path/to/style.css',
118+
op: 'resource.link',
119+
origin: 'auto.resource.browser.metrics',
120+
parent_span_id: spanId,
121+
span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
122+
start_timestamp: expect.any(Number),
123+
timestamp: expect.any(Number),
124+
trace_id: traceId,
125+
});
126+
127+
expect(customScriptSpan).toEqual({
128+
data: {
129+
'http.decoded_response_content_length': expect.any(Number),
130+
'http.response_content_length': expect.any(Number),
131+
'http.response_transfer_size': expect.any(Number),
132+
'network.protocol.name': '',
133+
'network.protocol.version': 'unknown',
134+
'sentry.op': 'resource.script',
135+
'sentry.origin': 'auto.resource.browser.metrics',
136+
'server.address': 'example.com',
137+
'url.same_origin': false,
138+
'url.scheme': 'https',
139+
...(!isWebkitRun && {
140+
'resource.render_blocking_status': 'non-blocking',
141+
'http.response_delivery_type': '',
142+
}),
143+
},
144+
description: 'https://example.com/path/to/script.js',
145+
op: 'resource.script',
146+
origin: 'auto.resource.browser.metrics',
147+
parent_span_id: spanId,
148+
span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
149+
start_timestamp: expect.any(Number),
150+
timestamp: expect.any(Number),
151+
trace_id: traceId,
152+
});
51153
});

packages/browser-utils/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export {
1717
registerInpInteractionListener,
1818
} from './metrics/browserMetrics';
1919

20+
export { extractNetworkProtocol } from './metrics/utils';
21+
2022
export { addClickKeypressInstrumentationHandler } from './instrument/dom';
2123

2224
export { addHistoryInstrumentationHandler } from './instrument/history';

packages/browser-utils/src/metrics/browserMetrics.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ import {
2020
addPerformanceInstrumentationHandler,
2121
addTtfbInstrumentationHandler,
2222
} from './instrument';
23-
import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils';
23+
import {
24+
extractNetworkProtocol,
25+
getBrowserPerformanceAPI,
26+
isMeasurementValue,
27+
msToSec,
28+
startAndEndSpan,
29+
} from './utils';
2430
import { getActivationStart } from './web-vitals/lib/getActivationStart';
2531
import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry';
2632
import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher';
@@ -596,6 +602,10 @@ export function _addResourceSpans(
596602

597603
attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin);
598604

605+
const { name, version } = extractNetworkProtocol(entry.nextHopProtocol);
606+
attributes['network.protocol.name'] = name;
607+
attributes['network.protocol.version'] = version;
608+
599609
const startTimestamp = timeOrigin + startTime;
600610
const endTimestamp = startTimestamp + duration;
601611

packages/browser-utils/src/metrics/utils.ts

+31
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,34 @@ export function getBrowserPerformanceAPI(): Performance | undefined {
134134
export function msToSec(time: number): number {
135135
return time / 1000;
136136
}
137+
138+
/**
139+
* Converts ALPN protocol ids to name and version.
140+
*
141+
* (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids)
142+
* @param nextHopProtocol PerformanceResourceTiming.nextHopProtocol
143+
*/
144+
export function extractNetworkProtocol(nextHopProtocol: string): { name: string; version: string } {
145+
let name = 'unknown';
146+
let version = 'unknown';
147+
let _name = '';
148+
for (const char of nextHopProtocol) {
149+
// http/1.1 etc.
150+
if (char === '/') {
151+
[name, version] = nextHopProtocol.split('/') as [string, string];
152+
break;
153+
}
154+
// h2, h3 etc.
155+
if (!isNaN(Number(char))) {
156+
name = _name === 'h' ? 'http' : _name;
157+
version = nextHopProtocol.split(_name)[1] as string;
158+
break;
159+
}
160+
_name += char;
161+
}
162+
if (_name === nextHopProtocol) {
163+
// webrtc, ftp, etc.
164+
name = _name;
165+
}
166+
return { name, version };
167+
}

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

+16
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ describe('_addResourceSpans', () => {
131131
encodedBodySize: 256,
132132
decodedBodySize: 256,
133133
renderBlockingStatus: 'non-blocking',
134+
nextHopProtocol: 'http/1.1',
134135
});
135136
_addResourceSpans(span, entry, resourceEntryName, 123, 456, 100);
136137

@@ -150,6 +151,7 @@ describe('_addResourceSpans', () => {
150151
encodedBodySize: 256,
151152
decodedBodySize: 256,
152153
renderBlockingStatus: 'non-blocking',
154+
nextHopProtocol: 'http/1.1',
153155
});
154156
_addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 456, 100);
155157

@@ -169,6 +171,7 @@ describe('_addResourceSpans', () => {
169171
encodedBodySize: 456,
170172
decodedBodySize: 593,
171173
renderBlockingStatus: 'non-blocking',
174+
nextHopProtocol: 'http/1.1',
172175
});
173176

174177
const timeOrigin = 100;
@@ -195,6 +198,8 @@ describe('_addResourceSpans', () => {
195198
['url.scheme']: 'https',
196199
['server.address']: 'example.com',
197200
['url.same_origin']: true,
201+
['network.protocol.name']: 'http',
202+
['network.protocol.version']: '1.1',
198203
},
199204
}),
200205
);
@@ -233,6 +238,7 @@ describe('_addResourceSpans', () => {
233238
const { initiatorType, op } = table[i]!;
234239
const entry = mockPerformanceResourceTiming({
235240
initiatorType,
241+
nextHopProtocol: 'http/1.1',
236242
});
237243
_addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 234, 465);
238244

@@ -254,6 +260,7 @@ describe('_addResourceSpans', () => {
254260
encodedBodySize: 0,
255261
decodedBodySize: 0,
256262
renderBlockingStatus: 'non-blocking',
263+
nextHopProtocol: 'h2',
257264
});
258265

259266
_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
@@ -271,6 +278,8 @@ describe('_addResourceSpans', () => {
271278
['url.scheme']: 'https',
272279
['server.address']: 'example.com',
273280
['url.same_origin']: true,
281+
['network.protocol.name']: 'http',
282+
['network.protocol.version']: '2',
274283
},
275284
}),
276285
);
@@ -288,6 +297,7 @@ describe('_addResourceSpans', () => {
288297
transferSize: 2147483647,
289298
encodedBodySize: 2147483647,
290299
decodedBodySize: 2147483647,
300+
nextHopProtocol: 'h3',
291301
});
292302

293303
_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
@@ -301,6 +311,8 @@ describe('_addResourceSpans', () => {
301311
'server.address': 'example.com',
302312
'url.same_origin': true,
303313
'url.scheme': 'https',
314+
['network.protocol.name']: 'http',
315+
['network.protocol.version']: '3',
304316
},
305317
description: '/assets/to/css',
306318
timestamp: 468,
@@ -325,6 +337,7 @@ describe('_addResourceSpans', () => {
325337
transferSize: null,
326338
encodedBodySize: null,
327339
decodedBodySize: null,
340+
nextHopProtocol: 'h3',
328341
} as unknown as PerformanceResourceTiming;
329342

330343
_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
@@ -338,6 +351,8 @@ describe('_addResourceSpans', () => {
338351
'server.address': 'example.com',
339352
'url.same_origin': true,
340353
'url.scheme': 'https',
354+
['network.protocol.name']: 'http',
355+
['network.protocol.version']: '3',
341356
},
342357
description: '/assets/to/css',
343358
timestamp: 468,
@@ -365,6 +380,7 @@ describe('_addResourceSpans', () => {
365380
encodedBodySize: 0,
366381
decodedBodySize: 0,
367382
deliveryType,
383+
nextHopProtocol: 'h3',
368384
});
369385

370386
_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);

packages/browser-utils/test/browser/utils.test.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SentrySpan, getCurrentScope, getIsolationScope, setCurrentClient, spanToJSON } from '@sentry/core';
2-
import { startAndEndSpan } from '../../src/metrics/utils';
2+
import { extractNetworkProtocol, startAndEndSpan } from '../../src/metrics/utils';
33
import { TestClient, getDefaultClientOptions } from '../utils/TestClient';
44

55
describe('startAndEndSpan()', () => {
@@ -54,3 +54,44 @@ describe('startAndEndSpan()', () => {
5454
expect(spanToJSON(parentSpan).start_timestamp).toEqual(123);
5555
});
5656
});
57+
58+
describe('HTTPTimings', () => {
59+
test.each([
60+
['http/0.9', { name: 'http', version: '0.9' }],
61+
['http/1.0', { name: 'http', version: '1.0' }],
62+
['http/1.1', { name: 'http', version: '1.1' }],
63+
['spdy/1', { name: 'spdy', version: '1' }],
64+
['spdy/2', { name: 'spdy', version: '2' }],
65+
['spdy/3', { name: 'spdy', version: '3' }],
66+
['stun.turn', { name: 'stun.turn', version: 'unknown' }],
67+
['stun.nat-discovery', { name: 'stun.nat-discovery', version: 'unknown' }],
68+
['h2', { name: 'http', version: '2' }],
69+
['h2c', { name: 'http', version: '2c' }],
70+
['webrtc', { name: 'webrtc', version: 'unknown' }],
71+
['c-webrtc', { name: 'c-webrtc', version: 'unknown' }],
72+
['ftp', { name: 'ftp', version: 'unknown' }],
73+
['imap', { name: 'imap', version: 'unknown' }],
74+
['pop3', { name: 'pop', version: '3' }],
75+
['managesieve', { name: 'managesieve', version: 'unknown' }],
76+
['coap', { name: 'coap', version: 'unknown' }],
77+
['xmpp-client', { name: 'xmpp-client', version: 'unknown' }],
78+
['xmpp-server', { name: 'xmpp-server', version: 'unknown' }],
79+
['acme-tls/1', { name: 'acme-tls', version: '1' }],
80+
['mqtt', { name: 'mqtt', version: 'unknown' }],
81+
['dot', { name: 'dot', version: 'unknown' }],
82+
['ntske/1', { name: 'ntske', version: '1' }],
83+
['sunrpc', { name: 'sunrpc', version: 'unknown' }],
84+
['h3', { name: 'http', version: '3' }],
85+
['smb', { name: 'smb', version: 'unknown' }],
86+
['irc', { name: 'irc', version: 'unknown' }],
87+
['nntp', { name: 'nntp', version: 'unknown' }],
88+
['nnsp', { name: 'nnsp', version: 'unknown' }],
89+
['doq', { name: 'doq', version: 'unknown' }],
90+
['sip/2', { name: 'sip', version: '2' }],
91+
['tds/8.0', { name: 'tds', version: '8.0' }],
92+
['dicom', { name: 'dicom', version: 'unknown' }],
93+
['', { name: '', version: 'unknown' }],
94+
])('Extracting version from ALPN protocol %s', (protocol, expected) => {
95+
expect(extractNetworkProtocol(protocol)).toMatchObject(expected);
96+
});
97+
});

0 commit comments

Comments
 (0)