From 313e844af3a4a62f96b1f6c9dc347550fbaa9755 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 7 Mar 2025 13:23:29 +0200 Subject: [PATCH 01/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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(), }));