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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

## Unreleased

### Fixes

- 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))

### Dependencies

- Bump CLI from v3.8.1 to v3.8.2 ([#6933](https://github.com/getsentry/sentry-react-native/pull/6933))
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
87 changes: 81 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,91 @@ 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.
*
* Expo Router 58 declares `exports`, so Metro warns about any deep path that is
* missing from it. Older versions have no `exports`, so a missing deep path
* there is a silent optional dependency.
*/
export function tryGetExpoRouterStore(): ExpoRouterStore | null {
let getNavigationContainerRef: (() => ExpoRouterNavigationRef | undefined) | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const expoRouter = require('expo-router') as {
useNavigationContainerRef?: () => ExpoRouterNavigationRef | undefined;
};
// Not a React hook: it reads a module-level store, so it is safe to call outside render.
getNavigationContainerRef = expoRouter?.useNavigationContainerRef;
} catch {
return null;
}
if (!getNavigationContainerRef) {
return null;
}
const getRef = getNavigationContainerRef;
const readNavigationContainerRef = (): ExpoRouterNavigationRef | undefined => {
try {
return getRef();
} catch {
return undefined;
}
};
const getRouteInfoFromState = tryGetRouteInfoFromState();

return {
// Before Expo Router 58 the ref is undefined until the Root Layout renders, so read it on every access.
get navigationRef() {
return readNavigationContainerRef();
},
getRouteInfo: getRouteInfoFromState
? () => {
const navigationRef = readNavigationContainerRef();
const state = navigationRef?.current ? navigationRef.getRootState?.() : undefined;
return state ? getRouteInfoFromState(state) : undefined;
}
: undefined,
};
}

/** `getRouteInfoFromState` exists only in Expo Router 56 and later. */
function tryGetRouteInfoFromState(): ((state: unknown) => ExpoRouterUrlObject) | undefined {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mod = require('expo-router/build/global-state/getRouteInfoFromState') as {
getRouteInfoFromState?: (state: unknown) => ExpoRouterUrlObject;
};
return mod?.getRouteInfoFromState;
} catch {
return undefined;
}
}

/**
* 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/router-store') as {
store?: ExpoRouterStore;
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