Skip to content

feat(expo): Add RNSentrySDK APIs support to @sentry/react-native/expo plugin #4633

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

Open
wants to merge 30 commits into
base: capture-app-start-errors
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
313e844
useNativeInit Android implementation
antonis Mar 7, 2025
2e97acc
Adds changelog
antonis Mar 7, 2025
6eedaae
useNativeInit iOS implementation
antonis Mar 7, 2025
9ae5475
Fix indentation
antonis Mar 7, 2025
566550e
Extend test cases with realistic data
antonis Mar 7, 2025
770c9f4
Adds code sample in the changelog
antonis Mar 7, 2025
f8b37b5
Fix CHANGELOG.md
antonis Apr 4, 2025
d25db30
Warn if RESentySDK.init/start wasn't injected
antonis Apr 4, 2025
adc81a5
Make useNativeInit opt-in
antonis Apr 15, 2025
8c2cd73
Make Android failure warning more clear
antonis Apr 15, 2025
a2b5575
Make Android no update warning more clear
antonis Apr 15, 2025
5f4f7c5
Use path.basename to get last path component
antonis Apr 15, 2025
0431cc3
Update tests to account for the new warnings
antonis Apr 15, 2025
62d39cc
Explicitly check for kotlin
antonis Apr 16, 2025
235f3ef
Add filename in the warning message
antonis Apr 16, 2025
369cce7
Import only if init injection succeeds
antonis Apr 16, 2025
a53c7f4
Explicitly check for Objective-C
antonis Apr 16, 2025
5e4a98f
Add filename in the warning
antonis Apr 16, 2025
dce74b2
Make iOS file not found warning more clear
antonis Apr 16, 2025
0ffd26c
Import only if init injection succeeds
antonis Apr 16, 2025
744993c
Reset test mock config in a function
antonis Apr 16, 2025
5447be9
Lint issue
antonis Apr 16, 2025
0b3423f
Add missing quote
antonis Apr 24, 2025
5c615fd
Remove unneeded async
antonis Jun 10, 2025
c356288
Set useNativeInit = false by default
antonis Jun 10, 2025
8e32556
dynamically fill white spaces
antonis Jun 10, 2025
a20984c
Add unsupported language in warning message
antonis Jun 11, 2025
d1db4fa
Add objcpp in detected languages
antonis Jun 11, 2025
7c25c2c
Merge branch 'antonis/4625-expo-useNativeInit' of https://github.com/…
antonis Jun 11, 2025
1918baf
Update tests for objcpp
antonis Jun 11, 2025
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 @@

### Features

- Add RNSentrySDK APIs support to @sentry/react-native/expo plugin ([#4633](https://github.com/getsentry/sentry-react-native/pull/4633))
- User Feedback Widget Beta ([#4435](https://github.com/getsentry/sentry-react-native/pull/4435))

To collect user feedback from inside your application call `Sentry.showFeedbackWidget()`.
Expand Down
5 changes: 3 additions & 2 deletions packages/core/plugin/src/withSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface PluginProps {
project?: string;
authToken?: string;
url?: string;
useNativeInit?: boolean;
experimental_android?: SentryAndroidGradlePluginOptions;
}

Expand All @@ -26,7 +27,7 @@ const withSentryPlugin: ConfigPlugin<PluginProps | void> = (config, props) => {
let cfg = config;
if (sentryProperties !== null) {
try {
cfg = withSentryAndroid(cfg, sentryProperties);
cfg = withSentryAndroid(cfg, { sentryProperties, useNativeInit: props?.useNativeInit });
} catch (e) {
warnOnce(`There was a problem with configuring your native Android project: ${e}`);
}
Expand All @@ -39,7 +40,7 @@ const withSentryPlugin: ConfigPlugin<PluginProps | void> = (config, props) => {
}
}
try {
cfg = withSentryIOS(cfg, sentryProperties);
cfg = withSentryIOS(cfg, { sentryProperties, useNativeInit: props?.useNativeInit });
} catch (e) {
warnOnce(`There was a problem with configuring your native iOS project: ${e}`);
}
Expand Down
71 changes: 67 additions & 4 deletions packages/core/plugin/src/withSentryAndroid.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import type { ExpoConfig } from '@expo/config-types';
import type { ConfigPlugin } from 'expo/config-plugins';
import { withAppBuildGradle, withDangerousMod } from 'expo/config-plugins';
import { withAppBuildGradle, withDangerousMod, withMainApplication } from 'expo/config-plugins';
import * as path from 'path';

import { warnOnce, writeSentryPropertiesTo } from './utils';

export const withSentryAndroid: ConfigPlugin<string> = (config, sentryProperties: string) => {
const cfg = withAppBuildGradle(config, config => {
export const withSentryAndroid: ConfigPlugin<{ sentryProperties: string; useNativeInit: boolean | undefined }> = (
config,
{ sentryProperties, useNativeInit = false },
) => {
const appBuildGradleCfg = withAppBuildGradle(config, config => {
if (config.modResults.language === 'groovy') {
config.modResults.contents = modifyAppBuildGradle(config.modResults.contents);
} else {
throw new Error('Cannot configure Sentry in the app gradle because the build.gradle is not groovy');
}
return config;
});
return withDangerousMod(cfg, [

const mainApplicationCfg = useNativeInit ? modifyMainApplication(appBuildGradleCfg) : appBuildGradleCfg;

return withDangerousMod(mainApplicationCfg, [
'android',
config => {
writeSentryPropertiesTo(path.resolve(config.modRequest.projectRoot, 'android'), sentryProperties);
Expand Down Expand Up @@ -49,3 +56,59 @@ export function modifyAppBuildGradle(buildGradle: string): string {

return buildGradle.replace(pattern, match => `${applyFrom}\n\n${match}`);
}

export function modifyMainApplication(config: ExpoConfig): ExpoConfig {
return withMainApplication(config, config => {
if (!config.modResults || !config.modResults.path) {
warnOnce("Can't add 'RNSentrySDK.init' to Android MainApplication, because the file was not found.");
return config;
}

const fileName = path.basename(config.modResults.path);

if (config.modResults.contents.includes('RNSentrySDK.init')) {
warnOnce(`Your '${fileName}' already contains 'RNSentrySDK.init', the native code won't be updated.`);
return config;
}

if (config.modResults.language === 'java') {
// Add RNSentrySDK.init
const originalContents = config.modResults.contents;
config.modResults.contents = config.modResults.contents.replace(
/(super\.onCreate\(\)[;\n]*)([ \t]*)/,
`$1\n$2RNSentrySDK.init(this);\n$2`,
);
if (config.modResults.contents === originalContents) {
warnOnce(`Failed to insert 'RNSentrySDK.init' in '${fileName}'.`);
} else if (!config.modResults.contents.includes('import io.sentry.react.RNSentrySDK;')) {
// Insert import statement after package declaration
config.modResults.contents = config.modResults.contents.replace(
/(package .*;\n\n?)/,
`$1import io.sentry.react.RNSentrySDK;\n`,
);
}
} else if (config.modResults.language === 'kt') {
// Add RNSentrySDK.init
const originalContents = config.modResults.contents;
config.modResults.contents = config.modResults.contents.replace(
/(super\.onCreate\(\)[;\n]*)([ \t]*)/,
`$1\n$2RNSentrySDK.init(this)\n$2`,
);
if (config.modResults.contents === originalContents) {
warnOnce(`Failed to insert 'RNSentrySDK.init' in '${fileName}'.`);
} else if (!config.modResults.contents.includes('import io.sentry.react.RNSentrySDK')) {
// Insert import statement after package declaration
config.modResults.contents = config.modResults.contents.replace(
/(package .*\n\n?)/,
`$1import io.sentry.react.RNSentrySDK\n`,
);
}
} else {
warnOnce(
`Unsupported language '${config.modResults.language}' detected in '${fileName}', the native code won't be updated.`,
);
}

return config;
});
}
70 changes: 66 additions & 4 deletions packages/core/plugin/src/withSentryIOS.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ExpoConfig } from '@expo/config-types';
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import type { ConfigPlugin, XcodeProject } from 'expo/config-plugins';
import { withDangerousMod, withXcodeProject } from 'expo/config-plugins';
import { withAppDelegate, withDangerousMod, withXcodeProject } from 'expo/config-plugins';
import * as path from 'path';

import { warnOnce, writeSentryPropertiesTo } from './utils';
Expand All @@ -12,8 +13,11 @@ const SENTRY_REACT_NATIVE_XCODE_PATH =
const SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH =
"`${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";

export const withSentryIOS: ConfigPlugin<string> = (config, sentryProperties: string) => {
const cfg = withXcodeProject(config, config => {
export const withSentryIOS: ConfigPlugin<{ sentryProperties: string; useNativeInit: boolean | undefined }> = (
config,
{ sentryProperties, useNativeInit = false },
) => {
const xcodeProjectCfg = withXcodeProject(config, config => {
const xcodeProject: XcodeProject = config.modResults;

const sentryBuildPhase = xcodeProject.pbxItemByComment(
Expand All @@ -36,7 +40,9 @@ export const withSentryIOS: ConfigPlugin<string> = (config, sentryProperties: st
return config;
});

return withDangerousMod(cfg, [
const appDelegateCfc = useNativeInit ? modifyAppDelegate(xcodeProjectCfg) : xcodeProjectCfg;

return withDangerousMod(appDelegateCfc, [
'ios',
config => {
writeSentryPropertiesTo(path.resolve(config.modRequest.projectRoot, 'ios'), sentryProperties);
Expand Down Expand Up @@ -79,3 +85,59 @@ export function addSentryWithBundledScriptsToBundleShellScript(script: string):
(match: string) => `/bin/sh ${SENTRY_REACT_NATIVE_XCODE_PATH} ${match}`,
);
}

export function modifyAppDelegate(config: ExpoConfig): ExpoConfig {
return withAppDelegate(config, async config => {
if (!config.modResults || !config.modResults.path) {
warnOnce("Can't add 'RNSentrySDK.start()' to the iOS AppDelegate, because the file was not found.");
return config;
}

const fileName = path.basename(config.modResults.path);

if (config.modResults.language === 'swift') {
if (config.modResults.contents.includes('RNSentrySDK.start()')) {
warnOnce(`Your '${fileName}' already contains 'RNSentrySDK.start()'.`);
return config;
}
// Add RNSentrySDK.start() at the beginning of application method
const originalContents = config.modResults.contents;
config.modResults.contents = config.modResults.contents.replace(
/(func application\([^)]*\) -> Bool \{)\s*\n(\s*)/s,
`$1\n$2RNSentrySDK.start()\n$2`,
);
if (config.modResults.contents === originalContents) {
warnOnce(`Failed to insert 'RNSentrySDK.start()' in '${fileName}'.`);
} else if (!config.modResults.contents.includes('import RNSentry')) {
// Insert import statement after UIKit import
config.modResults.contents = config.modResults.contents.replace(/(import UIKit\n)/, `$1import RNSentry\n`);
}
} else if (['objcpp', 'objc'].includes(config.modResults.language)) {
if (config.modResults.contents.includes('[RNSentrySDK start]')) {
warnOnce(`Your '${fileName}' already contains '[RNSentrySDK start]'.`);
return config;
}
// Add [RNSentrySDK start] at the beginning of application:didFinishLaunchingWithOptions method
const originalContents = config.modResults.contents;
config.modResults.contents = config.modResults.contents.replace(
/(- \(BOOL\)application:[\s\S]*?didFinishLaunchingWithOptions:[\s\S]*?\{\n)(\s*)/s,
`$1$2[RNSentrySDK start];\n$2`,
);
if (config.modResults.contents === originalContents) {
warnOnce(`Failed to insert '[RNSentrySDK start]' in '${fileName}.`);
} else if (!config.modResults.contents.includes('#import <RNSentry/RNSentry.h>')) {
// Add import after AppDelegate.h
config.modResults.contents = config.modResults.contents.replace(
/(#import "AppDelegate.h"\n)/,
`$1#import <RNSentry/RNSentry.h>\n`,
);
}
} else {
warnOnce(
`Unsupported language '${config.modResults.language}' detected in '${fileName}', the native code won't be updated.`,
);
}

return config;
});
}
Loading
Loading