UNPKG

@sentry/react-native

Version:
631 lines 34.1 kB
import { addBreadcrumb, debug, getClient, isPlainObject, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_OK, spanToJSON, startInactiveSpan, timestampInSeconds, } from '@sentry/core'; import { getAppRegistryIntegration } from '../integrations/appRegistry'; import { sanitizeDeepLinkUrl } from '../integrations/deeplink'; import { isSentrySpan } from '../utils/span'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; import { NATIVE } from '../wrapper'; import { ignoreEmptyBackNavigation, ignoreEmptyRouteChangeTransactions, markRootSpanForDiscard, } from './onSpanEndUtils'; import { SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION } from './origin'; import { consumePendingDeepLink, nextEventSeq, peekPendingDeepLink, setPendingDeepLinkListener, } from './pendingDeepLink'; import { consumePendingExpoRouterNavigation } from './pendingExpoRouterNavigation'; import { getReactNativeTracingIntegration } from './reactnativetracing'; import { SEMANTIC_ATTRIBUTE_NAVIGATION_ACTION_TYPE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from './semanticAttributes'; import { DEFAULT_NAVIGATION_SPAN_NAME, defaultIdleOptions, getDefaultIdleNavigationSpanOptions, startIdleNavigationSpan as startGenericIdleNavigationSpan, } from './span'; import { addTimeToInitialDisplayFallback } from './timeToDisplayFallback'; export const INTEGRATION_NAME = 'ReactNavigation'; const NAVIGATION_HISTORY_MAX_SIZE = 200; /** * Extracts dynamic route parameters from a route name and its params. * Matches Expo Router style dynamic segments like `[id]` and `[...slug]`. * * Only params whose keys appear as dynamic segments in the route name are returned, * filtering out non-structural params (query params, etc.) that may contain PII. * * Note: dynamic segment values (e.g. the `123` in `profile/[id]`) may be user-identifiable. * This function only extracts params — callers are responsible for checking `sendDefaultPii` * before including the result in span attributes. * * Previous route params are intentionally not captured — only the current route's * structural params are needed for trace attribution. */ export function extractDynamicRouteParams(routeName, params) { if (!params) { return undefined; } const dynamicKeys = new Set(); const pattern = /\[(?:\.\.\.)?(\w+)\]/g; let match; while ((match = pattern.exec(routeName)) !== null) { if (match[1]) { dynamicKeys.add(match[1]); } } if (dynamicKeys.size === 0) { return undefined; } const result = {}; for (const key of dynamicKeys) { if (key in params) { const value = params[key]; result[`route.params.${key}`] = Array.isArray(value) ? value.join('/') : String(value !== null && value !== void 0 ? value : ''); } } return Object.keys(result).length > 0 ? result : undefined; } /** * Builds a full path from the navigation state by traversing nested navigators. * For example, with nested navigators: "Home/Settings/Profile" */ function getPathFromState(state) { var _a; if (!state) { return undefined; } const routeNames = []; let currentState = state; while (currentState) { const index = (_a = currentState.index) !== null && _a !== void 0 ? _a : 0; const route = currentState.routes[index]; if (route === null || route === void 0 ? void 0 : route.name) { routeNames.push(route.name); } currentState = route === null || route === void 0 ? void 0 : route.state; } return routeNames.length > 0 ? routeNames.join('/') : undefined; } /** * Instrumentation for React-Navigation V5 and above. See docs or sample app for usage. * * How this works: * - `_onDispatch` is called every time a dispatch happens and sets an IdleTransaction on the scope without any route context. * - `_onStateChange` is then called AFTER the state change happens due to a dispatch and sets the route context onto the active transaction. * - If `_onStateChange` isn't called within `STATE_CHANGE_TIMEOUT_DURATION` of the dispatch, then the transaction is not sampled and finished. */ export const reactNavigationIntegration = ({ routeChangeTimeoutMs = 1000, enableTimeToInitialDisplay = false, ignoreEmptyBackNavigationTransactions = true, enableTimeToInitialDisplayForPreloadedRoutes = false, useDispatchedActionData = false, useFullPathsForNavigationRoutes = false, enablePrefetchTracking = false, } = {}) => { let navigationContainer; let routeOverrideProvider; let tracing; let idleSpanOptions = defaultIdleOptions; let latestRoute; let latestNavigationSpan; let latestNavigationSpanNameCustomized = false; let navigationProcessingSpan; /** * The first nav span that successfully completed a state change — i.e. the * span that mounted the app's initial route. Used (and only used) by the * deep-link listener as a retroactive attribution target for `cold-start` * links that arrive after that state change but before the span's idle * timeout fires. This is the Expo-Router-auto-handled-cold-start case. * * Never updated after the first successful state change — a `cold-start` * link must never retroactively tag a span the user navigated to later. * `warm-open` links bypass this entirely and always wait in the pending * slot for the next dispatched navigation. */ let initialFinalizedNavSpan; /** * Monotonic dispatch sequence per span, drawn from the shared `nextEventSeq` * counter. Used to reject warm-open links that arrived *before* a dispatch — * such links cannot have caused that dispatch, so the span must not be * tagged with them. */ const spanDispatchSeq = new WeakMap(); /** * Attempts to attach the pending deep link to the given span. * * Warm-open links only attach to spans dispatched *after* the link was * received. This prevents an unrelated, already-in-flight navigation from * being tagged when a deep link arrives mid-dispatch but is actually the * trigger of a subsequent navigation. * * Cold-start links attach unconditionally — retroactive attribution to the * initial nav span is the whole point of that source. * * Rejected warm-open links are left in the slot to be picked up by the next * eligible span. */ const applyPendingDeepLinkToSpan = (span, maxAgeMs) => { const pending = peekPendingDeepLink(maxAgeMs); if (!pending) { return; } if (pending.source === 'warm-open') { const spanSeq = spanDispatchSeq.get(span); if (spanSeq === undefined || spanSeq <= pending.seq) { // Span was dispatched before (or at the same tick as) the link arrived // — it cannot be the navigation the link triggered. Leave the link in // the slot for the next eligible span. return; } } consumePendingDeepLink(maxAgeMs); tagSpanWithDeepLink(span, pending); }; /** * Synchronous listener invoked the moment a deep link is recorded. Returns * `true` only when the link was actually attached to a span — in that case * the pendingDeepLink module skips storing the value. Returns `false` to let * the link fall through to the pending slot for the next dispatched nav. * * Only `cold-start` links may retroactively tag an existing span. The * realistic warm-open flow is "`'url'` event → user handler synchronously * calls `navigation.navigate`": at listener invocation time no link-driven * dispatch has happened yet, so any span we could reach belongs to an * unrelated, prior navigation. */ const handleLateDeepLink = (link) => { if (link.source !== 'cold-start') { return false; } // Prefer an in-flight span (dispatch happened, state change pending). if (latestNavigationSpan && isSpanRecording(latestNavigationSpan)) { return tagSpanWithDeepLink(latestNavigationSpan, link); } // Fallback: the initial nav span may have already mounted its route but // still be recording within its idle window (e.g. Expo Router auto-handled // the link before our own `getInitialURL()` chain resolved). if (initialFinalizedNavSpan && isSpanRecording(initialFinalizedNavSpan)) { return tagSpanWithDeepLink(initialFinalizedNavSpan, link); } return false; }; let initialStateHandled = false; let isSetupComplete = false; let stateChangeTimeout; let recentRouteKeys = []; if (enableTimeToInitialDisplay) { NATIVE.initNativeReactNavigationNewFrameTracking().catch((reason) => { debug.error(`${INTEGRATION_NAME} Failed to initialize native new frame tracking: ${reason}`); }); } /** * Set the initial state and start initial navigation span for the current screen. */ const afterAllSetup = (client) => { var _a; tracing = getReactNativeTracingIntegration(client); if (tracing) { idleSpanOptions = { finalTimeout: tracing.options.finalTimeoutMs, idleTimeout: tracing.options.idleTimeoutMs, }; } // Listen for deep links as they arrive so we can attribute a span that has // already mounted its route but not yet ended (e.g. Expo Router auto-handled // the link before our integration's `getInitialURL()` chain resolved). setPendingDeepLinkListener(handleLateDeepLink); client.on('close', () => { setPendingDeepLinkListener(undefined); }); if (initialStateHandled) { // We create an initial state here to ensure a transaction gets created before the first route mounts. // This assumes that the Sentry.init() call is made before the first route mounts. // If this is not the case, the first transaction will be nameless 'Route Changed' return undefined; } (_a = getAppRegistryIntegration(client)) === null || _a === void 0 ? void 0 : _a.onRunApplication(() => { if (initialStateHandled) { // To avoid conflict with the initial transaction we check if it was already handled. // This ensures runApplication calls after the initial start are correctly traced. // This is used for example when Activity is (re)started on Android. debug.log('[ReactNavigationIntegration] Starting new idle navigation span based on runApplication call.'); startIdleNavigationSpan(undefined, true); } }); isSetupComplete = true; if (!navigationContainer) { // This is expected as navigation container is registered after the root component is mounted. return undefined; } // Navigation container already registered, create and populate initial span startIdleNavigationSpan(); updateLatestNavigationSpanWithCurrentRoute(); initialStateHandled = true; }; const registerNavigationContainer = (maybeNewNavigationContainer) => { if (RN_GLOBAL_OBJ.__sentry_rn_v5_registered) { debug.log(`${INTEGRATION_NAME} Instrumentation already exists, but registering again...`); // In the past we have not allowed re-registering the navigation container to avoid unexpected behavior. // But this doesn't work for Android and re-recreating application main activity. // Where new navigation container is created and the old one is discarded. We need to re-register to // trace the new navigation container navigation. } let newNavigationContainer; if (isPlainObject(maybeNewNavigationContainer) && 'current' in maybeNewNavigationContainer) { newNavigationContainer = maybeNewNavigationContainer.current; } else { newNavigationContainer = maybeNewNavigationContainer; } if (navigationContainer === newNavigationContainer) { debug.log(`${INTEGRATION_NAME} Navigation container ref is the same as the one already registered.`); return; } navigationContainer = newNavigationContainer; if (!navigationContainer) { debug.warn(`${INTEGRATION_NAME} Received invalid navigation container ref!`); return undefined; } // This action is emitted on every dispatch navigationContainer.addListener('__unsafe_action__', startIdleNavigationSpan); // React Navigation fires `emit('state')` synchronously BEFORE the // `onStateChange` prop callback. Integrations like Expo Router refresh // their route cache (which our route override provider reads) inside that // `onStateChange`, so reading the override synchronously here would return // the previous route for the current transition. Defer with a microtask // so the read happens after the synchronous state-change chain completes // and downstream caches have caught up. See #6436. navigationContainer.addListener('state', scheduleUpdateLatestNavigationSpanWithCurrentRoute); RN_GLOBAL_OBJ.__sentry_rn_v5_registered = true; if (initialStateHandled) { return undefined; } if (!latestNavigationSpan) { if (!isSetupComplete) { debug.log(`${INTEGRATION_NAME} Navigation container registered before integration setup. Initial span will be created when setup completes.`); return undefined; } startIdleNavigationSpan(); } // Navigation Container is registered after the first navigation // Initial navigation span was started, after integration setup, // so now we populate it with the current route. updateLatestNavigationSpanWithCurrentRoute(); initialStateHandled = true; }; /** * To be called on every React-Navigation action dispatch. * It does not name the transaction or populate it with route information. Instead, it waits for the state to fully change * and gets the route information from there, @see updateLatestNavigationSpanWithCurrentRoute * * @param unknownEvent - The event object that contains navigation action data * @param isAppRestart - Whether this span is being started due to an app restart rather than a normal navigation action */ // oxlint-disable-next-line eslint(complexity) const startIdleNavigationSpan = (unknownEvent, isAppRestart = false) => { var _a, _b, _c, _d; const event = unknownEvent; const actionType = (_b = (_a = event === null || event === void 0 ? void 0 : event.data) === null || _a === void 0 ? void 0 : _a.action) === null || _b === void 0 ? void 0 : _b.type; const targetRouteName = getRouteNameFromAction(event); // Always drain the pending Expo Router value on this listener invocation — // even if we end up short-circuiting below (noop / PRELOAD / drawer / // missing route name). If the underlying router call did not produce an // idle nav span, the value must not leak onto the next, unrelated // navigation. Apply it only if we actually create `latestNavigationSpan`. const pendingExpoRouter = consumePendingExpoRouterNavigation(); if (event && !isAppRestart && !((_c = event.data) === null || _c === void 0 ? void 0 : _c.noop)) { addBreadcrumb({ category: 'navigation.dispatch', type: 'navigation', message: targetRouteName ? `Dispatched ${actionType !== null && actionType !== void 0 ? actionType : 'NAVIGATE'} to ${targetRouteName}` : `Dispatched ${actionType !== null && actionType !== void 0 ? actionType : 'NAVIGATE'}`, data: Object.assign(Object.assign({}, (actionType ? { action_type: actionType } : undefined)), (targetRouteName ? { to: targetRouteName } : undefined)), level: 'info', }); } if (useDispatchedActionData && (event === null || event === void 0 ? void 0 : event.data.noop)) { debug.log(`${INTEGRATION_NAME} Navigation action is a noop, not starting navigation span.`); return; } const navigationActionType = useDispatchedActionData ? actionType : undefined; // Handle PRELOAD actions separately if prefetch tracking is enabled if (enablePrefetchTracking && navigationActionType === 'PRELOAD') { const preloadData = event === null || event === void 0 ? void 0 : event.data.action; const payload = preloadData === null || preloadData === void 0 ? void 0 : preloadData.payload; const targetRoute = payload && typeof payload === 'object' && 'name' in payload && typeof payload.name === 'string' ? payload.name : 'Unknown Route'; debug.log(`${INTEGRATION_NAME} Starting prefetch span for route: ${targetRoute}`); const prefetchSpan = startInactiveSpan({ op: 'navigation.prefetch', name: `Prefetch ${targetRoute}`, attributes: { 'route.name': targetRoute, }, }); // Store prefetch span to end it when state changes or timeout navigationProcessingSpan = prefetchSpan; // Set timeout to ensure we don't leave hanging spans stateChangeTimeout = setTimeout(() => { if (navigationProcessingSpan === prefetchSpan) { debug.log(`${INTEGRATION_NAME} Prefetch span timed out for route: ${targetRoute}`); prefetchSpan === null || prefetchSpan === void 0 ? void 0 : prefetchSpan.setStatus({ code: SPAN_STATUS_OK }); prefetchSpan === null || prefetchSpan === void 0 ? void 0 : prefetchSpan.end(); navigationProcessingSpan = undefined; } }, routeChangeTimeoutMs); return; } if (useDispatchedActionData && navigationActionType && [ // Process common actions 'PRELOAD', // Still filter PRELOAD when enablePrefetchTracking is false 'SET_PARAMS', // Drawer actions 'OPEN_DRAWER', 'CLOSE_DRAWER', 'TOGGLE_DRAWER', ].includes(navigationActionType)) { debug.log(`${INTEGRATION_NAME} Navigation action is ${navigationActionType}, not starting navigation span.`); return; } // Extract route name from dispatch action payload when available const dispatchedRouteName = useDispatchedActionData ? targetRouteName : undefined; if (useDispatchedActionData && event && !dispatchedRouteName && !isAppRestart) { debug.log(`${INTEGRATION_NAME} Navigation action has no route name in payload, not starting navigation span.`); return; } if (latestNavigationSpan) { debug.log(`${INTEGRATION_NAME} A transaction was detected that turned out to be a noop, discarding.`); _discardLatestTransaction(); clearStateChangeTimeout(); } const spanOptions = getDefaultIdleNavigationSpanOptions(); if (dispatchedRouteName) { spanOptions.name = dispatchedRouteName; } const originalName = spanOptions.name; const finalSpanOptions = (tracing === null || tracing === void 0 ? void 0 : tracing.options.beforeStartSpan) ? tracing.options.beforeStartSpan(spanOptions) : spanOptions; latestNavigationSpanNameCustomized = finalSpanOptions.name !== originalName; latestNavigationSpan = startGenericIdleNavigationSpan(finalSpanOptions, Object.assign(Object.assign({}, idleSpanOptions), { isAppRestart })); latestNavigationSpan === null || latestNavigationSpan === void 0 ? void 0 : latestNavigationSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION); latestNavigationSpan === null || latestNavigationSpan === void 0 ? void 0 : latestNavigationSpan.setAttribute(SEMANTIC_ATTRIBUTE_NAVIGATION_ACTION_TYPE, navigationActionType); if (pendingExpoRouter && latestNavigationSpan) { latestNavigationSpan.setAttribute('navigation.method', pendingExpoRouter.method); } // Stamp the span with a monotonic sequence so the deep-link consumer can // determine whether a pending link arrived before or after this dispatch. if (latestNavigationSpan) { spanDispatchSeq.set(latestNavigationSpan, nextEventSeq()); } // We deliberately do NOT consume the pending deep link here — if this span // is later discarded (noop / timeout / empty route), a still-fresh pending // value must remain available for the next nav. The pending value is // consumed once a span actually mounts its route (see // `updateLatestNavigationSpanWithCurrentRoute`). if (ignoreEmptyBackNavigationTransactions) { ignoreEmptyBackNavigation(getClient(), latestNavigationSpan); } // Always discard transactions that never receive route information const spanToCheck = latestNavigationSpan; const spanName = (_d = finalSpanOptions.name) !== null && _d !== void 0 ? _d : DEFAULT_NAVIGATION_SPAN_NAME; ignoreEmptyRouteChangeTransactions(getClient(), spanToCheck, spanName, () => latestNavigationSpan === spanToCheck); if (enableTimeToInitialDisplay && latestNavigationSpan) { NATIVE.setActiveSpanId(latestNavigationSpan.spanContext().spanId); navigationProcessingSpan = startInactiveSpan({ op: 'navigation.processing', name: 'Navigation dispatch to navigation cancelled or screen mounted', startTime: spanToJSON(latestNavigationSpan).start_timestamp, }); navigationProcessingSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION); } stateChangeTimeout = setTimeout(_discardLatestTransaction, routeChangeTimeoutMs); }; /** * Defer {@link updateLatestNavigationSpanWithCurrentRoute} until the current * synchronous state-change chain unwinds so route override providers backed * by a downstream cache (e.g. Expo Router's router-store, which is refreshed * via `NavigationContainer.onStateChange`) have picked up the new route. See * #6436. */ const scheduleUpdateLatestNavigationSpanWithCurrentRoute = () => { const g = globalThis; if (typeof g.queueMicrotask === 'function') { g.queueMicrotask(updateLatestNavigationSpanWithCurrentRoute); return; } // Fallback for runtimes without `queueMicrotask`. `.catch()` handler is // there only to satisfy the `no-floating-promises` lint — the update // function does not throw. Promise.resolve() .then(updateLatestNavigationSpanWithCurrentRoute) .catch(() => { }); }; /** * To be called AFTER the state has been changed to populate the transaction with the current route. */ // oxlint-disable-next-line eslint(complexity) const updateLatestNavigationSpanWithCurrentRoute = () => { var _a, _b, _c, _d; const stateChangedTimestamp = timestampInSeconds(); const previousRoute = latestRoute; if (!navigationContainer) { debug.warn(`${INTEGRATION_NAME} Missing navigation container ref. Route transactions will not be sent.`); return undefined; } const route = navigationContainer.getCurrentRoute(); if (!route) { debug.log(`[${INTEGRATION_NAME}] Navigation state changed, but no route is rendered.`); return undefined; } if (!latestNavigationSpan) { debug.log(`[${INTEGRATION_NAME}] Navigation state changed, but navigation transaction was not started on dispatch.`); return undefined; } if (enableTimeToInitialDisplay) { addTimeToInitialDisplayFallback(latestNavigationSpan.spanContext().spanId, NATIVE.getNewScreenTimeToDisplay()); } if ((previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.key) === route.key) { debug.log(`[${INTEGRATION_NAME}] Navigation state changed, but route is the same as previous.`); // Even a same-route state change is a legitimate destination for a // deep link (e.g. deep-linking to the screen you're already on). Make // sure the pending link still gets attributed before we drop the span // reference. applyPendingDeepLinkToSpan(latestNavigationSpan, routeChangeTimeoutMs); pushRecentRouteKey(route.key); latestRoute = route; // Clear the latest transaction as it has been handled. latestNavigationSpan = undefined; return undefined; } const routeHasBeenSeen = recentRouteKeys.includes(route.key); // Resolve route name. Order of preference: // 1. Route override provider (e.g. Expo Router templated path) // 2. Full path joined from React Navigation state // 3. React Navigation's leaf route name let routeName = route.name; let routePath; let routeUrl; let routeParams = route.params; let override; try { override = routeOverrideProvider === null || routeOverrideProvider === void 0 ? void 0 : routeOverrideProvider(); } catch (e) { debug.warn(`${INTEGRATION_NAME} Route override provider threw, falling back to React Navigation route.`, e); } if (override === null || override === void 0 ? void 0 : override.templatedPath) { routeName = override.templatedPath; routePath = override.templatedPath; routeUrl = override.concreteUrl; routeParams = (_a = override.params) !== null && _a !== void 0 ? _a : route.params; } else if (useFullPathsForNavigationRoutes) { const navigationState = navigationContainer.getState(); routeName = getPathFromState(navigationState) || route.name; } // Consume any pending deep link and attach it to this span. Done here // (after route info is known) so the link is only attributed to a span // that actually mounted a route — not one that was later discarded. applyPendingDeepLinkToSpan(latestNavigationSpan, routeChangeTimeoutMs); // Capture the first finalized nav span for the cold-start late-arrival // fallback. Set exactly once, then frozen — a cold-start link must never // retroactively tag a navigation the user performed later. if (!initialFinalizedNavSpan) { initialFinalizedNavSpan = latestNavigationSpan; } navigationProcessingSpan === null || navigationProcessingSpan === void 0 ? void 0 : navigationProcessingSpan.updateName(`Navigation dispatch to screen ${routeName} mounted`); navigationProcessingSpan === null || navigationProcessingSpan === void 0 ? void 0 : navigationProcessingSpan.setStatus({ code: SPAN_STATUS_OK }); navigationProcessingSpan === null || navigationProcessingSpan === void 0 ? void 0 : navigationProcessingSpan.end(stateChangedTimestamp); navigationProcessingSpan = undefined; if (!latestNavigationSpanNameCustomized) { latestNavigationSpan.updateName(routeName); } const sendDefaultPii = (_d = (_c = (_b = getClient()) === null || _b === void 0 ? void 0 : _b.getOptions()) === null || _c === void 0 ? void 0 : _c.sendDefaultPii) !== null && _d !== void 0 ? _d : false; latestNavigationSpan.setAttributes(Object.assign(Object.assign(Object.assign(Object.assign({ 'route.name': routeName, 'route.key': route.key }, (routePath ? { 'route.path': routePath } : undefined)), (sendDefaultPii && routeUrl ? { 'route.url': routeUrl } : undefined)), (sendDefaultPii ? extractDynamicRouteParams(routeName, routeParams) : undefined)), { 'route.has_been_seen': routeHasBeenSeen, 'previous_route.name': previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.name, 'previous_route.key': previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.key, [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation' })); // Clear the timeout so the transaction does not get cancelled. clearStateChangeTimeout(); addBreadcrumb({ category: 'navigation', type: 'navigation', message: `Navigation to ${routeName}`, data: { from: previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.name, to: routeName, }, }); tracing === null || tracing === void 0 ? void 0 : tracing.setCurrentRoute(routeName); pushRecentRouteKey(route.key); if ((override === null || override === void 0 ? void 0 : override.templatedPath) || useFullPathsForNavigationRoutes) { latestRoute = Object.assign(Object.assign({}, route), { name: routeName }); } else { latestRoute = route; } // Clear the latest transaction as it has been handled. latestNavigationSpan = undefined; }; /** Pushes a recent route key, and removes earlier routes when there is greater than the max length */ const pushRecentRouteKey = (key) => { recentRouteKeys.push(key); if (recentRouteKeys.length > NAVIGATION_HISTORY_MAX_SIZE) { recentRouteKeys = recentRouteKeys.slice(recentRouteKeys.length - NAVIGATION_HISTORY_MAX_SIZE); } }; /** Cancels the latest transaction so it does not get sent to Sentry. */ const _discardLatestTransaction = () => { if (latestNavigationSpan) { if (isSentrySpan(latestNavigationSpan)) { markRootSpanForDiscard(latestNavigationSpan, 'discarded_latest_navigation'); } // TODO: What if it's not SentrySpan? latestNavigationSpan.end(); latestNavigationSpan = undefined; } if (navigationProcessingSpan) { navigationProcessingSpan = undefined; } }; const clearStateChangeTimeout = () => { if (typeof stateChangeTimeout !== 'undefined') { clearTimeout(stateChangeTimeout); stateChangeTimeout = undefined; } }; const _setRouteOverrideProvider = (provider) => { routeOverrideProvider = provider; }; return { name: INTEGRATION_NAME, afterAllSetup, registerNavigationContainer, _setRouteOverrideProvider, options: { routeChangeTimeoutMs, enableTimeToInitialDisplay, ignoreEmptyBackNavigationTransactions, enableTimeToInitialDisplayForPreloadedRoutes, useDispatchedActionData, useFullPathsForNavigationRoutes, enablePrefetchTracking, }, }; }; /** * Per-span guard against double-tagging deep-link attributes. Shared between * the synchronous listener path (late arrival) and the post-state-change path. */ const taggedDeepLinkSpans = new WeakSet(); /** * Annotates the given span with deep-link attributes if it has not already * been annotated. Returns `true` when the span was newly tagged, `false` when * it was already tagged (so callers can decide whether to keep the link * around for another span). */ function tagSpanWithDeepLink(span, link) { var _a, _b, _c; if (taggedDeepLinkSpans.has(span)) { return false; } taggedDeepLinkSpans.add(span); const sendDefaultPii = (_c = (_b = (_a = getClient()) === null || _a === void 0 ? void 0 : _a.getOptions()) === null || _b === void 0 ? void 0 : _b.sendDefaultPii) !== null && _c !== void 0 ? _c : false; const url = sendDefaultPii ? link.url : sanitizeDeepLinkUrl(link.url); span.setAttributes({ 'navigation.trigger': 'deeplink', 'deeplink.url': url, // Duration between URL receipt and the moment the span is annotated — // approximates the gap between "link received" and "navigation dispatched // / handled". 'deeplink.dispatch_delay_ms': Math.max(0, Date.now() - link.receivedAtMs), }); return true; } /** Returns true if the span is still recording (has not been ended). */ function isSpanRecording(span) { return spanToJSON(span).timestamp === undefined; } /** * Extracts the route name from a React Navigation dispatch action payload. * * Actions like NAVIGATE, PUSH, REPLACE, JUMP_TO carry the target route name * in `action.payload.name`. Actions like GO_BACK, POP, POP_TO_TOP do not. */ function getRouteNameFromAction(event) { var _a, _b; const payload = (_b = (_a = event === null || event === void 0 ? void 0 : event.data) === null || _a === void 0 ? void 0 : _a.action) === null || _b === void 0 ? void 0 : _b.payload; if (payload && typeof payload === 'object' && 'name' in payload && typeof payload.name === 'string') { return payload.name; } return undefined; } /** * Returns React Navigation integration of the given client. */ export function getReactNavigationIntegration(client) { return client.getIntegrationByName(INTEGRATION_NAME); } //# sourceMappingURL=reactnavigation.js.map