-
-
Notifications
You must be signed in to change notification settings - Fork 354
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
base: capture-app-start-errors
Are you sure you want to change the base?
Changes from all commits
313e844
2e97acc
6eedaae
9ae5475
566550e
770c9f4
f8b37b5
d25db30
adc81a5
8c2cd73
a2b5575
5f4f7c5
0431cc3
62d39cc
235f3ef
369cce7
a53c7f4
5e4a98f
dce74b2
0ffd26c
744993c
5447be9
0b3423f
5c615fd
c356288
8e32556
a20984c
d1db4fa
7c25c2c
1918baf
b2a89f2
3885d70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -284,6 +284,7 @@ | |
| ### Features | ||
|
|
||
| - Add RNSentrySDK APIs support to @sentry/react-native/expo plugin ([#4633](https://github.com/getsentry/sentry-react-native/pull/4633)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| - 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()`. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| const warningMap = new Map<string, boolean>(); | ||
|
|
||
| /** | ||
| * Log a warning message only once per run. | ||
| * This is used to avoid spamming the console with the same message. | ||
| */ | ||
| export function warnOnce(message: string): void { | ||
| if (!warningMap.has(message)) { | ||
| warningMap.set(message, true); | ||
| // eslint-disable-next-line no-console | ||
| console.warn(yellow(prefix(message))); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Prefix message with `› [value]`. | ||
| * | ||
| * Example: | ||
| * ``` | ||
| * › [@sentry/react-native/expo] This is a warning message | ||
| * ``` | ||
| */ | ||
| export function prefix(value: string): string { | ||
| return `› ${bold('[@sentry/react-native/expo]')} ${value}`; | ||
| } | ||
|
|
||
| /** | ||
| * The same as `chalk.yellow` | ||
| * This code is part of the SDK, we don't want to introduce a dependency on `chalk` just for this. | ||
| */ | ||
| export function yellow(message: string): string { | ||
| return `\x1b[33m${message}\x1b[0m`; | ||
| } | ||
|
|
||
| /** | ||
| * The same as `chalk.bold` | ||
| * This code is part of the SDK, we don't want to introduce a dependency on `chalk` just for this. | ||
| */ | ||
| export function bold(message: string): string { | ||
| return `\x1b[1m${message}\x1b[22m`; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| const packageJson: { | ||
| name: string; | ||
| version: string; | ||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | ||
| } = require('../../package.json'); | ||
|
|
||
| export const PLUGIN_NAME = `${packageJson.name}/expo`; | ||
| export const PLUGIN_VERSION = packageJson.version; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,27 @@ | ||
| 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'; | ||
| import { warnOnce } from './logger'; | ||
| import { 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); | ||
|
|
@@ -49,3 +57,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`, | ||
krystofwoldrich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ); | ||
|
Comment on lines
+78
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regex pattern Did we get this right? 👍 / 👎 to inform future reviews. |
||
| 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; | ||
|
Comment on lines
+82
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the regex replacement fails and the contents remain unchanged, only a warning is logged but the import statement might still be added in the else block. This could result in an orphaned import statement without the corresponding initialization call. Consider checking if the replacement was successful before adding the import statement. Did we get this right? 👍 / 👎 to inform future reviews. |
||
| 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; | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,11 @@ | ||
| 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'; | ||
| import { warnOnce } from './logger'; | ||
| import { writeSentryPropertiesTo } from './utils'; | ||
|
|
||
| type BuildPhase = { shellScript: string }; | ||
|
|
||
|
|
@@ -12,8 +14,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( | ||
|
|
@@ -36,7 +41,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); | ||
|
|
@@ -79,3 +86,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`, | ||
| ); | ||
|
Comment on lines
+107
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regex pattern /(func application\([^)]*\)\s*->\s*Bool\s*\{)\s*\n(\s*)/sDid we get this right? 👍 / 👎 to inform future reviews. |
||
| 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`); | ||
| } | ||
|
Comment on lines
+109
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the regex replacement fails and the contents remain unchanged, only a warning is logged but the import statement might still be added in the else block. This could result in an orphaned import statement without the corresponding initialization call. Consider checking if the replacement was successful before adding the import statement. Did we get this right? 👍 / 👎 to inform future reviews. |
||
| } 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`, | ||
|
Comment on lines
+123
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regex pattern /(- \(BOOL\)application:\(UIApplication \*\)\w+\s+didFinishLaunchingWithOptions:\(NSDictionary \*\)\w+\s*\{\n)(\s*)/sDid we get this right? 👍 / 👎 to inform future reviews. |
||
| ); | ||
| 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; | ||
| }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit. It would be nice to include an example code snippet and a small summary of what will the flag do.