From 313e844af3a4a62f96b1f6c9dc347550fbaa9755 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 7 Mar 2025 13:23:29 +0200 Subject: [PATCH 01/23] useNativeInit Android implementation --- packages/core/plugin/src/withSentry.ts | 3 +- packages/core/plugin/src/withSentryAndroid.ts | 62 +++++++++++++- .../expo-plugin/modifyMainApplication.test.ts | 84 +++++++++++++++++++ samples/expo/app.json | 3 +- 4 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 packages/core/test/expo-plugin/modifyMainApplication.test.ts diff --git a/packages/core/plugin/src/withSentry.ts b/packages/core/plugin/src/withSentry.ts index 70d4c8932b..e3c4f82da2 100644 --- a/packages/core/plugin/src/withSentry.ts +++ b/packages/core/plugin/src/withSentry.ts @@ -12,6 +12,7 @@ interface PluginProps { project?: string; authToken?: string; url?: string; + useNativeInit?: boolean; experimental_android?: SentryAndroidGradlePluginOptions; } @@ -26,7 +27,7 @@ const withSentryPlugin: ConfigPlugin = (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}`); } diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index 9beaa23883..7d9073801b 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -1,11 +1,15 @@ +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 = (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 { @@ -13,7 +17,10 @@ export const withSentryAndroid: ConfigPlugin = (config, sentryProperties } 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 +56,50 @@ export function modifyAppBuildGradle(buildGradle: string): string { return buildGradle.replace(pattern, match => `${applyFrom}\n\n${match}`); } + +export function modifyMainApplication(config: ExpoConfig): ExpoConfig { + return withMainApplication(config, async config => { + if (!config.modResults || !config.modResults.path) { + warnOnce('Skipping MainApplication modification because the file does not exist.'); + return config; + } + + const fileName = config.modResults.path.split('/').pop(); + + if (config.modResults.contents.includes('RNSentrySDK.init')) { + warnOnce(`Your '${fileName}' already contains 'RNSentrySDK.init'.`); + return config; + } + + if (config.modResults.language === 'java') { + 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`, + ); + } + // Add RNSentrySDK.init + config.modResults.contents = config.modResults.contents.replace( + 'super.onCreate();', + `super.onCreate();\nRNSentrySDK.init(this);`, + ); + } else { + // Kotlin + 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`, + ); + } + // Add RNSentrySDK.init + config.modResults.contents = config.modResults.contents.replace( + 'super.onCreate()', + `super.onCreate()\nRNSentrySDK.init(this)`, + ); + } + + return config; + }); +} diff --git a/packages/core/test/expo-plugin/modifyMainApplication.test.ts b/packages/core/test/expo-plugin/modifyMainApplication.test.ts new file mode 100644 index 0000000000..e8305f132e --- /dev/null +++ b/packages/core/test/expo-plugin/modifyMainApplication.test.ts @@ -0,0 +1,84 @@ +import type { ExpoConfig } from '@expo/config-types'; + +import { warnOnce } from '../../plugin/src/utils'; +import { modifyMainApplication } from '../../plugin/src/withSentryAndroid'; + +// Mock dependencies +jest.mock('@expo/config-plugins', () => ({ + ...jest.requireActual('@expo/config-plugins'), + withMainApplication: jest.fn((config, callback) => callback(config)), +})); + +jest.mock('../../plugin/src/utils', () => ({ + warnOnce: jest.fn(), +})); + +interface MockedExpoConfig extends ExpoConfig { + modResults: { + path: string; + contents: string; + language: 'java' | 'kotlin'; + }; +} + +describe('modifyMainApplication', () => { + let config: MockedExpoConfig; + + beforeEach(() => { + jest.clearAllMocks(); + // Reset to a mocked Java config after each test + config = { + name: 'test', + slug: 'test', + modResults: { + path: '/android/app/src/main/java/com/example/MainApplication.java', + contents: 'package com.example;\nsuper.onCreate();', + language: 'java', + }, + }; + }); + + it('should skip modification if modResults or path is missing', async () => { + config.modResults.path = undefined; + + const result = await modifyMainApplication(config); + + expect(warnOnce).toHaveBeenCalledWith('Skipping MainApplication modification because the file does not exist.'); + expect(result).toBe(config); // No modification + }); + + it('should warn if RNSentrySDK.init is already present', async () => { + config.modResults.contents = 'package com.example;\nsuper.onCreate();\nRNSentrySDK.init(this);'; + + const result = await modifyMainApplication(config); + + expect(warnOnce).toHaveBeenCalledWith(`Your 'MainApplication.java' already contains 'RNSentrySDK.init'.`); + expect(result).toBe(config); // No modification + }); + + it('should modify a Java file by adding the RNSentrySDK import and init', async () => { + const result = (await modifyMainApplication(config)) as MockedExpoConfig; + + expect(result.modResults.contents).toContain('import io.sentry.react.RNSentrySDK;'); + expect(result.modResults.contents).toContain('super.onCreate();\nRNSentrySDK.init(this);'); + }); + + it('should modify a Kotlin file by adding the RNSentrySDK import and init', async () => { + config.modResults.language = 'kotlin'; + config.modResults.contents = 'package com.example\nsuper.onCreate()'; + + const result = (await modifyMainApplication(config)) as MockedExpoConfig; + + expect(result.modResults.contents).toContain('import io.sentry.react.RNSentrySDK'); + expect(result.modResults.contents).toContain('super.onCreate()\nRNSentrySDK.init(this)'); + }); + + it('should insert import statements only once', async () => { + config.modResults.contents = 'package com.example;\nimport io.sentry.react.RNSentrySDK;\nsuper.onCreate();'; + + const result = (await modifyMainApplication(config)) as MockedExpoConfig; + + const importCount = (result.modResults.contents.match(/import io.sentry.react.RNSentrySDK/g) || []).length; + expect(importCount).toBe(1); + }); +}); diff --git a/samples/expo/app.json b/samples/expo/app.json index 1f1c89980d..2978475605 100644 --- a/samples/expo/app.json +++ b/samples/expo/app.json @@ -45,6 +45,7 @@ "url": "https://sentry.io/", "project": "sentry-react-native", "organization": "sentry-sdks", + "useNativeInit": true, "experimental_android": { "enableAndroidGradlePlugin": true, "autoUploadProguardMapping": true, @@ -71,4 +72,4 @@ ] ] } -} \ No newline at end of file +} From 2e97acc65f114fd2ffeb25986ee00963c0c7560b Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 7 Mar 2025 13:26:38 +0200 Subject: [PATCH 02/23] Adds changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef04ec37c7..c4a0dfb748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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()`. From 6eedaae68b2ecc1156923d0bec0197e65d50f9b5 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 7 Mar 2025 16:18:03 +0200 Subject: [PATCH 03/23] useNativeInit iOS implementation --- packages/core/plugin/src/withSentry.ts | 2 +- packages/core/plugin/src/withSentryIOS.ts | 61 +++++++++- .../expo-plugin/modifyAppDelegate.test.ts | 109 ++++++++++++++++++ 3 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 packages/core/test/expo-plugin/modifyAppDelegate.test.ts diff --git a/packages/core/plugin/src/withSentry.ts b/packages/core/plugin/src/withSentry.ts index e3c4f82da2..3da8885e7b 100644 --- a/packages/core/plugin/src/withSentry.ts +++ b/packages/core/plugin/src/withSentry.ts @@ -40,7 +40,7 @@ const withSentryPlugin: ConfigPlugin = (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}`); } diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index db25261839..04cc43e06c 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -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'; @@ -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 = (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 +40,9 @@ export const withSentryIOS: ConfigPlugin = (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 +85,50 @@ 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('Skipping AppDelegate modification because the file does not exist.'); + return config; + } + + const fileName = config.modResults.path.split('/').pop(); + + if (config.modResults.language === 'swift') { + if (config.modResults.contents.includes('RNSentrySDK.start()')) { + warnOnce(`Your '${fileName}' already contains 'RNSentrySDK.start()'.`); + return config; + } + if (!config.modResults.contents.includes('import RNSentrySDK')) { + // Insert import statement after UIKit import + config.modResults.contents = config.modResults.contents.replace(/(import UIKit\n)/, `$1import RNSentrySDK\n`); + } + // Add RNSentrySDK.start() at the beginning of application method + config.modResults.contents = config.modResults.contents.replace( + /(func application\([^)]*\) -> Bool \{)/s, // Match method signature even if split across multiple lines + `$1\n RNSentrySDK.start()`, + ); + } else { + // Objective-C + if (config.modResults.contents.includes('[RNSentrySDK start]')) { + warnOnce(`Your '${fileName}' already contains '[RNSentrySDK start]'.`); + return config; + } + if (!config.modResults.contents.includes('#import ')) { + // Add import after AppDelegate.h + config.modResults.contents = config.modResults.contents.replace( + /(#import "AppDelegate.h"\n)/, + `$1#import \n`, + ); + } + // Add [RNSentrySDK start] at the beginning of application:didFinishLaunchingWithOptions method + config.modResults.contents = config.modResults.contents.replace( + /(- \(BOOL\)application:[\s\S]*?didFinishLaunchingWithOptions:[\s\S]*?\{)/s, + `$1\n [RNSentrySDK start];`, + ); + } + + return config; + }); +} diff --git a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts new file mode 100644 index 0000000000..7b291cae05 --- /dev/null +++ b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts @@ -0,0 +1,109 @@ +import type { ExpoConfig } from '@expo/config-types'; + +import { warnOnce } from '../../plugin/src/utils'; +import { modifyAppDelegate } from '../../plugin/src/withSentryIOS'; + +// Mock dependencies +jest.mock('@expo/config-plugins', () => ({ + ...jest.requireActual('@expo/config-plugins'), + withAppDelegate: jest.fn((config, callback) => callback(config)), +})); + +jest.mock('../../plugin/src/utils', () => ({ + warnOnce: jest.fn(), +})); + +interface MockedExpoConfig extends ExpoConfig { + modResults: { + path: string; + contents: string; + language: 'swift' | 'objc'; + }; +} + +describe('modifyAppDelegate', () => { + let config: MockedExpoConfig; + + beforeEach(() => { + jest.clearAllMocks(); + // Reset to a mocked Swift config after each test + config = { + name: 'test', + slug: 'test', + modResults: { + path: 'samples/react-native/ios/AppDelegate.swift', + contents: + 'import UIKit\n\noverride func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {', + language: 'swift', + }, + }; + }); + + it('should skip modification if modResults or path is missing', async () => { + config.modResults.path = undefined; + + const result = await modifyAppDelegate(config); + + expect(warnOnce).toHaveBeenCalledWith('Skipping AppDelegate modification because the file does not exist.'); + expect(result).toBe(config); // No modification + }); + + it('should warn if RNSentrySDK.start() is already present in a Swift project', async () => { + config.modResults.contents = 'RNSentrySDK.start();'; + + const result = await modifyAppDelegate(config); + + expect(warnOnce).toHaveBeenCalledWith(`Your 'AppDelegate.swift' already contains 'RNSentrySDK.start()'.`); + expect(result).toBe(config); // No modification + }); + + it('should warn if [RNSentrySDK start] is already present in an Objective-C project', async () => { + config.modResults.language = 'objc'; + config.modResults.path = 'samples/react-native/ios/AppDelegate.mm'; + config.modResults.contents = '[RNSentrySDK start];'; + + const result = await modifyAppDelegate(config); + + expect(warnOnce).toHaveBeenCalledWith(`Your 'AppDelegate.mm' already contains '[RNSentrySDK start]'.`); + expect(result).toBe(config); // No modification + }); + + it('should modify a Swift file by adding the RNSentrySDK import and start', async () => { + const result = (await modifyAppDelegate(config)) as MockedExpoConfig; + + expect(result.modResults.contents).toContain('import RNSentrySDK'); + expect(result.modResults.contents).toContain('RNSentrySDK.start()'); + }); + + it('should modify an Objective-C file by adding the RNSentrySDK import and start', async () => { + config.modResults.language = 'objc'; + config.modResults.contents = + '#import "AppDelegate.h"\n\n- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {'; + + const result = (await modifyAppDelegate(config)) as MockedExpoConfig; + + expect(result.modResults.contents).toContain('#import '); + expect(result.modResults.contents).toContain('[RNSentrySDK start];'); + }); + + it('should insert import statements only once in an Swift project', async () => { + config.modResults.contents = + 'import UIKit\nimport RNSentrySDK\n\noverride func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {'; + + const result = (await modifyAppDelegate(config)) as MockedExpoConfig; + + const importCount = (result.modResults.contents.match(/import RNSentrySDK/g) || []).length; + expect(importCount).toBe(1); + }); + + it('should insert import statements only once in an Objective-C project', async () => { + config.modResults.language = 'objc'; + config.modResults.contents = + '#import "AppDelegate.h"\n#import \n\n- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {'; + + const result = (await modifyAppDelegate(config)) as MockedExpoConfig; + + const importCount = (result.modResults.contents.match(/#import /g) || []).length; + expect(importCount).toBe(1); + }); +}); From 9ae5475269d681874654da30175afc08fd6f7051 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 7 Mar 2025 18:01:27 +0200 Subject: [PATCH 04/23] Fix indentation --- packages/core/plugin/src/withSentryAndroid.ts | 8 ++++---- packages/core/plugin/src/withSentryIOS.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index 7d9073801b..9f1f3c7474 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -81,8 +81,8 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { } // Add RNSentrySDK.init config.modResults.contents = config.modResults.contents.replace( - 'super.onCreate();', - `super.onCreate();\nRNSentrySDK.init(this);`, + /(super\.onCreate\(\)[;\n]*)([ \t]*)/, + `$1\n$2RNSentrySDK.init(this);\n$2`, ); } else { // Kotlin @@ -95,8 +95,8 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { } // Add RNSentrySDK.init config.modResults.contents = config.modResults.contents.replace( - 'super.onCreate()', - `super.onCreate()\nRNSentrySDK.init(this)`, + /(super\.onCreate\(\)[;\n]*)([ \t]*)/, + `$1\n$2RNSentrySDK.init(this)\n$2`, ); } diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index 04cc43e06c..cb5f4552ea 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -100,13 +100,13 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { warnOnce(`Your '${fileName}' already contains 'RNSentrySDK.start()'.`); return config; } - if (!config.modResults.contents.includes('import RNSentrySDK')) { + if (!config.modResults.contents.includes('import RNSentry')) { // Insert import statement after UIKit import - config.modResults.contents = config.modResults.contents.replace(/(import UIKit\n)/, `$1import RNSentrySDK\n`); + config.modResults.contents = config.modResults.contents.replace(/(import UIKit\n)/, `$1import RNSentry\n`); } // Add RNSentrySDK.start() at the beginning of application method config.modResults.contents = config.modResults.contents.replace( - /(func application\([^)]*\) -> Bool \{)/s, // Match method signature even if split across multiple lines + /(func application\([^)]*\) -> Bool \{)/s, `$1\n RNSentrySDK.start()`, ); } else { @@ -124,8 +124,8 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { } // Add [RNSentrySDK start] at the beginning of application:didFinishLaunchingWithOptions method config.modResults.contents = config.modResults.contents.replace( - /(- \(BOOL\)application:[\s\S]*?didFinishLaunchingWithOptions:[\s\S]*?\{)/s, - `$1\n [RNSentrySDK start];`, + /(- \(BOOL\)application:[\s\S]*?didFinishLaunchingWithOptions:[\s\S]*?\{\n)(\s*)/s, + `$1$2[RNSentrySDK start];\n$2`, ); } From 566550e151ab39bd6321f2f0246a86db78b7a53a Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 7 Mar 2025 18:02:01 +0200 Subject: [PATCH 05/23] Extend test cases with realistic data --- .../expo-plugin/modifyAppDelegate.test.ts | 78 ++++++++++++- .../expo-plugin/modifyMainApplication.test.ts | 108 +++++++++++++++++- 2 files changed, 177 insertions(+), 9 deletions(-) diff --git a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts index 7b291cae05..266da50f3d 100644 --- a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts +++ b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts @@ -21,6 +21,74 @@ interface MockedExpoConfig extends ExpoConfig { }; } +const objcContents = `#import "AppDelegate.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.moduleName = @"main"; + + // You can add your custom initial props in the dictionary below. + // They will be passed down to the ViewController used by React Native. + self.initialProps = @{}; + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end +`; + +const objcExpected = `#import "AppDelegate.h" +#import + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + [RNSentrySDK start]; + self.moduleName = @"main"; + + // You can add your custom initial props in the dictionary below. + // They will be passed down to the ViewController used by React Native. + self.initialProps = @{}; + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end +`; + +const swiftContents = `import React +import React_RCTAppDelegate +import ReactAppDependencyProvider +import UIKit + +@main +class AppDelegate: RCTAppDelegate { + override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + self.moduleName = "sentry-react-native-sample" + self.dependencyProvider = RCTAppDependencyProvider() + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +}`; + +const swiftExpected = `import React +import React_RCTAppDelegate +import ReactAppDependencyProvider +import UIKit +import RNSentry + +@main +class AppDelegate: RCTAppDelegate { + override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + RNSentrySDK.start() + self.moduleName = "sentry-react-native-sample" + self.dependencyProvider = RCTAppDependencyProvider() + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +}`; + describe('modifyAppDelegate', () => { let config: MockedExpoConfig; @@ -32,8 +100,7 @@ describe('modifyAppDelegate', () => { slug: 'test', modResults: { path: 'samples/react-native/ios/AppDelegate.swift', - contents: - 'import UIKit\n\noverride func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {', + contents: swiftContents, language: 'swift', }, }; @@ -71,19 +138,20 @@ describe('modifyAppDelegate', () => { it('should modify a Swift file by adding the RNSentrySDK import and start', async () => { const result = (await modifyAppDelegate(config)) as MockedExpoConfig; - expect(result.modResults.contents).toContain('import RNSentrySDK'); + expect(result.modResults.contents).toContain('import RNSentry'); expect(result.modResults.contents).toContain('RNSentrySDK.start()'); + expect(result.modResults.contents).toBe(swiftExpected); }); it('should modify an Objective-C file by adding the RNSentrySDK import and start', async () => { config.modResults.language = 'objc'; - config.modResults.contents = - '#import "AppDelegate.h"\n\n- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {'; + config.modResults.contents = objcContents; const result = (await modifyAppDelegate(config)) as MockedExpoConfig; expect(result.modResults.contents).toContain('#import '); expect(result.modResults.contents).toContain('[RNSentrySDK start];'); + expect(result.modResults.contents).toBe(objcExpected); }); it('should insert import statements only once in an Swift project', async () => { diff --git a/packages/core/test/expo-plugin/modifyMainApplication.test.ts b/packages/core/test/expo-plugin/modifyMainApplication.test.ts index e8305f132e..82c145bd17 100644 --- a/packages/core/test/expo-plugin/modifyMainApplication.test.ts +++ b/packages/core/test/expo-plugin/modifyMainApplication.test.ts @@ -21,6 +21,104 @@ interface MockedExpoConfig extends ExpoConfig { }; } +const kotlinContents = `package io.sentry.expo.sample + +import android.app.Application + +import com.facebook.react.ReactApplication +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.react.soloader.OpenSourceMergedSoMapping +import com.facebook.soloader.SoLoader + +import expo.modules.ApplicationLifecycleDispatcher + +class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() + SoLoader.init(this, OpenSourceMergedSoMapping) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + ApplicationLifecycleDispatcher.onApplicationCreate(this) + } +} +`; + +const kotlinExpected = `package io.sentry.expo.sample + +import io.sentry.react.RNSentrySDK +import android.app.Application + +import com.facebook.react.ReactApplication +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.react.soloader.OpenSourceMergedSoMapping +import com.facebook.soloader.SoLoader + +import expo.modules.ApplicationLifecycleDispatcher + +class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() + + RNSentrySDK.init(this) + SoLoader.init(this, OpenSourceMergedSoMapping) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + load() + } + ApplicationLifecycleDispatcher.onApplicationCreate(this) + } +} +`; + +const javaContents = `package com.testappplain; + +import android.app.Application; +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.soloader.SoLoader; + +public class MainApplication extends Application implements ReactApplication { + @Override + public void onCreate() { + super.onCreate(); + // If you opted-in for the New Architecture, we enable the TurboModule system + ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; + SoLoader.init(this, /* native exopackage */ false); + initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); + } +} +`; + +const javaExpected = `package com.testappplain; + +import io.sentry.react.RNSentrySDK; +import android.app.Application; +import com.facebook.react.ReactApplication; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactNativeHost; +import com.facebook.react.config.ReactFeatureFlags; +import com.facebook.soloader.SoLoader; + +public class MainApplication extends Application implements ReactApplication { + @Override + public void onCreate() { + super.onCreate(); + + RNSentrySDK.init(this); + // If you opted-in for the New Architecture, we enable the TurboModule system + ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; + SoLoader.init(this, /* native exopackage */ false); + initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); + } +} +`; + describe('modifyMainApplication', () => { let config: MockedExpoConfig; @@ -32,7 +130,7 @@ describe('modifyMainApplication', () => { slug: 'test', modResults: { path: '/android/app/src/main/java/com/example/MainApplication.java', - contents: 'package com.example;\nsuper.onCreate();', + contents: javaContents, language: 'java', }, }; @@ -60,17 +158,19 @@ describe('modifyMainApplication', () => { const result = (await modifyMainApplication(config)) as MockedExpoConfig; expect(result.modResults.contents).toContain('import io.sentry.react.RNSentrySDK;'); - expect(result.modResults.contents).toContain('super.onCreate();\nRNSentrySDK.init(this);'); + expect(result.modResults.contents).toContain('RNSentrySDK.init(this);'); + expect(result.modResults.contents).toBe(javaExpected); }); it('should modify a Kotlin file by adding the RNSentrySDK import and init', async () => { config.modResults.language = 'kotlin'; - config.modResults.contents = 'package com.example\nsuper.onCreate()'; + config.modResults.contents = kotlinContents; const result = (await modifyMainApplication(config)) as MockedExpoConfig; expect(result.modResults.contents).toContain('import io.sentry.react.RNSentrySDK'); - expect(result.modResults.contents).toContain('super.onCreate()\nRNSentrySDK.init(this)'); + expect(result.modResults.contents).toContain('RNSentrySDK.init(this)'); + expect(result.modResults.contents).toBe(kotlinExpected); }); it('should insert import statements only once', async () => { From 770c9f4ebabd888d85cdc56291bf84a90fc5df7d Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 7 Mar 2025 18:15:15 +0200 Subject: [PATCH 06/23] Adds code sample in the changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4a0dfb748..1be96215da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,19 @@ ### Features - Add RNSentrySDK APIs support to @sentry/react-native/expo plugin ([#4633](https://github.com/getsentry/sentry-react-native/pull/4633)) + + This feature is opt-out to enable it set `useNativeInit` to `true` in your `@sentry/react-native/expo` plugin configuration. + + ```js + "plugins": [ + [ + "@sentry/react-native/expo", + { + "useNativeInit": true + } + ], + ``` + - 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()`. From f8b37b526211a65a4ea5d1c9635f572ec0f11cec Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 4 Apr 2025 11:01:36 +0300 Subject: [PATCH 07/23] Fix CHANGELOG.md Co-authored-by: LucasZF --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1be96215da..b8a3542526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - Add RNSentrySDK APIs support to @sentry/react-native/expo plugin ([#4633](https://github.com/getsentry/sentry-react-native/pull/4633)) - This feature is opt-out to enable it set `useNativeInit` to `true` in your `@sentry/react-native/expo` plugin configuration. + This feature is opt-out, to enable it set `useNativeInit` to `true` in your `@sentry/react-native/expo` plugin configuration. ```js "plugins": [ From d25db305aaed11e2a8d5e9657ffae4fcc5287eee Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 4 Apr 2025 12:44:44 +0300 Subject: [PATCH 08/23] Warn if RESentySDK.init/start wasn't injected --- packages/core/plugin/src/withSentryAndroid.ts | 8 ++++++++ packages/core/plugin/src/withSentryIOS.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index 9f1f3c7474..a1c65c6d36 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -80,10 +80,14 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { ); } // 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'.`); + } } else { // Kotlin if (!config.modResults.contents.includes('import io.sentry.react.RNSentrySDK')) { @@ -94,10 +98,14 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { ); } // 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'.`); + } } return config; diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index cb5f4552ea..a43273a3d0 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -105,10 +105,14 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { config.modResults.contents = config.modResults.contents.replace(/(import UIKit\n)/, `$1import RNSentry\n`); } // 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, `$1\n RNSentrySDK.start()`, ); + if (config.modResults.contents === originalContents) { + warnOnce(`Failed to insert 'RNSentrySDK.start()'.`); + } } else { // Objective-C if (config.modResults.contents.includes('[RNSentrySDK start]')) { @@ -123,10 +127,14 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { ); } // 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]'.`); + } } return config; From adc81a54d475203b1c69e92855a5e7631339cb5f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 15 Apr 2025 12:58:50 +0300 Subject: [PATCH 09/23] Make useNativeInit opt-in --- CHANGELOG.md | 13 ------------- packages/core/plugin/src/withSentryAndroid.ts | 2 +- packages/core/plugin/src/withSentryIOS.ts | 2 +- 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8a3542526..c4a0dfb748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,19 +11,6 @@ ### Features - Add RNSentrySDK APIs support to @sentry/react-native/expo plugin ([#4633](https://github.com/getsentry/sentry-react-native/pull/4633)) - - This feature is opt-out, to enable it set `useNativeInit` to `true` in your `@sentry/react-native/expo` plugin configuration. - - ```js - "plugins": [ - [ - "@sentry/react-native/expo", - { - "useNativeInit": true - } - ], - ``` - - 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()`. diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index a1c65c6d36..21679c43bc 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -7,7 +7,7 @@ import { warnOnce, writeSentryPropertiesTo } from './utils'; export const withSentryAndroid: ConfigPlugin<{ sentryProperties: string; useNativeInit: boolean | undefined }> = ( config, - { sentryProperties, useNativeInit = false }, + { sentryProperties, useNativeInit = true }, ) => { const appBuildGradleCfg = withAppBuildGradle(config, config => { if (config.modResults.language === 'groovy') { diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index a43273a3d0..53555979f2 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -15,7 +15,7 @@ const SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH = export const withSentryIOS: ConfigPlugin<{ sentryProperties: string; useNativeInit: boolean | undefined }> = ( config, - { sentryProperties, useNativeInit = false }, + { sentryProperties, useNativeInit = true }, ) => { const xcodeProjectCfg = withXcodeProject(config, config => { const xcodeProject: XcodeProject = config.modResults; From 8c2cd73d9027e668e87d15080e900d24756b1468 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 15 Apr 2025 13:35:34 +0300 Subject: [PATCH 10/23] Make Android failure warning more clear Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> --- packages/core/plugin/src/withSentryAndroid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index 21679c43bc..ee0531e772 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -60,7 +60,7 @@ export function modifyAppBuildGradle(buildGradle: string): string { export function modifyMainApplication(config: ExpoConfig): ExpoConfig { return withMainApplication(config, async config => { if (!config.modResults || !config.modResults.path) { - warnOnce('Skipping MainApplication modification because the file does not exist.'); + warnOnce("Can't add 'RNSentrySDK.init' to Android MainApplication, because the file was not found."); return config; } From a2b5575c8d3ab28b601fb9d834fec7db7867f1c5 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 15 Apr 2025 13:36:11 +0300 Subject: [PATCH 11/23] Make Android no update warning more clear Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> --- packages/core/plugin/src/withSentryAndroid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index ee0531e772..b9dcc65cf1 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -67,7 +67,7 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { const fileName = config.modResults.path.split('/').pop(); if (config.modResults.contents.includes('RNSentrySDK.init')) { - warnOnce(`Your '${fileName}' already contains 'RNSentrySDK.init'.`); + warnOnce(`Your '${fileName}' already contains 'RNSentrySDK.init', the native code won't be updated.`); return config; } From 5f4f7c59856acb85d0261fe859acfdf7debc2504 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 15 Apr 2025 13:44:30 +0300 Subject: [PATCH 12/23] Use path.basename to get last path component --- packages/core/plugin/src/withSentryAndroid.ts | 2 +- packages/core/plugin/src/withSentryIOS.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index b9dcc65cf1..6e51c2c6a9 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -64,7 +64,7 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { return config; } - const fileName = config.modResults.path.split('/').pop(); + 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.`); diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index 53555979f2..083f0fd8a6 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -93,7 +93,7 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { return config; } - const fileName = config.modResults.path.split('/').pop(); + const fileName = path.basename(config.modResults.path); if (config.modResults.language === 'swift') { if (config.modResults.contents.includes('RNSentrySDK.start()')) { From 0431cc3d1f9a8aad0e604a30a8c38e81939fb007 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 15 Apr 2025 13:44:55 +0300 Subject: [PATCH 13/23] Update tests to account for the new warnings --- .../core/test/expo-plugin/modifyMainApplication.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/test/expo-plugin/modifyMainApplication.test.ts b/packages/core/test/expo-plugin/modifyMainApplication.test.ts index 82c145bd17..cda755373d 100644 --- a/packages/core/test/expo-plugin/modifyMainApplication.test.ts +++ b/packages/core/test/expo-plugin/modifyMainApplication.test.ts @@ -141,7 +141,9 @@ describe('modifyMainApplication', () => { const result = await modifyMainApplication(config); - expect(warnOnce).toHaveBeenCalledWith('Skipping MainApplication modification because the file does not exist.'); + expect(warnOnce).toHaveBeenCalledWith( + `Can't add 'RNSentrySDK.init' to Android MainApplication, because the file was not found.`, + ); expect(result).toBe(config); // No modification }); @@ -150,7 +152,9 @@ describe('modifyMainApplication', () => { const result = await modifyMainApplication(config); - expect(warnOnce).toHaveBeenCalledWith(`Your 'MainApplication.java' already contains 'RNSentrySDK.init'.`); + expect(warnOnce).toHaveBeenCalledWith( + `Your 'MainApplication.java' already contains 'RNSentrySDK.init', the native code won't be updated.`, + ); expect(result).toBe(config); // No modification }); From 62d39ccb4f99530bd9ee3188cf4b1220c3d5ab4d Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 16 Apr 2025 11:09:32 +0300 Subject: [PATCH 14/23] Explicitly check for kotlin --- packages/core/plugin/src/withSentryAndroid.ts | 5 +++-- packages/core/test/expo-plugin/modifyMainApplication.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index 6e51c2c6a9..7031998c4c 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -88,8 +88,7 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { if (config.modResults.contents === originalContents) { warnOnce(`Failed to insert 'RNSentrySDK.init'.`); } - } else { - // Kotlin + } else if (config.modResults.language === 'kt') { if (!config.modResults.contents.includes('import io.sentry.react.RNSentrySDK')) { // Insert import statement after package declaration config.modResults.contents = config.modResults.contents.replace( @@ -106,6 +105,8 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { if (config.modResults.contents === originalContents) { warnOnce(`Failed to insert 'RNSentrySDK.init'.`); } + } else { + warnOnce(`Unrecognized language detected in '${fileName}', the native code won't be updated.`); } return config; diff --git a/packages/core/test/expo-plugin/modifyMainApplication.test.ts b/packages/core/test/expo-plugin/modifyMainApplication.test.ts index cda755373d..65aceee826 100644 --- a/packages/core/test/expo-plugin/modifyMainApplication.test.ts +++ b/packages/core/test/expo-plugin/modifyMainApplication.test.ts @@ -17,7 +17,7 @@ interface MockedExpoConfig extends ExpoConfig { modResults: { path: string; contents: string; - language: 'java' | 'kotlin'; + language: 'java' | 'kt'; }; } @@ -167,7 +167,7 @@ describe('modifyMainApplication', () => { }); it('should modify a Kotlin file by adding the RNSentrySDK import and init', async () => { - config.modResults.language = 'kotlin'; + config.modResults.language = 'kt'; config.modResults.contents = kotlinContents; const result = (await modifyMainApplication(config)) as MockedExpoConfig; From 235f3efbe6180b41d45a748b04fd4d95393873b7 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 16 Apr 2025 11:11:39 +0300 Subject: [PATCH 15/23] Add filename in the warning message --- packages/core/plugin/src/withSentryAndroid.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index 7031998c4c..30bc687c05 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -86,7 +86,7 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { `$1\n$2RNSentrySDK.init(this);\n$2`, ); if (config.modResults.contents === originalContents) { - warnOnce(`Failed to insert 'RNSentrySDK.init'.`); + warnOnce(`Failed to insert 'RNSentrySDK.init' in '${fileName}'.`); } } else if (config.modResults.language === 'kt') { if (!config.modResults.contents.includes('import io.sentry.react.RNSentrySDK')) { @@ -103,7 +103,7 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { `$1\n$2RNSentrySDK.init(this)\n$2`, ); if (config.modResults.contents === originalContents) { - warnOnce(`Failed to insert 'RNSentrySDK.init'.`); + warnOnce(`Failed to insert 'RNSentrySDK.init' in '${fileName}'.`); } } else { warnOnce(`Unrecognized language detected in '${fileName}', the native code won't be updated.`); From 369cce774d10f252e53399b708f7c3d6c6042190 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 16 Apr 2025 11:14:57 +0300 Subject: [PATCH 16/23] Import only if init injection succeeds --- packages/core/plugin/src/withSentryAndroid.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index 30bc687c05..e7228f473c 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -72,13 +72,6 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { } if (config.modResults.language === 'java') { - 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`, - ); - } // Add RNSentrySDK.init const originalContents = config.modResults.contents; config.modResults.contents = config.modResults.contents.replace( @@ -87,15 +80,14 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { ); if (config.modResults.contents === originalContents) { warnOnce(`Failed to insert 'RNSentrySDK.init' in '${fileName}'.`); - } - } else if (config.modResults.language === 'kt') { - if (!config.modResults.contents.includes('import io.sentry.react.RNSentrySDK')) { + } 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`, + /(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( @@ -104,6 +96,12 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { ); 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(`Unrecognized language detected in '${fileName}', the native code won't be updated.`); From a53c7f46a14361e8f3b2b1da7f52b4f404799751 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 16 Apr 2025 11:18:56 +0300 Subject: [PATCH 17/23] Explicitly check for Objective-C --- packages/core/plugin/src/withSentryIOS.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index 083f0fd8a6..76d952db98 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -113,8 +113,7 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { if (config.modResults.contents === originalContents) { warnOnce(`Failed to insert 'RNSentrySDK.start()'.`); } - } else { - // Objective-C + } else if (config.modResults.language === 'objc') { if (config.modResults.contents.includes('[RNSentrySDK start]')) { warnOnce(`Your '${fileName}' already contains '[RNSentrySDK start]'.`); return config; @@ -135,6 +134,8 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { if (config.modResults.contents === originalContents) { warnOnce(`Failed to insert '[RNSentrySDK start]'.`); } + } else { + warnOnce(`Unsupported language detected in '${fileName}', the native code won't be updated.`); } return config; From 5e4a98f38718397760521367950467722fe2ef66 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 16 Apr 2025 11:20:06 +0300 Subject: [PATCH 18/23] Add filename in the warning --- packages/core/plugin/src/withSentryIOS.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index 76d952db98..0b2ef0f712 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -111,7 +111,7 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { `$1\n RNSentrySDK.start()`, ); if (config.modResults.contents === originalContents) { - warnOnce(`Failed to insert 'RNSentrySDK.start()'.`); + warnOnce(`Failed to insert 'RNSentrySDK.start()' in '${fileName}.`); } } else if (config.modResults.language === 'objc') { if (config.modResults.contents.includes('[RNSentrySDK start]')) { @@ -132,7 +132,7 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { `$1$2[RNSentrySDK start];\n$2`, ); if (config.modResults.contents === originalContents) { - warnOnce(`Failed to insert '[RNSentrySDK start]'.`); + warnOnce(`Failed to insert '[RNSentrySDK start]' in '${fileName}.`); } } else { warnOnce(`Unsupported language detected in '${fileName}', the native code won't be updated.`); From dce74b25792f6d9b1b1da7e894642b365c9890ed Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 16 Apr 2025 11:24:05 +0300 Subject: [PATCH 19/23] Make iOS file not found warning more clear --- packages/core/plugin/src/withSentryIOS.ts | 2 +- packages/core/test/expo-plugin/modifyAppDelegate.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index 0b2ef0f712..a8d04db901 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -89,7 +89,7 @@ export function addSentryWithBundledScriptsToBundleShellScript(script: string): export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { return withAppDelegate(config, async config => { if (!config.modResults || !config.modResults.path) { - warnOnce('Skipping AppDelegate modification because the file does not exist.'); + warnOnce("Can't add 'RNSentrySDK.start()' to the iOS AppDelegate, because the file was not found."); return config; } diff --git a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts index 266da50f3d..e4c4c705df 100644 --- a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts +++ b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts @@ -111,7 +111,7 @@ describe('modifyAppDelegate', () => { const result = await modifyAppDelegate(config); - expect(warnOnce).toHaveBeenCalledWith('Skipping AppDelegate modification because the file does not exist.'); + expect(warnOnce).toHaveBeenCalledWith(`Can't add 'RNSentrySDK.start()' to the iOS AppDelegate, because the file was not found.`); expect(result).toBe(config); // No modification }); From 0ffd26c8f16ee2e1a1bb04bca13be7a95ff2726b Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 16 Apr 2025 11:25:41 +0300 Subject: [PATCH 20/23] Import only if init injection succeeds --- packages/core/plugin/src/withSentryIOS.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index a8d04db901..a3539a6be4 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -100,10 +100,6 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { warnOnce(`Your '${fileName}' already contains 'RNSentrySDK.start()'.`); return config; } - 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`); - } // Add RNSentrySDK.start() at the beginning of application method const originalContents = config.modResults.contents; config.modResults.contents = config.modResults.contents.replace( @@ -112,19 +108,15 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { ); 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 (config.modResults.language === 'objc') { if (config.modResults.contents.includes('[RNSentrySDK start]')) { warnOnce(`Your '${fileName}' already contains '[RNSentrySDK start]'.`); return config; } - if (!config.modResults.contents.includes('#import ')) { - // Add import after AppDelegate.h - config.modResults.contents = config.modResults.contents.replace( - /(#import "AppDelegate.h"\n)/, - `$1#import \n`, - ); - } // Add [RNSentrySDK start] at the beginning of application:didFinishLaunchingWithOptions method const originalContents = config.modResults.contents; config.modResults.contents = config.modResults.contents.replace( @@ -133,6 +125,12 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { ); if (config.modResults.contents === originalContents) { warnOnce(`Failed to insert '[RNSentrySDK start]' in '${fileName}.`); + } else if (!config.modResults.contents.includes('#import ')) { + // Add import after AppDelegate.h + config.modResults.contents = config.modResults.contents.replace( + /(#import "AppDelegate.h"\n)/, + `$1#import \n`, + ); } } else { warnOnce(`Unsupported language detected in '${fileName}', the native code won't be updated.`); From 744993c8255ca160e41673f01cc09f7e58eb3c69 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 16 Apr 2025 11:32:47 +0300 Subject: [PATCH 21/23] Reset test mock config in a function --- .../expo-plugin/modifyAppDelegate.test.ts | 22 +++++++++++-------- .../expo-plugin/modifyMainApplication.test.ts | 22 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts index e4c4c705df..27991394f1 100644 --- a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts +++ b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts @@ -95,15 +95,7 @@ describe('modifyAppDelegate', () => { beforeEach(() => { jest.clearAllMocks(); // Reset to a mocked Swift config after each test - config = { - name: 'test', - slug: 'test', - modResults: { - path: 'samples/react-native/ios/AppDelegate.swift', - contents: swiftContents, - language: 'swift', - }, - }; + config = createMockConfig(); }); it('should skip modification if modResults or path is missing', async () => { @@ -175,3 +167,15 @@ describe('modifyAppDelegate', () => { expect(importCount).toBe(1); }); }); + +function createMockConfig(): MockedExpoConfig { + return { + name: 'test', + slug: 'test', + modResults: { + path: 'samples/react-native/ios/AppDelegate.swift', + contents: swiftContents, + language: 'swift', + }, + }; +} diff --git a/packages/core/test/expo-plugin/modifyMainApplication.test.ts b/packages/core/test/expo-plugin/modifyMainApplication.test.ts index 65aceee826..e55319f8a9 100644 --- a/packages/core/test/expo-plugin/modifyMainApplication.test.ts +++ b/packages/core/test/expo-plugin/modifyMainApplication.test.ts @@ -125,15 +125,7 @@ describe('modifyMainApplication', () => { beforeEach(() => { jest.clearAllMocks(); // Reset to a mocked Java config after each test - config = { - name: 'test', - slug: 'test', - modResults: { - path: '/android/app/src/main/java/com/example/MainApplication.java', - contents: javaContents, - language: 'java', - }, - }; + config = createMockConfig(); }); it('should skip modification if modResults or path is missing', async () => { @@ -186,3 +178,15 @@ describe('modifyMainApplication', () => { expect(importCount).toBe(1); }); }); + +function createMockConfig(): MockedExpoConfig { + return { + name: 'test', + slug: 'test', + modResults: { + path: '/android/app/src/main/java/com/example/MainApplication.java', + contents: javaContents, + language: 'java', + }, + }; +} From 5447be9146f7e46c309bed149ca89397131ae46e Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 16 Apr 2025 11:33:26 +0300 Subject: [PATCH 22/23] Lint issue --- packages/core/test/expo-plugin/modifyAppDelegate.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts index 27991394f1..3933c2a4e7 100644 --- a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts +++ b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts @@ -103,7 +103,9 @@ describe('modifyAppDelegate', () => { const result = await modifyAppDelegate(config); - expect(warnOnce).toHaveBeenCalledWith(`Can't add 'RNSentrySDK.start()' to the iOS AppDelegate, because the file was not found.`); + expect(warnOnce).toHaveBeenCalledWith( + `Can't add 'RNSentrySDK.start()' to the iOS AppDelegate, because the file was not found.`, + ); expect(result).toBe(config); // No modification }); From 0b3423fd3dbe268f7b66071097e05c5f75f8e708 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 24 Apr 2025 09:12:40 +0300 Subject: [PATCH 23/23] Add missing quote Co-authored-by: LucasZF --- packages/core/plugin/src/withSentryIOS.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index a3539a6be4..7be1e0af0c 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -107,7 +107,7 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { `$1\n RNSentrySDK.start()`, ); if (config.modResults.contents === originalContents) { - warnOnce(`Failed to insert 'RNSentrySDK.start()' in '${fileName}.`); + 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`);