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
22 changes: 17 additions & 5 deletions packages/browser-utils/src/metrics/resourceTiming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import type { SpanAttributes } from '@sentry/core';
import { browserPerformanceTimeOrigin } from '@sentry/core';
import { extractNetworkProtocol, getBrowserPerformanceAPI } from './utils';

function getAbsoluteTime(time = 0): number {
return ((browserPerformanceTimeOrigin() || performance.timeOrigin) + time) / 1000;
function getAbsoluteTime(time: number | undefined): number | undefined {
// falsy values should be preserved so that we can later on drop undefined values and
// preserve 0 vals for cross-origin resources without proper `Timing-Allow-Origin` header.
return time ? ((browserPerformanceTimeOrigin() || performance.timeOrigin) + time) / 1000 : time;
}

/**
Expand All @@ -30,7 +32,7 @@ export function resourceTimingToSpanAttributes(resourceTiming: PerformanceResour
return timingSpanData;
}

return {
return dropUndefinedKeysFromObject({
...timingSpanData,

'http.request.redirect_start': getAbsoluteTime(resourceTiming.redirectStart),
Expand All @@ -55,6 +57,16 @@ export function resourceTimingToSpanAttributes(resourceTiming: PerformanceResour
// For TTFB we actually want the relative time from timeOrigin to responseStart
// This way, TTFB always measures the "first page load" experience.
// see: https://web.dev/articles/ttfb#measure-resource-requests
'http.request.time_to_first_byte': (resourceTiming.responseStart ?? 0) / 1000,
};
'http.request.time_to_first_byte':
resourceTiming.responseStart != null ? resourceTiming.responseStart / 1000 : undefined,
});
}

/**
* Remove properties with `undefined` as value from an object.
* In contrast to `dropUndefinedKeys` in core this funciton only works on first-level
* key-value objects and does not recursively go into object properties or arrays.
*/
function dropUndefinedKeysFromObject<T extends object>(attrs: T): Partial<T> {
return Object.fromEntries(Object.entries(attrs).filter(([, value]) => value != null)) as Partial<T>;
}
28 changes: 26 additions & 2 deletions packages/browser-utils/test/browser/browserMetrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,18 @@ describe('_addResourceSpans', () => {
decodedBodySize: 593,
renderBlockingStatus: 'non-blocking',
nextHopProtocol: 'http/1.1',
connectStart: 1000,
connectEnd: 1001,
redirectStart: 1002,
redirectEnd: 1003,
fetchStart: 1004,
domainLookupStart: 1005,
domainLookupEnd: 1006,
requestStart: 1007,
responseStart: 1008,
responseEnd: 1009,
secureConnectionStart: 1005,
workerStart: 1006,
});

const timeOrigin = 100;
Expand Down Expand Up @@ -305,7 +317,7 @@ describe('_addResourceSpans', () => {
'http.request.response_end': expect.any(Number),
'http.request.response_start': expect.any(Number),
'http.request.secure_connection_start': expect.any(Number),
'http.request.time_to_first_byte': 0,
'http.request.time_to_first_byte': 1.008,
'http.request.worker_start': expect.any(Number),
},
}),
Expand Down Expand Up @@ -492,6 +504,18 @@ describe('_addResourceSpans', () => {
encodedBodySize: null,
decodedBodySize: null,
nextHopProtocol: 'h3',
connectStart: 1000,
connectEnd: 1001,
redirectStart: 1002,
redirectEnd: 1003,
fetchStart: 1004,
domainLookupStart: 1005,
domainLookupEnd: 1006,
requestStart: 1007,
responseStart: 1008,
responseEnd: 1009,
secureConnectionStart: 1005,
workerStart: 1006,
} as unknown as PerformanceResourceTiming;

_addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);
Expand All @@ -518,7 +542,7 @@ describe('_addResourceSpans', () => {
'http.request.response_end': expect.any(Number),
'http.request.response_start': expect.any(Number),
'http.request.secure_connection_start': expect.any(Number),
'http.request.time_to_first_byte': 0,
'http.request.time_to_first_byte': 1.008,
'http.request.worker_start': expect.any(Number),
},
description: '/assets/to/css',
Expand Down
62 changes: 30 additions & 32 deletions packages/browser-utils/test/metrics/resourceTiming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('resourceTimingToSpanAttributes', () => {
duration: 200,
initiatorType: 'fetch',
nextHopProtocol: 'h2',
workerStart: 0,
workerStart: 1,
redirectStart: 10,
redirectEnd: 20,
fetchStart: 25,
Expand Down Expand Up @@ -276,6 +276,13 @@ describe('resourceTimingToSpanAttributes', () => {
});

it('handles zero timing values', () => {
/**
* Most resource timing entries have a 0 value if the resource was requested from
* a cross-origin source which does not return a matching `Timing-Allow-Origin` header.
*
* see: https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Resource_timing#cross-origin_timing_information
*/

extractNetworkProtocolSpy.mockReturnValue({
name: '',
version: 'unknown',
Expand All @@ -284,34 +291,36 @@ describe('resourceTimingToSpanAttributes', () => {
const mockResourceTiming = createMockResourceTiming({
nextHopProtocol: '',
redirectStart: 0,
fetchStart: 0,
redirectEnd: 0,
workerStart: 0,
fetchStart: 1000100, // fetchStart is not restricted by `Timing-Allow-Origin` header
domainLookupStart: 0,
domainLookupEnd: 0,
connectStart: 0,
secureConnectionStart: 0,
connectEnd: 0,
secureConnectionStart: 0,
requestStart: 0,
responseStart: 0,
responseEnd: 0,
responseEnd: 1000200, // responseEnd is not restricted by `Timing-Allow-Origin` header
});

const result = resourceTimingToSpanAttributes(mockResourceTiming);

expect(result).toEqual({
'network.protocol.version': 'unknown',
'network.protocol.name': '',
'http.request.redirect_start': 1000, // (1000000 + 0) / 1000
'http.request.redirect_end': 1000.02,
'http.request.worker_start': 1000,
'http.request.fetch_start': 1000,
'http.request.domain_lookup_start': 1000,
'http.request.domain_lookup_end': 1000,
'http.request.connect_start': 1000,
'http.request.secure_connection_start': 1000,
'http.request.connection_end': 1000,
'http.request.request_start': 1000,
'http.request.response_start': 1000,
'http.request.response_end': 1000,
'http.request.redirect_start': 0,
'http.request.redirect_end': 0,
'http.request.worker_start': 0,
'http.request.fetch_start': 2000.1,
'http.request.domain_lookup_start': 0,
'http.request.domain_lookup_end': 0,
'http.request.connect_start': 0,
'http.request.secure_connection_start': 0,
'http.request.connection_end': 0,
'http.request.request_start': 0,
'http.request.response_start': 0,
'http.request.response_end': 2000.2,
'http.request.time_to_first_byte': 0,
});
});
Expand Down Expand Up @@ -343,7 +352,7 @@ describe('resourceTimingToSpanAttributes', () => {
'network.protocol.name': 'http',
'http.request.redirect_start': 1000.005,
'http.request.redirect_end': 1000.02,
'http.request.worker_start': 1000,
'http.request.worker_start': 1000.001,
'http.request.fetch_start': 1000.01,
'http.request.domain_lookup_start': 1000.015,
'http.request.domain_lookup_end': 1000.02,
Expand Down Expand Up @@ -470,7 +479,7 @@ describe('resourceTimingToSpanAttributes', () => {
});

describe('edge cases', () => {
it('handles undefined timing values', () => {
it("doesn't include undefined timing values", () => {
browserPerformanceTimeOriginSpy.mockReturnValue(1000000);

extractNetworkProtocolSpy.mockReturnValue({
Expand All @@ -481,6 +490,7 @@ describe('resourceTimingToSpanAttributes', () => {
const mockResourceTiming = createMockResourceTiming({
nextHopProtocol: '',
redirectStart: undefined as any,
redirectEnd: undefined as any,
fetchStart: undefined as any,
workerStart: undefined as any,
domainLookupStart: undefined as any,
Expand All @@ -498,19 +508,6 @@ describe('resourceTimingToSpanAttributes', () => {
expect(result).toEqual({
'network.protocol.version': 'unknown',
'network.protocol.name': '',
'http.request.redirect_start': 1000, // (1000000 + 0) / 1000
'http.request.redirect_end': 1000.02,
'http.request.worker_start': 1000,
'http.request.fetch_start': 1000,
'http.request.domain_lookup_start': 1000,
'http.request.domain_lookup_end': 1000,
'http.request.connect_start': 1000,
'http.request.secure_connection_start': 1000,
'http.request.connection_end': 1000,
'http.request.request_start': 1000,
'http.request.response_start': 1000,
'http.request.response_end': 1000,
'http.request.time_to_first_byte': 0,
});
});

Expand All @@ -534,6 +531,7 @@ describe('resourceTimingToSpanAttributes', () => {
requestStart: 999999,
responseStart: 999999,
responseEnd: 999999,
workerStart: 999999,
});

const result = resourceTimingToSpanAttributes(mockResourceTiming);
Expand All @@ -543,7 +541,7 @@ describe('resourceTimingToSpanAttributes', () => {
'network.protocol.name': '',
'http.request.redirect_start': 1999.999, // (1000000 + 999999) / 1000
'http.request.redirect_end': 1000.02,
'http.request.worker_start': 1000,
'http.request.worker_start': 1999.999,
'http.request.fetch_start': 1999.999,
'http.request.domain_lookup_start': 1999.999,
'http.request.domain_lookup_end': 1999.999,
Expand Down
Loading