From 313e844af3a4a62f96b1f6c9dc347550fbaa9755 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 7 Mar 2025 13:23:29 +0200 Subject: [PATCH 01/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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`); From 5c615fda05dd67897d5157e27c7a0e144b7ee806 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Jun 2025 17:45:14 +0300 Subject: [PATCH 24/34] Remove unneeded async 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 e7228f473c..393da63545 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -58,7 +58,7 @@ export function modifyAppBuildGradle(buildGradle: string): string { } export function modifyMainApplication(config: ExpoConfig): ExpoConfig { - return withMainApplication(config, async config => { + 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; From c356288dbbb4afb1b82f897ecc720bf1e457b015 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Jun 2025 18:18:57 +0300 Subject: [PATCH 25/34] Set useNativeInit = false by default --- 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 393da63545..edc7cee911 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 = true }, + { sentryProperties, useNativeInit = false }, ) => { 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 7be1e0af0c..91c1fc21ff 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 = true }, + { sentryProperties, useNativeInit = false }, ) => { const xcodeProjectCfg = withXcodeProject(config, config => { const xcodeProject: XcodeProject = config.modResults; From 8e32556b26c27a334e47b425356602b003240cb2 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 10 Jun 2025 18:29:10 +0300 Subject: [PATCH 26/34] dynamically fill white spaces --- 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 91c1fc21ff..6cd7013441 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -103,8 +103,8 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { // 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()`, + /(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}'.`); From a20984c8fdc30deff9a04c8eac7dc9501404e81d Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Jun 2025 13:23:57 +0300 Subject: [PATCH 27/34] Add unsupported language in warning message --- packages/core/plugin/src/withSentryAndroid.ts | 4 +++- packages/core/plugin/src/withSentryIOS.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index edc7cee911..24b569724c 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -104,7 +104,9 @@ export function modifyMainApplication(config: ExpoConfig): ExpoConfig { ); } } else { - warnOnce(`Unrecognized language detected in '${fileName}', the native code won't be updated.`); + warnOnce( + `Unsupported language '${config.modResults.language}' detected in '${fileName}', the native code won't be updated.`, + ); } return config; diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index 6cd7013441..c07c7eebcf 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -133,7 +133,9 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { ); } } else { - warnOnce(`Unsupported language detected in '${fileName}', the native code won't be updated.`); + warnOnce( + `Unsupported language '${config.modResults.language}' detected in '${fileName}', the native code won't be updated.`, + ); } return config; From d1db4fad82dde3b34cc412d2c6f68aa2123f910e Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Jun 2025 13:24:58 +0300 Subject: [PATCH 28/34] Add objcpp in detected languages Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> --- 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 6cd7013441..c8b3bd604c 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -112,7 +112,7 @@ export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { // 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') { + } else if (['objcpp', 'objc'].includes(config.modResults.language)) { if (config.modResults.contents.includes('[RNSentrySDK start]')) { warnOnce(`Your '${fileName}' already contains '[RNSentrySDK start]'.`); return config; From 1918baf722b662fc79a0f93d52880b8d54585842 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 11 Jun 2025 13:48:17 +0300 Subject: [PATCH 29/34] Update tests for objcpp --- .../expo-plugin/modifyAppDelegate.test.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts index 3933c2a4e7..032892cd66 100644 --- a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts +++ b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts @@ -17,7 +17,7 @@ interface MockedExpoConfig extends ExpoConfig { modResults: { path: string; contents: string; - language: 'swift' | 'objc'; + language: 'swift' | 'objc' | 'objcpp' | string; }; } @@ -148,6 +148,30 @@ describe('modifyAppDelegate', () => { expect(result.modResults.contents).toBe(objcExpected); }); + it('should modify an Objective-C++ file by adding the RNSentrySDK import and start', async () => { + config.modResults.language = 'objcpp'; + 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 not modify a source file if the language is not supported', async () => { + config.modResults.language = 'cpp'; + config.modResults.contents = objcContents; + config.modResults.path = 'samples/react-native/ios/AppDelegate.cpp'; + + const result = (await modifyAppDelegate(config)) as MockedExpoConfig; + + expect(warnOnce).toHaveBeenCalledWith( + `Unsupported language 'cpp' detected in 'AppDelegate.cpp', the native code won't be updated.`, + ); + expect(result).toBe(config); // No modification + }); + 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 {'; From b2a89f29e04c1eb9e05b0678369a911dde85fd9e Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:46:48 +0200 Subject: [PATCH 30/34] ref(expo-plugin): Split utils to logger, version and utils (#4906) Co-authored-by: Antonis Lilis --- packages/core/plugin/src/logger.ts | 41 +++++++++++++++++++ packages/core/plugin/src/utils.ts | 39 ------------------ packages/core/plugin/src/version.ts | 8 ++++ packages/core/plugin/src/withSentry.ts | 5 ++- packages/core/plugin/src/withSentryAndroid.ts | 3 +- .../src/withSentryAndroidGradlePlugin.ts | 2 +- packages/core/plugin/src/withSentryIOS.ts | 3 +- .../expo-plugin/modifyAppBuildGradle.test.ts | 4 +- .../expo-plugin/modifyAppDelegate.test.ts | 4 +- .../expo-plugin/modifyMainApplication.test.ts | 4 +- .../expo-plugin/modifyXcodeProject.test.ts | 4 +- .../withSentryAndroidGradlePlugin.test.ts | 4 +- 12 files changed, 67 insertions(+), 54 deletions(-) create mode 100644 packages/core/plugin/src/logger.ts create mode 100644 packages/core/plugin/src/version.ts diff --git a/packages/core/plugin/src/logger.ts b/packages/core/plugin/src/logger.ts new file mode 100644 index 0000000000..c72df9eaed --- /dev/null +++ b/packages/core/plugin/src/logger.ts @@ -0,0 +1,41 @@ +const warningMap = new Map(); + +/** + * 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`; +} diff --git a/packages/core/plugin/src/utils.ts b/packages/core/plugin/src/utils.ts index c587426b4f..9f4d154e12 100644 --- a/packages/core/plugin/src/utils.ts +++ b/packages/core/plugin/src/utils.ts @@ -8,42 +8,3 @@ export function writeSentryPropertiesTo(filepath: string, sentryProperties: stri fs.writeFileSync(path.resolve(filepath, 'sentry.properties'), sentryProperties); } - -const sdkPackage: { - name: string; - version: string; - // eslint-disable-next-line @typescript-eslint/no-var-requires -} = require('../../package.json'); - -const SDK_PACKAGE_NAME = `${sdkPackage.name}/expo`; - -const warningMap = new Map(); -export function warnOnce(message: string): void { - if (!warningMap.has(message)) { - warningMap.set(message, true); - // eslint-disable-next-line no-console - console.warn(yellow(`${logPrefix()} ${message}`)); - } -} - -export function logPrefix(): string { - return `› ${bold('[@sentry/react-native/expo]')}`; -} - -/** - * 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`; -} - -export { sdkPackage, SDK_PACKAGE_NAME }; diff --git a/packages/core/plugin/src/version.ts b/packages/core/plugin/src/version.ts new file mode 100644 index 0000000000..92d091ff71 --- /dev/null +++ b/packages/core/plugin/src/version.ts @@ -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; diff --git a/packages/core/plugin/src/withSentry.ts b/packages/core/plugin/src/withSentry.ts index 3da8885e7b..68068c9b23 100644 --- a/packages/core/plugin/src/withSentry.ts +++ b/packages/core/plugin/src/withSentry.ts @@ -1,7 +1,8 @@ import type { ConfigPlugin } from 'expo/config-plugins'; import { createRunOncePlugin } from 'expo/config-plugins'; -import { bold, sdkPackage, warnOnce } from './utils'; +import { bold, warnOnce } from './logger'; +import { PLUGIN_NAME, PLUGIN_VERSION } from './version'; import { withSentryAndroid } from './withSentryAndroid'; import type { SentryAndroidGradlePluginOptions } from './withSentryAndroidGradlePlugin'; import { withSentryAndroidGradlePlugin } from './withSentryAndroidGradlePlugin'; @@ -80,6 +81,6 @@ ${authToken ? `${existingAuthTokenMessage}\nauth.token=${authToken}` : missingAu } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -const withSentry = createRunOncePlugin(withSentryPlugin, sdkPackage.name, sdkPackage.version); +const withSentry = createRunOncePlugin(withSentryPlugin, PLUGIN_NAME, PLUGIN_VERSION); export { withSentry }; diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index 24b569724c..629986eefd 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -3,7 +3,8 @@ import type { ConfigPlugin } 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<{ sentryProperties: string; useNativeInit: boolean | undefined }> = ( config, diff --git a/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts b/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts index b4df682137..2e5880c646 100644 --- a/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts +++ b/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts @@ -1,6 +1,6 @@ import { withAppBuildGradle, withProjectBuildGradle } from '@expo/config-plugins'; -import { warnOnce } from './utils'; +import { warnOnce } from './logger'; export interface SentryAndroidGradlePluginOptions { enableAndroidGradlePlugin?: boolean; diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index c0073e3978..ba5b9e9d1a 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -4,7 +4,8 @@ import type { ConfigPlugin, XcodeProject } 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 }; diff --git a/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts b/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts index e1363983da..0dcc9b33d6 100644 --- a/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts +++ b/packages/core/test/expo-plugin/modifyAppBuildGradle.test.ts @@ -1,7 +1,7 @@ -import { warnOnce } from '../../plugin/src/utils'; +import { warnOnce } from '../../plugin/src/logger'; import { modifyAppBuildGradle } from '../../plugin/src/withSentryAndroid'; -jest.mock('../../plugin/src/utils'); +jest.mock('../../plugin/src/logger'); const buildGradleWithSentry = ` apply from: new File(["node", "--print", "require('path').dirname(require.resolve('@sentry/react-native/package.json'))"].execute().text.trim(), "sentry.gradle") diff --git a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts index 032892cd66..fe42e3f123 100644 --- a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts +++ b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts @@ -1,6 +1,6 @@ import type { ExpoConfig } from '@expo/config-types'; -import { warnOnce } from '../../plugin/src/utils'; +import { warnOnce } from '../../plugin/src/logger'; import { modifyAppDelegate } from '../../plugin/src/withSentryIOS'; // Mock dependencies @@ -9,7 +9,7 @@ jest.mock('@expo/config-plugins', () => ({ withAppDelegate: jest.fn((config, callback) => callback(config)), })); -jest.mock('../../plugin/src/utils', () => ({ +jest.mock('../../plugin/src/logger', () => ({ warnOnce: jest.fn(), })); diff --git a/packages/core/test/expo-plugin/modifyMainApplication.test.ts b/packages/core/test/expo-plugin/modifyMainApplication.test.ts index e55319f8a9..8ff7329c2e 100644 --- a/packages/core/test/expo-plugin/modifyMainApplication.test.ts +++ b/packages/core/test/expo-plugin/modifyMainApplication.test.ts @@ -1,6 +1,6 @@ import type { ExpoConfig } from '@expo/config-types'; -import { warnOnce } from '../../plugin/src/utils'; +import { warnOnce } from '../../plugin/src/logger'; import { modifyMainApplication } from '../../plugin/src/withSentryAndroid'; // Mock dependencies @@ -9,7 +9,7 @@ jest.mock('@expo/config-plugins', () => ({ withMainApplication: jest.fn((config, callback) => callback(config)), })); -jest.mock('../../plugin/src/utils', () => ({ +jest.mock('../../plugin/src/logger', () => ({ warnOnce: jest.fn(), })); diff --git a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts index bbd570fdf9..92dc615835 100644 --- a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts +++ b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts @@ -1,7 +1,7 @@ -import { warnOnce } from '../../plugin/src/utils'; +import { warnOnce } from '../../plugin/src/logger'; import { modifyExistingXcodeBuildScript } from '../../plugin/src/withSentryIOS'; -jest.mock('../../plugin/src/utils'); +jest.mock('../../plugin/src/logger'); const buildScriptWithoutSentry = { shellScript: JSON.stringify(`" diff --git a/packages/core/test/expo-plugin/withSentryAndroidGradlePlugin.test.ts b/packages/core/test/expo-plugin/withSentryAndroidGradlePlugin.test.ts index 0ed9a95551..f00c90d098 100644 --- a/packages/core/test/expo-plugin/withSentryAndroidGradlePlugin.test.ts +++ b/packages/core/test/expo-plugin/withSentryAndroidGradlePlugin.test.ts @@ -1,6 +1,6 @@ import { withAppBuildGradle, withProjectBuildGradle } from '@expo/config-plugins'; -import { warnOnce } from '../../plugin/src/utils'; +import { warnOnce } from '../../plugin/src/logger'; import type { SentryAndroidGradlePluginOptions } from '../../plugin/src/withSentryAndroidGradlePlugin'; import { withSentryAndroidGradlePlugin } from '../../plugin/src/withSentryAndroidGradlePlugin'; @@ -9,7 +9,7 @@ jest.mock('@expo/config-plugins', () => ({ withAppBuildGradle: jest.fn(), })); -jest.mock('../../plugin/src/utils', () => ({ +jest.mock('../../plugin/src/logger', () => ({ warnOnce: jest.fn(), })); From c318d40e67f1370be0a970a77d94793cbc8fbac7 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 15 Dec 2025 16:16:54 +0100 Subject: [PATCH 31/34] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 347353e0c0..4bf68a4b81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Report slow and frozen frames as TTID/TTFD span data ([#5419](https://github.com/getsentry/sentry-react-native/pull/5419)) - Report slow and frozen frames on spans created through the API ([#5420](https://github.com/getsentry/sentry-react-native/issues/5420)) - Improve performance by adding caching to `getReplayId` ([#5449](https://github.com/getsentry/sentry-react-native/pull/5449)) +- Add RNSentrySDK APIs support to @sentry/react-native/expo plugin ([#4633](https://github.com/getsentry/sentry-react-native/pull/4633)) ### Fixes @@ -1168,7 +1169,6 @@ We apologize for any inconvenience caused! ### 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 283e0a0089ecdf1d8150415fb66ec36f74c8f983 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 21 Jan 2026 12:49:30 +0100 Subject: [PATCH 32/34] fix(ios): Add Swift module support for RNSentrySDK native init Fixes Swift compilation errors when using the useNativeInit Expo plugin feature. Changes: - Updated RNSentry.h to use angle bracket import for RNSentrySDK, properly exposing it through the module system - Added DEFINES_MODULE to RNSentry.podspec to enable Swift module generation - Fixed Expo plugin to insert import after first import statement (supports modern Expo AppDelegate structure without UIKit import) This enables Swift code to successfully import RNSentry and call RNSentrySDK.start() when using native initialization. Co-Authored-By: Claude Sonnet 4.5 --- packages/core/RNSentry.podspec | 4 ++++ packages/core/ios/RNSentry.h | 2 +- packages/core/plugin/src/withSentryIOS.ts | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index c4afcc1eae..efb369c5c0 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -46,6 +46,10 @@ Pod::Spec.new do |s| s.compiler_flags = other_cflags + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES' + } + s.dependency 'Sentry/HybridSDK', '8.58.0' if defined? install_modules_dependencies diff --git a/packages/core/ios/RNSentry.h b/packages/core/ios/RNSentry.h index 73e27678cf..2d8d3f3b58 100644 --- a/packages/core/ios/RNSentry.h +++ b/packages/core/ios/RNSentry.h @@ -10,7 +10,7 @@ #import // This import exposes public RNSentrySDK start -#import "RNSentrySDK.h" +#import typedef int (*SymbolicateCallbackType)(const void *, Dl_info *); diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index d95dd33c3e..83682f5f3e 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -109,8 +109,8 @@ 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'); + // Insert import statement after the first import (works for both UIKit and Expo imports) + config.modResults.contents = config.modResults.contents.replace(/(import \S+\n)/, '$1import RNSentry\n'); } } else if (['objcpp', 'objc'].includes(config.modResults.language)) { if (config.modResults.contents.includes('[RNSentrySDK start]')) { From 05327d47ac00e845cb3fda4ca57dccfefce6b857 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 21 Jan 2026 13:00:51 +0100 Subject: [PATCH 33/34] Fix test --- packages/core/test/expo-plugin/modifyAppDelegate.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts index 59d32ffbec..d4e0e324da 100644 --- a/packages/core/test/expo-plugin/modifyAppDelegate.test.ts +++ b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts @@ -73,10 +73,10 @@ class AppDelegate: RCTAppDelegate { }`; const swiftExpected = `import React +import RNSentry import React_RCTAppDelegate import ReactAppDependencyProvider import UIKit -import RNSentry @main class AppDelegate: RCTAppDelegate { From a1b22f7a2b081cbb9a65063b3de7901d5d01537c Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 21 Jan 2026 13:09:04 +0100 Subject: [PATCH 34/34] Update changelog --- CHANGELOG.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 669d53a26f..a564d936fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,25 @@ ## Unreleased +### Features + +- Add RNSentrySDK APIs support to @sentry/react-native/expo plugin ([#4633](https://github.com/getsentry/sentry-react-native/pull/4633)) + - Adds `useNativeInit` option to automatically initialize Sentry natively before JavaScript loads, enabling capture of app start errors + ```json + { + "expo": { + "plugins": [ + [ + "@sentry/react-native/expo", + { + "useNativeInit": true + } + ] + ] + } + } + ``` + ### Dependencies - Bump Bundler Plugins from v4.6.2 to v4.7.0 ([#5554](https://github.com/getsentry/sentry-react-native/pull/5554)) @@ -72,7 +91,6 @@ - Report slow and frozen frames as TTID/TTFD span data ([#5419](https://github.com/getsentry/sentry-react-native/pull/5419)) - Report slow and frozen frames on spans created through the API ([#5420](https://github.com/getsentry/sentry-react-native/issues/5420)) - Improve performance by adding caching to `getReplayId` ([#5449](https://github.com/getsentry/sentry-react-native/pull/5449)) -- Add RNSentrySDK APIs support to @sentry/react-native/expo plugin ([#4633](https://github.com/getsentry/sentry-react-native/pull/4633)) ### Fixes