@sentry/react-native
Version:
Official Sentry SDK for react-native
282 lines • 15.4 kB
JavaScript
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 { isSentrySpan } from '../utils/span';
import { RN_GLOBAL_OBJ } from '../utils/worldwide';
import { NATIVE } from '../wrapper';
import { ignoreEmptyBackNavigation, ignoreEmptyRouteChangeTransactions } from './onSpanEndUtils';
import { SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION } from './origin';
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;
/**
* 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, } = {}) => {
let navigationContainer;
let tracing;
let idleSpanOptions = defaultIdleOptions;
let latestRoute;
let latestNavigationSpan;
let navigationProcessingSpan;
let initialStateHandled = 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,
};
}
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);
}
});
startIdleNavigationSpan();
if (!navigationContainer) {
// This is expected as navigation container is registered after the root component is mounted.
return undefined;
}
// Navigation container already registered, just populate with route state
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);
navigationContainer.addListener('state', updateLatestNavigationSpanWithCurrentRoute);
RN_GLOBAL_OBJ.__sentry_rn_v5_registered = true;
if (initialStateHandled) {
return undefined;
}
if (!latestNavigationSpan) {
debug.log(`${INTEGRATION_NAME} Navigation container registered, but integration has not been setup yet.`);
return undefined;
}
// 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
*/
const startIdleNavigationSpan = (unknownEvent, isAppRestart = false) => {
const event = unknownEvent;
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 ? event === null || event === void 0 ? void 0 : event.data.action.type : undefined;
if (useDispatchedActionData &&
navigationActionType &&
[
// Process common actions
'PRELOAD',
'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;
}
if (latestNavigationSpan) {
debug.log(`${INTEGRATION_NAME} A transaction was detected that turned out to be a noop, discarding.`);
_discardLatestTransaction();
clearStateChangeTimeout();
}
latestNavigationSpan = startGenericIdleNavigationSpan((tracing === null || tracing === void 0 ? void 0 : tracing.options.beforeStartSpan)
? tracing.options.beforeStartSpan(getDefaultIdleNavigationSpanOptions())
: getDefaultIdleNavigationSpanOptions(), 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 (ignoreEmptyBackNavigationTransactions) {
ignoreEmptyBackNavigation(getClient(), latestNavigationSpan);
}
// Always discard transactions that never receive route information
const spanToCheck = latestNavigationSpan;
ignoreEmptyRouteChangeTransactions(getClient(), spanToCheck, DEFAULT_NAVIGATION_SPAN_NAME, () => 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);
};
/**
* To be called AFTER the state has been changed to populate the transaction with the current route.
*/
const updateLatestNavigationSpanWithCurrentRoute = () => {
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;
}
addTimeToInitialDisplayFallback(latestNavigationSpan.spanContext().spanId, NATIVE.getNewScreenTimeToDisplay());
if (previousRoute && previousRoute.key === route.key) {
debug.log(`[${INTEGRATION_NAME}] Navigation state changed, but route is the same as previous.`);
pushRecentRouteKey(route.key);
latestRoute = route;
// Clear the latest transaction as it has been handled.
latestNavigationSpan = undefined;
return undefined;
}
const routeHasBeenSeen = recentRouteKeys.includes(route.key);
navigationProcessingSpan === null || navigationProcessingSpan === void 0 ? void 0 : navigationProcessingSpan.updateName(`Navigation dispatch to screen ${route.name} 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 (spanToJSON(latestNavigationSpan).description === DEFAULT_NAVIGATION_SPAN_NAME) {
latestNavigationSpan.updateName(route.name);
}
latestNavigationSpan.setAttributes({
'route.name': route.name,
'route.key': route.key,
// TODO: filter PII params instead of dropping them all
// 'route.params': {},
'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,
// TODO: filter PII params instead of dropping them all
// 'previous_route.params': {},
[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 ${route.name}`,
data: {
from: previousRoute === null || previousRoute === void 0 ? void 0 : previousRoute.name,
to: route.name,
},
});
tracing === null || tracing === void 0 ? void 0 : tracing.setCurrentRoute(route.name);
pushRecentRouteKey(route.key);
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)) {
latestNavigationSpan['_sampled'] = false;
}
// 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;
}
};
return {
name: INTEGRATION_NAME,
afterAllSetup,
registerNavigationContainer,
options: {
routeChangeTimeoutMs,
enableTimeToInitialDisplay,
ignoreEmptyBackNavigationTransactions,
enableTimeToInitialDisplayForPreloadedRoutes,
useDispatchedActionData,
},
};
};
/**
* Returns React Navigation integration of the given client.
*/
export function getReactNavigationIntegration(client) {
return client.getIntegrationByName(INTEGRATION_NAME);
}
//# sourceMappingURL=reactnavigation.js.map