UNPKG

@sentry/react-native

Version:
365 lines 17.1 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 { getCapturedScopesOnSpan, getClient, getCurrentScope, logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, startInactiveSpan, timestampInSeconds, } from '@sentry/core'; import { APP_START_COLD as APP_START_COLD_MEASUREMENT, APP_START_WARM as APP_START_WARM_MEASUREMENT, } from '../../measurements'; import { convertSpanToTransaction, isRootSpan, setEndTimeValue } from '../../utils/span'; import { NATIVE } from '../../wrapper'; import { APP_START_COLD as APP_START_COLD_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 { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '../semanticAttributes'; import { setMainThreadInfo } from '../span'; import { createChildSpanJSON, createSpanJSON, getBundleStartTimestampMs } from '../utils'; const INTEGRATION_NAME = 'AppStart'; /** * 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'; let recordedAppStartEndTimestampMs = undefined; let isRecordedAppStartEndTimestampMsManual = false; let rootComponentCreationTimestampMs = undefined; let isRootComponentCreationTimestampMsManual = false; /** * Records the application start end. * Used automatically by `Sentry.wrap` and `Sentry.ReactNativeProfiler`. */ export function captureAppStart() { return _captureAppStart({ isManual: true }); } /** * For internal use only. * * @private */ export function _captureAppStart({ isManual }) { var _a; return __awaiter(this, void 0, void 0, function* () { const client = getClient(); if (!client) { logger.warn('[AppStart] Could not capture App Start, missing client.'); return; } isRecordedAppStartEndTimestampMsManual = isManual; _setAppStartEndTimestampMs(timestampInSeconds() * 1000); yield ((_a = client.getIntegrationByName(INTEGRATION_NAME)) === null || _a === void 0 ? void 0 : _a.captureStandaloneAppStart()); }); } /** * Sets the root component first constructor call timestamp. * Used automatically by `Sentry.wrap` and `Sentry.ReactNativeProfiler`. */ export function setRootComponentCreationTimestampMs(timestampMs) { recordedAppStartEndTimestampMs && logger.warn('Setting Root component creation timestamp after app start end is set.'); rootComponentCreationTimestampMs && logger.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 _setAppStartEndTimestampMs = (timestampMs) => { recordedAppStartEndTimestampMs && logger.warn('Overwriting already set app start.'); recordedAppStartEndTimestampMs = timestampMs; }; /** * For testing purposes only. * * @private */ export function _clearRootComponentCreationTimestampMs() { rootComponentCreationTimestampMs = undefined; } /** * 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 firstStartedActiveRootSpanId = undefined; const setup = (client) => { _client = client; const { enableAppStartTracking } = client.getOptions(); if (!enableAppStartTracking) { isEnabled = false; logger.warn('[AppStart] App start tracking is disabled.'); } client.on('spanStart', recordFirstStartedActiveRootSpanId); }; const afterAllSetup = (_client) => { // TODO: automatically set standalone based on the presence of the native layer navigation integration }; 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) { return; } if (!isRootSpan(rootSpan)) { return; } setFirstStartedActiveRootSpanId(rootSpan.spanContext().spanId); }; /** * For testing purposes only. * @private */ const setFirstStartedActiveRootSpanId = (spanId) => { firstStartedActiveRootSpanId = spanId; logger.debug('[AppStart] First started active root span id recorded.', firstStartedActiveRootSpanId); }; function captureStandaloneAppStart() { return __awaiter(this, void 0, void 0, function* () { if (!standalone) { logger.debug('[AppStart] App start tracking is enabled. App start will be added to the first transaction as a child span.'); return; } logger.debug('[AppStart] App start tracking standalone root span (transaction).'); const span = startInactiveSpan({ forceTransaction: true, name: APP_START_TX_NAME, op: UI_LOAD_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) { logger.warn('[AppStart] Failed to convert App Start span to transaction.'); return; } yield attachAppStartToTransactionEvent(event); if (!event.spans || event.spans.length === 0) { // No spans were added to the transaction, so we don't need to send it return; } const scope = getCapturedScopesOnSpan(span).scope || getCurrentScope(); scope.captureEvent(event); }); } function attachAppStartToTransactionEvent(event) { return __awaiter(this, void 0, void 0, function* () { if (appStartDataFlushed) { // App start data is only relevant for the first transaction return; } if (!firstStartedActiveRootSpanId) { logger.warn('[AppStart] No first started active root span id recorded. Can not attach app start.'); return; } if (!event.contexts || !event.contexts.trace) { logger.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.'); return; } if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) { logger.warn('[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.'); return; } const appStart = yield NATIVE.fetchNativeAppStart(); if (!appStart) { logger.warn('[AppStart] Failed to retrieve the app start metrics from the native layer.'); return; } if (appStart.has_fetched) { logger.warn('[AppStart] Measured app start metrics were already reported from the native layer.'); return; } const appStartTimestampMs = appStart.app_start_timestamp_ms; if (!appStartTimestampMs) { logger.warn('[AppStart] App start timestamp could not be loaded from the native layer.'); return; } const appStartEndTimestampMs = recordedAppStartEndTimestampMs || getBundleStartTimestampMs(); if (!appStartEndTimestampMs) { logger.warn('[AppStart] Javascript failed to record app start end. `setAppStartEndTimestampMs` was not called nor could the bundle start be found.'); return; } const isAppStartWithinBounds = !!event.start_timestamp && appStartTimestampMs >= event.start_timestamp - MAX_APP_START_AGE_MS; if (!__DEV__ && !isAppStartWithinBounds) { logger.warn('[AppStart] App start timestamp is too far in the past to be used for app start span.'); return; } const appStartDurationMs = appStartEndTimestampMs - appStartTimestampMs; if (!__DEV__ && appStartDurationMs >= MAX_APP_START_DURATION_MS) { // Dev builds can have long app start waiting over minute for the first bundle to be produced logger.warn('[AppStart] App start duration is over a minute long, not adding app start span.'); return; } appStartDataFlushed = true; event.contexts.trace.data = event.contexts.trace.data || {}; event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = UI_LOAD_OP; event.contexts.trace.op = UI_LOAD_OP; const origin = isRecordedAppStartEndTimestampMsManual ? SPAN_ORIGIN_MANUAL_APP_START : SPAN_ORIGIN_AUTO_APP_START; event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = origin; event.contexts.trace.origin = origin; const appStartTimestampSeconds = appStartTimestampMs / 1000; event.start_timestamp = appStartTimestampSeconds; event.spans = event.spans || []; /** event.spans reference */ const children = event.spans; 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); } const appStartEndTimestampSeconds = appStartEndTimestampMs / 1000; if (event.timestamp && event.timestamp < appStartEndTimestampSeconds) { logger.debug('[AppStart] Transaction event timestamp is before app start end. Adjusting transaction event timestamp.'); event.timestamp = appStartEndTimestampSeconds; } const op = appStart.type === 'cold' ? APP_START_COLD_OP : APP_START_WARM_OP; const appStartSpanJSON = createSpanJSON({ op, description: appStart.type === 'cold' ? 'Cold App Start' : 'Warm App Start', start_timestamp: appStartTimestampSeconds, timestamp: appStartEndTimestampSeconds, trace_id: event.contexts.trace.trace_id, parent_span_id: event.contexts.trace.span_id, origin, }); const jsExecutionSpanJSON = createJSExecutionStartSpan(appStartSpanJSON, rootComponentCreationTimestampMs); const appStartSpans = [ appStartSpanJSON, ...(jsExecutionSpanJSON ? [jsExecutionSpanJSON] : []), ...convertNativeSpansToSpanJSON(appStartSpanJSON, appStart.spans), ]; children.push(...appStartSpans); logger.debug('[AppStart] Added app start spans to transaction event.', JSON.stringify(appStartSpans, undefined, 2)); 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; logger.debug(`[AppStart] Added app start measurement to transaction event.`, JSON.stringify(measurementValue, undefined, 2)); }); } return { name: INTEGRATION_NAME, setup, afterAllSetup, processEvent, captureStandaloneAppStart, setFirstStartedActiveRootSpanId, }; }; function setSpanDurationAsMeasurementOnTransactionEvent(event, label, span) { if (!span.timestamp || !span.start_timestamp) { logger.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; } if (!rootComponentCreationTimestampMs) { logger.warn('Missing the root component first constructor call timestamp.'); return createChildSpanJSON(parentSpan, { description: 'JS Bundle Execution Start', start_timestamp: bundleStartTimestampMs / 1000, timestamp: bundleStartTimestampMs / 1000, origin: SPAN_ORIGIN_AUTO_APP_START, }); } return createChildSpanJSON(parentSpan, { description: 'JS Bundle Execution Before React Root', start_timestamp: bundleStartTimestampMs / 1000, 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.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 measured by the native layers till the native SDK start * RN initializes the native SDK later, the end timestamp would be wrong */ function createUIKitSpan(parentSpan, nativeUIKitSpan) { const bundleStart = getBundleStartTimestampMs(); // If UIKit init ends after the bundle start, the native SDK was auto-initialized // and so the end timestamp is incorrect. // The timestamps can't equal, as RN initializes after UIKit. if (bundleStart && bundleStart < nativeUIKitSpan.end_timestamp_ms) { return createChildSpanJSON(parentSpan, { description: 'UIKit Init to JS Exec Start', start_timestamp: nativeUIKitSpan.start_timestamp_ms / 1000, timestamp: bundleStart / 1000, origin: SPAN_ORIGIN_AUTO_APP_START, }); } else { return createChildSpanJSON(parentSpan, { description: 'UIKit Init', start_timestamp: nativeUIKitSpan.start_timestamp_ms / 1000, timestamp: nativeUIKitSpan.end_timestamp_ms / 1000, origin: SPAN_ORIGIN_AUTO_APP_START, }); } } //# sourceMappingURL=appStart.js.map