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 @@ -10,6 +10,7 @@

### Fixes

- Detect `expo/fetch` in Expo Router apps on `@expo/metro-runtime` versions that hide it, so fetch tracing and fetch breadcrumbs are enabled by default ([#6965](https://github.com/getsentry/sentry-react-native/pull/6965))
- Enable native logs on Android when `enableLogs` is set ([#6967](https://github.com/getsentry/sentry-react-native/pull/6967))

## 8.33.0
Expand Down
42 changes: 40 additions & 2 deletions packages/core/src/js/utils/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { RN_GLOBAL_OBJ } from '../utils/worldwide';
import { getExpoConstants, getExpoGo } from './expomodules';
import { ReactNativeLibraries } from './rnlibraries';

// Provided by Metro. Declared here because the SDK isn't built with Node types.
declare const process: { env: Record<string, string | undefined> };

/** Checks if the React Native Hermes engine is running */
export function isHermesEnabled(): boolean {
return !!RN_GLOBAL_OBJ.HermesInternal;
Expand Down Expand Up @@ -66,12 +69,47 @@ export function isWeb(): boolean {
* fetch instrumentation instead of relying on XHR instrumentation.
*/
export function isExpoFetchEnabled(): boolean {
if (typeof globalThis.fetch !== 'function') {
return false;
}
const fetch = globalThis.fetch as unknown as Record<symbol, unknown>;
if (fetch[Symbol.for('expo.builtin')] === true) {
return true;
}
// `@expo/metro-runtime` < 58.0.15 wraps fetch in Expo Router apps and drops the `expo.builtin` marker.
// Behind the wrapper, `expo/fetch` is active on Expo SDK 56+ unless the app opted out.
return (
typeof globalThis.fetch === 'function' &&
(globalThis.fetch as unknown as Record<symbol, unknown>)[Symbol.for('expo.builtin')] === true
fetch[Symbol.for('expo.polyfillFetchWithWindowLocation')] === true &&
(getExpoSdkMajorVersion() ?? 0) >= 56 &&
!isReactNativeFetchForced()
);
}

function getExpoSdkMajorVersion(): number | undefined {
// The native module exposes the embedded app config as `manifest` (a JSON string on Android).
// `expoConfig` is only built by the `expo-constants` JS module.
const manifest: unknown = getExpoConstants()?.manifest;
let sdkVersion: unknown;
try {
const appConfig = typeof manifest === 'string' ? JSON.parse(manifest) : manifest;
sdkVersion = (appConfig as { sdkVersion?: unknown } | null | undefined)?.sdkVersion;
} catch {
return undefined;
}
const major = typeof sdkVersion === 'string' ? parseInt(sdkVersion, 10) : NaN;
return isNaN(major) ? undefined : major;
}

function isReactNativeFetchForced(): boolean {
try {
// Read exactly like `expo`'s runtime does, so both resolve the same value after babel-preset-expo.
const useRnFetch = process.env.EXPO_PUBLIC_USE_RN_FETCH;
return useRnFetch === '1' || useRnFetch === 'true';
} catch {
return false;
}
}

/**
* Checks if React Native reports `resource` performance entries for its network requests.
* Reported by default since React Native 0.83 (0.81 and 0.82 have it behind a feature flag).
Expand Down
77 changes: 77 additions & 0 deletions packages/core/test/utils/environment.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isExpoFetchEnabled, isResourceTimingSupported } from '../../src/js/utils/environment';
import { ReactNativeLibraries } from '../../src/js/utils/rnlibraries';
import { RN_GLOBAL_OBJ } from '../../src/js/utils/worldwide';

describe('isExpoFetchEnabled', () => {
const originalFetch = globalThis.fetch;
Expand Down Expand Up @@ -43,6 +44,82 @@ describe('isExpoFetchEnabled', () => {
globalThis.fetch = expoFetch;
expect(isExpoFetchEnabled()).toBe(false);
});

describe('with the Expo Router fetch wrapper', () => {
const originalExpo = RN_GLOBAL_OBJ.expo;

const mockRouterFetchWrapper = (): void => {
const wrapper = jest.fn() as unknown as typeof fetch;
Object.defineProperty(wrapper, Symbol.for('expo.polyfillFetchWithWindowLocation'), { value: true });
globalThis.fetch = wrapper;
};

const mockExpoManifest = (manifest: unknown): void => {
RN_GLOBAL_OBJ.expo = {
modules: { ExponentConstants: { manifest } },
} as typeof RN_GLOBAL_OBJ.expo;
};

const mockExpoSdkVersion = (sdkVersion: string | undefined): void => {
mockExpoManifest({ sdkVersion });
};

afterEach(() => {
RN_GLOBAL_OBJ.expo = originalExpo;
delete process.env.EXPO_PUBLIC_USE_RN_FETCH;
});

it.each(['56.0.0', '57.0.0', '58.0.0'])('returns true on Expo SDK %s', sdkVersion => {
mockRouterFetchWrapper();
mockExpoSdkVersion(sdkVersion);

expect(isExpoFetchEnabled()).toBe(true);
});

it('returns false on Expo SDK 55, which does not install expo/fetch', () => {
mockRouterFetchWrapper();
mockExpoSdkVersion('55.0.0');

expect(isExpoFetchEnabled()).toBe(false);
});

it('reads the Expo SDK version from a JSON string manifest (Android)', () => {
mockRouterFetchWrapper();
mockExpoManifest(JSON.stringify({ sdkVersion: '57.0.0' }));

expect(isExpoFetchEnabled()).toBe(true);
});

it('returns false when the manifest is not valid JSON', () => {
mockRouterFetchWrapper();
mockExpoManifest('{');

expect(isExpoFetchEnabled()).toBe(false);
});

it('returns false when the Expo SDK version is unknown', () => {
mockRouterFetchWrapper();
mockExpoSdkVersion(undefined);

expect(isExpoFetchEnabled()).toBe(false);
});

it.each(['1', 'true'])('returns false when EXPO_PUBLIC_USE_RN_FETCH is %s', value => {
mockRouterFetchWrapper();
mockExpoSdkVersion('57.0.0');
process.env.EXPO_PUBLIC_USE_RN_FETCH = value;

expect(isExpoFetchEnabled()).toBe(false);
});

it('returns true when EXPO_PUBLIC_USE_RN_FETCH is not an opt-in value', () => {
mockRouterFetchWrapper();
mockExpoSdkVersion('57.0.0');
process.env.EXPO_PUBLIC_USE_RN_FETCH = '0';

expect(isExpoFetchEnabled()).toBe(true);
});
});
});

describe('isResourceTimingSupported', () => {
Expand Down
Loading