@sentry/react-native
Version:
Official Sentry SDK for react-native
171 lines • 8.18 kB
JavaScript
import { addBreadcrumb, getClient, SPAN_STATUS_ERROR, SPAN_STATUS_OK, startInactiveSpan } from '@sentry/core';
import { SPAN_ORIGIN_AUTO_EXPO_ROUTER_NAVIGATION, SPAN_ORIGIN_AUTO_EXPO_ROUTER_PREFETCH } from './origin';
import { clearPendingExpoRouterNavigation, setPendingExpoRouterNavigation } from './pendingExpoRouterNavigation';
/**
* Wraps Expo Router methods to add automated performance monitoring and breadcrumbs.
*
* Currently wraps:
* - `prefetch` — wraps the call in a `navigation.prefetch` span.
* - `push` / `replace` / `navigate` / `back` / `dismiss` — adds a navigation
* breadcrumb, wraps the call in a short-lived span that mirrors prefetch's
* error/status handling, and tags the subsequent idle navigation transaction
* with the initiating `navigation.method` so the resulting span can be
* attributed back to the call site.
*
* Safe to call repeatedly — guarded by a single `__sentryWrapped` flag.
*
* @param router - The Expo Router instance from `useRouter()` hook
* @returns The same router instance with instrumented methods
*/
export function wrapExpoRouter(router) {
if (!router) {
return router;
}
const wrappedRouter = router;
if (wrappedRouter.__sentryWrapped) {
return router;
}
if (router.prefetch) {
wrapPrefetch(router);
}
if (router.push) {
router.push = wrapNavigationMethod(router, 'push', router.push.bind(router));
}
if (router.replace) {
router.replace = wrapNavigationMethod(router, 'replace', router.replace.bind(router));
}
if (router.navigate) {
router.navigate = wrapNavigationMethod(router, 'navigate', router.navigate.bind(router));
}
if (router.back) {
router.back = wrapNavigationMethod(router, 'back', router.back.bind(router));
}
if (router.dismiss) {
const originalDismiss = router.dismiss.bind(router);
router.dismiss = wrapNavigationMethod(router, 'dismiss', originalDismiss);
}
wrappedRouter.__sentryWrapped = true;
return router;
}
function wrapPrefetch(router) {
const originalPrefetch = router.prefetch.bind(router);
router.prefetch = ((href) => {
const parsed = parseHref(href);
const sendPii = isSendDefaultPiiEnabled();
// For concrete string hrefs (e.g. `/users/42`), `routeName` may carry
// user identifiers — gate it behind `sendDefaultPii`. For templated
// object hrefs (e.g. `{ pathname: '/users/[id]' }`) it is structural.
const safeRouteName = parsed.concretePathname && !sendPii ? 'unknown' : parsed.routeName;
const span = startInactiveSpan({
op: 'navigation.prefetch',
name: `Prefetch ${safeRouteName}`,
attributes: Object.assign({ 'sentry.origin': SPAN_ORIGIN_AUTO_EXPO_ROUTER_PREFETCH, 'route.name': safeRouteName }, (sendPii ? { 'route.href': serializeHref(href) } : undefined)),
});
try {
const result = originalPrefetch(href);
if (result && typeof result === 'object' && 'then' in result && typeof result.then === 'function') {
return result
.then(res => {
span === null || span === void 0 ? void 0 : span.setStatus({ code: SPAN_STATUS_OK });
span === null || span === void 0 ? void 0 : span.end();
return res;
})
.catch((error) => {
span === null || span === void 0 ? void 0 : span.setStatus({ code: SPAN_STATUS_ERROR, message: String(error) });
span === null || span === void 0 ? void 0 : span.end();
throw error;
});
}
span === null || span === void 0 ? void 0 : span.setStatus({ code: SPAN_STATUS_OK });
span === null || span === void 0 ? void 0 : span.end();
return result;
}
catch (error) {
span === null || span === void 0 ? void 0 : span.setStatus({ code: SPAN_STATUS_ERROR, message: String(error) });
span === null || span === void 0 ? void 0 : span.end();
throw error;
}
});
}
function wrapNavigationMethod(router, method, original) {
return (...args) => {
const parsed = parseMethodArgs(method, args);
const sendPii = isSendDefaultPiiEnabled();
// For concrete string hrefs (e.g. `/users/42`) the pathname carries the
// resolved URL — gate it behind `sendDefaultPii`. Templated pathnames from
// object hrefs (e.g. `{ pathname: '/users/[id]' }`) are structural and safe.
const safePathname = parsed.concretePathname && !sendPii ? undefined : parsed.pathname;
const safeRouteName = parsed.concretePathname && !sendPii ? method : parsed.routeName;
addBreadcrumb({
category: 'navigation',
type: 'navigation',
message: `Expo Router ${method}${safePathname ? ` to ${safePathname}` : ''}`,
data: Object.assign(Object.assign(Object.assign({ method }, (safePathname ? { pathname: safePathname } : undefined)), (sendPii && parsed.href !== undefined ? { href: serializeHref(parsed.href) } : undefined)), (sendPii && parsed.params ? { params: parsed.params } : undefined)),
});
setPendingExpoRouterNavigation({ method });
const span = startInactiveSpan({
op: `navigation.${method}`,
name: `Navigation ${method}${safePathname ? ` to ${safePathname}` : ''}`,
attributes: Object.assign(Object.assign({ 'sentry.origin': SPAN_ORIGIN_AUTO_EXPO_ROUTER_NAVIGATION, 'navigation.method': method }, (safeRouteName ? { 'route.name': safeRouteName } : undefined)), (sendPii && parsed.href !== undefined ? { 'route.href': serializeHref(parsed.href) } : undefined)),
});
try {
const result = original.apply(router, args);
span === null || span === void 0 ? void 0 : span.setStatus({ code: SPAN_STATUS_OK });
span === null || span === void 0 ? void 0 : span.end();
return result;
}
catch (error) {
// Clear the pending value so a failed navigation does not leak its
// method/href onto the next successful idle navigation span.
clearPendingExpoRouterNavigation();
span === null || span === void 0 ? void 0 : span.setStatus({ code: SPAN_STATUS_ERROR, message: String(error) });
span === null || span === void 0 ? void 0 : span.end();
throw error;
}
};
}
function parseMethodArgs(method, args) {
if (method === 'back' || method === 'dismiss') {
return { routeName: method, concretePathname: false };
}
return parseHref(args[0]);
}
function parseHref(href) {
if (typeof href === 'string') {
return { href, routeName: href, pathname: href, concretePathname: true };
}
if (href && typeof href === 'object') {
const pathname = typeof href.pathname === 'string' ? href.pathname : undefined;
return {
href,
routeName: pathname !== null && pathname !== void 0 ? pathname : 'unknown',
pathname,
params: href.params,
concretePathname: false,
};
}
return { routeName: 'unknown', concretePathname: false };
}
/**
* Serializes an href into a string for inclusion in spans/breadcrumbs.
*
* Wrapped in `try/catch` because `params` may contain values that `JSON.stringify`
* cannot serialize (BigInt, Symbol, circular references). A failure here must
* never prevent the underlying navigation from running.
*/
function serializeHref(href) {
if (typeof href === 'string') {
return href;
}
try {
return JSON.stringify(href);
}
catch (_a) {
return '[unserializable href]';
}
}
function isSendDefaultPiiEnabled() {
var _a, _b, _c;
return (_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;
}
//# sourceMappingURL=expoRouter.js.map