Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const core = require('@sentry/core'); const htmlTreeAsString = require('../htmlTreeAsString.js'); const types = require('../types.js'); const cls = require('./cls.js'); const instrument = require('./instrument.js'); const lcp = require('./lcp.js'); const resourceTiming = require('./resourceTiming.js'); const utils = require('./utils.js'); const getActivationStart = require('./web-vitals/lib/getActivationStart.js'); const getNavigationEntry = require('./web-vitals/lib/getNavigationEntry.js'); const getVisibilityWatcher = require('./web-vitals/lib/getVisibilityWatcher.js'); const debugBuild = require('../debug-build.js'); const MAX_INT_AS_BYTES = 2147483647; let _performanceCursor = 0; let _measurements = {}; let _lcpEntry; let _clsEntry; function startTrackingWebVitals({ recordClsStandaloneSpans, recordLcpStandaloneSpans, client }) { const performance = utils.getBrowserPerformanceAPI(); if (performance && core.browserPerformanceTimeOrigin()) { if (performance.mark) { types.WINDOW.performance.mark("sentry-tracing-init"); } const lcpCleanupCallback = recordLcpStandaloneSpans ? lcp.trackLcpAsStandaloneSpan(client) : recordLcpStandaloneSpans === false ? _trackLCP() : void 0; const clsCleanupCallback = recordClsStandaloneSpans ? cls.trackClsAsStandaloneSpan(client) : recordClsStandaloneSpans === false ? _trackCLS() : void 0; const ttfbCleanupCallback = _trackTtfb(); return () => { ttfbCleanupCallback(); lcpCleanupCallback?.(); clsCleanupCallback?.(); }; } return () => void 0; } function startTrackingLongTasks() { instrument.addPerformanceInstrumentationHandler("longtask", ({ entries }) => { const parent = core.getActiveSpan(); if (!parent) { return; } const { op: parentOp, start_timestamp: parentStartTimestamp } = core.spanToJSON(parent); for (const entry of entries) { const startTime = utils.msToSec(core.browserPerformanceTimeOrigin() + entry.startTime); const duration = utils.msToSec(entry.duration); if (parentOp === "navigation" && parentStartTimestamp && startTime < parentStartTimestamp) { continue; } utils.startAndEndSpan(parent, startTime, startTime + duration, { name: "Main UI thread blocked", op: "ui.long-task", attributes: { [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.ui.browser.metrics" } }); } }); } function startTrackingLongAnimationFrames() { const observer = new PerformanceObserver((list) => { const parent = core.getActiveSpan(); if (!parent) { return; } for (const entry of list.getEntries()) { if (!entry.scripts[0]) { continue; } const startTime = utils.msToSec(core.browserPerformanceTimeOrigin() + entry.startTime); const { start_timestamp: parentStartTimestamp, op: parentOp } = core.spanToJSON(parent); if (parentOp === "navigation" && parentStartTimestamp && startTime < parentStartTimestamp) { continue; } const duration = utils.msToSec(entry.duration); const attributes = { [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.ui.browser.metrics" }; const initialScript = entry.scripts[0]; const { invoker, invokerType, sourceURL, sourceFunctionName, sourceCharPosition } = initialScript; attributes["browser.script.invoker"] = invoker; attributes["browser.script.invoker_type"] = invokerType; if (sourceURL) { attributes["code.filepath"] = sourceURL; } if (sourceFunctionName) { attributes["code.function"] = sourceFunctionName; } if (sourceCharPosition !== -1) { attributes["browser.script.source_char_position"] = sourceCharPosition; } utils.startAndEndSpan(parent, startTime, startTime + duration, { name: "Main UI thread blocked", op: "ui.long-animation-frame", attributes }); } }); observer.observe({ type: "long-animation-frame", buffered: true }); } function startTrackingInteractions() { instrument.addPerformanceInstrumentationHandler("event", ({ entries }) => { const parent = core.getActiveSpan(); if (!parent) { return; } for (const entry of entries) { if (entry.name === "click") { const startTime = utils.msToSec(core.browserPerformanceTimeOrigin() + entry.startTime); const duration = utils.msToSec(entry.duration); const spanOptions = { name: htmlTreeAsString.htmlTreeAsString(entry.target), op: `ui.interaction.${entry.name}`, startTime, attributes: { [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.ui.browser.metrics" } }; const componentName = core.getComponentName(entry.target); if (componentName) { spanOptions.attributes["ui.component_name"] = componentName; } utils.startAndEndSpan(parent, startTime, startTime + duration, spanOptions); } } }); } function _trackCLS() { return instrument.addClsInstrumentationHandler(({ metric }) => { const entry = metric.entries[metric.entries.length - 1]; if (!entry) { return; } _measurements["cls"] = { value: metric.value, unit: "" }; _clsEntry = entry; }, true); } function _trackLCP() { return instrument.addLcpInstrumentationHandler(({ metric }) => { const entry = metric.entries[metric.entries.length - 1]; if (!entry || !lcp.isValidLcpMetric(metric.value)) { return; } _measurements["lcp"] = { value: metric.value, unit: "millisecond" }; _lcpEntry = entry; }, true); } function _trackTtfb() { return instrument.addTtfbInstrumentationHandler(({ metric }) => { const entry = metric.entries[metric.entries.length - 1]; if (!entry) { return; } _measurements["ttfb"] = { value: metric.value, unit: "millisecond" }; }); } function addPerformanceEntries(span, options) { const performance = utils.getBrowserPerformanceAPI(); const origin = core.browserPerformanceTimeOrigin(); if (!performance?.getEntries || !origin) { return; } const { spanStreamingEnabled, ignorePerformanceApiSpans, ignoreResourceSpans, recordClsOnPageloadSpan, recordLcpOnPageloadSpan } = options; const timeOrigin = utils.msToSec(origin); const performanceEntries = performance.getEntries(); const { op, start_timestamp: transactionStartTime } = core.spanToJSON(span); performanceEntries.slice(_performanceCursor).forEach((entry) => { const startTime = utils.msToSec(entry.startTime); const duration = utils.msToSec( // Inexplicably, Chrome sometimes emits a negative duration. We need to work around this. // There is a SO post attempting to explain this, but it leaves one with open questions: https://stackoverflow.com/questions/23191918/peformance-getentries-and-negative-duration-display // The way we clamp the value is probably not accurate, since we have observed this happen for things that may take a while to load, like for example the replay worker. // TODO: Investigate why this happens and how to properly mitigate. For now, this is a workaround to prevent transactions being dropped due to negative duration spans. Math.max(0, entry.duration) ); if (op === "navigation" && transactionStartTime && timeOrigin + startTime < transactionStartTime) { return; } switch (entry.entryType) { case "navigation": { _addNavigationSpans(span, entry, timeOrigin); break; } case "mark": case "paint": case "measure": { _addMeasureSpans(span, entry, startTime, duration, timeOrigin, ignorePerformanceApiSpans); const firstHidden = getVisibilityWatcher.getVisibilityWatcher(); const shouldRecord = entry.startTime < firstHidden.firstHiddenTime; if (entry.name === "first-paint" && shouldRecord) { _measurements["fp"] = { value: entry.startTime, unit: "millisecond" }; } if (entry.name === "first-contentful-paint" && shouldRecord) { _measurements["fcp"] = { value: entry.startTime, unit: "millisecond" }; } break; } case "resource": { _addResourceSpans( span, entry, entry.name, startTime, duration, timeOrigin, ignoreResourceSpans ); break; } } }); _performanceCursor = Math.max(performanceEntries.length - 1, 0); _trackNavigator(span, spanStreamingEnabled); if (op === "pageload") { _addTtfbRequestTimeToMeasurements(_measurements); if (spanStreamingEnabled) { const setAttr = (shortWebVitalName, value, customAttrName) => { const attrKey = customAttrName ?? `browser.web_vital.${shortWebVitalName}.value`; span.setAttribute(attrKey, value); debugBuild.DEBUG_BUILD && core.debug.log("Setting web vital attribute", { [attrKey]: value }, "on pageload span"); }; ["ttfb", "fp", "fcp"].forEach((measurementName) => { if (_measurements[measurementName]) { setAttr(measurementName, _measurements[measurementName].value); } }); if (_measurements["ttfb.requestTime"]) { setAttr("ttfb.requestTime", _measurements["ttfb.requestTime"].value, "browser.web_vital.ttfb.request_time"); } } else { if (!recordClsOnPageloadSpan) { delete _measurements.cls; } if (!recordLcpOnPageloadSpan) { delete _measurements.lcp; } Object.entries(_measurements).forEach(([measurementName, measurement]) => { core.setMeasurement(measurementName, measurement.value, measurement.unit); }); _setWebVitalAttributes(span, options); } span.setAttribute(spanStreamingEnabled ? "browser.performance.time_origin" : "performance.timeOrigin", timeOrigin); span.setAttribute( spanStreamingEnabled ? "browser.performance.navigation.activation_start" : "performance.activationStart", getActivationStart.getActivationStart() ); } _lcpEntry = void 0; _clsEntry = void 0; _measurements = {}; } function isReact19MeasureEntry(entry) { if (entry?.entryType !== "measure") { return; } try { return entry.detail.devtools.track === "Components \u269B"; } catch { return; } } function _addMeasureSpans(span, entry, startTime, duration, timeOrigin, ignorePerformanceApiSpans) { if (isReact19MeasureEntry(entry)) { return; } if (["mark", "measure"].includes(entry.entryType) && core.stringMatchesSomePattern(entry.name, ignorePerformanceApiSpans)) { return; } const navEntry = getNavigationEntry.getNavigationEntry(false); const requestTime = utils.msToSec(navEntry ? navEntry.requestStart : 0); const measureStartTimestamp = timeOrigin + Math.max(startTime, requestTime); const startTimeStamp = timeOrigin + startTime; const measureEndTimestamp = startTimeStamp + duration; const attributes = { [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.resource.browser.metrics" }; if (measureStartTimestamp !== startTimeStamp) { attributes["sentry.browser.measure_happened_before_request"] = true; attributes["sentry.browser.measure_start_time"] = measureStartTimestamp; } _addDetailToSpanAttributes(attributes, entry); if (measureStartTimestamp <= measureEndTimestamp) { utils.startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, { name: entry.name, op: entry.entryType, attributes }); } } function _addDetailToSpanAttributes(attributes, performanceMeasure) { try { const detail = performanceMeasure.detail; if (!detail) { return; } if (typeof detail === "object") { for (const [key, value] of Object.entries(detail)) { if (value && core.isPrimitive(value)) { attributes[`sentry.browser.measure.detail.${key}`] = value; } else if (value !== void 0) { try { attributes[`sentry.browser.measure.detail.${key}`] = JSON.stringify(value); } catch { } } } return; } if (core.isPrimitive(detail)) { attributes["sentry.browser.measure.detail"] = detail; return; } try { attributes["sentry.browser.measure.detail"] = JSON.stringify(detail); } catch { } } catch { } } function _addNavigationSpans(span, entry, timeOrigin) { ["unloadEvent", "redirect", "domContentLoadedEvent", "loadEvent", "connect"].forEach((event) => { _addPerformanceNavigationTiming(span, entry, event, timeOrigin); }); _addPerformanceNavigationTiming(span, entry, "secureConnection", timeOrigin, "TLS/SSL"); _addPerformanceNavigationTiming(span, entry, "fetch", timeOrigin, "cache"); _addPerformanceNavigationTiming(span, entry, "domainLookup", timeOrigin, "DNS"); _addRequest(span, entry, timeOrigin); } function _addPerformanceNavigationTiming(span, entry, event, timeOrigin, name = event) { const eventEnd = _getEndPropertyNameForNavigationTiming(event); const end = entry[eventEnd]; const start = entry[`${event}Start`]; if (!start || !end) { return; } utils.startAndEndSpan(span, timeOrigin + utils.msToSec(start), timeOrigin + utils.msToSec(end), { op: `browser.${name}`, name: entry.name, attributes: { [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.ui.browser.metrics", ...event === "redirect" && entry.redirectCount != null ? { "http.redirect_count": entry.redirectCount } : {} } }); } function _getEndPropertyNameForNavigationTiming(event) { if (event === "secureConnection") { return "connectEnd"; } if (event === "fetch") { return "domainLookupStart"; } return `${event}End`; } function _addRequest(span, entry, timeOrigin) { const requestStartTimestamp = timeOrigin + utils.msToSec(entry.requestStart); const responseEndTimestamp = timeOrigin + utils.msToSec(entry.responseEnd); const responseStartTimestamp = timeOrigin + utils.msToSec(entry.responseStart); if (entry.responseEnd) { utils.startAndEndSpan(span, requestStartTimestamp, responseEndTimestamp, { op: "browser.request", name: entry.name, attributes: { [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.ui.browser.metrics" } }); utils.startAndEndSpan(span, responseStartTimestamp, responseEndTimestamp, { op: "browser.response", name: entry.name, attributes: { [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.ui.browser.metrics" } }); } } function _addResourceSpans(span, entry, resourceUrl, startTime, duration, timeOrigin, ignoredResourceSpanOps) { if (entry.initiatorType === "xmlhttprequest" || entry.initiatorType === "fetch") { return; } const op = entry.initiatorType ? `resource.${entry.initiatorType}` : "resource.other"; if (ignoredResourceSpanOps?.includes(op)) { return; } const attributes = { [core.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.resource.browser.metrics" }; const parsedUrl = core.parseUrl(resourceUrl); if (parsedUrl.protocol) { attributes["url.scheme"] = parsedUrl.protocol.split(":").pop(); } if (parsedUrl.host) { attributes["server.address"] = parsedUrl.host; } attributes["url.same_origin"] = resourceUrl.includes(types.WINDOW.location.origin); _setResourceRequestAttributes(entry, attributes, [ // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStatus ["responseStatus", "http.response.status_code"], ["transferSize", "http.response_transfer_size"], ["encodedBodySize", "http.response_content_length"], ["decodedBodySize", "http.decoded_response_content_length"], // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/renderBlockingStatus ["renderBlockingStatus", "resource.render_blocking_status"], // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/deliveryType ["deliveryType", "http.response_delivery_type"] ]); const attributesWithResourceTiming = { ...attributes, ...resourceTiming.resourceTimingToSpanAttributes(entry) }; const startTimestamp = timeOrigin + startTime; const endTimestamp = startTimestamp + duration; utils.startAndEndSpan(span, startTimestamp, endTimestamp, { name: resourceUrl.replace(types.WINDOW.location.origin, ""), op, attributes: attributesWithResourceTiming }); } function _trackNavigator(span, spanStreamingEnabled) { const navigator = types.WINDOW.navigator; if (!navigator) { return; } const connection = navigator.connection; if (connection) { if (connection.effectiveType) { span.setAttribute( spanStreamingEnabled ? "network.connection.effective_type" : "effectiveConnectionType", connection.effectiveType ); } if (connection.type) { span.setAttribute(spanStreamingEnabled ? "network.connection.type" : "connectionType", connection.type); } if (utils.isMeasurementValue(connection.rtt)) { _measurements["connection.rtt"] = { value: connection.rtt, unit: "millisecond" }; if (spanStreamingEnabled) { span.setAttribute("network.connection.rtt", connection.rtt); } } } if (utils.isMeasurementValue(navigator.deviceMemory)) { if (spanStreamingEnabled) { span.setAttribute("device.memory.estimated_capacity", navigator.deviceMemory); } else { span.setAttribute("deviceMemory", `${navigator.deviceMemory} GB`); } } if (utils.isMeasurementValue(navigator.hardwareConcurrency)) { if (spanStreamingEnabled) { span.setAttribute("device.processor_count", navigator.hardwareConcurrency); } else { span.setAttribute("hardwareConcurrency", String(navigator.hardwareConcurrency)); } } } function _setWebVitalAttributes(span, options) { if (_lcpEntry && options.recordLcpOnPageloadSpan) { if (_lcpEntry.element) { span.setAttribute("lcp.element", htmlTreeAsString.htmlTreeAsString(_lcpEntry.element)); } if (_lcpEntry.id) { span.setAttribute("lcp.id", _lcpEntry.id); } if (_lcpEntry.url) { span.setAttribute("lcp.url", _lcpEntry.url.trim().slice(0, 200)); } if (_lcpEntry.loadTime != null) { span.setAttribute("lcp.loadTime", _lcpEntry.loadTime); } if (_lcpEntry.renderTime != null) { span.setAttribute("lcp.renderTime", _lcpEntry.renderTime); } span.setAttribute("lcp.size", _lcpEntry.size); } if (_clsEntry?.sources && options.recordClsOnPageloadSpan) { _clsEntry.sources.forEach( (source, index) => span.setAttribute(`cls.source.${index + 1}`, htmlTreeAsString.htmlTreeAsString(source.node)) ); } } function _setResourceRequestAttributes(entry, attributes, properties) { properties.forEach(([entryKey, attributeKey]) => { const entryVal = entry[entryKey]; if (entryVal != null && (typeof entryVal === "number" && entryVal < MAX_INT_AS_BYTES || typeof entryVal === "string")) { attributes[attributeKey] = entryVal; } }); } function _addTtfbRequestTimeToMeasurements(_measurements2) { const navEntry = getNavigationEntry.getNavigationEntry(false); if (!navEntry) { return; } const { responseStart, requestStart } = navEntry; if (requestStart <= responseStart) { _measurements2["ttfb.requestTime"] = { value: responseStart - requestStart, unit: "millisecond" }; } } exports._addMeasureSpans = _addMeasureSpans; exports._addNavigationSpans = _addNavigationSpans; exports._addResourceSpans = _addResourceSpans; exports._setResourceRequestAttributes = _setResourceRequestAttributes; exports.addPerformanceEntries = addPerformanceEntries; exports.startTrackingInteractions = startTrackingInteractions; exports.startTrackingLongAnimationFrames = startTrackingLongAnimationFrames; exports.startTrackingLongTasks = startTrackingLongTasks; exports.startTrackingWebVitals = startTrackingWebVitals; //# sourceMappingURL=browserMetrics.js.map