Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

- Find `ios/.xcode.env` in the Expo plugin "Upload Debug Symbols to Sentry" phase when the iOS project uses Swift Package Manager instead of CocoaPods ([#6911](https://github.com/getsentry/sentry-react-native/pull/6911))
- Fix HTTP timing listeners leaking on React Native below 0.83 and when `expo/fetch` is detected, and respect the `enableHTTPTimings` option ([#6912](https://github.com/getsentry/sentry-react-native/pull/6912))
- Support Expo Router 58 in `expoRouterIntegration`. Metro no longer warns about the removed `expo-router/build/global-state/router-store` module, and the integration creates navigation spans again ([#6923](https://github.com/getsentry/sentry-react-native/pull/6923))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 🚫 The changelog entry seems to be part of an already released section ## 8.32.0.
    Consider moving the entry to the ## Unreleased section, please.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is accurate for v58. If dropping older Expo Router versions is intentional, it's a breaking change and the entry should say so. Otherwise it goes away once the two blockers are fixed.

- Remove the `@sentry/browser` feedback modules and the server-only `@sentry/core` modules from native bundles built on Windows. Before this fix, `includeWebFeedback: false` made these bundles fail at launch ([#6915](https://github.com/getsentry/sentry-react-native/pull/6915))
- Stop wrapping the SDK's own `RNSentry` module in `turboModuleContextIntegration` by default. Each wrapped call wrote to the native scope several times, which added CPU time to every SDK call on iOS ([#6921](https://github.com/getsentry/sentry-react-native/pull/6921))
- To wrap `RNSentry` again, add `{ name: 'RNSentry' }` to the `modules` option.
Expand Down
2 changes: 2 additions & 0 deletions packages/core/etc/sentry-react-native.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ export interface ReactNativeWrapperOptions {
export const reactNavigationIntegration: (input?: Partial<ReactNavigationIntegrationOptions>) => Integration & {
registerNavigationContainer: (navigationContainerRef: unknown) => void;
_setRouteOverrideProvider: (provider: RouteOverrideProvider | undefined) => void;
_startIdleNavigationSpan: (event: UnsafeAction) => void;
options: ReactNavigationIntegrationOptions;
};

Expand Down Expand Up @@ -1337,6 +1338,7 @@ export function wrapTurboModule<T extends object>(name: string, module: T | null
//
// src/js/tracing/reactnativetracing.ts:93:3 - (ae-forgotten-export) The symbol "ReactNativeTracingState" needs to be exported by the entry point index.d.ts
// src/js/tracing/reactnavigation.ts:228:3 - (ae-forgotten-export) The symbol "RouteOverrideProvider" needs to be exported by the entry point index.d.ts
// src/js/tracing/reactnavigation.ts:233:3 - (ae-forgotten-export) The symbol "UnsafeAction" needs to be exported by the entry point index.d.ts

// (No @packageDocumentation comment for this package)

Expand Down
23 changes: 21 additions & 2 deletions packages/core/src/js/tracing/expoRouterIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { debug } from '@sentry/core';
import type { ExpoRouterStore, ExpoRouterUrlObject } from './expoRouterStore';
import type { RouteOverride } from './reactnavigation';

import { buildExpoRouterTemplatedPath, tryGetExpoRouterStore } from './expoRouterStore';
import {
buildExpoRouterTemplatedPath,
tryGetExpoRouterNavigationEvents,
tryGetExpoRouterStore,
} from './expoRouterStore';
import { getReactNavigationIntegration, reactNavigationIntegration } from './reactnavigation';

export { buildExpoRouterTemplatedPath };
Expand All @@ -30,6 +34,7 @@ export type ExpoRouterIntegrationOptions = Parameters<typeof reactNavigationInte
*/
export const expoRouterIntegration = (options: ExpoRouterIntegrationOptions = {}): Integration => {
let pollTimer: ReturnType<typeof setTimeout> | undefined;
let removeActionListener: (() => void) | undefined;

const afterAllSetup = (client: Client): void => {
const store = tryGetExpoRouterStore();
Expand Down Expand Up @@ -58,14 +63,26 @@ export const expoRouterIntegration = (options: ExpoRouterIntegrationOptions = {}
}
reactNavigation._setRouteOverrideProvider?.(() => buildExpoRouterRouteOverride(store));
reactNavigation.registerNavigationContainer(navigationRef);
removeActionListener = tryGetExpoRouterNavigationEvents()?.addListener('actionDispatched', event => {
if (!event.actionType) {
return;
}
reactNavigation._startIdleNavigationSpan?.({
data: {
action: { type: event.actionType, payload: event.payload as object | undefined },
noop: false,
stack: undefined,
},
});
});
Comment thread
cursor[bot] marked this conversation as resolved.
pollTimer = undefined;
return;
}

if (Date.now() - startedAt >= POLL_MAX_DURATION_MS) {
if (!navigationRef) {
debug.warn(
`${INTEGRATION_NAME} Found expo-router router-store but it does not expose a \`navigationRef\`. ` +
`${INTEGRATION_NAME} Found expo-router but it does not expose a \`navigationRef\`. ` +
`This likely means the installed expo-router version is incompatible with this integration.`,
);
} else {
Expand All @@ -85,6 +102,8 @@ export const expoRouterIntegration = (options: ExpoRouterIntegrationOptions = {}
clearTimeout(pollTimer);
pollTimer = undefined;
}
removeActionListener?.();
removeActionListener = undefined;
});
};

Expand Down
63 changes: 57 additions & 6 deletions packages/core/src/js/tracing/expoRouterStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

export interface ExpoRouterNavigationRef {
current: unknown | null;
getRootState?: () => unknown;
}

export interface ExpoRouterUrlObject {
Expand All @@ -22,7 +23,7 @@ export interface ExpoRouterUrlObject {

export interface ExpoRouterStore {
navigationRef?: ExpoRouterNavigationRef;
getRouteInfo?: () => ExpoRouterUrlObject;
getRouteInfo?: () => ExpoRouterUrlObject | undefined;
}

export interface NormalizedExpoRouterRouteInfo {
Expand All @@ -42,17 +43,67 @@ export interface NormalizedExpoRouterRouteInfo {
segments?: string[];
}

export interface ExpoRouterActionDispatchedEvent {
actionType?: string;
payload?: unknown;
}

export interface ExpoRouterInternalNavigationEvents {
addListener: (
name: 'actionDispatched',
callback: (event: ExpoRouterActionDispatchedEvent) => void,
) => (() => void) | undefined;
}

/**
* Returns Expo Router's internal router store, or `null` if `expo-router` is
* not installed or the build does not expose the expected module path.
* Returns the navigation container ref and route info of Expo Router, or `null`
* if `expo-router` is not installed or does not expose the expected API.
*
* Only module paths that exist in every supported Expo Router version are
* required here. Expo Router 58 declares `exports`, so Metro warns about any
* deep path that is missing from the installed version.
*/
export function tryGetExpoRouterStore(): ExpoRouterStore | null {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mod = require('expo-router/build/global-state/router-store') as {
store?: ExpoRouterStore;
const expoRouter = require('expo-router') as {
useNavigationContainerRef?: () => ExpoRouterNavigationRef | undefined;
};
// eslint-disable-next-line @typescript-eslint/no-var-requires
const routeInfoModule = require('expo-router/build/global-state/getRouteInfoFromState') as {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker: this breaks Expo Router ≤55. getRouteInfoFromState doesn't exist before v56 (I checked the 4.x, 5.x, 6.x and 55.x packages), so this require throws and the function returns null. Then expoRouterIntegration.ts:41-43 exits as if expo-router weren't installed, with no log. Our peer range is "expo-router": "*" (package.json:80).

Suggestion: treat route info as optional. When it's missing, still register the container and skip the route override. Going back to router-store isn't an option, since that brings the v58 Metro warning back.

getRouteInfoFromState?: (state: unknown) => ExpoRouterUrlObject;
};
// Not a React hook: it returns a module-level ref in Expo Router 57 and 58.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is only true for v58 (global-state/navigationRef.js creates the ref when the module loads). In v56/57, useNavigationContainerRef() returns store.navigationRef, which reads storeRef.current. That stays {} until useStore() runs during the Root Layout render.

const getNavigationContainerRef = expoRouter?.useNavigationContainerRef;
const getRouteInfoFromState = routeInfoModule?.getRouteInfoFromState;
if (!getNavigationContainerRef || !getRouteInfoFromState) {
return null;
}
const navigationRef = getNavigationContainerRef();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker: this regresses #6431 on v56/57. The ref is read once here, and expoRouterIntegration.ts:54 then polls that saved value. With Sentry.init() at module load (the documented setup), it's undefined here, so polling times out after 5s and logs the misleading "does not expose a navigationRef" warning (expoRouterIntegration.ts:85).

Suggestion: call useNavigationContainerRef() on every poll tick instead of once.

return {
navigationRef,
getRouteInfo: () => {
const state = navigationRef?.current ? navigationRef.getRootState?.() : undefined;
return state ? getRouteInfoFromState(state) : undefined;
},
};
} catch {
return null;
}
}

/**
* Returns the internal navigation event emitter of Expo Router 58 and later, or
* `null` for older versions. Expo Router 58 does not emit React Navigation's
* `__unsafe_action__` event, so this is the only signal of a dispatched action.
*/
export function tryGetExpoRouterNavigationEvents(): ExpoRouterInternalNavigationEvents | null {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mod = require('expo-router/build/global-state/internalNavigationEvents') as {
internalNavigationEvents?: ExpoRouterInternalNavigationEvents;
};
return mod?.store ?? null;
return mod?.internalNavigationEvents ?? null;
Comment thread
alwx marked this conversation as resolved.
} catch {
return null;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/js/tracing/reactnavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ export const reactNavigationIntegration = ({
* supply the canonical route info on every state change. Use `undefined` to clear.
*/
_setRouteOverrideProvider: (provider: RouteOverrideProvider | undefined) => void;
/**
* @internal API: start a navigation span for a dispatched action, for navigators that do not
* emit `__unsafe_action__` (for example, Expo Router 58 and later).
*/
_startIdleNavigationSpan: (event: UnsafeAction) => void;
options: ReactNavigationIntegrationOptions;
} => {
let navigationContainer: NavigationContainer | undefined;
Expand Down Expand Up @@ -896,6 +901,7 @@ export const reactNavigationIntegration = ({
afterAllSetup,
registerNavigationContainer,
_setRouteOverrideProvider,
_startIdleNavigationSpan: event => startIdleNavigationSpan(event),
options: {
routeChangeTimeoutMs,
enableTimeToInitialDisplay,
Expand Down
Loading
Loading