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
26 changes: 18 additions & 8 deletions packages/browser-utils/src/performance/elementTiming.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import {
SENTRY_ORIGIN,
UI_ELEMENT_HEIGHT,
UI_ELEMENT_ID,
UI_ELEMENT_IDENTIFIER,
UI_ELEMENT_PAINT_TYPE,
UI_ELEMENT_TYPE,
UI_ELEMENT_URL,
UI_ELEMENT_WIDTH,
} from '@sentry/conventions/attributes';
import type { IntegrationFn } from '@sentry/core';
import { browserPerformanceTimeOrigin, defineIntegration, filterCollectedUrl, metrics } from '@sentry/core';
import { addPerformanceInstrumentationHandler } from '../instrumentation/performanceObserver';
Expand Down Expand Up @@ -41,32 +51,32 @@ const _elementTimingIntegration = (() => {
const loadTime = elementEntry.loadTime;

const metricAttributes: Record<string, string | number> = {
'sentry.origin': 'auto.ui.browser.element_timing',
'ui.element.identifier': identifier,
[SENTRY_ORIGIN]: 'auto.ui.browser.element_timing',
[UI_ELEMENT_IDENTIFIER]: identifier,
};

if (paintType) {
metricAttributes['ui.element.paint_type'] = paintType;
metricAttributes[UI_ELEMENT_PAINT_TYPE] = paintType;
}

if (elementEntry.id) {
metricAttributes['ui.element.id'] = elementEntry.id;
metricAttributes[UI_ELEMENT_ID] = elementEntry.id;
}

if (elementEntry.element) {
metricAttributes['ui.element.type'] = elementEntry.element.tagName.toLowerCase();
metricAttributes[UI_ELEMENT_TYPE] = elementEntry.element.tagName.toLowerCase();
}

if (elementEntry.url) {
metricAttributes['ui.element.url'] = filterCollectedUrl(elementEntry.url);
metricAttributes[UI_ELEMENT_URL] = filterCollectedUrl(elementEntry.url);
}

if (elementEntry.naturalWidth) {
metricAttributes['ui.element.width'] = elementEntry.naturalWidth;
metricAttributes[UI_ELEMENT_WIDTH] = elementEntry.naturalWidth;
}

if (elementEntry.naturalHeight) {
metricAttributes['ui.element.height'] = elementEntry.naturalHeight;
metricAttributes[UI_ELEMENT_HEIGHT] = elementEntry.naturalHeight;
}

if (renderTime > 0) {
Expand Down
37 changes: 26 additions & 11 deletions packages/browser-utils/src/performance/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ import {
URL_FULL,
URL_SCHEME,
SENTRY_ORIGIN,
BROWSER_SCRIPT_INVOKER,
BROWSER_SCRIPT_INVOKER_TYPE,
BROWSER_SCRIPT_SOURCE_CHAR_POSITION,
CONNECTIONTYPE,
DEVICEMEMORY,
DEVICE_MEMORY_ESTIMATED_CAPACITY,
DEVICE_PROCESSOR_COUNT,
EFFECTIVECONNECTIONTYPE,
HARDWARECONCURRENCY,
HTTP_RESPONSE_BODY_DECODED_SIZE,
RESOURCE_RENDER_BLOCKING_STATUS,
} from '@sentry/conventions/attributes';
import {
BROWSER_CACHE,
Expand Down Expand Up @@ -171,16 +182,16 @@ export function startTrackingLongAnimationFrames(): void {

const initialScript = entry.scripts[0];
const { invoker, invokerType, sourceURL, sourceFunctionName, sourceCharPosition } = initialScript;
attributes['browser.script.invoker'] = invoker;
attributes['browser.script.invoker_type'] = invokerType;
attributes[BROWSER_SCRIPT_INVOKER] = invoker;
attributes[BROWSER_SCRIPT_INVOKER_TYPE] = invokerType;
if (sourceURL) {
attributes[CODE_FILE_PATH] = sourceURL;
}
if (sourceFunctionName) {
attributes[CODE_FUNCTION_NAME] = sourceFunctionName;
}
if (sourceCharPosition !== -1) {
attributes['browser.script.source_char_position'] = sourceCharPosition;
attributes[BROWSER_SCRIPT_SOURCE_CHAR_POSITION] = sourceCharPosition;
}

startAndEndSpan(parent, startTime, startTime + duration, {
Expand Down Expand Up @@ -478,10 +489,10 @@ export function _addResourceSpans(

['transferSize', HTTP_RESPONSE_SIZE],
['encodedBodySize', HTTP_RESPONSE_BODY_SIZE],
['decodedBodySize', 'http.response.body.decoded_size'],
['decodedBodySize', HTTP_RESPONSE_BODY_DECODED_SIZE],

// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/renderBlockingStatus
['renderBlockingStatus', 'resource.render_blocking_status'],
['renderBlockingStatus', RESOURCE_RENDER_BLOCKING_STATUS],

// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/deliveryType
['deliveryType', 'http.response_delivery_type'],
Expand Down Expand Up @@ -516,13 +527,15 @@ function _trackNavigator(span: Span, spanStreamingEnabled: boolean | undefined):
if (connection) {
if (connection.effectiveType) {
span.setAttribute(
spanStreamingEnabled ? NETWORK_CONNECTION_EFFECTIVE_TYPE : 'effectiveConnectionType',
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
spanStreamingEnabled ? NETWORK_CONNECTION_EFFECTIVE_TYPE : EFFECTIVECONNECTIONTYPE,
connection.effectiveType,
);
}

if (connection.type) {
span.setAttribute(spanStreamingEnabled ? NETWORK_CONNECTION_TYPE : 'connectionType', connection.type);
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
span.setAttribute(spanStreamingEnabled ? NETWORK_CONNECTION_TYPE : CONNECTIONTYPE, connection.type);
}

if (isMeasurementValue(connection.rtt)) {
Expand All @@ -538,17 +551,19 @@ function _trackNavigator(span: Span, spanStreamingEnabled: boolean | undefined):

if (isMeasurementValue(navigator.deviceMemory)) {
if (spanStreamingEnabled) {
span.setAttribute('device.memory.estimated_capacity', navigator.deviceMemory);
span.setAttribute(DEVICE_MEMORY_ESTIMATED_CAPACITY, navigator.deviceMemory);
} else {
span.setAttribute('deviceMemory', `${navigator.deviceMemory} GB`);
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
span.setAttribute(DEVICEMEMORY, `${navigator.deviceMemory} GB`);
}
}

if (isMeasurementValue(navigator.hardwareConcurrency)) {
if (spanStreamingEnabled) {
span.setAttribute('device.processor_count', navigator.hardwareConcurrency);
span.setAttribute(DEVICE_PROCESSOR_COUNT, navigator.hardwareConcurrency);
} else {
span.setAttribute('hardwareConcurrency', String(navigator.hardwareConcurrency));
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
span.setAttribute(HARDWARECONCURRENCY, String(navigator.hardwareConcurrency));
}
}
}
Expand Down
47 changes: 32 additions & 15 deletions packages/browser-utils/src/performance/resourceTiming.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
import {
HTTP_REQUEST_CONNECTION_END,
HTTP_REQUEST_CONNECT_START,
HTTP_REQUEST_DOMAIN_LOOKUP_END,
HTTP_REQUEST_DOMAIN_LOOKUP_START,
HTTP_REQUEST_FETCH_START,
HTTP_REQUEST_REDIRECT_END,
HTTP_REQUEST_REDIRECT_START,
HTTP_REQUEST_REQUEST_START,
HTTP_REQUEST_RESPONSE_END,
HTTP_REQUEST_RESPONSE_START,
HTTP_REQUEST_SECURE_CONNECTION_START,
HTTP_REQUEST_TIME_TO_FIRST_BYTE,
HTTP_REQUEST_WORKER_START,
NETWORK_PROTOCOL_NAME,
NETWORK_PROTOCOL_VERSION,
} from '@sentry/conventions/attributes';
import type { SpanAttributes } from '@sentry/core';
import { _INTERNAL_performanceTimeToSeconds } from '@sentry/core';
import { extractNetworkProtocol, msToSec } from './utils';
Expand All @@ -18,8 +35,8 @@ export function resourceTimingToSpanAttributes(resourceTiming: PerformanceResour
// valid for `nextHopProtocol` to be an empty string.
if (resourceTiming.nextHopProtocol != undefined) {
const { name, version } = extractNetworkProtocol(resourceTiming.nextHopProtocol);
timingSpanData['network.protocol.version'] = version;
timingSpanData['network.protocol.name'] = name;
timingSpanData[NETWORK_PROTOCOL_VERSION] = version;
timingSpanData[NETWORK_PROTOCOL_NAME] = name;
}

if (_INTERNAL_performanceTimeToSeconds(resourceTiming.startTime) === undefined) {
Expand All @@ -35,29 +52,29 @@ export function resourceTimingToSpanAttributes(resourceTiming: PerformanceResour
return dropUndefinedKeysFromObject({
...timingSpanData,

'http.request.redirect_start': getAbsoluteTime(resourceTiming.redirectStart),
'http.request.redirect_end': getAbsoluteTime(resourceTiming.redirectEnd),
[HTTP_REQUEST_REDIRECT_START]: getAbsoluteTime(resourceTiming.redirectStart),
[HTTP_REQUEST_REDIRECT_END]: getAbsoluteTime(resourceTiming.redirectEnd),

'http.request.worker_start': getAbsoluteTime(resourceTiming.workerStart),
[HTTP_REQUEST_WORKER_START]: getAbsoluteTime(resourceTiming.workerStart),

'http.request.fetch_start': getAbsoluteTime(resourceTiming.fetchStart),
[HTTP_REQUEST_FETCH_START]: getAbsoluteTime(resourceTiming.fetchStart),

'http.request.domain_lookup_start': getAbsoluteTime(resourceTiming.domainLookupStart),
'http.request.domain_lookup_end': getAbsoluteTime(resourceTiming.domainLookupEnd),
[HTTP_REQUEST_DOMAIN_LOOKUP_START]: getAbsoluteTime(resourceTiming.domainLookupStart),
[HTTP_REQUEST_DOMAIN_LOOKUP_END]: getAbsoluteTime(resourceTiming.domainLookupEnd),

'http.request.connect_start': getAbsoluteTime(resourceTiming.connectStart),
'http.request.secure_connection_start': getAbsoluteTime(resourceTiming.secureConnectionStart),
'http.request.connection_end': getAbsoluteTime(resourceTiming.connectEnd),
[HTTP_REQUEST_CONNECT_START]: getAbsoluteTime(resourceTiming.connectStart),
[HTTP_REQUEST_SECURE_CONNECTION_START]: getAbsoluteTime(resourceTiming.secureConnectionStart),
[HTTP_REQUEST_CONNECTION_END]: getAbsoluteTime(resourceTiming.connectEnd),

'http.request.request_start': getAbsoluteTime(resourceTiming.requestStart),
[HTTP_REQUEST_REQUEST_START]: getAbsoluteTime(resourceTiming.requestStart),

'http.request.response_start': getAbsoluteTime(resourceTiming.responseStart),
'http.request.response_end': getAbsoluteTime(resourceTiming.responseEnd),
[HTTP_REQUEST_RESPONSE_START]: getAbsoluteTime(resourceTiming.responseStart),
[HTTP_REQUEST_RESPONSE_END]: getAbsoluteTime(resourceTiming.responseEnd),

// 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':
[HTTP_REQUEST_TIME_TO_FIRST_BYTE]:
resourceTiming.responseStart != null ? msToSec(resourceTiming.responseStart) : undefined,
});
}
Expand Down
3 changes: 2 additions & 1 deletion packages/browser-utils/src/web-vitals/emitSpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
USER_AGENT_ORIGINAL,
SENTRY_OP,
SENTRY_ORIGIN,
SENTRY_PAGELOAD_SPAN_ID,
} from '@sentry/conventions/attributes';
import { WINDOW } from '../types';
import type { MetricNavigationType } from '../instrumentation/performanceObserver';
Expand Down Expand Up @@ -100,7 +101,7 @@ export function _emitWebVitalSpan(options: WebVitalSpanOptions): void {

if (parentSpan && spanToJSON(parentSpan).attributes[SENTRY_OP] === 'pageload') {
// for LCP and CLS, we collect the pageload span id as an attribute
attributes['sentry.pageload.span_id'] = parentSpan.spanContext().spanId;
attributes[SENTRY_PAGELOAD_SPAN_ID] = parentSpan.spanContext().spanId;
}

if (softNavigationId != null) {
Expand Down
18 changes: 12 additions & 6 deletions packages/browser-utils/src/web-vitals/spans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ import {
BROWSER_WEB_VITAL_INP_TARGET,
UI_COMPONENT_NAME,
SENTRY_OP,
BROWSER_WEB_VITAL_LCP_ELEMENT,
BROWSER_WEB_VITAL_LCP_ID,
BROWSER_WEB_VITAL_LCP_LOAD_TIME,
BROWSER_WEB_VITAL_LCP_RENDER_TIME,
BROWSER_WEB_VITAL_LCP_SIZE,
BROWSER_WEB_VITAL_LCP_URL,
} from '@sentry/conventions/attributes';
import { _emitWebVitalSpan } from './emitSpan';
import { isValidLcpMetric } from './lcp';
Expand Down Expand Up @@ -214,16 +220,16 @@ export function _sendLcpSpan(
const attributes: SpanAttributes = {};

if (selector) {
attributes['browser.web_vital.lcp.element'] = selector;
attributes[BROWSER_WEB_VITAL_LCP_ELEMENT] = selector;
}
if (componentName) {
attributes[UI_COMPONENT_NAME] = componentName;
}
entry?.id && (attributes['browser.web_vital.lcp.id'] = entry.id);
entry?.url && (attributes['browser.web_vital.lcp.url'] = filterCollectedUrl(entry.url, client));
entry?.loadTime != null && (attributes['browser.web_vital.lcp.load_time'] = entry.loadTime);
entry?.renderTime != null && (attributes['browser.web_vital.lcp.render_time'] = entry.renderTime);
entry?.size != null && (attributes['browser.web_vital.lcp.size'] = entry.size);
entry?.id && (attributes[BROWSER_WEB_VITAL_LCP_ID] = entry.id);
entry?.url && (attributes[BROWSER_WEB_VITAL_LCP_URL] = filterCollectedUrl(entry.url, client));
entry?.loadTime != null && (attributes[BROWSER_WEB_VITAL_LCP_LOAD_TIME] = entry.loadTime);
entry?.renderTime != null && (attributes[BROWSER_WEB_VITAL_LCP_RENDER_TIME] = entry.renderTime);
entry?.size != null && (attributes[BROWSER_WEB_VITAL_LCP_SIZE] = entry.size);

_emitWebVitalSpan({
name,
Expand Down
42 changes: 32 additions & 10 deletions packages/browser-utils/src/web-vitals/tracking.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import type { Client, Measurements, Span } from '@sentry/core';
import { browserPerformanceTimeOrigin, debug, filterCollectedUrl, setMeasurement, spanToJSON } from '@sentry/core';
import { BROWSER_NAVIGATION_TYPE, SENTRY_OP } from '@sentry/conventions/attributes';
import {
BROWSER_NAVIGATION_TYPE,
SENTRY_OP,
BROWSER_PERFORMANCE_NAVIGATION_ACTIVATION_START,
BROWSER_PERFORMANCE_TIME_ORIGIN,
BROWSER_WEB_VITAL_TTFB_REQUEST_TIME,
LCP_ELEMENT,
LCP_ID,
LCP_LOADTIME,
LCP_RENDERTIME,
LCP_SIZE,
LCP_URL,
PERFORMANCE_ACTIVATIONSTART,
PERFORMANCE_TIMEORIGIN,
} from '@sentry/conventions/attributes';
import { DEBUG_BUILD } from '../debug-build';
import { htmlTreeAsString } from '../htmlTreeAsString';
import type { MetricNavigationType } from '../instrumentation/performanceObserver';
Expand Down Expand Up @@ -186,7 +200,7 @@ export function addWebVitalsToSpan(span: Span, options: AddWebVitalsToSpanOption
}
});
if (_measurements['ttfb.requestTime']) {
setAttr('ttfb.requestTime', _measurements['ttfb.requestTime'].value, 'browser.web_vital.ttfb.request_time');
setAttr('ttfb.requestTime', _measurements['ttfb.requestTime'].value, BROWSER_WEB_VITAL_TTFB_REQUEST_TIME);
}
} else {
// If CLS is tracked as a span (span streaming), don't record CLS as a measurement
Expand All @@ -213,7 +227,8 @@ export function addWebVitalsToSpan(span: Span, options: AddWebVitalsToSpanOption
}

// Set timeOrigin which denotes the timestamp which to base the LCP/FCP/FP/TTFB measurements on
span.setAttribute(spanStreamingEnabled ? 'browser.performance.time_origin' : 'performance.timeOrigin', timeOrigin);
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
span.setAttribute(spanStreamingEnabled ? BROWSER_PERFORMANCE_TIME_ORIGIN : PERFORMANCE_TIMEORIGIN, timeOrigin);

// In prerendering scenarios, where a page might be prefetched and pre-rendered before the user clicks the link,
// the navigation starts earlier than when the user clicks it. Web Vitals should always be based on the
Expand All @@ -222,7 +237,8 @@ export function addWebVitalsToSpan(span: Span, options: AddWebVitalsToSpanOption
// This is user action is called "activation" and the time between navigation and activation is stored in
// the `activationStart` attribute of the "navigation" PerformanceEntry.
span.setAttribute(
spanStreamingEnabled ? 'browser.performance.navigation.activation_start' : 'performance.activationStart',
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
spanStreamingEnabled ? BROWSER_PERFORMANCE_NAVIGATION_ACTIVATION_START : PERFORMANCE_ACTIVATIONSTART,
getActivationStart(),
);
}
Expand All @@ -244,31 +260,37 @@ function _setWebVitalAttributes(span: Span, options: AddWebVitalsToSpanOptions):
// Capture Properties of the LCP element that contributes to the LCP.

if (_lcpEntry.element) {
span.setAttribute('lcp.element', htmlTreeAsString(_lcpEntry.element));
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
span.setAttribute(LCP_ELEMENT, htmlTreeAsString(_lcpEntry.element));
}

if (_lcpEntry.id) {
span.setAttribute('lcp.id', _lcpEntry.id);
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
span.setAttribute(LCP_ID, _lcpEntry.id);
}

if (_lcpEntry.url) {
// Trim URL to the first 200 characters.
span.setAttribute('lcp.url', filterCollectedUrl(_lcpEntry.url.trim()).slice(0, 200));
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
span.setAttribute(LCP_URL, filterCollectedUrl(_lcpEntry.url.trim()).slice(0, 200));
}

if (_lcpEntry.loadTime != null) {
// loadTime is the time of LCP that's related to receiving the LCP element response..
span.setAttribute('lcp.loadTime', _lcpEntry.loadTime);
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
span.setAttribute(LCP_LOADTIME, _lcpEntry.loadTime);
}

if (_lcpEntry.renderTime != null) {
// renderTime is loadTime + rendering time
// it's 0 if the LCP element is loaded from a 3rd party origin that doesn't send the
// `Timing-Allow-Origin` header.
span.setAttribute('lcp.renderTime', _lcpEntry.renderTime);
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
span.setAttribute(LCP_RENDERTIME, _lcpEntry.renderTime);
}

span.setAttribute('lcp.size', _lcpEntry.size);
// oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name.
span.setAttribute(LCP_SIZE, _lcpEntry.size);
}

// Only add CLS attributes if CLS is being recorded on the pageload span
Expand Down
Loading