UNPKG

@sentry/react-native

Version:
171 lines 8.18 kB
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