Skip to content

Commit

Permalink
ref(v8/browser): Add protocol attributes to resource spans (#15224)
Browse files Browse the repository at this point in the history
Backport of #15161

---------

Signed-off-by: Kaung Zin Hein <[email protected]>
Co-authored-by: Kaung Zin Hein <[email protected]>
  • Loading branch information
Lms24 and Zen-cronic authored Jan 30, 2025
1 parent a0470da commit 6152ef5
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 99 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

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

Work in this release was contributed by @Zen-cronic. Thank you for your contribution!

## 8.52.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
import type { Route } from '@playwright/test';
import { expect } from '@playwright/test';
import type { Event } from '@sentry/core';
import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';

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

sentryTest('should add resource spans to pageload transaction', async ({ getLocalTestUrl, page }) => {
sentryTest('should add resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const isWebkitRun = browserName === 'webkit';

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

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

const scriptSpans = resourceSpans?.filter(({ op }) => op === 'resource.script');
const linkSpans = resourceSpans?.filter(({ op }) => op === 'resource.link');
const imgSpans = resourceSpans?.filter(({ op }) => op === 'resource.img');
const linkSpan = resourceSpans?.filter(({ op }) => op === 'resource.link')[0];
const imgSpan = resourceSpans?.filter(({ op }) => op === 'resource.img')[0];

const spanId = eventData.contexts?.trace?.span_id;
const traceId = eventData.contexts?.trace?.trace_id;

expect(imgSpans).toHaveLength(1);
expect(linkSpans).toHaveLength(1);
expect(spanId).toBeDefined();
expect(traceId).toBeDefined();

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

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

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

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

expect(spanId).toBeDefined();
expect(imgSpans?.[0].parent_span_id).toBe(spanId);
expect(linkSpans?.[0].parent_span_id).toBe(spanId);
expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId));
expect(imgSpan).toEqual({
data: {
'http.decoded_response_content_length': expect.any(Number),
'http.response_content_length': expect.any(Number),
'http.response_transfer_size': expect.any(Number),
'network.protocol.name': '',
'network.protocol.version': 'unknown',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.img',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
'server.address': 'example.com',
'url.same_origin': false,
'url.scheme': 'https',
...(!isWebkitRun && {
'resource.render_blocking_status': 'non-blocking',
'http.response_delivery_type': '',
}),
},
description: 'https://example.com/path/to/image.svg',
op: 'resource.img',
origin: 'auto.resource.browser.metrics',
parent_span_id: spanId,
span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: traceId,
});

expect(linkSpan).toEqual({
data: {
'http.decoded_response_content_length': expect.any(Number),
'http.response_content_length': expect.any(Number),
'http.response_transfer_size': expect.any(Number),
'network.protocol.name': '',
'network.protocol.version': 'unknown',
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.link',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics',
'server.address': 'example.com',
'url.same_origin': false,
'url.scheme': 'https',
...(!isWebkitRun && {
'resource.render_blocking_status': 'non-blocking',
'http.response_delivery_type': '',
}),
},
description: 'https://example.com/path/to/style.css',
op: 'resource.link',
origin: 'auto.resource.browser.metrics',
parent_span_id: spanId,
span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: traceId,
});

expect(customScriptSpan).toEqual({
data: {
'http.decoded_response_content_length': expect.any(Number),
'http.response_content_length': expect.any(Number),
'http.response_transfer_size': expect.any(Number),
'network.protocol.name': '',
'network.protocol.version': 'unknown',
'sentry.op': 'resource.script',
'sentry.origin': 'auto.resource.browser.metrics',
'server.address': 'example.com',
'url.same_origin': false,
'url.scheme': 'https',
...(!isWebkitRun && {
'resource.render_blocking_status': 'non-blocking',
'http.response_delivery_type': '',
}),
},
description: 'https://example.com/path/to/script.js',
op: 'resource.script',
origin: 'auto.resource.browser.metrics',
parent_span_id: spanId,
span_id: expect.stringMatching(/^[a-f0-9]{16}$/),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: traceId,
});
});
2 changes: 2 additions & 0 deletions packages/browser-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export {
registerInpInteractionListener,
} from './metrics/browserMetrics';

export { extractNetworkProtocol } from './metrics/utils';

export { addClickKeypressInstrumentationHandler } from './instrument/dom';

export { addHistoryInstrumentationHandler } from './instrument/history';
Expand Down
12 changes: 11 additions & 1 deletion packages/browser-utils/src/metrics/browserMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ import {
addPerformanceInstrumentationHandler,
addTtfbInstrumentationHandler,
} from './instrument';
import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils';
import {
extractNetworkProtocol,
getBrowserPerformanceAPI,
isMeasurementValue,
msToSec,
startAndEndSpan,
} from './utils';
import { getActivationStart } from './web-vitals/lib/getActivationStart';
import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry';
import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher';
Expand Down Expand Up @@ -596,6 +602,10 @@ export function _addResourceSpans(

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

const { name, version } = extractNetworkProtocol(entry.nextHopProtocol);
attributes['network.protocol.name'] = name;
attributes['network.protocol.version'] = version;

const startTimestamp = timeOrigin + startTime;
const endTimestamp = startTimestamp + duration;

Expand Down
31 changes: 31 additions & 0 deletions packages/browser-utils/src/metrics/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,34 @@ export function getBrowserPerformanceAPI(): Performance | undefined {
export function msToSec(time: number): number {
return time / 1000;
}

/**
* Converts ALPN protocol ids to name and version.
*
* (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids)
* @param nextHopProtocol PerformanceResourceTiming.nextHopProtocol
*/
export function extractNetworkProtocol(nextHopProtocol: string): { name: string; version: string } {
let name = 'unknown';
let version = 'unknown';
let _name = '';
for (const char of nextHopProtocol) {
// http/1.1 etc.
if (char === '/') {
[name, version] = nextHopProtocol.split('/') as [string, string];
break;
}
// h2, h3 etc.
if (!isNaN(Number(char))) {
name = _name === 'h' ? 'http' : _name;
version = nextHopProtocol.split(_name)[1] as string;
break;
}
_name += char;
}
if (_name === nextHopProtocol) {
// webrtc, ftp, etc.
name = _name;
}
return { name, version };
}
16 changes: 16 additions & 0 deletions packages/browser-utils/test/browser/browserMetrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 256,
decodedBodySize: 256,
renderBlockingStatus: 'non-blocking',
nextHopProtocol: 'http/1.1',
});
_addResourceSpans(span, entry, resourceEntryName, 123, 456, 100);

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

Expand All @@ -169,6 +171,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 456,
decodedBodySize: 593,
renderBlockingStatus: 'non-blocking',
nextHopProtocol: 'http/1.1',
});

const timeOrigin = 100;
Expand All @@ -195,6 +198,8 @@ describe('_addResourceSpans', () => {
['url.scheme']: 'https',
['server.address']: 'example.com',
['url.same_origin']: true,
['network.protocol.name']: 'http',
['network.protocol.version']: '1.1',
},
}),
);
Expand Down Expand Up @@ -233,6 +238,7 @@ describe('_addResourceSpans', () => {
const { initiatorType, op } = table[i]!;
const entry = mockPerformanceResourceTiming({
initiatorType,
nextHopProtocol: 'http/1.1',
});
_addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 234, 465);

Expand All @@ -254,6 +260,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 0,
decodedBodySize: 0,
renderBlockingStatus: 'non-blocking',
nextHopProtocol: 'h2',
});

_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
Expand All @@ -271,6 +278,8 @@ describe('_addResourceSpans', () => {
['url.scheme']: 'https',
['server.address']: 'example.com',
['url.same_origin']: true,
['network.protocol.name']: 'http',
['network.protocol.version']: '2',
},
}),
);
Expand All @@ -288,6 +297,7 @@ describe('_addResourceSpans', () => {
transferSize: 2147483647,
encodedBodySize: 2147483647,
decodedBodySize: 2147483647,
nextHopProtocol: 'h3',
});

_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
Expand All @@ -301,6 +311,8 @@ describe('_addResourceSpans', () => {
'server.address': 'example.com',
'url.same_origin': true,
'url.scheme': 'https',
['network.protocol.name']: 'http',
['network.protocol.version']: '3',
},
description: '/assets/to/css',
timestamp: 468,
Expand All @@ -325,6 +337,7 @@ describe('_addResourceSpans', () => {
transferSize: null,
encodedBodySize: null,
decodedBodySize: null,
nextHopProtocol: 'h3',
} as unknown as PerformanceResourceTiming;

_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
Expand All @@ -338,6 +351,8 @@ describe('_addResourceSpans', () => {
'server.address': 'example.com',
'url.same_origin': true,
'url.scheme': 'https',
['network.protocol.name']: 'http',
['network.protocol.version']: '3',
},
description: '/assets/to/css',
timestamp: 468,
Expand Down Expand Up @@ -365,6 +380,7 @@ describe('_addResourceSpans', () => {
encodedBodySize: 0,
decodedBodySize: 0,
deliveryType,
nextHopProtocol: 'h3',
});

_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
Expand Down
43 changes: 42 additions & 1 deletion packages/browser-utils/test/browser/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SentrySpan, getCurrentScope, getIsolationScope, setCurrentClient, spanToJSON } from '@sentry/core';
import { startAndEndSpan } from '../../src/metrics/utils';
import { extractNetworkProtocol, startAndEndSpan } from '../../src/metrics/utils';
import { TestClient, getDefaultClientOptions } from '../utils/TestClient';

describe('startAndEndSpan()', () => {
Expand Down Expand Up @@ -54,3 +54,44 @@ describe('startAndEndSpan()', () => {
expect(spanToJSON(parentSpan).start_timestamp).toEqual(123);
});
});

describe('HTTPTimings', () => {
test.each([
['http/0.9', { name: 'http', version: '0.9' }],
['http/1.0', { name: 'http', version: '1.0' }],
['http/1.1', { name: 'http', version: '1.1' }],
['spdy/1', { name: 'spdy', version: '1' }],
['spdy/2', { name: 'spdy', version: '2' }],
['spdy/3', { name: 'spdy', version: '3' }],
['stun.turn', { name: 'stun.turn', version: 'unknown' }],
['stun.nat-discovery', { name: 'stun.nat-discovery', version: 'unknown' }],
['h2', { name: 'http', version: '2' }],
['h2c', { name: 'http', version: '2c' }],
['webrtc', { name: 'webrtc', version: 'unknown' }],
['c-webrtc', { name: 'c-webrtc', version: 'unknown' }],
['ftp', { name: 'ftp', version: 'unknown' }],
['imap', { name: 'imap', version: 'unknown' }],
['pop3', { name: 'pop', version: '3' }],
['managesieve', { name: 'managesieve', version: 'unknown' }],
['coap', { name: 'coap', version: 'unknown' }],
['xmpp-client', { name: 'xmpp-client', version: 'unknown' }],
['xmpp-server', { name: 'xmpp-server', version: 'unknown' }],
['acme-tls/1', { name: 'acme-tls', version: '1' }],
['mqtt', { name: 'mqtt', version: 'unknown' }],
['dot', { name: 'dot', version: 'unknown' }],
['ntske/1', { name: 'ntske', version: '1' }],
['sunrpc', { name: 'sunrpc', version: 'unknown' }],
['h3', { name: 'http', version: '3' }],
['smb', { name: 'smb', version: 'unknown' }],
['irc', { name: 'irc', version: 'unknown' }],
['nntp', { name: 'nntp', version: 'unknown' }],
['nnsp', { name: 'nnsp', version: 'unknown' }],
['doq', { name: 'doq', version: 'unknown' }],
['sip/2', { name: 'sip', version: '2' }],
['tds/8.0', { name: 'tds', version: '8.0' }],
['dicom', { name: 'dicom', version: 'unknown' }],
['', { name: '', version: 'unknown' }],
])('Extracting version from ALPN protocol %s', (protocol, expected) => {
expect(extractNetworkProtocol(protocol)).toMatchObject(expected);
});
});
Loading

0 comments on commit 6152ef5

Please sign in to comment.