Skip to content

Commit 4fb79c3

Browse files
feat(expo): Add Expo Modules Integration (#3466)
1 parent a48c9b5 commit 4fb79c3

14 files changed

+403
-9
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ This release is compatible with `[email protected]` and newer.
3030
- Resolve Default Integrations based on current platform ([#3465](https://github.com/getsentry/sentry-react-native/pull/3465))
3131
- Native Integrations are only added if Native Module is available
3232
- Web Integrations only for React Native Web builds
33+
- Remove Native Modules warning from platform where the absence is expected ([#3466](https://github.com/getsentry/sentry-react-native/pull/3466))
34+
- Add Expo Context information using Expo Native Modules ([#3466](https://github.com/getsentry/sentry-react-native/pull/3466))
3335

3436
### Fixes
3537

src/js/integrations/default.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import type { Integration } from '@sentry/types';
66
import type { ReactNativeClientOptions } from '../options';
77
import { HermesProfiling } from '../profiling/integration';
88
import { ReactNativeTracing } from '../tracing';
9-
import { notWeb } from '../utils/environment';
9+
import { isExpoGo, notWeb } from '../utils/environment';
1010
import { DebugSymbolicator } from './debugsymbolicator';
1111
import { DeviceContext } from './devicecontext';
1212
import { EventOrigin } from './eventorigin';
13+
import { ExpoContext } from './expocontext';
1314
import { ModulesLoader } from './modulesloader';
1415
import { NativeLinkedErrors } from './nativelinkederrors';
1516
import { ReactNativeErrorHandlers } from './reactnativeerrorhandlers';
@@ -83,5 +84,9 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ
8384
integrations.push(new HttpClient());
8485
}
8586

87+
if (isExpoGo()) {
88+
integrations.push(new ExpoContext());
89+
}
90+
8691
return integrations;
8792
}

src/js/integrations/expocontext.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { DeviceContext, Event, EventProcessor, Hub, Integration, OsContext } from '@sentry/types';
2+
3+
import { getExpoDevice } from '../utils/expomodules';
4+
5+
/** Load device context from expo modules. */
6+
export class ExpoContext implements Integration {
7+
/**
8+
* @inheritDoc
9+
*/
10+
public static id: string = 'ExpoContext';
11+
12+
/**
13+
* @inheritDoc
14+
*/
15+
public name: string = ExpoContext.id;
16+
17+
/**
18+
* @inheritDoc
19+
*/
20+
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
21+
addGlobalEventProcessor(async (event: Event) => {
22+
const self = getCurrentHub().getIntegration(ExpoContext);
23+
if (!self) {
24+
return event;
25+
}
26+
27+
const expoDeviceContext = getExpoDeviceContext();
28+
if (expoDeviceContext) {
29+
event.contexts = event.contexts || {};
30+
event.contexts.device = { ...expoDeviceContext, ...event.contexts.device };
31+
}
32+
33+
const expoOsContext = getExpoOsContext();
34+
if (expoOsContext) {
35+
event.contexts = event.contexts || {};
36+
event.contexts.os = { ...expoOsContext, ...event.contexts.os };
37+
}
38+
39+
return event;
40+
});
41+
}
42+
}
43+
44+
/**
45+
* Returns the Expo Device context if present
46+
*/
47+
function getExpoDeviceContext(): DeviceContext | undefined {
48+
const expoDevice = getExpoDevice();
49+
50+
if (!expoDevice) {
51+
return undefined;
52+
}
53+
54+
return {
55+
name: expoDevice.deviceName,
56+
simulator: !expoDevice?.isDevice,
57+
model: expoDevice.modelName,
58+
manufacturer: expoDevice.manufacturer,
59+
memory_size: expoDevice.totalMemory,
60+
};
61+
}
62+
63+
/**
64+
* Returns the Expo OS context if present
65+
*/
66+
function getExpoOsContext(): OsContext | undefined {
67+
const expoDevice = getExpoDevice();
68+
69+
if (!expoDevice) {
70+
return undefined;
71+
}
72+
73+
return {
74+
build: expoDevice.osBuildId,
75+
version: expoDevice.osVersion,
76+
name: expoDevice.osName,
77+
};
78+
}

src/js/integrations/reactnativeinfo.ts

+14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Context, Event, EventHint, EventProcessor, Integration } from '@sentry/types';
22

33
import {
4+
getExpoGoVersion,
5+
getExpoSdkVersion,
46
getHermesVersion,
57
getReactNativeVersion,
68
isExpo,
@@ -19,6 +21,8 @@ export interface ReactNativeContext extends Context {
1921
react_native_version?: string;
2022
component_stack?: string;
2123
hermes_debug_info?: boolean;
24+
expo_go_version?: string;
25+
expo_sdk_version?: string;
2226
}
2327

2428
/** Loads React Native context at runtime */
@@ -69,6 +73,16 @@ export class ReactNativeInfo implements Integration {
6973
reactNativeContext.component_stack = reactNativeError.componentStack;
7074
}
7175

76+
const expoGoVersion = getExpoGoVersion();
77+
if (expoGoVersion) {
78+
reactNativeContext.expo_go_version = expoGoVersion;
79+
}
80+
81+
const expoSdkVersion = getExpoSdkVersion();
82+
if (expoSdkVersion) {
83+
reactNativeContext.expo_sdk_version = expoSdkVersion;
84+
}
85+
7286
event.contexts = {
7387
react_native_context: reactNativeContext,
7488
...event.contexts,

src/js/integrations/sdkinfo.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { EventProcessor, Integration, Package, SdkInfo as SdkInfoType } from '@sentry/types';
22
import { logger } from '@sentry/utils';
33

4+
import { isExpoGo, notWeb } from '../utils/environment';
45
import { SDK_NAME, SDK_PACKAGE_NAME, SDK_VERSION } from '../version';
56
import { NATIVE } from '../wrapper';
67

@@ -42,10 +43,12 @@ export class SdkInfo implements Integration {
4243
this._nativeSdkPackage = await NATIVE.fetchNativeSdkInfo();
4344
} catch (e) {
4445
// If this fails, go ahead as usual as we would rather have the event be sent with a package missing.
45-
logger.warn(
46-
'[SdkInfo] Native SDK Info retrieval failed...something could be wrong with your Sentry installation:',
47-
);
48-
logger.warn(e);
46+
if (notWeb() && !isExpoGo()) {
47+
logger.warn(
48+
'[SdkInfo] Native SDK Info retrieval failed...something could be wrong with your Sentry installation:',
49+
);
50+
logger.warn(e);
51+
}
4952
}
5053
}
5154

src/js/options.ts

+27
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { BrowserTransportOptions } from '@sentry/browser/types/transports/types';
22
import type { ProfilerProps } from '@sentry/react/types/profiler';
33
import type { CaptureContext, ClientOptions, Options } from '@sentry/types';
4+
import { Platform } from 'react-native';
45

56
import type { TouchEventBoundaryProps } from './touchevents';
7+
import { getExpoConstants } from './utils/expomodules';
68

79
export interface BaseReactNativeOptions {
810
/**
@@ -182,3 +184,28 @@ export interface ReactNativeWrapperOptions {
182184
/** Props for the root touch event boundary */
183185
touchEventBoundaryProps?: TouchEventBoundaryProps;
184186
}
187+
188+
/**
189+
* If the user has not explicitly set `enableNativeNagger`
190+
* the function enables native nagging based on the current
191+
* environment.
192+
*/
193+
export function shouldEnableNativeNagger(userOptions: unknown): boolean {
194+
if (typeof userOptions === 'boolean') {
195+
// User can override the default behavior
196+
return userOptions;
197+
}
198+
199+
if (Platform.OS === 'web' || Platform.OS === 'windows') {
200+
// We don't want to nag on known platforms that don't support native
201+
return false;
202+
}
203+
204+
const expoConstants = getExpoConstants();
205+
if (expoConstants && expoConstants.appOwnership === 'expo') {
206+
// If the app is running in Expo Go, we don't want to nag
207+
return false;
208+
}
209+
210+
return true;
211+
}

src/js/sdk.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import * as React from 'react';
1313
import { ReactNativeClient } from './client';
1414
import { getDefaultIntegrations } from './integrations/default';
1515
import type { ReactNativeClientOptions, ReactNativeOptions, ReactNativeWrapperOptions } from './options';
16+
import { shouldEnableNativeNagger } from './options';
1617
import { ReactNativeScope } from './scope';
1718
import { TouchEventBoundary } from './touchevents';
1819
import { ReactNativeProfiler, ReactNativeTracing } from './tracing';
1920
import { DEFAULT_BUFFER_SIZE, makeNativeTransportFactory } from './transports/native';
2021
import { makeUtf8TextEncoder } from './transports/TextEncoder';
21-
import { getDefaultEnvironment } from './utils/environment';
22+
import { getDefaultEnvironment, isExpoGo } from './utils/environment';
2223
import { safeFactory, safeTracesSampler } from './utils/safe';
2324
import { NATIVE } from './wrapper';
2425

@@ -58,6 +59,7 @@ export function init(passedOptions: ReactNativeOptions): void {
5859
...DEFAULT_OPTIONS,
5960
...passedOptions,
6061
enableNative,
62+
enableNativeNagger: shouldEnableNativeNagger(passedOptions.enableNativeNagger),
6163
// If custom transport factory fails the SDK won't initialize
6264
transport: passedOptions.transport
6365
|| makeNativeTransportFactory({
@@ -89,6 +91,11 @@ export function init(passedOptions: ReactNativeOptions): void {
8991
defaultIntegrations,
9092
});
9193
initAndBind(ReactNativeClient, options);
94+
95+
if (isExpoGo()) {
96+
logger.info('Offline caching, native errors features are not available in Expo Go.');
97+
logger.info('Use EAS Build / Native Release Build to test these features.');
98+
}
9299
}
93100

94101
/**

src/js/utils/environment.ts

+21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Platform } from 'react-native';
22

33
import { RN_GLOBAL_OBJ } from '../utils/worldwide';
4+
import { getExpoConstants } from './expomodules';
45
import { ReactNativeLibraries } from './rnlibraries';
56

67
/** Checks if the React Native Hermes engine is running */
@@ -32,6 +33,26 @@ export function isExpo(): boolean {
3233
return RN_GLOBAL_OBJ.expo != null;
3334
}
3435

36+
/** Check if JS runs in Expo Go */
37+
export function isExpoGo(): boolean {
38+
const expoConstants = getExpoConstants();
39+
return (expoConstants && expoConstants.appOwnership) === 'expo';
40+
}
41+
42+
/** Check Expo Go version if available */
43+
export function getExpoGoVersion(): string | undefined {
44+
const expoConstants = getExpoConstants();
45+
return typeof expoConstants?.expoVersion === 'string' ? expoConstants.expoVersion : undefined;
46+
}
47+
48+
/** Returns Expo SDK version if available */
49+
export function getExpoSdkVersion(): string | undefined {
50+
const expoConstants = getExpoConstants();
51+
const [, expoSdkVersion] =
52+
typeof expoConstants?.manifest?.runtimeVersion === 'string' ? expoConstants.manifest.runtimeVersion.split(':') : [];
53+
return expoSdkVersion;
54+
}
55+
3556
/** Checks if the current platform is not web */
3657
export function notWeb(): boolean {
3758
return Platform.OS !== 'web';

src/js/utils/expoglobalobject.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Interface from the Expo SDK defined here
3+
* (we are describing the native Module
4+
* the TS typing is only guideline):
5+
*
6+
* https://github.com/expo/expo/blob/b51b5139f2caa2a9495e4132437d7ca612276158/packages/expo-constants/src/Constants.ts
7+
* https://github.com/expo/expo/blob/b51b5139f2caa2a9495e4132437d7ca612276158/packages/expo-manifests/src/Manifests.ts
8+
*/
9+
export interface ExpoConstants {
10+
appOwnership?: 'standalone' | 'expo' | 'guest';
11+
/**
12+
* Deprecated. But until removed we can use it as user ID to match the native SDKs.
13+
*/
14+
installationId?: string;
15+
/**
16+
* Version of the Expo Go app
17+
*/
18+
expoVersion?: string | null;
19+
manifest?: null | {
20+
[key: string]: unknown;
21+
/**
22+
* Expo SDK version should match `expo` version from the app `package.json`.
23+
* Example "exposdk:50.0.0"
24+
*/
25+
runtimeVersion?: string;
26+
};
27+
}
28+
29+
/**
30+
* Interface from the Expo SDK defined here
31+
* (we are describing the native module
32+
* the TS typing is only guideline)
33+
*
34+
* https://github.com/expo/expo/blob/5d1153e6ae7c497fa1281ffee85fabe90d2321c2/packages/expo-device/src/Device.ts
35+
*/
36+
export interface ExpoDevice {
37+
deviceName?: string;
38+
isDevice?: boolean;
39+
manufacturer?: string;
40+
modelName?: string;
41+
osName?: string;
42+
osBuildId?: string;
43+
osVersion?: string;
44+
totalMemory?: number;
45+
}
46+
47+
export interface ExpoGlobalObject {
48+
modules?: {
49+
ExponentConstants?: ExpoConstants;
50+
ExpoDevice?: ExpoDevice;
51+
};
52+
}

src/js/utils/expomodules.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { ExpoConstants, ExpoDevice } from './expoglobalobject';
2+
import { RN_GLOBAL_OBJ } from './worldwide';
3+
4+
/**
5+
* Returns the Expo Constants module if present
6+
*/
7+
export function getExpoConstants(): ExpoConstants | undefined {
8+
return (
9+
(RN_GLOBAL_OBJ.expo && RN_GLOBAL_OBJ.expo.modules && RN_GLOBAL_OBJ.expo.modules.ExponentConstants) || undefined
10+
);
11+
}
12+
13+
/**
14+
* Returns the Expo Device module if present
15+
*/
16+
export function getExpoDevice(): ExpoDevice | undefined {
17+
return (RN_GLOBAL_OBJ.expo && RN_GLOBAL_OBJ.expo.modules && RN_GLOBAL_OBJ.expo.modules.ExpoDevice) || undefined;
18+
}

src/js/utils/worldwide.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { InternalGlobal } from '@sentry/utils';
22
import { GLOBAL_OBJ } from '@sentry/utils';
33
import type { ErrorUtils } from 'react-native/types';
44

5+
import type { ExpoGlobalObject } from './expoglobalobject';
6+
57
/** Internal Global object interface with common and Sentry specific properties */
68
export interface ReactNativeInternalGlobal extends InternalGlobal {
79
__sentry_rn_v4_registered?: boolean;
@@ -13,7 +15,7 @@ export interface ReactNativeInternalGlobal extends InternalGlobal {
1315
__turboModuleProxy: unknown;
1416
nativeFabricUIManager: unknown;
1517
ErrorUtils?: ErrorUtils;
16-
expo: unknown;
18+
expo?: ExpoGlobalObject;
1719
}
1820

1921
/** Get's the global object for the current JavaScript runtime */

0 commit comments

Comments
 (0)