UNPKG

@sentry/react-native

Version:
940 lines 51.2 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, getCapturedScopesOnSpan, getClient, getCurrentScope, getSpanDescendants, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, SPAN_STATUS_ERROR, spanIsSampled, spanToJSON, startInactiveSpan, timestampInSeconds, } from '@sentry/core'; import { getAppRegistryIntegration } from '../../integrations/appRegistry'; import { APP_START_COLD as APP_START_COLD_MEASUREMENT, APP_START_WARM as APP_START_WARM_MEASUREMENT, } from '../../measurements'; import { registerFeatureMarker } from '../../utils/featureMarkers'; import { convertSpanToTransaction, isRootSpan, setEndTimeValue } from '../../utils/span'; import { NATIVE } from '../../wrapper'; import { getRootSpanDiscardReason, getTransactionEventDiscardReason } from '../onSpanEndUtils'; import { APP_START as APP_START_OP, APP_START_COLD as APP_START_COLD_OP, APP_START_EXTENDED as APP_START_EXTENDED_OP, APP_START_WARM as APP_START_WARM_OP, UI_LOAD as UI_LOAD_OP, } from '../ops'; import { SPAN_ORIGIN_AUTO_APP_START, SPAN_ORIGIN_MANUAL_APP_START } from '../origin'; import { getCurrentReactNativeTracingIntegration } from '../reactnativetracing'; import { SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN, SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE, SEMANTIC_ATTRIBUTE_APP_VITALS_START_VALUE, SEMANTIC_ATTRIBUTE_SENTRY_OP, } from '../semanticAttributes'; import { setMainThreadInfo } from '../span'; import { createChildSpanJSON, createSpanJSON, getBundleStartTimestampMs, getLatestChildSpanEndTimestamp, } from '../utils'; const INTEGRATION_NAME = 'AppStart'; const APP_LOADED_INTEGRATION_NAME = 'AppLoaded'; const EXTENDED_APP_START_INTEGRATION_NAME = 'ExtendedAppStart'; /** * We filter out app start more than 60s. * This could be due to many different reasons. * We've seen app starts with hours, days and even months. */ const MAX_APP_START_DURATION_MS = 60000; /** We filter out App starts which timestamp is 60s and more before the transaction start */ const MAX_APP_START_AGE_MS = 60000; /** App Start transaction name */ const APP_START_TX_NAME = 'App Start'; /** Extended app start span name */ const EXTENDED_APP_START_SPAN_NAME = 'Extended App Start'; /** * If `finishExtendedAppStart()` is never called, the extended app start auto-finishes after this * deadline. The transaction is still captured, but its `app.vitals.start` measurement is suppressed * so we never emit a ~30s app start. */ const EXTEND_APP_START_DEADLINE_MS = 30000; let appStartEndData = undefined; let isRecordedAppStartEndTimestampMsManual = false; let isAppLoadedManuallyInvoked = false; let rootComponentCreationTimestampMs = undefined; let isRootComponentCreationTimestampMsManual = false; /** * Records the application start end. * Used automatically by `Sentry.wrap` and `Sentry.ReactNativeProfiler`. * * @deprecated Use {@link appLoaded} from the public SDK API instead (`Sentry.appLoaded()`). */ export function captureAppStart() { return _captureAppStart({ isManual: true }); } /** * Signals that the app has finished loading and is ready for user interaction. * Called internally by `appLoaded()` from the public SDK API. * * @private */ export function _appLoaded() { return __awaiter(this, void 0, void 0, function* () { if (isAppLoadedManuallyInvoked) { debug.warn('[AppStart] appLoaded() was already called. Subsequent calls are ignored.'); return; } const client = getClient(); if (!client) { debug.warn('[AppStart] appLoaded() was called before Sentry.init(). App start end will not be recorded.'); return; } registerFeatureMarker(APP_LOADED_INTEGRATION_NAME); isAppLoadedManuallyInvoked = true; const timestampMs = timestampInSeconds() * 1000; // If auto-capture already ran (ReactNativeProfiler.componentDidMount), overwrite the timestamp. // The transaction hasn't been sent yet in non-standalone mode so this is safe. if (appStartEndData) { debug.log('[AppStart] appLoaded() overwriting auto-detected app start end timestamp.'); appStartEndData.timestampMs = timestampMs; appStartEndData.endFrames = null; } else { _setAppStartEndData({ timestampMs, endFrames: null }); } isRecordedAppStartEndTimestampMsManual = true; yield fetchAndUpdateEndFrames(); const integration = client.getIntegrationByName(INTEGRATION_NAME); if (integration) { // appLoaded() overrides the auto-detected end timestamp by cancelling the deferred standalone // send (if it hasn't fired yet) and sending a single transaction with the manual timestamp. // If the deferred send already fired, the standalone capture is already done, so the call // below bails — we keep the auto timestamp rather than emitting a duplicate. integration.cancelDeferredStandaloneCapture(); yield integration.captureStandaloneAppStart(); } }); } /** * For internal use only. * * @private */ export function _captureAppStart(_a) { return __awaiter(this, arguments, void 0, function* ({ isManual }) { // If appLoaded() was already called manually, skip the auto-capture to avoid // overwriting the manual end timestamp (race B: appLoaded before componentDidMount). if (!isManual && isAppLoadedManuallyInvoked) { debug.log('[AppStart] Skipping auto app start capture because appLoaded() was already called.'); return; } const client = getClient(); if (!client) { debug.warn('[AppStart] Could not capture App Start, missing client.'); return; } isRecordedAppStartEndTimestampMsManual = isManual; const timestampMs = timestampInSeconds() * 1000; // Set end timestamp immediately to avoid race with processEvent // Frames data will be updated after the async fetch _setAppStartEndData({ timestampMs, endFrames: null, }); yield fetchAndUpdateEndFrames(); const integration = client.getIntegrationByName(INTEGRATION_NAME); if (integration) { if (!isManual) { // For auto-capture, defer the standalone send to give appLoaded() a chance // to override the end timestamp before the transaction is sent. // If appLoaded() is called, it cancels this deferred send and sends its own. // In non-standalone mode, scheduleDeferredStandaloneCapture is a no-op. integration.scheduleDeferredStandaloneCapture(); } else { yield integration.captureStandaloneAppStart(); } } }); } /** * Extends the app start window. Called internally by `extendAppStart()` from the public SDK API. * * @private */ export function _extendAppStart() { var _a, _b; registerFeatureMarker(EXTENDED_APP_START_INTEGRATION_NAME); (_b = (_a = getClient()) === null || _a === void 0 ? void 0 : _a.getIntegrationByName(INTEGRATION_NAME)) === null || _b === void 0 ? void 0 : _b.extendAppStart(); } /** * Returns the extended app start span (a no-op span when there's no active extension). * Called internally by `getExtendedAppStartSpan()` from the public SDK API. * * @private */ export function _getExtendedAppStartSpan() { var _a, _b, _c; registerFeatureMarker(EXTENDED_APP_START_INTEGRATION_NAME); return ((_c = (_b = (_a = getClient()) === null || _a === void 0 ? void 0 : _a.getIntegrationByName(INTEGRATION_NAME)) === null || _b === void 0 ? void 0 : _b.getExtendedAppStartSpan()) !== null && _c !== void 0 ? _c : new SentryNonRecordingSpan()); } /** * Finishes the extended app start. Called internally by `finishExtendedAppStart()` from the public * SDK API. * * @private */ export function _finishExtendedAppStart() { return __awaiter(this, void 0, void 0, function* () { var _a, _b; registerFeatureMarker(EXTENDED_APP_START_INTEGRATION_NAME); yield ((_b = (_a = getClient()) === null || _a === void 0 ? void 0 : _a.getIntegrationByName(INTEGRATION_NAME)) === null || _b === void 0 ? void 0 : _b.finishExtendedAppStart()); }); } /** * Fetches native frames data and attaches it to the current app start end data. */ function fetchAndUpdateEndFrames() { return __awaiter(this, void 0, void 0, function* () { if (NATIVE.enableNative) { try { const endFrames = yield NATIVE.fetchNativeFrames(); debug.log('[AppStart] Captured end frames for app start.', endFrames); _updateAppStartEndFrames(endFrames); } catch (error) { debug.log('[AppStart] Failed to capture end frames for app start.', error); } } }); } /** * Sets the root component first constructor call timestamp. * Used automatically by `Sentry.wrap` and `Sentry.ReactNativeProfiler`. */ export function setRootComponentCreationTimestampMs(timestampMs) { (appStartEndData === null || appStartEndData === void 0 ? void 0 : appStartEndData.timestampMs) && debug.warn('Setting Root component creation timestamp after app start end is set.'); rootComponentCreationTimestampMs && debug.warn('Overwriting already set root component creation timestamp.'); rootComponentCreationTimestampMs = timestampMs; isRootComponentCreationTimestampMsManual = true; } /** * For internal use only. * * @private */ export function _setRootComponentCreationTimestampMs(timestampMs) { setRootComponentCreationTimestampMs(timestampMs); isRootComponentCreationTimestampMsManual = false; } /** * For internal use only. * * @private */ export const _setAppStartEndData = (data) => { appStartEndData && debug.warn('Overwriting already set app start end data.'); appStartEndData = data; }; /** * Updates only the endFrames on existing appStartEndData. * Used after the async fetchNativeFrames completes to attach frame data * without triggering the overwrite warning from _setAppStartEndData. * * @private */ export const _updateAppStartEndFrames = (endFrames) => { if (appStartEndData) { appStartEndData.endFrames = endFrames; } }; /** * For testing purposes only. * * @private */ export function _clearRootComponentCreationTimestampMs() { rootComponentCreationTimestampMs = undefined; } /** * For testing purposes only. * * @private */ export function _clearAppStartEndData() { appStartEndData = undefined; isRecordedAppStartEndTimestampMsManual = false; isAppLoadedManuallyInvoked = false; } /** * Attaches frame data to a span's data object. */ function attachFrameDataToSpan(span, frames) { if (frames.totalFrames <= 0 && frames.slowFrames <= 0 && frames.frozenFrames <= 0) { debug.warn(`[AppStart] Detected zero slow or frozen frames. Not adding measurements to spanId (${span.span_id}).`); return; } span.data = span.data || {}; span.data['frames.total'] = frames.totalFrames; span.data['frames.slow'] = frames.slowFrames; span.data['frames.frozen'] = frames.frozenFrames; debug.log('[AppStart] Attached frame data to span.', { spanId: span.span_id, frameData: { total: frames.totalFrames, slow: frames.slowFrames, frozen: frames.frozenFrames, }, }); } /** * Adds AppStart spans from the native layer to the transaction event. */ export const appStartIntegration = ({ standalone = false, } = {}) => { let _client = undefined; let isEnabled = true; let appStartDataFlushed = false; let afterAllSetupCalled = false; let firstStartedActiveRootSpanId = undefined; let firstStartedActiveRootSpan = undefined; let cachedNativeAppStart = undefined; let deferredStandaloneTimeout = undefined; // Ensures at most one standalone `app.start` transaction per app run. Set synchronously at the // start of `captureStandaloneAppStart` (before any `await`), so a second trigger for the same run // — a late `appLoaded()`, or one racing the in-flight deferred auto-capture — observes it and // bails. Reset on `runApplication` so the next app run captures again. let standaloneAppStartCaptured = false; // Extend-app-start state (standalone mode). `extendAppStart()` keeps the standalone transaction // open and hosts an `app.start.extended` span for user-instrumented work; `finishExtendedAppStart()` // or the deadline finalizes it. `openStandaloneAppStartSpan` is the held-open root transaction. let extendedAppStartSpan = undefined; let openStandaloneAppStartSpan = undefined; let extendDeadlineTimeout = undefined; let extendedAppStartFinalized = false; // Incremented on every `runApplication` reset. Captured at the start of an app start attach so a // finalize suspended at the native-data await can detect that a new app run began and bail instead // of corrupting the new run's state. let appStartRunGeneration = 0; const setup = (client) => { _client = client; const { enableAppStartTracking } = client.getOptions(); if (!enableAppStartTracking) { isEnabled = false; debug.warn('[AppStart] App start tracking is disabled.'); } client.on('spanStart', recordFirstStartedActiveRootSpanId); }; const afterAllSetup = (client) => { var _a; if (afterAllSetupCalled) { return; } afterAllSetupCalled = true; // TODO: automatically set standalone based on the presence of the native layer navigation integration (_a = getAppRegistryIntegration(client)) === null || _a === void 0 ? void 0 : _a.onRunApplication(() => { // Reset once the current run's app start has begun capturing (flushed, or a standalone capture // started) so a remount / hot reload starts fresh for the new run. The initial runApplication // (root mount) fires before the first capture starts, so both are false then and we correctly // wait. For the non-standalone path `standaloneAppStartCaptured` is always false, so this // reduces to the original `appStartDataFlushed` check. if (appStartDataFlushed || standaloneAppStartCaptured) { debug.log('[AppStartIntegration] Resetting app start state based on runApplication call.'); // Mark a new app run. Any standalone attach still suspended at the native-data await (e.g. an // in-flight `finishExtendedAppStart()` from the previous run) detects the generation change // when it resumes and bails without mutating the new run's state. appStartRunGeneration += 1; appStartDataFlushed = false; firstStartedActiveRootSpanId = undefined; firstStartedActiveRootSpan = undefined; isAppLoadedManuallyInvoked = false; cachedNativeAppStart = undefined; standaloneAppStartCaptured = false; extendedAppStartSpan = undefined; openStandaloneAppStartSpan = undefined; extendedAppStartFinalized = false; if (deferredStandaloneTimeout !== undefined) { clearTimeout(deferredStandaloneTimeout); deferredStandaloneTimeout = undefined; } if (extendDeadlineTimeout !== undefined) { clearTimeout(extendDeadlineTimeout); extendDeadlineTimeout = undefined; } } else { debug.log('[AppStartIntegration] Waiting for initial app start was flush, before updating based on runApplication call.'); } }); }; const processEvent = (event) => __awaiter(void 0, void 0, void 0, function* () { if (!isEnabled || standalone) { return event; } if (event.type !== 'transaction') { // App start data is only relevant for transactions return event; } yield attachAppStartToTransactionEvent(event); return event; }); const recordFirstStartedActiveRootSpanId = (rootSpan) => { if (firstStartedActiveRootSpanId) { // Check if the previously locked span was dropped after it ended (e.g., by // ignoreEmptyRouteChangeTransactions or ignoreEmptyBackNavigation marking // it for discard during spanEnd). If so, reset and allow this new span. // We check here (at the next spanStart) rather than at spanEnd because // the discard listeners run after the app start listener in registration order, // so the discard attribute is not yet set when our own spanEnd listener would fire. if (firstStartedActiveRootSpan && getRootSpanDiscardReason(firstStartedActiveRootSpan) !== undefined) { debug.log('[AppStart] Previously locked root span was marked for discard after ending. Resetting to allow next transaction.'); resetFirstStartedActiveRootSpanId(); // Fall through to lock to this new span } else { return; } } if (!isRootSpan(rootSpan)) { return; } if (!spanIsSampled(rootSpan)) { return; } firstStartedActiveRootSpan = rootSpan; setFirstStartedActiveRootSpanId(rootSpan.spanContext().spanId); }; /** * Resets the first started active root span id and span reference to allow * the next root span's transaction to attempt app start attachment. */ const resetFirstStartedActiveRootSpanId = () => { debug.log('[AppStart] Resetting first started active root span id to allow retry on next transaction.'); firstStartedActiveRootSpanId = undefined; firstStartedActiveRootSpan = undefined; }; /** * For testing purposes only. * @private */ const setFirstStartedActiveRootSpanId = (spanId) => { firstStartedActiveRootSpanId = spanId; debug.log('[AppStart] First started active root span id recorded.', firstStartedActiveRootSpanId); }; function captureStandaloneAppStart() { return __awaiter(this, void 0, void 0, function* () { if (!_client) { // If client is not set, SDK was not initialized, logger is thus disabled // oxlint-disable-next-line eslint(no-console) console.warn('[AppStart] Could not capture App Start, missing client, call `Sentry.init` first.'); return; } if (!standalone) { debug.log('[AppStart] App start tracking is enabled. App start will be added to the first transaction as a child span.'); return; } if (standaloneAppStartCaptured) { // At most one standalone transaction per app run. Set synchronously below, so a second // trigger for the same run — a late appLoaded(), or one racing the in-flight deferred // auto-capture — bails here instead of emitting a duplicate. debug.log('[AppStart] Standalone app start already captured for this app run. Skipping.'); return; } // Claimed synchronously (no await before this point) so a racing trigger observes it. standaloneAppStartCaptured = true; debug.log('[AppStart] App start tracking standalone root span (transaction).'); if (!(appStartEndData === null || appStartEndData === void 0 ? void 0 : appStartEndData.endFrames) && NATIVE.enableNative) { try { const endFrames = yield NATIVE.fetchNativeFrames(); debug.log('[AppStart] Captured end frames for standalone app start.', endFrames); const currentTimestamp = (appStartEndData === null || appStartEndData === void 0 ? void 0 : appStartEndData.timestampMs) || timestampInSeconds() * 1000; _setAppStartEndData({ timestampMs: currentTimestamp, endFrames, }); } catch (error) { debug.log('[AppStart] Failed to capture frames for standalone app start.', error); } } const span = startInactiveSpan({ forceTransaction: true, name: APP_START_TX_NAME, op: APP_START_OP, }); if (span instanceof SentryNonRecordingSpan) { // Tracing is disabled or the transaction was sampled return; } setEndTimeValue(span, timestampInSeconds()); _client.emit('spanEnd', span); const event = convertSpanToTransaction(span); if (!event) { debug.warn('[AppStart] Failed to convert App Start span to transaction.'); return; } // If attachment was skipped (e.g. already flushed, or native data unavailable) there's nothing // to send. App start data is carried as Span V2 attributes on the root transaction, so the // standalone transaction is meaningful even without breakdown child spans. const attached = yield attachAppStartToTransactionEvent(event); if (!attached) { debug.log('[AppStart] No app start data attached to the standalone transaction. Skipping send.'); return; } const scope = getCapturedScopesOnSpan(span).scope || getCurrentScope(); scope.captureEvent(event); }); } function attachAppStartToTransactionEvent(event_1) { return __awaiter(this, arguments, void 0, function* (event, { suppressMeasurement = false, extendedEndTimestampMs, } = {}) { var _a, _b; // Snapshot the current app run. If a `runApplication` reset happens while we're awaiting native // data below, the resumed call belongs to a stale run and must not touch the new run's state. const generationAtStart = appStartRunGeneration; if (appStartDataFlushed) { // App start data is only relevant for the first transaction of the app run debug.log('[AppStart] App start data already flushed. Skipping.'); return false; } // Don't attach (and don't flip the flushed flag) for transactions the // tracing integration is about to drop — e.g. empty back-navigations, // route-change spans that never received route info, or childless idle // spans. Otherwise the next real transaction would be left without app // start data because `appStartDataFlushed` would already be `true`. if (getTransactionEventDiscardReason(event)) { debug.log('[AppStart] Skipping app start attach for transaction marked for discard.'); return false; } if (!((_a = event.contexts) === null || _a === void 0 ? void 0 : _a.trace)) { debug.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.'); return false; } // When standalone is true, we create our own transaction and don't need to verify // it matches the first navigation transaction. When standalone is false, we need to // ensure we're attaching app start to the first transaction (not a later one). if (!standalone) { if (!firstStartedActiveRootSpanId) { debug.warn('[AppStart] No first started active root span id recorded. Can not attach app start.'); return false; } if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) { debug.warn('[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.'); return false; } } // All failure paths below set appStartDataFlushed = true to prevent // wasteful retries — these conditions won't change within the same app start. // // Use cached response if available (e.g. when _appLoaded() re-triggers // standalone capture after auto-capture already fetched from the native layer). // The native layer sets has_fetched = true after the first fetch, so a second // NATIVE.fetchNativeAppStart() call would incorrectly bail out. const isCached = cachedNativeAppStart !== undefined; const appStart = isCached ? cachedNativeAppStart : yield NATIVE.fetchNativeAppStart(); if (generationAtStart !== appStartRunGeneration) { // A `runApplication` reset began a new app run while we awaited native data. Bail without // mutating shared state (`cachedNativeAppStart`, `appStartDataFlushed`) so the new run can // still capture its own app start. debug.log('[AppStart] App run changed during app start attach. Discarding stale attach.'); return false; } cachedNativeAppStart = appStart; if (!appStart) { debug.warn('[AppStart] Failed to retrieve the app start metrics from the native layer.'); appStartDataFlushed = true; return false; } // Skip the has_fetched check when using a cached response — the native layer // sets has_fetched = true after the first fetch, but we intentionally re-use // the data when _appLoaded() overrides the app start end timestamp. if (!isCached && appStart.has_fetched) { debug.warn('[AppStart] Measured app start metrics were already reported from the native layer.'); appStartDataFlushed = true; return false; } const appStartTimestampMs = appStart.app_start_timestamp_ms; if (!appStartTimestampMs) { debug.warn('[AppStart] App start timestamp could not be loaded from the native layer.'); appStartDataFlushed = true; return false; } const originalAppStartEndTimestampMs = (appStartEndData === null || appStartEndData === void 0 ? void 0 : appStartEndData.timestampMs) || getBundleStartTimestampMs(); if (!originalAppStartEndTimestampMs) { debug.warn('[AppStart] Javascript failed to record app start end. `_setAppStartEndData` was not called nor could the bundle start be found.'); appStartDataFlushed = true; return false; } // When finalizing an *extended* app start the end is pushed past the native app start end. The // extended end drives the measurement value and transaction timestamp; the 60s sanity cap below // still applies to the original (native) window, so a legitimately extended start is never // silently dropped just for being long. const appStartEndTimestampMs = extendedEndTimestampMs || originalAppStartEndTimestampMs; // The age check guards against attaching a stale app start to a much-later navigation // transaction. It is meaningless for standalone, where the transaction *is* the app start // and `event.start_timestamp` still reflects the span creation time at this point (it is // corrected to the native app start time further below). Applying it to standalone would // discard valid app starts on slow devices, so skip it there — the duration check below // still filters genuinely bogus (too long) app starts. const isAppStartWithinBounds = !!event.start_timestamp && appStartTimestampMs >= event.start_timestamp * 1000 - MAX_APP_START_AGE_MS; if (!standalone && !__DEV__ && !isAppStartWithinBounds) { debug.warn('[AppStart] App start timestamp is too far in the past to be used for app start span.'); appStartDataFlushed = true; return false; } if (!__DEV__ && originalAppStartEndTimestampMs - appStartTimestampMs >= MAX_APP_START_DURATION_MS) { // Dev builds can have long app start waiting over minute for the first bundle to be produced debug.warn('[AppStart] App start duration is over a minute long, not adding app start span.'); appStartDataFlushed = true; return false; } const appStartDurationMs = appStartEndTimestampMs - appStartTimestampMs; if (appStartDurationMs < 0) { // This can happen when MainActivity on Android is recreated, // and the app start end timestamp is not updated, for example // due to missing `Sentry.wrap(RootComponent)` call. debug.warn('[AppStart] Last recorded app start end timestamp is before the app start timestamp.', 'This is usually caused by missing `Sentry.wrap(RootComponent)` call.'); appStartDataFlushed = true; return false; } appStartDataFlushed = true; const origin = isRecordedAppStartEndTimestampMsManual ? SPAN_ORIGIN_MANUAL_APP_START : SPAN_ORIGIN_AUTO_APP_START; // Standalone uses the Span V2 `app.start` op; non-standalone keeps the legacy `ui.load` op // on the carrier navigation transaction. const traceOp = standalone ? APP_START_OP : UI_LOAD_OP; event.contexts.trace.data = event.contexts.trace.data || {}; event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = traceOp; event.contexts.trace.op = traceOp; event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = origin; event.contexts.trace.origin = origin; const appStartTimestampSeconds = appStartTimestampMs / 1000; const appStartEndTimestampSeconds = appStartEndTimestampMs / 1000; event.start_timestamp = appStartTimestampSeconds; event.spans = event.spans || []; /** event.spans reference */ const children = event.spans; // Re-anchor the screen-load display spans to the process/app start time. These are only // present on the non-standalone `ui.load` transaction; a no-op for the standalone `app.start`. const maybeTtidSpan = children.find(({ op }) => op === 'ui.load.initial_display'); if (maybeTtidSpan) { maybeTtidSpan.start_timestamp = appStartTimestampSeconds; setSpanDurationAsMeasurementOnTransactionEvent(event, 'time_to_initial_display', maybeTtidSpan); } const maybeTtfdSpan = children.find(({ op }) => op === 'ui.load.full_display'); if (maybeTtfdSpan) { maybeTtfdSpan.start_timestamp = appStartTimestampSeconds; setSpanDurationAsMeasurementOnTransactionEvent(event, 'time_to_full_display', maybeTtfdSpan); } // Non-standalone only: extend the carrier transaction to cover the app start window if it ended // earlier. Standalone sets its end timestamp explicitly below. if (!standalone && event.timestamp && event.timestamp < appStartEndTimestampSeconds) { debug.log('[AppStart] Transaction event timestamp is before app start end. Adjusting transaction event timestamp.'); event.timestamp = appStartEndTimestampSeconds; } // Parent of the app start breakdown spans (JS bundle execution, native init): // - Standalone (Span V2): the root `app.start` transaction itself, carrying the app start // vitals as attributes. No legacy per-type span or `app_start_*` measurement is emitted — // Relay backfills the V1 encoding from these attributes. // - Non-standalone (legacy V1): a dedicated `app.start.cold`/`app.start.warm` child span plus // the `app_start_*` measurement, attached to the `ui.load` navigation transaction. let breakdownParent; if (standalone) { // Bound the standalone transaction exactly to the app start window. event.timestamp = appStartEndTimestampSeconds; // The measurement is suppressed on the extended-app-start deadline path so we never emit a // bogus ~30s app start; the transaction itself is still captured. if (!suppressMeasurement) { event.contexts.trace.data[SEMANTIC_ATTRIBUTE_APP_VITALS_START_VALUE] = appStartDurationMs; event.contexts.trace.data[SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE] = appStart.type; // Screen shown when app start completes. Unlike the non-standalone `ui.load` transaction // (whose name is the screen, which Relay backfills from), the standalone transaction is named // `App Start`, so we set the screen explicitly. Sourced from the current route tracked by the // tracing integration; omitted when no route has been registered yet at capture time. const screen = (_b = getCurrentReactNativeTracingIntegration()) === null || _b === void 0 ? void 0 : _b.state.currentRoute; if (screen) { event.contexts.trace.data[SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN] = screen; } } // Minimal parent referencing the root transaction span, so the breakdown spans attach // directly under it (the helpers only read op/origin/span_id/trace_id/start_timestamp). // `data` is shared with the root trace context so frame data lands on the root span. breakdownParent = { op: traceOp, origin, span_id: event.contexts.trace.span_id, trace_id: event.contexts.trace.trace_id, start_timestamp: appStartTimestampSeconds, data: event.contexts.trace.data, }; } else { breakdownParent = createSpanJSON({ op: appStart.type === 'cold' ? APP_START_COLD_OP : APP_START_WARM_OP, description: appStart.type === 'cold' ? 'Cold Start' : 'Warm Start', start_timestamp: appStartTimestampSeconds, timestamp: appStartEndTimestampSeconds, trace_id: event.contexts.trace.trace_id, parent_span_id: event.contexts.trace.span_id, origin, }); } if (appStartEndData === null || appStartEndData === void 0 ? void 0 : appStartEndData.endFrames) { attachFrameDataToSpan(breakdownParent, appStartEndData.endFrames); try { const framesDelay = yield Promise.race([ NATIVE.fetchNativeFramesDelay(appStartTimestampSeconds, appStartEndTimestampSeconds), new Promise(resolve => setTimeout(() => resolve(null), 2000)), ]); if (framesDelay != null) { breakdownParent.data = breakdownParent.data || {}; breakdownParent.data['frames.delay'] = framesDelay; } } catch (error) { debug.log('[AppStart] Error while fetching frames delay for app start span.', error); } } const jsExecutionSpanJSON = createJSExecutionStartSpan(breakdownParent, rootComponentCreationTimestampMs); const appStartSpans = [ // In standalone mode the parent IS the root transaction, so it is not pushed as a child // span; only its breakdown children are added. ...(standalone ? [] : [breakdownParent]), ...(jsExecutionSpanJSON ? [jsExecutionSpanJSON] : []), ...convertNativeSpansToSpanJSON(breakdownParent, appStart.spans), ]; children.push(...appStartSpans); debug.log('[AppStart] Added app start spans to transaction event.', JSON.stringify(appStartSpans, undefined, 2)); if (!standalone && !suppressMeasurement) { const measurementKey = appStart.type === 'cold' ? APP_START_COLD_MEASUREMENT : APP_START_WARM_MEASUREMENT; const measurementValue = { value: appStartDurationMs, unit: 'millisecond', }; event.measurements = event.measurements || {}; event.measurements[measurementKey] = measurementValue; debug.log('[AppStart] Added app start measurement to transaction event.', JSON.stringify(measurementValue, undefined, 2)); } // Re-check after the frames-delay await above (up to ~2s): if a new app run started in the // meantime, report failure so the caller doesn't send a stale standalone transaction. if (generationAtStart !== appStartRunGeneration) { debug.log('[AppStart] App run changed during app start attach. Discarding stale attach.'); return false; } return true; }); } /** * Ends any still-open descendant spans of the extended app start span with the given status. * Used when finalizing: open children are `cancelled` on an explicit finish, `deadline_exceeded` * when the deadline fires. */ const finishOpenExtendedChildren = (statusMessage) => { if (!extendedAppStartSpan) { return; } for (const child of getSpanDescendants(extendedAppStartSpan)) { if (child === extendedAppStartSpan || spanToJSON(child).timestamp !== undefined) { continue; // the extended span itself, or an already-finished child } child.setStatus({ code: SPAN_STATUS_ERROR, message: statusMessage }); child.end(); } }; /** * Finalizes a held-open standalone `app.start` transaction (used by the extend flow): sets its * end to the already-computed `trimmedEndMs`, enriches it with app start data, and sends it. When * `suppressMeasurement` is set (deadline path) the `app.vitals.start` attributes are removed so we * never emit a bogus ~30s app start, while the transaction itself is still captured. */ function finalizeStandaloneAppStart(span_1) { return __awaiter(this, arguments, void 0, function* (span, { suppressMeasurement = false, trimmedEndMs = 0 } = {}) { if (!_client) { return; } setEndTimeValue(span, (trimmedEndMs || timestampInSeconds() * 1000) / 1000); _client.emit('spanEnd', span); const event = convertSpanToTransaction(span); if (!event) { debug.warn('[AppStart] Failed to convert extended App Start span to transaction.'); return; } const attached = yield attachAppStartToTransactionEvent(event, { suppressMeasurement, extendedEndTimestampMs: trimmedEndMs || undefined, }); if (!attached) { debug.log('[AppStart] No app start data attached to the extended standalone transaction. Skipping send.'); return; } const scope = getCapturedScopesOnSpan(span).scope || getCurrentScope(); scope.captureEvent(event); }); } const finalizeExtendedAppStart = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* ({ deadlineExceeded = false, } = {}) { if (!extendedAppStartSpan || !openStandaloneAppStartSpan || extendedAppStartFinalized) { return; } extendedAppStartFinalized = true; if (extendDeadlineTimeout !== undefined) { clearTimeout(extendDeadlineTimeout); extendDeadlineTimeout = undefined; } finishOpenExtendedChildren(deadlineExceeded ? 'deadline_exceeded' : 'cancelled'); let extendedEndSeconds; if (deadlineExceeded) { // Deadline: keep the full window up to the deadline (diagnostic — the measurement is // suppressed) by ending the wrapper span at the current time. extendedAppStartSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }); extendedAppStartSpan.end(); extendedEndSeconds = spanToJSON(extendedAppStartSpan).timestamp; } else { // Explicit finish: trim the end to the latest finished child of the extended span (the user's // instrumented work), floored at the default app start end so extending never shortens a // normal app start, and at the wrapper's own start so its duration stays non-negative. The // wrapper span itself is excluded — it is computed before ending the wrapper, so its // finalization-time end doesn't pin the measurement to the `finishExtendedAppStart()` call. const defaultEndMs = (appStartEndData === null || appStartEndData === void 0 ? void 0 : appStartEndData.timestampMs) || getBundleStartTimestampMs() || 0; const extendedStartMs = (spanToJSON(extendedAppStartSpan).start_timestamp || 0) * 1000; const latestChildEndSeconds = getLatestChildSpanEndTimestamp(extendedAppStartSpan); const trimmedEndMs = Math.max(latestChildEndSeconds ? latestChildEndSeconds * 1000 : 0, defaultEndMs, extendedStartMs); extendedEndSeconds = trimmedEndMs ? trimmedEndMs / 1000 : timestampInSeconds(); extendedAppStartSpan.end(extendedEndSeconds); } const spanToFinalize = openStandaloneAppStartSpan; extendedAppStartSpan = undefined; openStandaloneAppStartSpan = undefined; yield finalizeStandaloneAppStart(spanToFinalize, { suppressMeasurement: deadlineExceeded, trimmedEndMs: extendedEndSeconds ? extendedEndSeconds * 1000 : 0, }); }); const extendAppStart = () => { if (!_client) { // oxlint-disable-next-line eslint(no-console) console.warn('[AppStart] Could not extend App Start, missing client, call `Sentry.init` first.'); return; } if (!standalone) { debug.warn('[AppStart] extendAppStart() requires standalone app start tracing. Ignoring.'); return; } if (extendedAppStartSpan) { debug.log('[AppStart] extendAppStart() already called for this app run. Ignoring.'); return; } if (standaloneAppStartCaptured) { debug.warn('[AppStart] extendAppStart() called after the app start transaction was created. Ignoring.'); return; } const rootSpan = startInactiveSpan({ forceTransaction: true, name: APP_START_TX_NAME, op: APP_START_OP, }); if (rootSpan instanceof SentryNonRecordingSpan) { // Tracing is disabled or this app start was sampled out. Don't claim the run and leave the // deferred auto-capture in place as a fallback — extending is a no-op here. debug.log('[AppStart] extendAppStart(): standalone app start transaction is not recording.'); return; } // Take over the send: cancel the deferred auto-capture and claim the run so the normal capture // path does not also finalize/send. Only after we have a recording span, so a non-recording // (sampled-out) run still falls back to the normal capture path. cancelDeferredStandaloneCapture(); standaloneAppStartCaptured = true; openStandaloneAppStartSpan = rootSpan; extendedAppStartSpan = startInactiveSpan({ parentSpan: rootSpan, op: APP_START_EXTENDED_OP, name: EXTENDED_APP_START_SPAN_NAME, }); extendDeadlineTimeout = setTimeout(() => { extendDeadlineTimeout = undefined; debug.warn('[AppStart] Extended app start deadline reached. Finalizing without a measurement.'); // oxlint-disable-next-line typescript-eslint(no-floating-promises) finalizeExtendedAppStart({ deadlineExceeded: true }); }, EXTEND_APP_START_DEADLINE_MS); }; const getExtendedAppStartSpan = () => { return extendedAppStartSpan || new SentryNonRecordingSpan(); }; const finishExtendedAppStart = () => __awaiter(void 0, void 0, void 0, function* () { if (!extendedAppStartSpan || extendedAppStartFinalized) { debug.log('[AppStart] finishExtendedAppStart(): no extended app start in progress. Ignoring.'); return; } yield finalizeExtendedAppStart({ deadlineExceeded: false }); }); const cancelDeferredStandaloneCapture = () => { if (deferredStandaloneTimeout !== undefined) { clearTimeout(deferredStandaloneTimeout); deferredStandaloneTimeout = undefined; debug.log('[AppStart] Cancelled deferred standalone app start capture.'); } }; const scheduleDeferredStandaloneCapture = () => { if (!standalone) { return; } deferredStandaloneTimeout = setTimeout(() => { deferredStandaloneTimeout = undefined; // oxlint-disable-next-line typescript-eslint(no-floating-promises) captureStandaloneAppStart(); }, 0); }; return { name: INTEGRATION_NAME, setup, afterAllSetup, processEvent, captureStandaloneAppStart, cancelDeferredStandaloneCapture, scheduleDeferredStandaloneCapture, extendAppStart, getExtendedAppStartSpan, finishExtendedAppStart, setFirstStartedActiveRootSpanId, }; }; function setSpanDurationAsMeasurementOnTransactionEvent(event, label, span) { if (!span.timestamp || !span.start_timestamp) { debug.warn('Span is missing start or end timestamp. Cam not set measurement on transaction event.'); return; } event.measurements = event.measurements || {}; event.measurements[label] = { value: (span.timestamp - span.start_timestamp) * 1000, unit: 'millisecond', }; } /** * Adds JS Execution before React Root. If `Sentry.wrap` is not used, create a span for the start of JS Bundle execution. */ function createJSExecutionStartSpan(parentSpan, rootComponentCreationTimestampMs) { const bundleStartTimestampMs = getBundleStartTimestampMs(); if (!bundleStartTimestampMs) { return undefined; } const bundleStartTimestampSeconds = bundleStartTimestampMs / 1000; if (bundleStartTimestampSeconds < parentSpan.start_timestamp) { debug.warn('Bundle start timestamp is before the app start span start timestamp. Skipping JS execution span.'); return undefined; } if (!rootComponentCreationTimestampMs) { debug.warn('Missing the root component first constructor call timestamp.'); return createChildSpanJSON(parentSpan, { description: 'JS Bundle Execution Start', start_timestamp: bundleStartTimestampSeconds, timestamp: bundleStartTimestampSeconds, origin: SPAN_ORIGIN_AUTO_APP_START, }); } return createChildSpanJSON(parentSpan, { description: 'JS Bundle Execution Before React Root', start_timestamp: bundleStartTimestampSeconds, timestamp: rootComponentCreationTimestampMs / 1000, origin: isRootComponentCreationTimestampMsManual ? SPAN_ORIGIN_MANUAL_APP_START : SPAN_ORIGIN_AUTO_APP_START, }); } /** * Adds native spans to the app start span. */ function convertNativeSpansToSpanJSON(parentSpan, nativeSpans) { return nativeSpans .filter(span => span.start_timestamp_ms / 1000 >= parentSpan.start_timestamp) .map(span => { if (span.description === 'UIKit init') { return setMainThreadInfo(createUIKitSpan(parentSpan, span)); } return setMainThreadInfo(createChildSpanJSON(parentSpan, { description: span.description, start_timestamp: span.start_timestamp_ms / 1000, timestamp: span.end_timestamp_ms / 1000, origin: SPAN_ORIGIN_AUTO_APP_START, })); }); } /** * UIKit init is m