-
-
Notifications
You must be signed in to change notification settings - Fork 354
feat(expo): Add RNSentrySDK APIs support to @sentry/react-native/expo plugin #4633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: capture-app-start-errors
Are you sure you want to change the base?
Changes from 6 commits
313e844
2e97acc
6eedaae
9ae5475
566550e
770c9f4
f8b37b5
d25db30
adc81a5
8c2cd73
a2b5575
5f4f7c5
0431cc3
62d39cc
235f3ef
369cce7
a53c7f4
5e4a98f
dce74b2
0ffd26c
744993c
5447be9
0b3423f
5c615fd
c356288
8e32556
a20984c
d1db4fa
7c25c2c
1918baf
b2a89f2
3885d70
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,177 @@ | ||||||||||||||||||||||||||||||||||||
| 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'; | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| 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 <RNSentry/RNSentry.h> | ||||||||||||||||||||||||||||||||||||
| @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; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| beforeEach(() => { | ||||||||||||||||||||||||||||||||||||
| jest.clearAllMocks(); | ||||||||||||||||||||||||||||||||||||
| // Reset to a mocked Swift config after each test | ||||||||||||||||||||||||||||||||||||
| config = { | ||||||||||||||||||||||||||||||||||||
krystofwoldrich marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
| name: 'test', | ||||||||||||||||||||||||||||||||||||
| slug: 'test', | ||||||||||||||||||||||||||||||||||||
| modResults: { | ||||||||||||||||||||||||||||||||||||
| path: 'samples/react-native/ios/AppDelegate.swift', | ||||||||||||||||||||||||||||||||||||
| contents: swiftContents, | ||||||||||||||||||||||||||||||||||||
| 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 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 = objcContents; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const result = (await modifyAppDelegate(config)) as MockedExpoConfig; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| expect(result.modResults.contents).toContain('#import <RNSentry/RNSentry.h>'); | ||||||||||||||||||||||||||||||||||||
| 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 () => { | ||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+174
to
+181
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test name and logic are inconsistent. The test checks for 'import RNSentrySDK' but the actual implementation imports 'import RNSentry'. This test should verify the correct import statement. The expected import should be
Suggested change
Did we get this right? 👍 / 👎 to inform future reviews. |
||||||||||||||||||||||||||||||||||||
| 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 <RNSentry/RNSentry.h>\n\n- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {'; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const result = (await modifyAppDelegate(config)) as MockedExpoConfig; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const importCount = (result.modResults.contents.match(/#import <RNSentry\/RNSentry.h>/g) || []).length; | ||||||||||||||||||||||||||||||||||||
| expect(importCount).toBe(1); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit. It would be nice to include an example code snippet and a small summary of what will the flag do.