Skip to content

feat(expo): Add Expo Modules Integration #3466

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7b2c4f7
feat(expo): Dynamically resolve default integrations based on platform
krystofwoldrich Dec 13, 2023
1fad9df
add more tests
krystofwoldrich Dec 13, 2023
59bf2c1
Merge remote-tracking branch 'origin/expo' into kw-add-dynamic-defaul…
krystofwoldrich Dec 13, 2023
e22570f
fix changelog
krystofwoldrich Dec 13, 2023
e55b180
feat(expo): Add Expo Modules Integration
krystofwoldrich Dec 13, 2023
a941cca
disable native nagging on unsupported platforms
krystofwoldrich Dec 13, 2023
c475c85
Add expo modules getters js docs
krystofwoldrich Dec 13, 2023
287e9f3
fix(logs): Do not report native SDK info error if native availability…
krystofwoldrich Dec 13, 2023
5163ae4
Add expo go message
krystofwoldrich Dec 13, 2023
7c1ea8c
update changelog
krystofwoldrich Dec 13, 2023
ecde2d0
Merge branch 'expo' into kw-add-dynamic-default-integrations
krystofwoldrich Jan 9, 2024
6799b06
Merge branch 'kw-add-dynamic-default-integrations' into kw-add-expo-m…
krystofwoldrich Jan 9, 2024
f48adb9
Merge remote-tracking branch 'origin/expo' into kw-add-expo-modules
krystofwoldrich Jan 9, 2024
133e929
Add expo to rect native context, add expo device and os context
krystofwoldrich Jan 9, 2024
19c9c24
WIP! Add Expo context integration tests
krystofwoldrich Jan 9, 2024
5dc939f
Finish expo context integration tests
krystofwoldrich Jan 9, 2024
d43b172
Add merge tests
krystofwoldrich Jan 9, 2024
085337e
Merge branch 'expo' into kw-add-expo-modules
krystofwoldrich Jan 9, 2024
eb26127
Update type source links
krystofwoldrich Jan 9, 2024
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ This release is compatible with `[email protected]` and newer.
- Resolve Default Integrations based on current platform ([#3465](https://github.com/getsentry/sentry-react-native/pull/3465))
- Native Integrations are only added if Native Module is available
- Web Integrations only for React Native Web builds
- Remove Native Modules warning from platform where the absence is expected ([#3466](https://github.com/getsentry/sentry-react-native/pull/3466))
- Add Expo Context information using Expo Native Modules ([#3466](https://github.com/getsentry/sentry-react-native/pull/3466))

### Fixes

Expand Down
7 changes: 6 additions & 1 deletion src/js/integrations/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import type { Integration } from '@sentry/types';
import type { ReactNativeClientOptions } from '../options';
import { HermesProfiling } from '../profiling/integration';
import { ReactNativeTracing } from '../tracing';
import { notWeb } from '../utils/environment';
import { isExpoGo, notWeb } from '../utils/environment';
import { DebugSymbolicator } from './debugsymbolicator';
import { DeviceContext } from './devicecontext';
import { EventOrigin } from './eventorigin';
import { ExpoContext } from './expocontext';
import { ModulesLoader } from './modulesloader';
import { NativeLinkedErrors } from './nativelinkederrors';
import { ReactNativeErrorHandlers } from './reactnativeerrorhandlers';
Expand Down Expand Up @@ -83,5 +84,9 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ
integrations.push(new HttpClient());
}

if (isExpoGo()) {
integrations.push(new ExpoContext());
}

return integrations;
}
78 changes: 78 additions & 0 deletions src/js/integrations/expocontext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { DeviceContext, Event, EventProcessor, Hub, Integration, OsContext } from '@sentry/types';

import { getExpoDevice } from '../utils/expomodules';

/** Load device context from expo modules. */
export class ExpoContext implements Integration {
/**
* @inheritDoc
*/
public static id: string = 'ExpoContext';

/**
* @inheritDoc
*/
public name: string = ExpoContext.id;

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
addGlobalEventProcessor(async (event: Event) => {
const self = getCurrentHub().getIntegration(ExpoContext);
if (!self) {
return event;
}

const expoDeviceContext = getExpoDeviceContext();
if (expoDeviceContext) {
event.contexts = event.contexts || {};
event.contexts.device = { ...expoDeviceContext, ...event.contexts.device };
}

const expoOsContext = getExpoOsContext();
if (expoOsContext) {
event.contexts = event.contexts || {};
event.contexts.os = { ...expoOsContext, ...event.contexts.os };
}

return event;
});
}
}

/**
* Returns the Expo Device context if present
*/
function getExpoDeviceContext(): DeviceContext | undefined {
const expoDevice = getExpoDevice();

if (!expoDevice) {
return undefined;
}

return {
name: expoDevice.deviceName,
simulator: !expoDevice?.isDevice,
model: expoDevice.modelName,
manufacturer: expoDevice.manufacturer,
memory_size: expoDevice.totalMemory,
};
}

/**
* Returns the Expo OS context if present
*/
function getExpoOsContext(): OsContext | undefined {
const expoDevice = getExpoDevice();

if (!expoDevice) {
return undefined;
}

return {
build: expoDevice.osBuildId,
version: expoDevice.osVersion,
name: expoDevice.osName,
};
}
14 changes: 14 additions & 0 deletions src/js/integrations/reactnativeinfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Context, Event, EventHint, EventProcessor, Integration } from '@sentry/types';

import {
getExpoGoVersion,
getExpoSdkVersion,
getHermesVersion,
getReactNativeVersion,
isExpo,
Expand All @@ -19,6 +21,8 @@ export interface ReactNativeContext extends Context {
react_native_version?: string;
component_stack?: string;
hermes_debug_info?: boolean;
expo_go_version?: string;
expo_sdk_version?: string;
}

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

const expoGoVersion = getExpoGoVersion();
if (expoGoVersion) {
reactNativeContext.expo_go_version = expoGoVersion;
}

const expoSdkVersion = getExpoSdkVersion();
if (expoSdkVersion) {
reactNativeContext.expo_sdk_version = expoSdkVersion;
}

event.contexts = {
react_native_context: reactNativeContext,
...event.contexts,
Expand Down
11 changes: 7 additions & 4 deletions src/js/integrations/sdkinfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { EventProcessor, Integration, Package, SdkInfo as SdkInfoType } from '@sentry/types';
import { logger } from '@sentry/utils';

import { isExpoGo, notWeb } from '../utils/environment';
import { SDK_NAME, SDK_PACKAGE_NAME, SDK_VERSION } from '../version';
import { NATIVE } from '../wrapper';

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

Expand Down
27 changes: 27 additions & 0 deletions src/js/options.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { BrowserTransportOptions } from '@sentry/browser/types/transports/types';
import type { ProfilerProps } from '@sentry/react/types/profiler';
import type { CaptureContext, ClientOptions, Options } from '@sentry/types';
import { Platform } from 'react-native';

import type { TouchEventBoundaryProps } from './touchevents';
import { getExpoConstants } from './utils/expomodules';

export interface BaseReactNativeOptions {
/**
Expand Down Expand Up @@ -182,3 +184,28 @@ export interface ReactNativeWrapperOptions {
/** Props for the root touch event boundary */
touchEventBoundaryProps?: TouchEventBoundaryProps;
}

/**
* If the user has not explicitly set `enableNativeNagger`
* the function enables native nagging based on the current
* environment.
*/
export function shouldEnableNativeNagger(userOptions: unknown): boolean {
if (typeof userOptions === 'boolean') {
// User can override the default behavior
return userOptions;
}

if (Platform.OS === 'web' || Platform.OS === 'windows') {
// We don't want to nag on known platforms that don't support native
return false;
}

const expoConstants = getExpoConstants();
if (expoConstants && expoConstants.appOwnership === 'expo') {
// If the app is running in Expo Go, we don't want to nag
return false;
}

return true;
}
9 changes: 8 additions & 1 deletion src/js/sdk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import * as React from 'react';
import { ReactNativeClient } from './client';
import { getDefaultIntegrations } from './integrations/default';
import type { ReactNativeClientOptions, ReactNativeOptions, ReactNativeWrapperOptions } from './options';
import { shouldEnableNativeNagger } from './options';
import { ReactNativeScope } from './scope';
import { TouchEventBoundary } from './touchevents';
import { ReactNativeProfiler, ReactNativeTracing } from './tracing';
import { DEFAULT_BUFFER_SIZE, makeNativeTransportFactory } from './transports/native';
import { makeUtf8TextEncoder } from './transports/TextEncoder';
import { getDefaultEnvironment } from './utils/environment';
import { getDefaultEnvironment, isExpoGo } from './utils/environment';
import { safeFactory, safeTracesSampler } from './utils/safe';
import { NATIVE } from './wrapper';

Expand Down Expand Up @@ -58,6 +59,7 @@ export function init(passedOptions: ReactNativeOptions): void {
...DEFAULT_OPTIONS,
...passedOptions,
enableNative,
enableNativeNagger: shouldEnableNativeNagger(passedOptions.enableNativeNagger),
// If custom transport factory fails the SDK won't initialize
transport: passedOptions.transport
|| makeNativeTransportFactory({
Expand Down Expand Up @@ -89,6 +91,11 @@ export function init(passedOptions: ReactNativeOptions): void {
defaultIntegrations,
});
initAndBind(ReactNativeClient, options);

if (isExpoGo()) {
logger.info('Offline caching, native errors features are not available in Expo Go.');
logger.info('Use EAS Build / Native Release Build to test these features.');
}
}

/**
Expand Down
21 changes: 21 additions & 0 deletions src/js/utils/environment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Platform } from 'react-native';

import { RN_GLOBAL_OBJ } from '../utils/worldwide';
import { getExpoConstants } from './expomodules';
import { ReactNativeLibraries } from './rnlibraries';

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

/** Check if JS runs in Expo Go */
export function isExpoGo(): boolean {
const expoConstants = getExpoConstants();
return (expoConstants && expoConstants.appOwnership) === 'expo';
}

/** Check Expo Go version if available */
export function getExpoGoVersion(): string | undefined {
const expoConstants = getExpoConstants();
return typeof expoConstants?.expoVersion === 'string' ? expoConstants.expoVersion : undefined;
}

/** Returns Expo SDK version if available */
export function getExpoSdkVersion(): string | undefined {
const expoConstants = getExpoConstants();
const [, expoSdkVersion] =
typeof expoConstants?.manifest?.runtimeVersion === 'string' ? expoConstants.manifest.runtimeVersion.split(':') : [];
return expoSdkVersion;
}

/** Checks if the current platform is not web */
export function notWeb(): boolean {
return Platform.OS !== 'web';
Expand Down
52 changes: 52 additions & 0 deletions src/js/utils/expoglobalobject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Interface from the Expo SDK defined here
* (we are describing the native Module
* the TS typing is only guideline):
*
* https://github.com/expo/expo/blob/b51b5139f2caa2a9495e4132437d7ca612276158/packages/expo-constants/src/Constants.ts
* https://github.com/expo/expo/blob/b51b5139f2caa2a9495e4132437d7ca612276158/packages/expo-manifests/src/Manifests.ts
*/
export interface ExpoConstants {
appOwnership?: 'standalone' | 'expo' | 'guest';
/**
* Deprecated. But until removed we can use it as user ID to match the native SDKs.
*/
installationId?: string;
/**
* Version of the Expo Go app
*/
expoVersion?: string | null;
manifest?: null | {
[key: string]: unknown;
/**
* Expo SDK version should match `expo` version from the app `package.json`.
* Example "exposdk:50.0.0"
*/
runtimeVersion?: string;
};
}

/**
* Interface from the Expo SDK defined here
* (we are describing the native module
* the TS typing is only guideline)
*
* https://github.com/expo/expo/blob/5d1153e6ae7c497fa1281ffee85fabe90d2321c2/packages/expo-device/src/Device.ts
*/
export interface ExpoDevice {
deviceName?: string;
isDevice?: boolean;
manufacturer?: string;
modelName?: string;
osName?: string;
osBuildId?: string;
osVersion?: string;
totalMemory?: number;
}

export interface ExpoGlobalObject {
modules?: {
ExponentConstants?: ExpoConstants;
ExpoDevice?: ExpoDevice;
};
}
18 changes: 18 additions & 0 deletions src/js/utils/expomodules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ExpoConstants, ExpoDevice } from './expoglobalobject';
import { RN_GLOBAL_OBJ } from './worldwide';

/**
* Returns the Expo Constants module if present
*/
export function getExpoConstants(): ExpoConstants | undefined {
return (
(RN_GLOBAL_OBJ.expo && RN_GLOBAL_OBJ.expo.modules && RN_GLOBAL_OBJ.expo.modules.ExponentConstants) || undefined
);
}

/**
* Returns the Expo Device module if present
*/
export function getExpoDevice(): ExpoDevice | undefined {
return (RN_GLOBAL_OBJ.expo && RN_GLOBAL_OBJ.expo.modules && RN_GLOBAL_OBJ.expo.modules.ExpoDevice) || undefined;
}
4 changes: 3 additions & 1 deletion src/js/utils/worldwide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { InternalGlobal } from '@sentry/utils';
import { GLOBAL_OBJ } from '@sentry/utils';
import type { ErrorUtils } from 'react-native/types';

import type { ExpoGlobalObject } from './expoglobalobject';

/** Internal Global object interface with common and Sentry specific properties */
export interface ReactNativeInternalGlobal extends InternalGlobal {
__sentry_rn_v4_registered?: boolean;
Expand All @@ -13,7 +15,7 @@ export interface ReactNativeInternalGlobal extends InternalGlobal {
__turboModuleProxy: unknown;
nativeFabricUIManager: unknown;
ErrorUtils?: ErrorUtils;
expo: unknown;
expo?: ExpoGlobalObject;
}

/** Get's the global object for the current JavaScript runtime */
Expand Down
Loading