UNPKG

@sentry/react-native

Version:
408 lines 18.8 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { debug, fill, getActiveSpan, getSpanDescendants, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, SPAN_STATUS_OK, spanToJSON, startInactiveSpan } from '@sentry/core'; import * as React from 'react'; import { useState } from 'react'; import { NATIVE } from '../wrapper'; import { SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY } from './origin'; import { getRNSentryOnDrawReporter } from './timetodisplaynative'; import { setSpanDurationAsMeasurement, setSpanDurationAsMeasurementOnSpan } from './utils'; /** * Timeout for fetching native frames */ const FETCH_FRAMES_TIMEOUT_MS = 2000; /** * Maximum time to keep frame data in memory before cleaning up. * Prevents memory leaks for spans that never complete. */ const FRAME_DATA_CLEANUP_TIMEOUT_MS = 60000; /** * Flags of active spans with manual initial display. */ export const manualInitialDisplaySpans = new WeakMap(); /** * Flag full display called before initial display for an active span. */ const fullDisplayBeforeInitialDisplay = new WeakMap(); /** * Stores frame data for in-flight TTID/TTFD spans. * Entries are automatically cleaned up when spans end (in captureEndFramesAndAttachToSpan finally block). * As a safety mechanism, entries are also cleaned up after FRAME_DATA_CLEANUP_TIMEOUT_MS * to prevent memory leaks for spans that never complete. */ const spanFrameDataMap = new Map(); /** * Component to measure time to initial display. * * The initial display is recorded when the component prop `record` is true. * * <TimeToInitialDisplay record /> */ export function TimeToInitialDisplay(props) { const activeSpan = getActiveSpan(); if (activeSpan) { manualInitialDisplaySpans.set(activeSpan, true); } const parentSpanId = activeSpan && spanToJSON(activeSpan).span_id; return React.createElement(TimeToDisplay, { initialDisplay: props.record, parentSpanId: parentSpanId }, props.children); } /** * Component to measure time to full display. * * The initial display is recorded when the component prop `record` is true. * * <TimeToInitialDisplay record /> */ export function TimeToFullDisplay(props) { const activeSpan = getActiveSpan(); const parentSpanId = activeSpan && spanToJSON(activeSpan).span_id; return React.createElement(TimeToDisplay, { fullDisplay: props.record, parentSpanId: parentSpanId }, props.children); } function TimeToDisplay(props) { const RNSentryOnDrawReporter = getRNSentryOnDrawReporter(); return (React.createElement(React.Fragment, null, React.createElement(RNSentryOnDrawReporter, { initialDisplay: props.initialDisplay, fullDisplay: props.fullDisplay, parentSpanId: props.parentSpanId }), props.children)); } /** * Starts a new span for the initial display. * * Returns current span if already exists in the currently active span. * * @deprecated Use `<TimeToInitialDisplay record={boolean}/>` component instead. */ export function startTimeToInitialDisplaySpan(options) { const activeSpan = getActiveSpan(); if (!activeSpan) { debug.warn('[TimeToDisplay] No active span found to attach ui.load.initial_display to.'); return undefined; } const existingSpan = getSpanDescendants(activeSpan).find((span) => spanToJSON(span).op === 'ui.load.initial_display'); if (existingSpan) { debug.log('[TimeToDisplay] Found existing ui.load.initial_display span.'); return existingSpan; } const initialDisplaySpan = startInactiveSpan(Object.assign({ op: 'ui.load.initial_display', name: 'Time To Initial Display', startTime: spanToJSON(activeSpan).start_timestamp }, options)); if (!initialDisplaySpan) { return undefined; } captureStartFramesForSpan(initialDisplaySpan.spanContext().spanId).catch((error) => { debug.log(`[TimeToDisplay] Failed to capture start frames for initial display span (${initialDisplaySpan.spanContext().spanId}).`, error); }); if (options === null || options === void 0 ? void 0 : options.isAutoInstrumented) { initialDisplaySpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY); } else { manualInitialDisplaySpans.set(activeSpan, true); initialDisplaySpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY); } return initialDisplaySpan; } /** * Starts a new span for the full display. * * Returns current span if already exists in the currently active span. * * @deprecated Use `<TimeToFullDisplay record={boolean}/>` component instead. */ export function startTimeToFullDisplaySpan(options = { timeoutMs: 30000, }) { const activeSpan = getActiveSpan(); if (!activeSpan) { debug.warn('[TimeToDisplay] No active span found to attach ui.load.full_display to.'); return undefined; } const descendantSpans = getSpanDescendants(activeSpan); const initialDisplaySpan = descendantSpans.find((span) => spanToJSON(span).op === 'ui.load.initial_display'); if (!initialDisplaySpan) { debug.warn('[TimeToDisplay] No initial display span found to attach ui.load.full_display to.'); return undefined; } const existingSpan = descendantSpans.find((span) => spanToJSON(span).op === 'ui.load.full_display'); if (existingSpan) { debug.log('[TimeToDisplay] Found existing ui.load.full_display span.'); return existingSpan; } const fullDisplaySpan = startInactiveSpan(Object.assign({ op: 'ui.load.full_display', name: 'Time To Full Display', startTime: spanToJSON(initialDisplaySpan).start_timestamp }, options)); if (!fullDisplaySpan) { return undefined; } captureStartFramesForSpan(fullDisplaySpan.spanContext().spanId).catch((error) => { debug.log(`[TimeToDisplay] Failed to capture start frames for full display span (${fullDisplaySpan.spanContext().spanId}).`, error); }); const timeout = setTimeout(() => { if (spanToJSON(fullDisplaySpan).timestamp) { return; } fullDisplaySpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }); captureEndFramesAndAttachToSpan(fullDisplaySpan).then(() => { debug.log(`[TimeToDisplay] span ${fullDisplaySpan.spanContext().spanId} updated with frame data.`); fullDisplaySpan.end(spanToJSON(initialDisplaySpan).timestamp); setSpanDurationAsMeasurement('time_to_full_display', fullDisplaySpan); }).catch(() => { debug.warn(`[TimeToDisplay] Failed to capture end frames for full display span (${fullDisplaySpan.spanContext().spanId}).`); fullDisplaySpan.end(spanToJSON(initialDisplaySpan).timestamp); setSpanDurationAsMeasurement('time_to_full_display', fullDisplaySpan); }); debug.warn('[TimeToDisplay] Full display span deadline_exceeded.'); }, options.timeoutMs); fill(fullDisplaySpan, 'end', (originalEnd) => (endTimestamp) => { clearTimeout(timeout); originalEnd.call(fullDisplaySpan, endTimestamp); }); if (options === null || options === void 0 ? void 0 : options.isAutoInstrumented) { fullDisplaySpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY); } else { fullDisplaySpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY); } return fullDisplaySpan; } /** * */ export function updateInitialDisplaySpan(frameTimestampSeconds, { activeSpan = getActiveSpan(), span = startTimeToInitialDisplaySpan(), } = {}) { if (!span) { debug.warn('[TimeToDisplay] No span found or created, possibly performance is disabled.'); return; } if (!activeSpan) { debug.warn('[TimeToDisplay] No active span found to attach ui.load.initial_display to.'); return; } if (spanToJSON(span).parent_span_id !== spanToJSON(activeSpan).span_id) { debug.warn('[TimeToDisplay] Initial display span is not a child of current active span.'); return; } if (spanToJSON(span).timestamp) { debug.warn(`[TimeToDisplay] ${spanToJSON(span).description} span already ended.`); return; } captureEndFramesAndAttachToSpan(span).then(() => { span.end(frameTimestampSeconds); span.setStatus({ code: SPAN_STATUS_OK }); debug.log(`[TimeToDisplay] ${spanToJSON(span).description} span updated with end timestamp and frame data.`); if (fullDisplayBeforeInitialDisplay.has(activeSpan)) { fullDisplayBeforeInitialDisplay.delete(activeSpan); debug.log(`[TimeToDisplay] Updating full display with initial display (${span.spanContext().spanId}) end.`); updateFullDisplaySpan(frameTimestampSeconds, span); } setSpanDurationAsMeasurementOnSpan('time_to_initial_display', span, activeSpan); }).catch((error) => { debug.log('[TimeToDisplay] Failed to capture frame data for initial display span.', error); span.end(frameTimestampSeconds); span.setStatus({ code: SPAN_STATUS_OK }); if (fullDisplayBeforeInitialDisplay.has(activeSpan)) { fullDisplayBeforeInitialDisplay.delete(activeSpan); debug.log(`[TimeToDisplay] Updating full display with initial display (${span.spanContext().spanId}) end.`); updateFullDisplaySpan(frameTimestampSeconds, span); } setSpanDurationAsMeasurementOnSpan('time_to_initial_display', span, activeSpan); }); } function updateFullDisplaySpan(frameTimestampSeconds, passedInitialDisplaySpan) { const activeSpan = getActiveSpan(); if (!activeSpan) { debug.warn('[TimeToDisplay] No active span found to update ui.load.full_display in.'); return; } const existingInitialDisplaySpan = passedInitialDisplaySpan || getSpanDescendants(activeSpan).find((span) => spanToJSON(span).op === 'ui.load.initial_display'); const initialDisplayEndTimestamp = existingInitialDisplaySpan && spanToJSON(existingInitialDisplaySpan).timestamp; if (!initialDisplayEndTimestamp) { fullDisplayBeforeInitialDisplay.set(activeSpan, true); debug.warn(`[TimeToDisplay] Full display called before initial display for active span (${activeSpan.spanContext().spanId}).`); return; } const span = startTimeToFullDisplaySpan({ isAutoInstrumented: true, }); if (!span) { debug.warn('[TimeToDisplay] No TimeToFullDisplay span found or created, possibly performance is disabled.'); return; } const spanJSON = spanToJSON(span); if (spanJSON.timestamp) { debug.warn(`[TimeToDisplay] ${spanJSON.description} (${spanJSON.span_id}) span already ended.`); return; } captureEndFramesAndAttachToSpan(span).then(() => { const endTimestamp = initialDisplayEndTimestamp > frameTimestampSeconds ? initialDisplayEndTimestamp : frameTimestampSeconds; if (initialDisplayEndTimestamp > frameTimestampSeconds) { debug.warn('[TimeToDisplay] Using initial display end. Full display end frame timestamp is before initial display end.'); } span.end(endTimestamp); span.setStatus({ code: SPAN_STATUS_OK }); debug.log(`[TimeToDisplay] span ${spanJSON.description} (${spanJSON.span_id}) updated with end timestamp and frame data.`); setSpanDurationAsMeasurement('time_to_full_display', span); }).catch((error) => { debug.log('[TimeToDisplay] Failed to capture frame data for full display span.', error); const endTimestamp = initialDisplayEndTimestamp > frameTimestampSeconds ? initialDisplayEndTimestamp : frameTimestampSeconds; span.end(endTimestamp); span.setStatus({ code: SPAN_STATUS_OK }); setSpanDurationAsMeasurement('time_to_full_display', span); }); } /** * Creates a new TimeToFullDisplay component which triggers the full display recording every time the component is focused. */ export function createTimeToFullDisplay({ useFocusEffect, }) { return createTimeToDisplay({ useFocusEffect, Component: TimeToFullDisplay }); } /** * Creates a new TimeToInitialDisplay component which triggers the initial display recording every time the component is focused. */ export function createTimeToInitialDisplay({ useFocusEffect, }) { return createTimeToDisplay({ useFocusEffect, Component: TimeToInitialDisplay }); } function createTimeToDisplay({ useFocusEffect, Component, }) { const TimeToDisplayWrapper = (props) => { const [focused, setFocused] = useState(false); useFocusEffect(() => { setFocused(true); return () => { setFocused(false); }; }); return React.createElement(Component, Object.assign({}, props, { record: focused && props.record })); }; TimeToDisplayWrapper.displayName = 'TimeToDisplayWrapper'; return TimeToDisplayWrapper; } /** * Attaches frame data to a span's data object. */ function attachFrameDataToSpan(span, startFrames, endFrames) { const totalFrames = endFrames.totalFrames - startFrames.totalFrames; const slowFrames = endFrames.slowFrames - startFrames.slowFrames; const frozenFrames = endFrames.frozenFrames - startFrames.frozenFrames; if (totalFrames <= 0 && slowFrames <= 0 && frozenFrames <= 0) { debug.warn(`[TimeToDisplay] Detected zero slow or frozen frames. Not adding measurements to span (${span.spanContext().spanId}).`); return; } span.setAttribute('frames.total', totalFrames); span.setAttribute('frames.slow', slowFrames); span.setAttribute('frames.frozen', frozenFrames); debug.log('[TimeToDisplay] Attached frame data to span.', { spanId: span.spanContext().spanId, frameData: { total: totalFrames, slow: slowFrames, frozen: frozenFrames, }, }); } /** * Captures start frames for a time-to-display span */ function captureStartFramesForSpan(spanId) { return __awaiter(this, void 0, void 0, function* () { if (!NATIVE.enableNative) { return; } try { const startFrames = yield fetchNativeFramesWithTimeout(); // Set up automatic cleanup as a safety mechanism for spans that never complete const cleanupTimeout = setTimeout(() => { const entry = spanFrameDataMap.get(spanId); if (entry) { spanFrameDataMap.delete(spanId); debug.log(`[TimeToDisplay] Cleaned up stale frame data for span ${spanId} after timeout.`); } }, FRAME_DATA_CLEANUP_TIMEOUT_MS); if (!spanFrameDataMap.has(spanId)) { spanFrameDataMap.set(spanId, { startFrames: null, endFrames: null, cleanupTimeout }); } // Re-check after async operations - entry might have been deleted by captureEndFramesAndAttachToSpan const frameData = spanFrameDataMap.get(spanId); if (!frameData) { // Span already ended and cleaned up, cancel the cleanup timeout clearTimeout(cleanupTimeout); debug.log(`[TimeToDisplay] Span ${spanId} already ended, discarding start frames.`); return; } frameData.startFrames = startFrames; frameData.cleanupTimeout = cleanupTimeout; debug.log(`[TimeToDisplay] Captured start frames for span ${spanId}.`, startFrames); } catch (error) { debug.log(`[TimeToDisplay] Failed to capture start frames for span ${spanId}.`, error); } }); } /** * Captures end frames and attaches frame data to span */ function captureEndFramesAndAttachToSpan(span) { return __awaiter(this, void 0, void 0, function* () { if (!NATIVE.enableNative) { return; } const spanId = span.spanContext().spanId; const frameData = spanFrameDataMap.get(spanId); if (!(frameData === null || frameData === void 0 ? void 0 : frameData.startFrames)) { debug.log(`[TimeToDisplay] No start frames found for span ${spanId}, skipping frame data collection.`); return; } try { const endFrames = yield fetchNativeFramesWithTimeout(); frameData.endFrames = endFrames; attachFrameDataToSpan(span, frameData.startFrames, endFrames); debug.log(`[TimeToDisplay] Captured and attached end frames for span ${spanId}.`, endFrames); } catch (error) { debug.log(`[TimeToDisplay] Failed to capture end frames for span ${spanId}.`, error); } finally { // Clear the cleanup timeout since we're cleaning up now if (frameData.cleanupTimeout) { clearTimeout(frameData.cleanupTimeout); } spanFrameDataMap.delete(spanId); } }); } /** * Fetches native frames with a timeout */ function fetchNativeFramesWithTimeout() { return new Promise((resolve, reject) => { let settled = false; const timeoutId = setTimeout(() => { if (!settled) { settled = true; reject('Fetching native frames took too long. Dropping frames.'); } }, FETCH_FRAMES_TIMEOUT_MS); NATIVE.fetchNativeFrames() .then(value => { if (settled) { return; } clearTimeout(timeoutId); settled = true; if (!value) { reject('Native frames response is null.'); return; } resolve(value); }) .then(undefined, (error) => { if (settled) { return; } clearTimeout(timeoutId); settled = true; reject(error); }); }); } //# sourceMappingURL=timetodisplay.js.map