Skip to content

Commit 05047ad

Browse files
authored
feat(tracing): Track wrapExpoRouterErrorBoundary adoption (#6425)
* feat(core): Track GlobalErrorBoundary adoption Register a no-op `GlobalErrorBoundary` integration when the component mounts so the name flows through to `event.sdk.integrations` — the same channel used for feature- adoption signals like `MobileFeedback` and `AppStart`. Also introduces a shared `registerFeatureMarker` helper. Subsequent markers for other opt-in features (NavigationContainer, ExpoRouter error boundary, AppLoaded, ...) will use this helper — see #6415. Refs: #6415 * feat(tracing): Track wrapExpoRouterErrorBoundary adoption Register a no-op `ExpoRouterErrorBoundary` integration when the wrapped Expo Router error boundary mounts so the name flows through to `event.sdk.integrations`. Refs: #6415 * fix(tracing): Fire ExpoRouterErrorBoundary marker at wrap-call time The wrapped boundary only mounts when Expo Router actually renders it (i.e., on an error), so a `useEffect`-based marker would undercount adoption for apps that ship the wrap but never hit a route error. Registering at wrap-call time fires as soon as the user's route file evaluates. No client is present if the wrap is called before `Sentry.init()`, but that is very rare in practice — Expo Router lazy-loads route files during navigation, so init has typically completed by then.
1 parent af67055 commit 05047ad

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

packages/core/src/js/tracing/expoRouterErrorBoundary.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ import {
1313
} from '@sentry/core';
1414
import * as React from 'react';
1515

16+
import { registerFeatureMarker } from '../utils/featureMarkers';
1617
import { getCurrentExpoRouterRouteInfo } from './expoRouterStore';
1718

19+
export const EXPO_ROUTER_ERROR_BOUNDARY_INTEGRATION_NAME = 'ExpoRouterErrorBoundary';
20+
1821
/**
1922
* Errors we have already reported. Module-scoped (rather than a per-instance
2023
* `useRef`) so that an unmount → remount cycle with the same error instance —
@@ -64,6 +67,13 @@ export interface ExpoRouterErrorBoundaryProps {
6467
export function wrapExpoRouterErrorBoundary<P extends ExpoRouterErrorBoundaryProps>(
6568
OriginalErrorBoundary: React.ComponentType<P>,
6669
): React.ComponentType<P> {
70+
// Register at wrap-call time (module evaluation) so the marker fires as soon
71+
// as the user's route file loads, not only when Expo Router actually renders
72+
// the boundary on an error. No-op if `Sentry.init()` has not run yet; route
73+
// files load lazily during navigation, so init has typically completed by
74+
// then.
75+
registerFeatureMarker(EXPO_ROUTER_ERROR_BOUNDARY_INTEGRATION_NAME);
76+
6777
const Wrapped: React.FC<P> = props => {
6878
// Reporting is intentionally done in `useEffect` (commit phase) rather than
6979
// during render: render must be pure, and in Concurrent Mode an in-progress

packages/core/test/tracing/expoRouterErrorBoundary.test.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import { render } from '@testing-library/react-native';
33
import * as React from 'react';
44
import { Text } from 'react-native';
55

6-
import { wrapExpoRouterErrorBoundary } from '../../src/js/tracing/expoRouterErrorBoundary';
6+
import {
7+
EXPO_ROUTER_ERROR_BOUNDARY_INTEGRATION_NAME,
8+
wrapExpoRouterErrorBoundary,
9+
} from '../../src/js/tracing/expoRouterErrorBoundary';
710

811
const mockCaptureException = jest.fn();
912
const mockAddBreadcrumb = jest.fn();
1013
const mockAddExceptionMechanism = jest.fn();
14+
const mockAddIntegration = jest.fn();
15+
const mockGetIntegrationByName = jest.fn().mockReturnValue(undefined);
1116
let mockSendDefaultPii = false;
1217
let mockActiveSpan: { setStatus: jest.Mock; __origin?: string } | undefined;
1318

@@ -18,7 +23,11 @@ jest.mock('@sentry/core', () => {
1823
captureException: (...args: unknown[]) => mockCaptureException(...args),
1924
addBreadcrumb: (...args: unknown[]) => mockAddBreadcrumb(...args),
2025
addExceptionMechanism: (...args: unknown[]) => mockAddExceptionMechanism(...args),
21-
getClient: () => ({ getOptions: () => ({ sendDefaultPii: mockSendDefaultPii }) }),
26+
getClient: () => ({
27+
getOptions: () => ({ sendDefaultPii: mockSendDefaultPii }),
28+
getIntegrationByName: mockGetIntegrationByName,
29+
addIntegration: mockAddIntegration,
30+
}),
2231
getActiveSpan: () => mockActiveSpan,
2332
getRootSpan: (span: unknown) => span,
2433
spanToJSON: (span: { __origin?: string } | undefined) => ({ origin: span?.__origin }),
@@ -80,6 +89,11 @@ describe('wrapExpoRouterErrorBoundary', () => {
8089
expect(getByTestId('fallback').props.children).toBe('boom');
8190
});
8291

92+
it('registers the ExpoRouterErrorBoundary marker at wrap-call time (before any mount)', () => {
93+
wrapExpoRouterErrorBoundary(OriginalErrorBoundary);
94+
expect(mockAddIntegration).toHaveBeenCalledWith({ name: EXPO_ROUTER_ERROR_BOUNDARY_INTEGRATION_NAME });
95+
});
96+
8397
it('captures the error to Sentry once per error instance', () => {
8498
const Wrapped = wrapExpoRouterErrorBoundary(OriginalErrorBoundary);
8599
const err = new Error('boom');

0 commit comments

Comments
 (0)