From c63eccc6050f8b66313b905f6f51b8f91de7e3e8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:34:56 +0100 Subject: [PATCH 01/12] chore: update scripts/update-cli.sh to 3.0.1 (#5471) Co-authored-by: GitHub --- CHANGELOG.md | 3 ++ package.json | 2 +- packages/core/package.json | 2 +- yarn.lock | 90 ++++++++++++++++++++------------------ 4 files changed, 53 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96463efba1..a2a1e1affa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ - Bump JavaScript SDK from v10.30.0 to v10.32.1 ([#5480](https://github.com/getsentry/sentry-react-native/pull/5480), [#5487](https://github.com/getsentry/sentry-react-native/pull/5487), [#5496](https://github.com/getsentry/sentry-react-native/pull/5496)) - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#10321) - [diff](https://github.com/getsentry/sentry-javascript/compare/10.30.0...10.32.1) +- Bump CLI from v2.58.4 to v3.0.1 ([#5471](https://github.com/getsentry/sentry-react-native/pull/5471)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#301) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.58.4...3.0.1) ## 7.8.0 diff --git a/package.json b/package.json index 5de0f61726..77fde368cf 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "@naturalcycles/ktlint": "^1.13.0", - "@sentry/cli": "2.58.4", + "@sentry/cli": "3.0.1", "downlevel-dts": "^0.11.0", "google-java-format": "^1.4.0", "lerna": "^8.1.8", diff --git a/packages/core/package.json b/packages/core/package.json index 6b089077dd..2b419bc45b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -70,7 +70,7 @@ "dependencies": { "@sentry/babel-plugin-component-annotate": "4.6.1", "@sentry/browser": "10.32.1", - "@sentry/cli": "2.58.4", + "@sentry/cli": "3.0.1", "@sentry/core": "10.32.1", "@sentry/react": "10.32.1", "@sentry/types": "10.32.1" diff --git a/yarn.lock b/yarn.lock index 6107a9e022..9a7ec0f9df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10005,78 +10005,77 @@ __metadata: languageName: node linkType: hard -"@sentry/cli-darwin@npm:2.58.4": - version: 2.58.4 - resolution: "@sentry/cli-darwin@npm:2.58.4" +"@sentry/cli-darwin@npm:3.0.1": + version: 3.0.1 + resolution: "@sentry/cli-darwin@npm:3.0.1" conditions: os=darwin languageName: node linkType: hard -"@sentry/cli-linux-arm64@npm:2.58.4": - version: 2.58.4 - resolution: "@sentry/cli-linux-arm64@npm:2.58.4" +"@sentry/cli-linux-arm64@npm:3.0.1": + version: 3.0.1 + resolution: "@sentry/cli-linux-arm64@npm:3.0.1" conditions: (os=linux | os=freebsd | os=android) & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-linux-arm@npm:2.58.4": - version: 2.58.4 - resolution: "@sentry/cli-linux-arm@npm:2.58.4" +"@sentry/cli-linux-arm@npm:3.0.1": + version: 3.0.1 + resolution: "@sentry/cli-linux-arm@npm:3.0.1" conditions: (os=linux | os=freebsd | os=android) & cpu=arm languageName: node linkType: hard -"@sentry/cli-linux-i686@npm:2.58.4": - version: 2.58.4 - resolution: "@sentry/cli-linux-i686@npm:2.58.4" +"@sentry/cli-linux-i686@npm:3.0.1": + version: 3.0.1 + resolution: "@sentry/cli-linux-i686@npm:3.0.1" conditions: (os=linux | os=freebsd | os=android) & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-linux-x64@npm:2.58.4": - version: 2.58.4 - resolution: "@sentry/cli-linux-x64@npm:2.58.4" +"@sentry/cli-linux-x64@npm:3.0.1": + version: 3.0.1 + resolution: "@sentry/cli-linux-x64@npm:3.0.1" conditions: (os=linux | os=freebsd | os=android) & cpu=x64 languageName: node linkType: hard -"@sentry/cli-win32-arm64@npm:2.58.4": - version: 2.58.4 - resolution: "@sentry/cli-win32-arm64@npm:2.58.4" +"@sentry/cli-win32-arm64@npm:3.0.1": + version: 3.0.1 + resolution: "@sentry/cli-win32-arm64@npm:3.0.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-win32-i686@npm:2.58.4": - version: 2.58.4 - resolution: "@sentry/cli-win32-i686@npm:2.58.4" +"@sentry/cli-win32-i686@npm:3.0.1": + version: 3.0.1 + resolution: "@sentry/cli-win32-i686@npm:3.0.1" conditions: os=win32 & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-win32-x64@npm:2.58.4": - version: 2.58.4 - resolution: "@sentry/cli-win32-x64@npm:2.58.4" +"@sentry/cli-win32-x64@npm:3.0.1": + version: 3.0.1 + resolution: "@sentry/cli-win32-x64@npm:3.0.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@sentry/cli@npm:2.58.4": - version: 2.58.4 - resolution: "@sentry/cli@npm:2.58.4" - dependencies: - "@sentry/cli-darwin": 2.58.4 - "@sentry/cli-linux-arm": 2.58.4 - "@sentry/cli-linux-arm64": 2.58.4 - "@sentry/cli-linux-i686": 2.58.4 - "@sentry/cli-linux-x64": 2.58.4 - "@sentry/cli-win32-arm64": 2.58.4 - "@sentry/cli-win32-i686": 2.58.4 - "@sentry/cli-win32-x64": 2.58.4 - https-proxy-agent: ^5.0.0 - node-fetch: ^2.6.7 +"@sentry/cli@npm:3.0.1": + version: 3.0.1 + resolution: "@sentry/cli@npm:3.0.1" + dependencies: + "@sentry/cli-darwin": 3.0.1 + "@sentry/cli-linux-arm": 3.0.1 + "@sentry/cli-linux-arm64": 3.0.1 + "@sentry/cli-linux-i686": 3.0.1 + "@sentry/cli-linux-x64": 3.0.1 + "@sentry/cli-win32-arm64": 3.0.1 + "@sentry/cli-win32-i686": 3.0.1 + "@sentry/cli-win32-x64": 3.0.1 progress: ^2.0.3 proxy-from-env: ^1.1.0 + undici: ^6.22.0 which: ^2.0.2 dependenciesMeta: "@sentry/cli-darwin": @@ -10097,7 +10096,7 @@ __metadata: optional: true bin: sentry-cli: bin/sentry-cli - checksum: 58ed99ce3811ff2238558e3d399134dfef18ee17fe404c152c95e24191cfd2b86b305f91abad4e2b8641442ab66e08c8cbb6e7d180eda8f40f074cbb33920194 + checksum: f6fdc70855dd17eefd881edb50fe358ad87ce7b180e136c0b4a14373d56453735e5ba6a24548dd321b22e38ad6729192fa1f71b5d488ccd87e21729368a79a99 languageName: node linkType: hard @@ -10206,7 +10205,7 @@ __metadata: "@sentry-internal/typescript": 10.32.1 "@sentry/babel-plugin-component-annotate": 4.6.1 "@sentry/browser": 10.32.1 - "@sentry/cli": 2.58.4 + "@sentry/cli": 3.0.1 "@sentry/core": 10.32.1 "@sentry/react": 10.32.1 "@sentry/types": 10.32.1 @@ -19342,7 +19341,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^5.0.0, https-proxy-agent@npm:^5.0.1": +"https-proxy-agent@npm:^5.0.1": version: 5.0.1 resolution: "https-proxy-agent@npm:5.0.1" dependencies: @@ -28419,7 +28418,7 @@ __metadata: resolution: "sentry-react-native@workspace:." dependencies: "@naturalcycles/ktlint": ^1.13.0 - "@sentry/cli": 2.58.4 + "@sentry/cli": 3.0.1 downlevel-dts: ^0.11.0 google-java-format: ^1.4.0 lerna: ^8.1.8 @@ -30735,6 +30734,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^6.22.0": + version: 6.22.0 + resolution: "undici@npm:6.22.0" + checksum: ec2d846cb7d360fd45c2e3848bbdadbe086c167be08dd578ed376c70afb2b977950b4c4919c18da0610c61a1ef53c079086d09390a96de2b62bc1fa16d7765f8 + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0" From f73c0ff366582e0f35ab3087dd5220cef2589f05 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 23 Dec 2025 13:05:16 +0100 Subject: [PATCH 02/12] chore(deps): update Cocoa SDK to v9 (#5356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update Cocoa SDK to v9.0.0-alpha.0 * Adds changelog * fix: Fixes SentryScreenFrames use after being converted to Swift (#5153) * fix: Fixes SentryScreenFrames use after being converted to Swift * Fix build --------- Co-authored-by: Antonis Lilis * Move `sentry-cocoa` `enableLogs` out of experimental (#5267) * fix: Fixes SentryScreenFrames use after being converted to Swift * Fix build * Use non-experimental enableLogs * bump target * remove enable tracing * Update enableLogs tests --------- Co-authored-by: Itay Brenner Co-authored-by: Itay Brenner Co-authored-by: Antonis Lilis * chore: Remove SentryFrameTracker imports (#5276) * fix: Fixes SentryScreenFrames use after being converted to Swift * Fix build * Use non-experimental enableLogs * bump target * remove enable tracing * Update enableLogs tests * chore: Fix RNSentry after SentryFramesTracker conversion to swift * Run linter * Remove unused debug image provider imports * Remove imports * chore: Remove deprecated user property * chore: Remove use of deprecate integrations API (#5304) * Remove use of integrations in tests * chore: Remove use of deprecated prop (#5322) * ref: Move options to wrapper --------- Co-authored-by: Antonis Lilis Co-authored-by: Denis Andrasec Co-authored-by: Noah Martin * fix(podspec): Cocoa V9 allow any alpha version * chore(sample): Cocoa-v9: Bump MacOS sample to macos 12.0 (#5359) * fix: Fixes SentryScreenFrames use after being converted to Swift * Fix build * Use non-experimental enableLogs * bump target * remove enable tracing * Update enableLogs tests * chore: Fix RNSentry after SentryFramesTracker conversion to swift * Run linter * Remove unused debug image provider imports * Remove imports * chore: Remove deprecated user property * chore: Remove use of deprecate integrations API (#5304) * Remove use of integrations in tests * chore: Remove use of deprecated prop (#5322) * ref: Move options to wrapper * chore(sample): Bump MacOS sample to macos 12.0 wich is the minimum for v9 --------- Co-authored-by: Itay Brenner Co-authored-by: Itay Brenner Co-authored-by: Denis Andrasec Co-authored-by: Noah Martin * chore(e2e): Cocoa-v9: Bump E2E to iOS 15.0 (#5369) * fix: Fixes SentryScreenFrames use after being converted to Swift * Fix build * Use non-experimental enableLogs * bump target * remove enable tracing * Update enableLogs tests * chore: Fix RNSentry after SentryFramesTracker conversion to swift * Run linter * Remove unused debug image provider imports * Remove imports * chore: Remove deprecated user property * chore: Remove use of deprecate integrations API (#5304) * Remove use of integrations in tests * chore: Remove use of deprecated prop (#5322) * ref: Move options to wrapper * chore(sample): Bump MacOS sample to macos 12.0 wich is the minimum for v9 * chore(e2e): Cocoa-v9: Bump E2E to iOS 15.0 * Bump to 15.1 due to RN 0.81.0 requirements --------- Co-authored-by: Itay Brenner Co-authored-by: Itay Brenner Co-authored-by: Denis Andrasec Co-authored-by: Noah Martin * chore(ci): Cocoa V9: Run the full CI checks for cocoa-v9 (#5370) * Update changelog * Bumpt to RC1 * Remove cocoa-v9 from ci branches * fix: Use new session replay name for hybrid SDK * Fix rename * chore(lint): Fixes lint issues * Update changelog to GA * Update changelog * Bump minimum to 9.1.0 * Set Cocoa version to 9.1.0 --------- Co-authored-by: Itay Brenner Co-authored-by: Denis AndraĊĦec Co-authored-by: Itay Brenner Co-authored-by: Noah Martin Co-authored-by: Philipp Hofmann --- .github/workflows/buildandtest.yml | 1 - .github/workflows/codegen.yml | 1 - .github/workflows/e2e-v2.yml | 1 - .github/workflows/native-tests.yml | 1 - .github/workflows/sample-application-expo.yml | 1 - .github/workflows/sample-application.yml | 1 - CHANGELOG.md | 3 ++ packages/core/RNSentry.podspec | 2 +- .../project.pbxproj | 16 +++++--- .../RNSentryDependencyContainerTests.h | 8 ---- .../RNSentryDependencyContainerTests.m | 2 +- .../RNSentryFramesTrackerListenerTests.h | 8 ---- .../RNSentryFramesTrackerListenerTests.m | 3 +- .../RNSentryOnDrawReporter+Test.h | 1 + .../RNSentryCocoaTesterTests/RNSentryTests.m | 12 +----- packages/core/ios/RNSentry.mm | 18 +++------ .../core/ios/RNSentryDependencyContainer.h | 3 +- .../core/ios/RNSentryDependencyContainer.m | 1 + packages/core/ios/RNSentryEmitNewFrameEvent.h | 3 ++ .../core/ios/RNSentryExperimentalOptions.m | 2 +- .../core/ios/RNSentryFramesTrackerListener.h | 4 +- .../core/ios/RNSentryFramesTrackerListener.m | 2 + packages/core/ios/RNSentryOnDrawReporter.h | 3 +- packages/core/ios/RNSentryOnDrawReporter.m | 2 + packages/core/ios/RNSentryRNSScreen.m | 7 ++-- .../ios/RNSentryReplayBreadcrumbConverter.m | 24 ++++++------ packages/core/ios/SentryScreenFramesWrapper.h | 14 +++++++ packages/core/ios/SentryScreenFramesWrapper.m | 39 +++++++++++++++++++ samples/react-native-macos/macos/Podfile | 2 +- 29 files changed, 110 insertions(+), 75 deletions(-) delete mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryDependencyContainerTests.h delete mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryFramesTrackerListenerTests.h create mode 100644 packages/core/ios/RNSentryEmitNewFrameEvent.h create mode 100644 packages/core/ios/SentryScreenFramesWrapper.h create mode 100644 packages/core/ios/SentryScreenFramesWrapper.m diff --git a/.github/workflows/buildandtest.yml b/.github/workflows/buildandtest.yml index 2cf04870e1..c6ae136ffc 100644 --- a/.github/workflows/buildandtest.yml +++ b/.github/workflows/buildandtest.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - v5 - release/** pull_request: diff --git a/.github/workflows/codegen.yml b/.github/workflows/codegen.yml index 1192121076..fc9f3563a8 100644 --- a/.github/workflows/codegen.yml +++ b/.github/workflows/codegen.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - v5 - release/** pull_request: diff --git a/.github/workflows/e2e-v2.yml b/.github/workflows/e2e-v2.yml index 6b9c445ab5..32b8d6b0dd 100644 --- a/.github/workflows/e2e-v2.yml +++ b/.github/workflows/e2e-v2.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - v5 - release/** pull_request: types: [opened, synchronize, reopened, labeled] diff --git a/.github/workflows/native-tests.yml b/.github/workflows/native-tests.yml index a09f68649e..051c340ba6 100644 --- a/.github/workflows/native-tests.yml +++ b/.github/workflows/native-tests.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - v5 - release/** pull_request: types: [opened, synchronize, reopened, labeled] diff --git a/.github/workflows/sample-application-expo.yml b/.github/workflows/sample-application-expo.yml index 6cae2cee91..2d592cde95 100644 --- a/.github/workflows/sample-application-expo.yml +++ b/.github/workflows/sample-application-expo.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - v5 pull_request: types: [opened, synchronize, reopened, labeled] diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 0976972c6c..ba1ce3c5dd 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - v5 pull_request: types: [opened, synchronize, reopened, labeled] diff --git a/CHANGELOG.md b/CHANGELOG.md index a2a1e1affa..b6ee003466 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ ### Dependencies +- Bump Cocoa SDK from v8.57.3 to v9.1.0 ([#5356](https://github.com/getsentry/sentry-react-native/pull/5356)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#910) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.57.3...9.1.0) - Bump JavaScript SDK from v10.30.0 to v10.32.1 ([#5480](https://github.com/getsentry/sentry-react-native/pull/5480), [#5487](https://github.com/getsentry/sentry-react-native/pull/5487), [#5496](https://github.com/getsentry/sentry-react-native/pull/5496)) - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#10321) - [diff](https://github.com/getsentry/sentry-javascript/compare/10.30.0...10.32.1) diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 73fc82f87f..d314d7e938 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -46,7 +46,7 @@ Pod::Spec.new do |s| s.compiler_flags = other_cflags - s.dependency 'Sentry/HybridSDK', '8.57.3' + s.dependency 'Sentry/HybridSDK', '9.1.0' if defined? install_modules_dependencies # Default React Native dependencies for 0.71 and above (new and legacy architecture) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index 7ee546e8af..4b74c4b06a 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -40,9 +40,7 @@ 338739072A7D7D2800950DDD /* RNSentryReplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryReplay.h; path = ../ios/RNSentryReplay.h; sourceTree = ""; }; 33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryOnDrawReporter.h; path = ../ios/RNSentryOnDrawReporter.h; sourceTree = ""; }; 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryFramesTrackerListenerTests.m; sourceTree = ""; }; - 33AFDFEE2B8D14C200AAB120 /* RNSentryFramesTrackerListenerTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryFramesTrackerListenerTests.h; sourceTree = ""; }; 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryDependencyContainerTests.m; sourceTree = ""; }; - 33AFDFF22B8D15F600AAB120 /* RNSentryDependencyContainerTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryDependencyContainerTests.h; sourceTree = ""; }; 33AFE0132B8F31AF00AAB120 /* RNSentryDependencyContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryDependencyContainer.h; path = ../ios/RNSentryDependencyContainer.h; sourceTree = ""; }; 33DEDFE92D8DBE5B006066E4 /* RNSentryOnDrawReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryOnDrawReporterTests.swift; sourceTree = ""; }; 33DEDFEB2D8DC800006066E4 /* RNSentryOnDrawReporter+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentryOnDrawReporter+Test.h"; sourceTree = ""; }; @@ -52,6 +50,8 @@ 33F58ACF2977037D008F60EA /* RNSentryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSentryTests.m; sourceTree = ""; }; 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNSentryCocoaTesterTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; E2321E7CFA55AB617247098E /* Pods-RNSentryCocoaTesterTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.debug.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.debug.xcconfig"; sourceTree = ""; }; + F48F26542EA2A481008A185E /* RNSentryEmitNewFrameEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryEmitNewFrameEvent.h; path = ../ios/RNSentryEmitNewFrameEvent.h; sourceTree = SOURCE_ROOT; }; + F48F26552EA2A4D4008A185E /* RNSentryFramesTrackerListener.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryFramesTrackerListener.h; path = ../ios/RNSentryFramesTrackerListener.h; sourceTree = SOURCE_ROOT; }; FADF868E2EBD053E00D6652D /* SentrySDKWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySDKWrapper.h; path = ../ios/SentrySDKWrapper.h; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ @@ -109,9 +109,7 @@ 33F58ACF2977037D008F60EA /* RNSentryTests.m */, 3339C4802D6625570088EB3A /* RNSentryUserTests.m */, 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */, - 33AFDFEE2B8D14C200AAB120 /* RNSentryFramesTrackerListenerTests.h */, 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */, - 33AFDFF22B8D15F600AAB120 /* RNSentryDependencyContainerTests.h */, 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */, 332D33462CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift */, 3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */, @@ -140,6 +138,8 @@ FADF868E2EBD053E00D6652D /* SentrySDKWrapper.h */, 33AFE0132B8F31AF00AAB120 /* RNSentryDependencyContainer.h */, 338739072A7D7D2800950DDD /* RNSentryReplay.h */, + F48F26542EA2A481008A185E /* RNSentryEmitNewFrameEvent.h */, + F48F26552EA2A4D4008A185E /* RNSentryFramesTrackerListener.h */, ); name = RNSentry; sourceTree = ""; @@ -238,10 +238,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests-resources.sh\"\n"; @@ -436,7 +440,7 @@ "\"$(PODS_TARGET_SRCROOT)/include/\"", "\"${PODS_ROOT}/Sentry/Sources/Sentry/include\"", ); - IPHONEOS_DEPLOYMENT_TARGET = 12.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.RNSentryCocoaTesterTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -503,7 +507,7 @@ "\"$(PODS_TARGET_SRCROOT)/include/\"", "\"${PODS_ROOT}/Sentry/Sources/Sentry/include\"", ); - IPHONEOS_DEPLOYMENT_TARGET = 12.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.RNSentryCocoaTesterTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryDependencyContainerTests.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryDependencyContainerTests.h deleted file mode 100644 index c987776703..0000000000 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryDependencyContainerTests.h +++ /dev/null @@ -1,8 +0,0 @@ -#import "SentryFramesTracker.h" -#import -#import - -@interface SentryDependencyContainer : NSObject -+ (instancetype)sharedInstance; -@property (nonatomic, strong) SentryFramesTracker *framesTracker; -@end diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryDependencyContainerTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryDependencyContainerTests.m index 1cef19682c..eb53a6322e 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryDependencyContainerTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryDependencyContainerTests.m @@ -1,8 +1,8 @@ -#import "RNSentryDependencyContainerTests.h" #import "RNSentryDependencyContainer.h" #import #import #import +@import Sentry; @interface RNSentryDependencyContainerTests : XCTestCase diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryFramesTrackerListenerTests.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryFramesTrackerListenerTests.h deleted file mode 100644 index c987776703..0000000000 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryFramesTrackerListenerTests.h +++ /dev/null @@ -1,8 +0,0 @@ -#import "SentryFramesTracker.h" -#import -#import - -@interface SentryDependencyContainer : NSObject -+ (instancetype)sharedInstance; -@property (nonatomic, strong) SentryFramesTracker *framesTracker; -@end diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryFramesTrackerListenerTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryFramesTrackerListenerTests.m index 7a877795d6..933d913cb5 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryFramesTrackerListenerTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryFramesTrackerListenerTests.m @@ -1,8 +1,9 @@ -#import "RNSentryFramesTrackerListenerTests.h" #import "RNSentryDependencyContainer.h" +#import "RNSentryFramesTrackerListener.h" #import #import #import +@import Sentry; @interface RNSentryFramesTrackerListenerTests : XCTestCase diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryOnDrawReporter+Test.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryOnDrawReporter+Test.h index 8a9df3a94e..753983baae 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryOnDrawReporter+Test.h +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryOnDrawReporter+Test.h @@ -1,3 +1,4 @@ +#import "RNSentryEmitNewFrameEvent.h" #import "RNSentryOnDrawReporter.h" #import diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m index d4cd8e957d..e24ba83756 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m @@ -40,7 +40,6 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties XCTAssertEqual( actualOptions.tracesSampleRate, nil, @"Traces sample rate should not be passed to native"); XCTAssertEqual(actualOptions.tracesSampler, nil, @"Traces sampler should not be passed to native"); -XCTAssertEqual(actualOptions.enableTracing, false, @"EnableTracing should not be passed to native"); } - (void)testCaptureFailedRequestsIsDisabled @@ -348,10 +347,7 @@ - (void)testCreateOptionsWithDictionaryEnableLogsEnabled XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); - id experimentalOptions = [actualOptions valueForKey:@"experimental"]; - XCTAssertNotNil(experimentalOptions, @"Experimental options should not be nil"); - - BOOL enableLogs = [[experimentalOptions valueForKey:@"enableLogs"] boolValue]; + BOOL enableLogs = [[actualOptions valueForKey:@"enableLogs"] boolValue]; XCTAssertTrue(enableLogs, @"enableLogs should be enabled"); } @@ -370,11 +366,7 @@ - (void)testCreateOptionsWithDictionaryEnableLogsDisabled XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); - - id experimentalOptions = [actualOptions valueForKey:@"experimental"]; - XCTAssertNotNil(experimentalOptions, @"Experimental options should not be nil"); - - BOOL enableLogs = [[experimentalOptions valueForKey:@"enableLogs"] boolValue]; + BOOL enableLogs = [[actualOptions valueForKey:@"enableLogs"] boolValue]; XCTAssertFalse(enableLogs, @"enableLogs should be disabled"); } diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 10586ab910..033c898e21 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -26,7 +26,6 @@ #import #import #import -#import #import // This guard prevents importing Hermes in JSC apps @@ -47,13 +46,14 @@ #endif #if SENTRY_HAS_UIKIT -# import "RNSentryFramesTrackerListener.h" +# import "RNSentryEmitNewFrameEvent.h" # import "RNSentryRNSScreen.h" #endif #import "RNSentryExperimentalOptions.h" #import "RNSentryVersion.h" #import "SentrySDKWrapper.h" +#import "SentryScreenFramesWrapper.h" static bool hasFetchedAppStart; @@ -486,21 +486,15 @@ - (void)stopObserving #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST if (PrivateSentrySDKOnly.isFramesTrackingRunning) { - SentryScreenFrames *frames = PrivateSentrySDKOnly.currentScreenFrames; - - if (frames == nil) { + if (![SentryScreenFramesWrapper canTrackFrames]) { resolve(nil); return; } - NSNumber *total = [NSNumber numberWithLong:frames.total]; - NSNumber *frozen = [NSNumber numberWithLong:frames.frozen]; - NSNumber *slow = [NSNumber numberWithLong:frames.slow]; - resolve(@ { - @"totalFrames" : total, - @"frozenFrames" : frozen, - @"slowFrames" : slow, + @"totalFrames" : [SentryScreenFramesWrapper totalFrames], + @"frozenFrames" : [SentryScreenFramesWrapper frozenFrames], + @"slowFrames" : [SentryScreenFramesWrapper slowFrames], }); } else { resolve(nil); diff --git a/packages/core/ios/RNSentryDependencyContainer.h b/packages/core/ios/RNSentryDependencyContainer.h index cd3eca59c7..b3b3ff5233 100644 --- a/packages/core/ios/RNSentryDependencyContainer.h +++ b/packages/core/ios/RNSentryDependencyContainer.h @@ -1,6 +1,7 @@ #import -#import "RNSentryFramesTrackerListener.h" +#import "RNSentryEmitNewFrameEvent.h" +@class RNSentryFramesTrackerListener; @interface RNSentryDependencyContainer : NSObject SENTRY_NO_INIT diff --git a/packages/core/ios/RNSentryDependencyContainer.m b/packages/core/ios/RNSentryDependencyContainer.m index a29b602d52..08c4f6a8bf 100644 --- a/packages/core/ios/RNSentryDependencyContainer.m +++ b/packages/core/ios/RNSentryDependencyContainer.m @@ -1,4 +1,5 @@ #import "RNSentryDependencyContainer.h" +#import "RNSentryFramesTrackerListener.h" @import Sentry; @implementation RNSentryDependencyContainer { diff --git a/packages/core/ios/RNSentryEmitNewFrameEvent.h b/packages/core/ios/RNSentryEmitNewFrameEvent.h new file mode 100644 index 0000000000..473c91cc77 --- /dev/null +++ b/packages/core/ios/RNSentryEmitNewFrameEvent.h @@ -0,0 +1,3 @@ +#import + +typedef void (^RNSentryEmitNewFrameEvent)(NSNumber *newFrameTimestampInSeconds); diff --git a/packages/core/ios/RNSentryExperimentalOptions.m b/packages/core/ios/RNSentryExperimentalOptions.m index 7e0974e527..7bb198ab60 100644 --- a/packages/core/ios/RNSentryExperimentalOptions.m +++ b/packages/core/ios/RNSentryExperimentalOptions.m @@ -24,7 +24,7 @@ + (void)setEnableLogs:(BOOL)enabled sentryOptions:(SentryOptions *)sentryOptions if (sentryOptions == nil) { return; } - sentryOptions.experimental.enableLogs = enabled; + sentryOptions.enableLogs = enabled; } + (void)setEnableSessionReplayInUnreliableEnvironment:(BOOL)enabled diff --git a/packages/core/ios/RNSentryFramesTrackerListener.h b/packages/core/ios/RNSentryFramesTrackerListener.h index 627b3059f4..630f0daeee 100644 --- a/packages/core/ios/RNSentryFramesTrackerListener.h +++ b/packages/core/ios/RNSentryFramesTrackerListener.h @@ -2,11 +2,11 @@ #if SENTRY_HAS_UIKIT +# import "RNSentryEmitNewFrameEvent.h" # import # import -# import -typedef void (^RNSentryEmitNewFrameEvent)(NSNumber *newFrameTimestampInSeconds); +@import Sentry; @protocol RNSentryFramesTrackerListenerProtocol diff --git a/packages/core/ios/RNSentryFramesTrackerListener.m b/packages/core/ios/RNSentryFramesTrackerListener.m index 4a3c7d99cd..2e090bc711 100644 --- a/packages/core/ios/RNSentryFramesTrackerListener.m +++ b/packages/core/ios/RNSentryFramesTrackerListener.m @@ -2,6 +2,8 @@ #if SENTRY_HAS_UIKIT +@import Sentry; + @implementation RNSentryFramesTrackerListener - (instancetype)initWithSentryFramesTracker:(SentryFramesTracker *)framesTracker diff --git a/packages/core/ios/RNSentryOnDrawReporter.h b/packages/core/ios/RNSentryOnDrawReporter.h index 5c4083015d..b462dc4f0b 100644 --- a/packages/core/ios/RNSentryOnDrawReporter.h +++ b/packages/core/ios/RNSentryOnDrawReporter.h @@ -2,10 +2,11 @@ #if SENTRY_HAS_UIKIT -# import "RNSentryFramesTrackerListener.h" # import # import +@protocol RNSentryFramesTrackerListenerProtocol; + @interface RNSentryOnDrawReporter : RCTViewManager @end diff --git a/packages/core/ios/RNSentryOnDrawReporter.m b/packages/core/ios/RNSentryOnDrawReporter.m index b069fcd6f6..6f63ca92d8 100644 --- a/packages/core/ios/RNSentryOnDrawReporter.m +++ b/packages/core/ios/RNSentryOnDrawReporter.m @@ -1,4 +1,6 @@ #import "RNSentryOnDrawReporter.h" +#import "RNSentryEmitNewFrameEvent.h" +#import "RNSentryFramesTrackerListener.h" #import "RNSentryTimeToDisplay.h" @import Sentry; diff --git a/packages/core/ios/RNSentryRNSScreen.m b/packages/core/ios/RNSentryRNSScreen.m index 20c42ab4c4..90b2e733d3 100644 --- a/packages/core/ios/RNSentryRNSScreen.m +++ b/packages/core/ios/RNSentryRNSScreen.m @@ -2,11 +2,10 @@ #if SENTRY_HAS_UIKIT -# import -# import -# import - # import "RNSentryDependencyContainer.h" +# import "RNSentryFramesTrackerListener.h" +# import +@import Sentry; @implementation RNSentryRNSScreen diff --git a/packages/core/ios/RNSentryReplayBreadcrumbConverter.m b/packages/core/ios/RNSentryReplayBreadcrumbConverter.m index 14dfdc32a0..59bf29e0e6 100644 --- a/packages/core/ios/RNSentryReplayBreadcrumbConverter.m +++ b/packages/core/ios/RNSentryReplayBreadcrumbConverter.m @@ -11,7 +11,7 @@ @implementation RNSentryReplayBreadcrumbConverter { - (instancetype _Nonnull)init { if (self = [super init]) { - self->defaultConverter = [SentrySessionReplayIntegration createDefaultBreadcrumbConverter]; + self->defaultConverter = [SentrySessionReplayHybridSDK createDefaultBreadcrumbConverter]; } return self; } @@ -36,11 +36,11 @@ - (instancetype _Nonnull)init } if ([breadcrumb.category isEqualToString:@"navigation"]) { - return [SentrySessionReplayIntegration createBreadcrumbwithTimestamp:breadcrumb.timestamp - category:breadcrumb.category - message:nil - level:breadcrumb.level - data:breadcrumb.data]; + return [SentrySessionReplayHybridSDK createBreadcrumbwithTimestamp:breadcrumb.timestamp + category:breadcrumb.category + message:nil + level:breadcrumb.level + data:breadcrumb.data]; } if ([breadcrumb.category isEqualToString:@"xhr"]) { @@ -68,11 +68,11 @@ - (instancetype _Nonnull)init NSMutableArray *path = [breadcrumb.data valueForKey:@"path"]; NSString *message = [RNSentryReplayBreadcrumbConverter getTouchPathMessageFrom:path]; - return [SentrySessionReplayIntegration createBreadcrumbwithTimestamp:breadcrumb.timestamp - category:@"ui.tap" - message:message - level:breadcrumb.level - data:breadcrumb.data]; + return [SentrySessionReplayHybridSDK createBreadcrumbwithTimestamp:breadcrumb.timestamp + category:@"ui.tap" + message:message + level:breadcrumb.level + data:breadcrumb.data]; } + (NSString *_Nullable)getTouchPathMessageFrom:(NSArray *_Nullable)path @@ -156,7 +156,7 @@ + (NSString *_Nullable)getTouchPathMessageFrom:(NSArray *_Nullable)path data[@"responseBodySize"] = breadcrumb.data[@"response_body_size"]; } - return [SentrySessionReplayIntegration + return [SentrySessionReplayHybridSDK createNetworkBreadcrumbWithTimestamp:[NSDate dateWithTimeIntervalSince1970:(startTimestamp .doubleValue diff --git a/packages/core/ios/SentryScreenFramesWrapper.h b/packages/core/ios/SentryScreenFramesWrapper.h new file mode 100644 index 0000000000..4c664140e0 --- /dev/null +++ b/packages/core/ios/SentryScreenFramesWrapper.h @@ -0,0 +1,14 @@ +#import + +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + +@interface SentryScreenFramesWrapper : NSObject + ++ (BOOL)canTrackFrames; ++ (NSNumber *)totalFrames; ++ (NSNumber *)frozenFrames; ++ (NSNumber *)slowFrames; + +@end + +#endif // TARGET_OS_IPHONE || TARGET_OS_MACCATALYST diff --git a/packages/core/ios/SentryScreenFramesWrapper.m b/packages/core/ios/SentryScreenFramesWrapper.m new file mode 100644 index 0000000000..9df4e13070 --- /dev/null +++ b/packages/core/ios/SentryScreenFramesWrapper.m @@ -0,0 +1,39 @@ +#import "SentryScreenFramesWrapper.h" +@import Sentry; + +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + +@implementation SentryScreenFramesWrapper + ++ (BOOL)canTrackFrames +{ + return PrivateSentrySDKOnly.currentScreenFrames != nil; +} + ++ (NSNumber *)totalFrames +{ + if (![self canTrackFrames]) { + return nil; + } + return [NSNumber numberWithLong:PrivateSentrySDKOnly.currentScreenFrames.total]; +} + ++ (NSNumber *)frozenFrames +{ + if (![self canTrackFrames]) { + return nil; + } + return [NSNumber numberWithLong:PrivateSentrySDKOnly.currentScreenFrames.frozen]; +} + ++ (NSNumber *)slowFrames +{ + if (![self canTrackFrames]) { + return nil; + } + return [NSNumber numberWithLong:PrivateSentrySDKOnly.currentScreenFrames.slow]; +} + +@end + +#endif // TARGET_OS_IPHONE || TARGET_OS_MACCATALYST diff --git a/samples/react-native-macos/macos/Podfile b/samples/react-native-macos/macos/Podfile index 2a5c39963b..500d613ed2 100644 --- a/samples/react-native-macos/macos/Podfile +++ b/samples/react-native-macos/macos/Podfile @@ -4,7 +4,7 @@ require_relative '../node_modules/@react-native-community/cli-platform-ios/nativ prepare_react_native_project! target 'sentry-react-native-sample-macOS' do - platform :macos, '10.15' + platform :macos, '12.0' use_native_modules! # Flags change depending on the env values. From 7345ba802ddcf1e55e82400fba5a167fecead73d Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 8 Jan 2026 15:00:44 +0100 Subject: [PATCH 03/12] chore(deps): update CLI to v3.0.2 (#5514) * chore(deps): update CLI to v3.0.2 * Update changelog * Bump in core --- CHANGELOG.md | 6 +-- package.json | 2 +- packages/core/package.json | 2 +- yarn.lock | 78 +++++++++++++++++++------------------- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a84e5e3ef..930c95da52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,9 @@ - Bump JavaScript SDK from v10.30.0 to v10.32.1 ([#5480](https://github.com/getsentry/sentry-react-native/pull/5480), [#5487](https://github.com/getsentry/sentry-react-native/pull/5487), [#5496](https://github.com/getsentry/sentry-react-native/pull/5496)) - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#10321) - [diff](https://github.com/getsentry/sentry-javascript/compare/10.30.0...10.32.1) -- Bump CLI from v2.58.4 to v3.0.1 ([#5471](https://github.com/getsentry/sentry-react-native/pull/5471)) - - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#301) - - [diff](https://github.com/getsentry/sentry-cli/compare/2.58.4...3.0.1) +- Bump CLI from v2.58.4 to v3.0.2 ([#5471](https://github.com/getsentry/sentry-react-native/pull/5471), [#5514](https://github.com/getsentry/sentry-react-native/pull/5514)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#302) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.58.4...3.0.2) ## 7.8.0 diff --git a/package.json b/package.json index 77fde368cf..aa7db49ba5 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "@naturalcycles/ktlint": "^1.13.0", - "@sentry/cli": "3.0.1", + "@sentry/cli": "3.0.2", "downlevel-dts": "^0.11.0", "google-java-format": "^1.4.0", "lerna": "^8.1.8", diff --git a/packages/core/package.json b/packages/core/package.json index 2b419bc45b..b814730b6c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -70,7 +70,7 @@ "dependencies": { "@sentry/babel-plugin-component-annotate": "4.6.1", "@sentry/browser": "10.32.1", - "@sentry/cli": "3.0.1", + "@sentry/cli": "3.0.2", "@sentry/core": "10.32.1", "@sentry/react": "10.32.1", "@sentry/types": "10.32.1" diff --git a/yarn.lock b/yarn.lock index 0a8c4eb472..20dd7ab29a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10083,74 +10083,74 @@ __metadata: languageName: node linkType: hard -"@sentry/cli-darwin@npm:3.0.1": - version: 3.0.1 - resolution: "@sentry/cli-darwin@npm:3.0.1" +"@sentry/cli-darwin@npm:3.0.2": + version: 3.0.2 + resolution: "@sentry/cli-darwin@npm:3.0.2" conditions: os=darwin languageName: node linkType: hard -"@sentry/cli-linux-arm64@npm:3.0.1": - version: 3.0.1 - resolution: "@sentry/cli-linux-arm64@npm:3.0.1" +"@sentry/cli-linux-arm64@npm:3.0.2": + version: 3.0.2 + resolution: "@sentry/cli-linux-arm64@npm:3.0.2" conditions: (os=linux | os=freebsd | os=android) & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-linux-arm@npm:3.0.1": - version: 3.0.1 - resolution: "@sentry/cli-linux-arm@npm:3.0.1" +"@sentry/cli-linux-arm@npm:3.0.2": + version: 3.0.2 + resolution: "@sentry/cli-linux-arm@npm:3.0.2" conditions: (os=linux | os=freebsd | os=android) & cpu=arm languageName: node linkType: hard -"@sentry/cli-linux-i686@npm:3.0.1": - version: 3.0.1 - resolution: "@sentry/cli-linux-i686@npm:3.0.1" +"@sentry/cli-linux-i686@npm:3.0.2": + version: 3.0.2 + resolution: "@sentry/cli-linux-i686@npm:3.0.2" conditions: (os=linux | os=freebsd | os=android) & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-linux-x64@npm:3.0.1": - version: 3.0.1 - resolution: "@sentry/cli-linux-x64@npm:3.0.1" +"@sentry/cli-linux-x64@npm:3.0.2": + version: 3.0.2 + resolution: "@sentry/cli-linux-x64@npm:3.0.2" conditions: (os=linux | os=freebsd | os=android) & cpu=x64 languageName: node linkType: hard -"@sentry/cli-win32-arm64@npm:3.0.1": - version: 3.0.1 - resolution: "@sentry/cli-win32-arm64@npm:3.0.1" +"@sentry/cli-win32-arm64@npm:3.0.2": + version: 3.0.2 + resolution: "@sentry/cli-win32-arm64@npm:3.0.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-win32-i686@npm:3.0.1": - version: 3.0.1 - resolution: "@sentry/cli-win32-i686@npm:3.0.1" +"@sentry/cli-win32-i686@npm:3.0.2": + version: 3.0.2 + resolution: "@sentry/cli-win32-i686@npm:3.0.2" conditions: os=win32 & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-win32-x64@npm:3.0.1": - version: 3.0.1 - resolution: "@sentry/cli-win32-x64@npm:3.0.1" +"@sentry/cli-win32-x64@npm:3.0.2": + version: 3.0.2 + resolution: "@sentry/cli-win32-x64@npm:3.0.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@sentry/cli@npm:3.0.1": - version: 3.0.1 - resolution: "@sentry/cli@npm:3.0.1" - dependencies: - "@sentry/cli-darwin": 3.0.1 - "@sentry/cli-linux-arm": 3.0.1 - "@sentry/cli-linux-arm64": 3.0.1 - "@sentry/cli-linux-i686": 3.0.1 - "@sentry/cli-linux-x64": 3.0.1 - "@sentry/cli-win32-arm64": 3.0.1 - "@sentry/cli-win32-i686": 3.0.1 - "@sentry/cli-win32-x64": 3.0.1 +"@sentry/cli@npm:3.0.2": + version: 3.0.2 + resolution: "@sentry/cli@npm:3.0.2" + dependencies: + "@sentry/cli-darwin": 3.0.2 + "@sentry/cli-linux-arm": 3.0.2 + "@sentry/cli-linux-arm64": 3.0.2 + "@sentry/cli-linux-i686": 3.0.2 + "@sentry/cli-linux-x64": 3.0.2 + "@sentry/cli-win32-arm64": 3.0.2 + "@sentry/cli-win32-i686": 3.0.2 + "@sentry/cli-win32-x64": 3.0.2 progress: ^2.0.3 proxy-from-env: ^1.1.0 undici: ^6.22.0 @@ -10174,7 +10174,7 @@ __metadata: optional: true bin: sentry-cli: bin/sentry-cli - checksum: f6fdc70855dd17eefd881edb50fe358ad87ce7b180e136c0b4a14373d56453735e5ba6a24548dd321b22e38ad6729192fa1f71b5d488ccd87e21729368a79a99 + checksum: 412ba81f84b84772f9d47ddebf168b02b140053e29e32979ad628e5910c3b44131b181a1ccbb00ed32851ee4f05d38c2680e2a0d230ae842eb69e2f3fc3773b2 languageName: node linkType: hard @@ -10283,7 +10283,7 @@ __metadata: "@sentry-internal/typescript": 10.32.1 "@sentry/babel-plugin-component-annotate": 4.6.1 "@sentry/browser": 10.32.1 - "@sentry/cli": 3.0.1 + "@sentry/cli": 3.0.2 "@sentry/core": 10.32.1 "@sentry/react": 10.32.1 "@sentry/types": 10.32.1 @@ -28955,7 +28955,7 @@ __metadata: resolution: "sentry-react-native@workspace:." dependencies: "@naturalcycles/ktlint": ^1.13.0 - "@sentry/cli": 3.0.1 + "@sentry/cli": 3.0.2 downlevel-dts: ^0.11.0 google-java-format: ^1.4.0 lerna: ^8.1.8 From 2eea94bb0cf552a6e2e6c47cc076e46ce54e136c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:45:57 +0100 Subject: [PATCH 04/12] chore: update scripts/update-cli.sh to 3.0.3 (#5502) Co-authored-by: GitHub Co-authored-by: Antonis Lilis --- CHANGELOG.md | 6 +-- package.json | 2 +- packages/core/package.json | 2 +- yarn.lock | 84 +++++++++++++++++++------------------- 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 930c95da52..c1ff262305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,9 @@ - Bump JavaScript SDK from v10.30.0 to v10.32.1 ([#5480](https://github.com/getsentry/sentry-react-native/pull/5480), [#5487](https://github.com/getsentry/sentry-react-native/pull/5487), [#5496](https://github.com/getsentry/sentry-react-native/pull/5496)) - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#10321) - [diff](https://github.com/getsentry/sentry-javascript/compare/10.30.0...10.32.1) -- Bump CLI from v2.58.4 to v3.0.2 ([#5471](https://github.com/getsentry/sentry-react-native/pull/5471), [#5514](https://github.com/getsentry/sentry-react-native/pull/5514)) - - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#302) - - [diff](https://github.com/getsentry/sentry-cli/compare/2.58.4...3.0.2) +- Bump CLI from v2.58.4 to v3.0.3 ([#5471](https://github.com/getsentry/sentry-react-native/pull/5471), [#5514](https://github.com/getsentry/sentry-react-native/pull/5514), [#5502](https://github.com/getsentry/sentry-react-native/pull/5502)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#303) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.58.4...3.0.3) ## 7.8.0 diff --git a/package.json b/package.json index aa7db49ba5..31659409c5 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "@naturalcycles/ktlint": "^1.13.0", - "@sentry/cli": "3.0.2", + "@sentry/cli": "3.0.3", "downlevel-dts": "^0.11.0", "google-java-format": "^1.4.0", "lerna": "^8.1.8", diff --git a/packages/core/package.json b/packages/core/package.json index b814730b6c..27df2575dc 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -70,7 +70,7 @@ "dependencies": { "@sentry/babel-plugin-component-annotate": "4.6.1", "@sentry/browser": "10.32.1", - "@sentry/cli": "3.0.2", + "@sentry/cli": "3.0.3", "@sentry/core": "10.32.1", "@sentry/react": "10.32.1", "@sentry/types": "10.32.1" diff --git a/yarn.lock b/yarn.lock index 20dd7ab29a..331d3f734a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10083,74 +10083,74 @@ __metadata: languageName: node linkType: hard -"@sentry/cli-darwin@npm:3.0.2": - version: 3.0.2 - resolution: "@sentry/cli-darwin@npm:3.0.2" +"@sentry/cli-darwin@npm:3.0.3": + version: 3.0.3 + resolution: "@sentry/cli-darwin@npm:3.0.3" conditions: os=darwin languageName: node linkType: hard -"@sentry/cli-linux-arm64@npm:3.0.2": - version: 3.0.2 - resolution: "@sentry/cli-linux-arm64@npm:3.0.2" +"@sentry/cli-linux-arm64@npm:3.0.3": + version: 3.0.3 + resolution: "@sentry/cli-linux-arm64@npm:3.0.3" conditions: (os=linux | os=freebsd | os=android) & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-linux-arm@npm:3.0.2": - version: 3.0.2 - resolution: "@sentry/cli-linux-arm@npm:3.0.2" +"@sentry/cli-linux-arm@npm:3.0.3": + version: 3.0.3 + resolution: "@sentry/cli-linux-arm@npm:3.0.3" conditions: (os=linux | os=freebsd | os=android) & cpu=arm languageName: node linkType: hard -"@sentry/cli-linux-i686@npm:3.0.2": - version: 3.0.2 - resolution: "@sentry/cli-linux-i686@npm:3.0.2" +"@sentry/cli-linux-i686@npm:3.0.3": + version: 3.0.3 + resolution: "@sentry/cli-linux-i686@npm:3.0.3" conditions: (os=linux | os=freebsd | os=android) & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-linux-x64@npm:3.0.2": - version: 3.0.2 - resolution: "@sentry/cli-linux-x64@npm:3.0.2" +"@sentry/cli-linux-x64@npm:3.0.3": + version: 3.0.3 + resolution: "@sentry/cli-linux-x64@npm:3.0.3" conditions: (os=linux | os=freebsd | os=android) & cpu=x64 languageName: node linkType: hard -"@sentry/cli-win32-arm64@npm:3.0.2": - version: 3.0.2 - resolution: "@sentry/cli-win32-arm64@npm:3.0.2" +"@sentry/cli-win32-arm64@npm:3.0.3": + version: 3.0.3 + resolution: "@sentry/cli-win32-arm64@npm:3.0.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-win32-i686@npm:3.0.2": - version: 3.0.2 - resolution: "@sentry/cli-win32-i686@npm:3.0.2" +"@sentry/cli-win32-i686@npm:3.0.3": + version: 3.0.3 + resolution: "@sentry/cli-win32-i686@npm:3.0.3" conditions: os=win32 & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-win32-x64@npm:3.0.2": - version: 3.0.2 - resolution: "@sentry/cli-win32-x64@npm:3.0.2" +"@sentry/cli-win32-x64@npm:3.0.3": + version: 3.0.3 + resolution: "@sentry/cli-win32-x64@npm:3.0.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@sentry/cli@npm:3.0.2": - version: 3.0.2 - resolution: "@sentry/cli@npm:3.0.2" - dependencies: - "@sentry/cli-darwin": 3.0.2 - "@sentry/cli-linux-arm": 3.0.2 - "@sentry/cli-linux-arm64": 3.0.2 - "@sentry/cli-linux-i686": 3.0.2 - "@sentry/cli-linux-x64": 3.0.2 - "@sentry/cli-win32-arm64": 3.0.2 - "@sentry/cli-win32-i686": 3.0.2 - "@sentry/cli-win32-x64": 3.0.2 +"@sentry/cli@npm:3.0.3": + version: 3.0.3 + resolution: "@sentry/cli@npm:3.0.3" + dependencies: + "@sentry/cli-darwin": 3.0.3 + "@sentry/cli-linux-arm": 3.0.3 + "@sentry/cli-linux-arm64": 3.0.3 + "@sentry/cli-linux-i686": 3.0.3 + "@sentry/cli-linux-x64": 3.0.3 + "@sentry/cli-win32-arm64": 3.0.3 + "@sentry/cli-win32-i686": 3.0.3 + "@sentry/cli-win32-x64": 3.0.3 progress: ^2.0.3 proxy-from-env: ^1.1.0 undici: ^6.22.0 @@ -10174,7 +10174,7 @@ __metadata: optional: true bin: sentry-cli: bin/sentry-cli - checksum: 412ba81f84b84772f9d47ddebf168b02b140053e29e32979ad628e5910c3b44131b181a1ccbb00ed32851ee4f05d38c2680e2a0d230ae842eb69e2f3fc3773b2 + checksum: 48fbb4108534dd8004dd60837b54344574c0fffc300040fba8297e9171e6d85db97e4bdc0b22f8fdc728638ba82b5df5ec2a798269de716bcff868e167b55946 languageName: node linkType: hard @@ -10283,7 +10283,7 @@ __metadata: "@sentry-internal/typescript": 10.32.1 "@sentry/babel-plugin-component-annotate": 4.6.1 "@sentry/browser": 10.32.1 - "@sentry/cli": 3.0.2 + "@sentry/cli": 3.0.3 "@sentry/core": 10.32.1 "@sentry/react": 10.32.1 "@sentry/types": 10.32.1 @@ -28955,7 +28955,7 @@ __metadata: resolution: "sentry-react-native@workspace:." dependencies: "@naturalcycles/ktlint": ^1.13.0 - "@sentry/cli": 3.0.2 + "@sentry/cli": 3.0.3 downlevel-dts: ^0.11.0 google-java-format: ^1.4.0 lerna: ^8.1.8 @@ -31305,9 +31305,9 @@ __metadata: linkType: hard "undici@npm:^6.22.0": - version: 6.22.0 - resolution: "undici@npm:6.22.0" - checksum: ec2d846cb7d360fd45c2e3848bbdadbe086c167be08dd578ed376c70afb2b977950b4c4919c18da0610c61a1ef53c079086d09390a96de2b62bc1fa16d7765f8 + version: 6.23.0 + resolution: "undici@npm:6.23.0" + checksum: f0953920330375e76d1614381af07da9d7c21ad3244d0785b3f7bd4072635c20a1f432ef3a129baa3e4a92278ce32e9ea2ca8b5f0e0554a5739222af332c08fe languageName: node linkType: hard From 54e4f2d9560d3289b9a0df549c6dfebf163880e2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:11:50 +0100 Subject: [PATCH 05/12] chore: update scripts/update-cli.sh to 3.1.0 (#5523) Co-authored-by: GitHub Co-authored-by: Antonis Lilis --- CHANGELOG.md | 6 +-- package.json | 2 +- packages/core/package.json | 2 +- yarn.lock | 78 +++++++++++++++++++------------------- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1ff262305..d086eaddaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,9 @@ - Bump JavaScript SDK from v10.30.0 to v10.32.1 ([#5480](https://github.com/getsentry/sentry-react-native/pull/5480), [#5487](https://github.com/getsentry/sentry-react-native/pull/5487), [#5496](https://github.com/getsentry/sentry-react-native/pull/5496)) - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#10321) - [diff](https://github.com/getsentry/sentry-javascript/compare/10.30.0...10.32.1) -- Bump CLI from v2.58.4 to v3.0.3 ([#5471](https://github.com/getsentry/sentry-react-native/pull/5471), [#5514](https://github.com/getsentry/sentry-react-native/pull/5514), [#5502](https://github.com/getsentry/sentry-react-native/pull/5502)) - - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#303) - - [diff](https://github.com/getsentry/sentry-cli/compare/2.58.4...3.0.3) +- Bump CLI from v2.58.4 to v3.1.0 ([#5523](https://github.com/getsentry/sentry-react-native/pull/5523), [#5471](https://github.com/getsentry/sentry-react-native/pull/5471), [#5514](https://github.com/getsentry/sentry-react-native/pull/5514), [#5502](https://github.com/getsentry/sentry-react-native/pull/5502)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#310) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.58.4...3.1.0) ## 7.8.0 diff --git a/package.json b/package.json index 31659409c5..aa8b6080be 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ }, "devDependencies": { "@naturalcycles/ktlint": "^1.13.0", - "@sentry/cli": "3.0.3", + "@sentry/cli": "3.1.0", "downlevel-dts": "^0.11.0", "google-java-format": "^1.4.0", "lerna": "^8.1.8", diff --git a/packages/core/package.json b/packages/core/package.json index 0ca9888673..4d3cb028a8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -70,7 +70,7 @@ "dependencies": { "@sentry/babel-plugin-component-annotate": "4.6.1", "@sentry/browser": "10.32.1", - "@sentry/cli": "3.0.3", + "@sentry/cli": "3.1.0", "@sentry/core": "10.32.1", "@sentry/react": "10.32.1", "@sentry/types": "10.32.1" diff --git a/yarn.lock b/yarn.lock index da43ac07ba..d7fd6f30ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10083,74 +10083,74 @@ __metadata: languageName: node linkType: hard -"@sentry/cli-darwin@npm:3.0.3": - version: 3.0.3 - resolution: "@sentry/cli-darwin@npm:3.0.3" +"@sentry/cli-darwin@npm:3.1.0": + version: 3.1.0 + resolution: "@sentry/cli-darwin@npm:3.1.0" conditions: os=darwin languageName: node linkType: hard -"@sentry/cli-linux-arm64@npm:3.0.3": - version: 3.0.3 - resolution: "@sentry/cli-linux-arm64@npm:3.0.3" +"@sentry/cli-linux-arm64@npm:3.1.0": + version: 3.1.0 + resolution: "@sentry/cli-linux-arm64@npm:3.1.0" conditions: (os=linux | os=freebsd | os=android) & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-linux-arm@npm:3.0.3": - version: 3.0.3 - resolution: "@sentry/cli-linux-arm@npm:3.0.3" +"@sentry/cli-linux-arm@npm:3.1.0": + version: 3.1.0 + resolution: "@sentry/cli-linux-arm@npm:3.1.0" conditions: (os=linux | os=freebsd | os=android) & cpu=arm languageName: node linkType: hard -"@sentry/cli-linux-i686@npm:3.0.3": - version: 3.0.3 - resolution: "@sentry/cli-linux-i686@npm:3.0.3" +"@sentry/cli-linux-i686@npm:3.1.0": + version: 3.1.0 + resolution: "@sentry/cli-linux-i686@npm:3.1.0" conditions: (os=linux | os=freebsd | os=android) & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-linux-x64@npm:3.0.3": - version: 3.0.3 - resolution: "@sentry/cli-linux-x64@npm:3.0.3" +"@sentry/cli-linux-x64@npm:3.1.0": + version: 3.1.0 + resolution: "@sentry/cli-linux-x64@npm:3.1.0" conditions: (os=linux | os=freebsd | os=android) & cpu=x64 languageName: node linkType: hard -"@sentry/cli-win32-arm64@npm:3.0.3": - version: 3.0.3 - resolution: "@sentry/cli-win32-arm64@npm:3.0.3" +"@sentry/cli-win32-arm64@npm:3.1.0": + version: 3.1.0 + resolution: "@sentry/cli-win32-arm64@npm:3.1.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-win32-i686@npm:3.0.3": - version: 3.0.3 - resolution: "@sentry/cli-win32-i686@npm:3.0.3" +"@sentry/cli-win32-i686@npm:3.1.0": + version: 3.1.0 + resolution: "@sentry/cli-win32-i686@npm:3.1.0" conditions: os=win32 & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-win32-x64@npm:3.0.3": - version: 3.0.3 - resolution: "@sentry/cli-win32-x64@npm:3.0.3" +"@sentry/cli-win32-x64@npm:3.1.0": + version: 3.1.0 + resolution: "@sentry/cli-win32-x64@npm:3.1.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@sentry/cli@npm:3.0.3": - version: 3.0.3 - resolution: "@sentry/cli@npm:3.0.3" - dependencies: - "@sentry/cli-darwin": 3.0.3 - "@sentry/cli-linux-arm": 3.0.3 - "@sentry/cli-linux-arm64": 3.0.3 - "@sentry/cli-linux-i686": 3.0.3 - "@sentry/cli-linux-x64": 3.0.3 - "@sentry/cli-win32-arm64": 3.0.3 - "@sentry/cli-win32-i686": 3.0.3 - "@sentry/cli-win32-x64": 3.0.3 +"@sentry/cli@npm:3.1.0": + version: 3.1.0 + resolution: "@sentry/cli@npm:3.1.0" + dependencies: + "@sentry/cli-darwin": 3.1.0 + "@sentry/cli-linux-arm": 3.1.0 + "@sentry/cli-linux-arm64": 3.1.0 + "@sentry/cli-linux-i686": 3.1.0 + "@sentry/cli-linux-x64": 3.1.0 + "@sentry/cli-win32-arm64": 3.1.0 + "@sentry/cli-win32-i686": 3.1.0 + "@sentry/cli-win32-x64": 3.1.0 progress: ^2.0.3 proxy-from-env: ^1.1.0 undici: ^6.22.0 @@ -10174,7 +10174,7 @@ __metadata: optional: true bin: sentry-cli: bin/sentry-cli - checksum: 48fbb4108534dd8004dd60837b54344574c0fffc300040fba8297e9171e6d85db97e4bdc0b22f8fdc728638ba82b5df5ec2a798269de716bcff868e167b55946 + checksum: 19ea444aceb0c97d8164b6cb595f5f7d88afd12a5d5ecb7f70f4eddd5d7fc16c3b669446887eca113c0a65eead2f8961cc125f8727d5d1a27e8fb2d65a9d9d12 languageName: node linkType: hard @@ -10283,7 +10283,7 @@ __metadata: "@sentry-internal/typescript": 10.32.1 "@sentry/babel-plugin-component-annotate": 4.6.1 "@sentry/browser": 10.32.1 - "@sentry/cli": 3.0.3 + "@sentry/cli": 3.1.0 "@sentry/core": 10.32.1 "@sentry/react": 10.32.1 "@sentry/types": 10.32.1 @@ -28955,7 +28955,7 @@ __metadata: resolution: "sentry-react-native@workspace:." dependencies: "@naturalcycles/ktlint": ^1.13.0 - "@sentry/cli": 3.0.3 + "@sentry/cli": 3.1.0 downlevel-dts: ^0.11.0 google-java-format: ^1.4.0 lerna: ^8.1.8 From 2663f1954ba0f9042c690937a80f483cf73addd9 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 16 Jan 2026 14:36:53 +0100 Subject: [PATCH 06/12] Fix changelog --- CHANGELOG.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81ef7710ad..4ee98a345e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. + +## Unreleased + +### Dependencies + +- Bump Cocoa SDK from v8.58.0 to v9.1.0 ([#5356](https://github.com/getsentry/sentry-react-native/pull/5356)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#910) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.58.0...9.1.0) +- Bump CLI from v2.58.4 to v3.1.0 ([#5523](https://github.com/getsentry/sentry-react-native/pull/5523), [#5471](https://github.com/getsentry/sentry-react-native/pull/5471), [#5514](https://github.com/getsentry/sentry-react-native/pull/5514), [#5502](https://github.com/getsentry/sentry-react-native/pull/5502)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#310) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.58.4...3.1.0) + ## 7.9.0 ### Features @@ -36,15 +48,12 @@ ### Dependencies -- Bump Cocoa SDK from v8.58.0 to v9.1.0 ([#5356](https://github.com/getsentry/sentry-react-native/pull/5356)) - - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#910) - - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.58.0...9.1.0) +- Bump Cocoa SDK from v8.57.3 to v8.58.0 ([#5524](https://github.com/getsentry/sentry-react-native/pull/5524)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8580) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.57.3...8.58.0) - Bump JavaScript SDK from v10.30.0 to v10.34.0 ([#5480](https://github.com/getsentry/sentry-react-native/pull/5480), [#5487](https://github.com/getsentry/sentry-react-native/pull/5487), [#5496](https://github.com/getsentry/sentry-react-native/pull/5496), [#5522](https://github.com/getsentry/sentry-react-native/pull/5522), [#5535](https://github.com/getsentry/sentry-react-native/pull/5535)) - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#10340) - [diff](https://github.com/getsentry/sentry-javascript/compare/10.30.0...10.34.0) -- Bump CLI from v2.58.4 to v3.1.0 ([#5523](https://github.com/getsentry/sentry-react-native/pull/5523), [#5471](https://github.com/getsentry/sentry-react-native/pull/5471), [#5514](https://github.com/getsentry/sentry-react-native/pull/5514), [#5502](https://github.com/getsentry/sentry-react-native/pull/5502)) - - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#310) - - [diff](https://github.com/getsentry/sentry-cli/compare/2.58.4...3.1.0) - Bump Bundler Plugins from v4.6.1 to v4.6.2 ([#5536](https://github.com/getsentry/sentry-react-native/pull/5536)) - [changelog](https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/CHANGELOG.md#462) - [diff](https://github.com/getsentry/sentry-javascript-bundler-plugins/compare/4.6.1...4.6.2) From e7ac65d0033deacb5ef56256765816c7d50f369a Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 23 Jan 2026 12:38:04 +0100 Subject: [PATCH 07/12] Revert "feat: Expose iOS options to ignore views from subtree traversal (#5545)" This reverts commit 49978922a30729e73f35856dbf2d959ffa86fda1. --- .../RNSentryReplayOptionsTests.swift | 64 +------------------ packages/core/ios/RNSentryReplay.mm | 5 -- packages/core/src/js/replay/mobilereplay.ts | 28 -------- 3 files changed, 1 insertion(+), 96 deletions(-) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift index 8709976231..0d7ef3aa12 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift @@ -48,7 +48,7 @@ final class RNSentryReplayOptions: XCTestCase { } func assertAllDefaultReplayOptionsAreNotNil(replayOptions: [String: Any]) { - XCTAssertEqual(replayOptions.count, 11) + XCTAssertEqual(replayOptions.count, 9) XCTAssertNotNil(replayOptions["sessionSampleRate"]) XCTAssertNotNil(replayOptions["errorSampleRate"]) XCTAssertNotNil(replayOptions["maskAllImages"]) @@ -58,8 +58,6 @@ final class RNSentryReplayOptions: XCTestCase { XCTAssertNotNil(replayOptions["enableViewRendererV2"]) XCTAssertNotNil(replayOptions["enableFastViewRendering"]) XCTAssertNotNil(replayOptions["quality"]) - XCTAssertNotNil(replayOptions["includedViewClasses"]) - XCTAssertNotNil(replayOptions["excludedViewClasses"]) } func testSessionSampleRate() { @@ -320,64 +318,4 @@ final class RNSentryReplayOptions: XCTestCase { XCTAssertEqual(actualOptions.sessionReplay.quality, SentryReplayOptions.SentryReplayQuality.medium) } - - func testIncludedViewClasses() { - let optionsDict = ([ - "dsn": "https://abc@def.ingest.sentry.io/1234567", - "replaysOnErrorSampleRate": 0.75, - "mobileReplayOptions": [ "includedViewClasses": ["UILabel", "UIView", "UITextView"] ] - ] as NSDictionary).mutableCopy() as! NSMutableDictionary - - RNSentryReplay.updateOptions(optionsDict) - - let actualOptions = try! SentryOptionsInternal.initWithDict(optionsDict as! [String: Any]) - - let includedViewClasses = actualOptions.sessionReplay.includedViewClasses - XCTAssertEqual(includedViewClasses.count, 3) - XCTAssertTrue(includedViewClasses.contains("UILabel")) - XCTAssertTrue(includedViewClasses.contains("UIView")) - XCTAssertTrue(includedViewClasses.contains("UITextView")) - } - - func testExcludedViewClasses() { - let optionsDict = ([ - "dsn": "https://abc@def.ingest.sentry.io/1234567", - "replaysOnErrorSampleRate": 0.75, - "mobileReplayOptions": [ "excludedViewClasses": ["UICollectionView", "UITableView", "UIScrollView"] ] - ] as NSDictionary).mutableCopy() as! NSMutableDictionary - - RNSentryReplay.updateOptions(optionsDict) - - let actualOptions = try! SentryOptionsInternal.initWithDict(optionsDict as! [String: Any]) - - let excludedViewClasses = actualOptions.sessionReplay.excludedViewClasses - XCTAssertEqual(excludedViewClasses.count, 3) - XCTAssertTrue(excludedViewClasses.contains("UICollectionView")) - XCTAssertTrue(excludedViewClasses.contains("UITableView")) - XCTAssertTrue(excludedViewClasses.contains("UIScrollView")) - } - - func testIncludedAndExcludedViewClasses() { - let optionsDict = ([ - "dsn": "https://abc@def.ingest.sentry.io/1234567", - "replaysOnErrorSampleRate": 0.75, - "mobileReplayOptions": [ - "includedViewClasses": ["UILabel", "UIView"], - "excludedViewClasses": ["UICollectionView"] - ] - ] as NSDictionary).mutableCopy() as! NSMutableDictionary - - RNSentryReplay.updateOptions(optionsDict) - - let actualOptions = try! SentryOptionsInternal.initWithDict(optionsDict as! [String: Any]) - - let includedViewClasses = actualOptions.sessionReplay.includedViewClasses - XCTAssertEqual(includedViewClasses.count, 2) - XCTAssertTrue(includedViewClasses.contains("UILabel")) - XCTAssertTrue(includedViewClasses.contains("UIView")) - - let excludedViewClasses = actualOptions.sessionReplay.excludedViewClasses - XCTAssertEqual(excludedViewClasses.count, 1) - XCTAssertTrue(excludedViewClasses.contains("UICollectionView")) - } } diff --git a/packages/core/ios/RNSentryReplay.mm b/packages/core/ios/RNSentryReplay.mm index 40575a9e4c..94fa30b4e4 100644 --- a/packages/core/ios/RNSentryReplay.mm +++ b/packages/core/ios/RNSentryReplay.mm @@ -27,9 +27,6 @@ + (BOOL)updateOptions:(NSMutableDictionary *)options NSString *qualityString = options[@"replaysSessionQuality"]; - NSArray *includedViewClasses = replayOptions[@"includedViewClasses"]; - NSArray *excludedViewClasses = replayOptions[@"excludedViewClasses"]; - [options setValue:@{ @"sessionSampleRate" : sessionSampleRate ?: [NSNull null], @"errorSampleRate" : errorSampleRate ?: [NSNull null], @@ -39,8 +36,6 @@ + (BOOL)updateOptions:(NSMutableDictionary *)options @"enableViewRendererV2" : replayOptions[@"enableViewRendererV2"] ?: [NSNull null], @"enableFastViewRendering" : replayOptions[@"enableFastViewRendering"] ?: [NSNull null], @"maskedViewClasses" : [RNSentryReplay getReplayRNRedactClasses:replayOptions], - @"includedViewClasses" : includedViewClasses ?: [NSNull null], - @"excludedViewClasses" : excludedViewClasses ?: [NSNull null], @"sdkInfo" : @ { @"name" : REACT_NATIVE_SDK_NAME, @"version" : REACT_NATIVE_SDK_PACKAGE_VERSION } } diff --git a/packages/core/src/js/replay/mobilereplay.ts b/packages/core/src/js/replay/mobilereplay.ts index d0ae691966..437df76d3c 100644 --- a/packages/core/src/js/replay/mobilereplay.ts +++ b/packages/core/src/js/replay/mobilereplay.ts @@ -81,34 +81,6 @@ export interface MobileReplayOptions { */ enableFastViewRendering?: boolean; - /** - * Array of view class names to include in subtree traversal during session replay and screenshot capture on iOS. - * - * Only views that are instances of these classes (or subclasses) will be traversed. - * This helps prevent crashes when traversing problematic view hierarchies by allowing you to explicitly include only safe view classes. - * - * If both `includedViewClasses` and `excludedViewClasses` are set, `excludedViewClasses` takes precedence: - * views matching excluded classes won't be traversed even if they match an included class. - * - * @default undefined - * @platform ios - */ - includedViewClasses?: string[]; - - /** - * Array of view class names to exclude from subtree traversal during session replay and screenshot capture on iOS. - * - * Views of these classes (or subclasses) will be skipped entirely, including all their children. - * This helps prevent crashes when traversing problematic view hierarchies by allowing you to explicitly exclude problematic view classes. - * - * If both `includedViewClasses` and `excludedViewClasses` are set, `excludedViewClasses` takes precedence: - * views matching excluded classes won't be traversed even if they match an included class. - * - * @default undefined - * @platform ios - */ - excludedViewClasses?: string[]; - /** * Sets the screenshot strategy used by the Session Replay integration on Android. * From 243a75e776bf8a821323cd9a998ddda7b8bf2979 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:38:27 +0100 Subject: [PATCH 08/12] chore: update scripts/update-sentry-android-gradle-plugin.sh to 6.0.0 (#5578) Co-authored-by: GitHub Co-authored-by: Antonis Lilis --- packages/core/plugin/src/withSentryAndroidGradlePlugin.ts | 2 +- samples/react-native/android/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts b/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts index 4104c5b38f..0d56ec63ad 100644 --- a/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts +++ b/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts @@ -13,7 +13,7 @@ export interface SentryAndroidGradlePluginOptions { includeSourceContext?: boolean; } -export const sentryAndroidGradlePluginVersion = '5.12.2'; +export const sentryAndroidGradlePluginVersion = '6.0.0'; /** * Adds the Sentry Android Gradle Plugin to the project. diff --git a/samples/react-native/android/build.gradle b/samples/react-native/android/build.gradle index 7b7cdf589d..7f6c897368 100644 --- a/samples/react-native/android/build.gradle +++ b/samples/react-native/android/build.gradle @@ -16,7 +16,7 @@ buildscript { classpath("com.android.tools.build:gradle") classpath("com.facebook.react:react-native-gradle-plugin") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") - classpath("io.sentry:sentry-android-gradle-plugin:5.12.2") + classpath("io.sentry:sentry-android-gradle-plugin:6.0.0") } } From 37088273c68fe895444f66389177f74f6db36da7 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 29 Jan 2026 13:52:36 +0100 Subject: [PATCH 09/12] chore(changelog): Add upgrade notice in the changelog (#5584) * chore(changelog): Add upgrade notice in the changelog * Update versions * Also bump header notice --- CHANGELOG.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d3107622..5c96d69774 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,35 @@ > [!IMPORTANT] -> If you are upgrading to the `7.x` versions of the Sentry React Native SDK from `6.x` or below, +> If you are upgrading to the `8.x` versions of the Sentry React Native SDK from `7.x` or below, > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. ## Unreleased +### Upgrading from 7.x to 8.0 + +Version 8 of the Sentry React Native SDK updates the underlying native SDKs (Cocoa v9, CLI v3, Android Gradle Plugin v6) which introduce breaking changes in minimum version requirements and build tooling. + +See our [migration docs](https://docs.sentry.io/platforms/react-native/migration/v7-to-v8/) for more information. + +### Breaking Changes + +#### Minimum Version Requirements + +- **iOS/macOS/tvOS**: ([#5356](https://github.com/getsentry/sentry-react-native/pull/5356)) + - iOS **15.0+** (previously 11.0+) + - macOS **10.14+** (previously 10.13+) + - tvOS **15.0+** (previously 11.0+) + +- **Android**: ([#5578](https://github.com/getsentry/sentry-react-native/pull/5578)) + - Sentry Android Gradle Plugin **6.0.0** (previously 5.x) + - Android Gradle Plugin **7.4.0+** (previously 7.3.0+) + - Kotlin **1.8+** + +- **Sentry Self-Hosted**: ([#5523](https://github.com/getsentry/sentry-react-native/pull/5523)) + - Sentry CLI v3 requires self-hosted **25.11.1+** (previously 25.2.0) + ### Features - Add experimental `sentry-span-attributes` prop to attach custom attributes to user interaction spans ([#5569](https://github.com/getsentry/sentry-react-native/pull/5569)) From d5c32e3c4252fc44a19ccd0a1c65e37ae4b032d2 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 29 Jan 2026 16:26:25 +0100 Subject: [PATCH 10/12] feat: v8: Capture app start errors before JS (#5582) * ref(ios): Extract Cocoa SDK init into standalone file (#4442) * ref(android): Extracts Android native initialization to standalone structures (#4445) * Extract Android SDK Init * Update tests * Adds changelog * Fix lint issues * Rename RNSentryStart instance for clarity * Converts RNSentryStart to utility class * Update CHANGELOG.md --------- Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> * feat(experimental): Add native `startWithConfigureOptions` for Apple platforms (#4444) * feat: Read `sentry.options.json` during cocoa init (#4447) * Adds utility class for converting `JsonObject` to `WritableMap` (#4479) * Convert json object to writable map * Make class/methods package-private(default) * feat: Automatically load `sentry.options.json` file (#4476) * feat(experimental): Initialize Android SDK from json configuration (#4451) * misc: Add `sentry.options.json` example to the changelog (#4509) * feat(init): Load options from `sentry.options.json` in JS (#4510) * release: 6.7.0-alpha.0 * misc(sample): Change RN Sample to use native file init by default (#4522) * chore(sample-rn): Remove duplicate init options from code (#4532) * chore(sample-rn): Always use fhe file option (including auto init) (#4533) * internal(sample-rn): Add Detox for integration/e2e tests of the rn sample (#4535) * internal(sample-rn): Add header and message envelope tests (#4536) * fix(sample-e2e): Fix type errors missing sentry/core and afterAll (#4564) * chore(samples): Add package scripts for native builds, dsn and testing (#4561) * test(e2e): Verify captured Errors Screen transaction (#4584) * test(e2e): Add auto init from JS tests (#4588) * test(e2e): Add app start crash test for iOS (#4593) * test(e2e): Avoid race conditions when waiting for captured message (#4595) * chore(sample-e2e): Move Detox related files to e2e-detox dir * fix: remove unused SentryPackage import Removed unused import that was causing CI lint failure * fix: update RNSentryStartTest for Sentry Android SDK v7 API changes - Removed assertions for packages getter (not available in v7) - Removed assertion for enableTracing property (removed in v7) - Added comments explaining the API changes * fix: remove unused addPackages method to fix PMD lint error The method was a no-op after v7 API changes, so removing it entirely to avoid unused parameter warnings from PMD. * fix: update RNSentrySDKTest for Sentry Android SDK v7 API changes - Removed assertions for enableTracing property (removed in v7) - Removed assertions for packages getter (not available in v7) - Added comments explaining the API changes * fix: use relative path for RNSentrySDK+Test.h import in bridging header The file is in the parent directory, so use ../ prefix to fix the import path * fix: remove deprecated enableTracing property in iOS RNSentryStart The property is deprecated in v7. Tracing is already disabled by setting tracesSampleRate and tracesSampler to nil. * fix: remove enableTracing assertions from iOS tests The enableTracing property is deprecated in Sentry Cocoa SDK v7. Tracing is already verified to be disabled by checking that tracesSampleRate and tracesSampler are nil. * Update Podspec * Fix lint issue * ref(sample-e2e): v7: Migrate from Detox to Maestro (#5473) * chore(sample-e2e): Migrate from Detox to Maestro * fix set dsn script path * fix: Update script paths after detox-to-maestro migration The set-dsn scripts were moved from scripts/detox/ to scripts/ during the migration * Update script paths * Fix tests * Fix test failure * Fix idle issue * fix(e2e): Fix Maestro flows for captureMessage and captureSpaceflightNewsScreen - Add scrollUntilVisible for 'Capture message' button (might be off-screen) - Add proper waiting and scrolling for SpaceflightNewsScreen to trigger auto-load - Wait for 'Load More Articles' button to appear after autoLoadCount threshold * chore(e2e-sample): Increase Maestro driver startup timeout (cherry picked from commit ee429b558dc6f62e733bee64ec398c963411dffb) * increase timeout * Increase timeouts on Android too --------- Co-authored-by: Krystof Woldrich * chore: Merge Android UI profiling on the capture startup crashes branch (#5544) * chore: Merge Android UI profiling on the capture startup crashes branch * Fix logger compilation issue * Properly check logging values * fix SR iOS issue (#5560) * Remove duplicate changelog entry * feat(expo): Add RNSentrySDK APIs support to @sentry/react-native/expo plugin (#4633) * useNativeInit Android implementation * Adds changelog * useNativeInit iOS implementation * Fix indentation * Extend test cases with realistic data * Adds code sample in the changelog * Fix CHANGELOG.md Co-authored-by: LucasZF * Warn if RESentySDK.init/start wasn't injected * Make useNativeInit opt-in * Make Android failure warning more clear Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> * Make Android no update warning more clear Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> * Use path.basename to get last path component * Update tests to account for the new warnings * Explicitly check for kotlin * Add filename in the warning message * Import only if init injection succeeds * Explicitly check for Objective-C * Add filename in the warning * Make iOS file not found warning more clear * Import only if init injection succeeds * Reset test mock config in a function * Lint issue * Add missing quote Co-authored-by: LucasZF * Remove unneeded async Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> * Set useNativeInit = false by default * dynamically fill white spaces * Add unsupported language in warning message * Add objcpp in detected languages Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> * Update tests for objcpp * ref(expo-plugin): Split utils to logger, version and utils (#4906) Co-authored-by: Antonis Lilis * Update changelog * fix(ios): Add Swift module support for RNSentrySDK native init Fixes Swift compilation errors when using the useNativeInit Expo plugin feature. Changes: - Updated RNSentry.h to use angle bracket import for RNSentrySDK, properly exposing it through the module system - Added DEFINES_MODULE to RNSentry.podspec to enable Swift module generation - Fixed Expo plugin to insert import after first import statement (supports modern Expo AppDelegate structure without UIKit import) This enables Swift code to successfully import RNSentry and call RNSentrySDK.start() when using native initialization. Co-Authored-By: Claude Sonnet 4.5 * Fix test * Update changelog * Fix native tests * Fix lint issue * Fix native tests * Revert unneeded changes * Fix sample app build --------- Co-authored-by: LucasZF Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 * Fix native ios tests * Mark area Co-authored-by: LucasZF * fix(android): Fix ConcurrentModificationException (#5588) * fix(android): Fix ConcurrentModificationException when disabling native crash handling When enableNativeCrashHandling is set to false, the code was iterating over the integrations list with a for-each loop while calling remove() directly, which causes a ConcurrentModificationException at runtime. Fixed by using Java 8's removeIf() method which safely handles iteration and removal in a single operation. This is more concise and follows modern Java best practices. Added unit tests to verify the fix and ensure integrations are properly removed without throwing exceptions. Co-Authored-By: Claude Sonnet 4.5 * Lint fix --------- Co-authored-by: Claude Sonnet 4.5 * fix(android): Fix crash when spotlight is enabled without defaultSidecarUrl The code attempted to read defaultSidecarUrl without checking if the key exists in the options map. This caused a NoSuchKeyException crash during startup when spotlight was set to true in sentry.options.json without providing defaultSidecarUrl. Added key existence check to match iOS implementation behavior and prevent the crash while maintaining backward compatibility. Co-Authored-By: Claude Sonnet 4.5 * test(e2e): Add auto init from JS tests for Android (#5583) * test(e2e): Add auto init from JS tests for Android Implements Android E2E testing infrastructure to verify both manual native initialization and auto initialization from JavaScript, matching the iOS implementation and resolving issue #4912. Key additions: - Jest configs for android.auto and android.manual test modes - Build scripts that toggle SENTRY_DISABLE_NATIVE_START at compile time - Test scripts to run auto and manual test suites separately - App start crash testing via flag file mechanism - TestControlModule to enable/disable crash-on-start from JS - Comprehensive E2E test documentation Unlike iOS which uses launch arguments at runtime, Android requires separate builds with different build configurations to control native initialization. Closes #4912 Co-Authored-By: Claude Sonnet 4.5 * fix(e2e): Add scrolling to find crash control buttons in Android test The crash control buttons are off-screen, so the Maestro flow needs to scroll to find them before tapping. This matches the pattern used in other Android E2E tests. Co-Authored-By: Claude Sonnet 4.5 * fix(e2e): Make Android crash flag auto-expire after one crash The crash flag file was persisting across app launches, causing the app to crash indefinitely. Now the flag auto-deletes when read, allowing: 1. First launch: Enable flag 2. Second launch: Read flag, delete it, then crash 3. Third launch: Start normally and send crash report This solves the chicken-and-egg problem where the app couldn't reach JavaScript to clear the flag because it kept crashing before JS loaded. Co-Authored-By: Claude Sonnet 4.5 * fix(e2e): Handle wrapped exceptions in Android crash test Android wraps exceptions thrown in Application.onCreate() with: "Unable to create application... RuntimeException: " Updated the test to check if ANY exception in the chain contains our intentional crash message, rather than expecting an exact match on the first exception. Test now passes locally and should pass in CI. Co-Authored-By: Claude Sonnet 4.5 * Clean up notes for now --------- Co-authored-by: Claude Sonnet 4.5 * fix(android): Fix crash when dsn or devServerUrl are missing from options Fixes a crash on Android startup when initializing from sentry.options.json without dsn or devServerUrl fields. The code was calling getString() on ReadableMap without checking if the keys exist first, which throws NoSuchKeyException for missing keys. Both fields are optional in configuration files, so the code now checks for key existence before accessing values, returning null when keys are missing. This matches the pattern used throughout the rest of the file and is already handled correctly by the null-checks in the breadcrumb filter logic. Co-Authored-By: Claude Sonnet 4.5 --------- Co-authored-by: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Co-authored-by: Krystof Woldrich Co-authored-by: getsentry-bot Co-authored-by: getsentry-bot Co-authored-by: LucasZF Co-authored-by: Claude Sonnet 4.5 --- .github/workflows/sample-application.yml | 69 +-- CHANGELOG.md | 69 +++ lerna.json | 2 +- packages/core/RNSentry.podspec | 10 +- .../androidTest/assets/invalid.options.json | 3 + .../androidTest/assets/invalid.options.txt | 1 + .../androidTest/assets/sentry.options.json | 5 + .../sentry/react/RNSentryJsonConverterTest.kt | 103 +++ .../java/io/sentry/react/RNSentrySDKTest.kt | 201 ++++++ ...SentryCompositeOptionsConfigurationTest.kt | 50 ++ .../io/sentry/react/RNSentryModuleImplTest.kt | 361 ----------- .../java/io/sentry/react/RNSentryStartTest.kt | 285 +++++++++ ...RNSentryCocoaTesterTests-Bridging-Header.h | 5 + .../RNSentryStart+Test.h | 7 + .../RNSentryStartFromFileTests.swift | 115 ++++ .../RNSentryStartTests.swift | 248 ++++++++ .../RNSentryCocoaTesterTests/RNSentryTests.h | 1 + .../RNSentryCocoaTesterTests/RNSentryTests.m | 342 ++++++++++ .../RNSentryCocoaTester/RNSentrySDK+Test.h | 9 + .../TestAssets/invalid.options.json | 5 + .../TestAssets/invalid.options.txt | 1 + .../TestAssets/valid.options.json | 4 + ...RNSentryCompositeOptionsConfiguration.java | 25 + .../sentry/react/RNSentryJsonConverter.java | 76 +++ .../io/sentry/react/RNSentryJsonUtils.java | 41 ++ .../io/sentry/react/RNSentryModuleImpl.java | 400 +----------- .../java/io/sentry/react/RNSentrySDK.java | 68 ++ .../java/io/sentry/react/RNSentryStart.java | 417 +++++++++++++ packages/core/ios/RNSentry.h | 3 + packages/core/ios/RNSentry.mm | 38 +- packages/core/ios/RNSentrySDK.h | 31 + packages/core/ios/RNSentrySDK.m | 78 +++ packages/core/ios/RNSentryStart.h | 25 + packages/core/ios/RNSentryStart.m | 228 +++++++ packages/core/jest.config.tools.js | 2 +- 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 | 10 +- packages/core/plugin/src/withSentryAndroid.ts | 80 ++- .../src/withSentryAndroidGradlePlugin.ts | 2 +- packages/core/plugin/src/withSentryIOS.ts | 73 ++- packages/core/scripts/sentry-xcode.sh | 20 + packages/core/sentry.gradle | 51 ++ packages/core/src/js/sdk.tsx | 48 +- packages/core/src/js/tools/metroconfig.ts | 20 +- .../src/js/tools/sentryMetroSerializer.ts | 1 + .../src/js/tools/sentryOptionsSerializer.ts | 108 ++++ packages/core/src/js/tools/utils.ts | 4 +- packages/core/src/js/utils/worldwide.ts | 2 + .../expo-plugin/modifyAppBuildGradle.test.ts | 4 +- .../expo-plugin/modifyAppDelegate.test.ts | 206 ++++++ .../expo-plugin/modifyMainApplication.test.ts | 191 ++++++ .../expo-plugin/modifyXcodeProject.test.ts | 4 +- .../withSentryAndroidGradlePlugin.test.ts | 4 +- packages/core/test/sdk.test.ts | 59 +- .../tools/sentryOptionsSerializer.test.ts | 208 +++++++ samples/expo/app.json | 1 + samples/expo/sentry.options.json | 18 + .../react-native-macos/scripts/pod-install.sh | 18 + samples/react-native/.detoxrc.js | 74 +++ samples/react-native/.gitignore | 2 + samples/react-native/android/app/build.gradle | 8 + .../android/app/proguard-rules.pro | 4 + .../sentry/reactnative/sample/DetoxTest.java | 28 + .../reactnative/sample/MainApplication.kt | 49 +- .../reactnative/sample/SamplePackage.java | 32 + samples/react-native/android/build.gradle | 8 + .../react-native/android/gradle.properties | 5 + .../captureErrorsScreenTransaction.test.yml | 13 - .../e2e/jest.config.android.auto.js | 13 + ...droid.js => jest.config.android.manual.js} | 7 +- .../react-native/e2e/jest.config.ios.auto.js | 13 + ...onfig.ios.js => jest.config.ios.manual.js} | 7 +- .../react-native/e2e/setup.android.auto.ts | 7 + samples/react-native/e2e/setup.android.ts | 7 - samples/react-native/e2e/setup.ios.auto.ts | 7 + samples/react-native/e2e/setup.ios.ts | 7 - ...aptureAppStartCrash.test.android.manual.ts | 118 ++++ ...ptureAppStartCrash.test.android.manual.yml | 30 + .../captureAppStartCrash.test.ios.manual.ts | 117 ++++ .../captureAppStartCrash.test.ios.manual.yml | 16 + .../captureErrorsScreenTransaction.test.ts | 14 +- .../captureErrorsScreenTransaction.test.yml | 17 + .../envelopeHeader.test.android.ts | 65 ++ .../captureHeader/envelopeHeader.test.ios.ts | 71 +++ .../captureHeader/envelopeHeader.test.yml | 9 + .../captureMessage.test.android.auto.ts | 99 +++ .../captureMessage.test.android.manual.ts | 148 +++++ .../captureMessage.test.ios.auto.yml | 10 + .../captureMessage/captureMessage.test.ios.ts | 134 ++++ .../captureMessage/captureMessage.test.yml | 19 + ...htNewsScreenTransaction.test.ios.auto.yml} | 1 + ...reSpaceflightNewsScreenTransaction.test.ts | 17 +- ...eSpaceflightNewsScreenTransaction.test.yml | 53 ++ samples/react-native/e2e/utils/environment.ts | 18 +- .../e2e/utils/mockedSentryServer.ts | 1 - .../project.pbxproj | 34 +- .../sentryreactnativesample.xcscheme | 12 +- .../sentryreactnativesample/AppDelegate.mm | 25 + samples/react-native/package.json | 32 +- .../scripts/build-android-debug-auto.sh | 22 + .../scripts/build-android-debug-legacy.sh | 11 + .../scripts/build-android-debug-manual.sh | 22 + .../scripts/build-android-debug.sh | 11 + .../scripts/build-android-release-legacy.sh | 11 + .../scripts/build-android-release.sh | 11 + samples/react-native/scripts/build-android.sh | 29 + .../react-native/scripts/build-ios-debug.sh | 10 + .../react-native/scripts/build-ios-release.sh | 10 + samples/react-native/scripts/build-ios.sh | 30 + .../pod-install-debug-dynamic-legacy.sh | 12 + .../scripts/pod-install-debug-dynamic.sh | 12 + .../pod-install-debug-static-legacy.sh | 12 + .../scripts/pod-install-debug-static.sh | 12 + .../pod-install-release-dynamic-legacy.sh | 12 + .../scripts/pod-install-release-dynamic.sh | 12 + .../pod-install-release-static-legacy.sh | 12 + .../scripts/pod-install-release-static.sh | 12 + samples/react-native/scripts/pod-install.sh | 18 + samples/react-native/scripts/set-dsn-aos.mjs | 5 + samples/react-native/scripts/set-dsn-ios.mjs | 5 + samples/react-native/scripts/set-dsn.mjs | 24 + .../react-native/scripts/test-android-auto.sh | 27 + ...test-android.sh => test-android-manual.sh} | 4 +- .../scripts/{test-ios.sh => test-ios-auto.sh} | 4 +- .../react-native/scripts/test-ios-manual.sh | 26 + samples/react-native/sentry.options.json | 18 + samples/react-native/src/App.tsx | 8 +- .../react-native/src/Screens/ErrorsScreen.tsx | 26 +- samples/react-native/src/utils.ts | 32 +- yarn.lock | 586 +++++++++++++++++- 132 files changed, 5863 insertions(+), 1082 deletions(-) create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentryJsonConverterTest.kt create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt create mode 100644 packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryCompositeOptionsConfigurationTest.kt delete mode 100644 packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt create mode 100644 packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt create mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h create mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift create mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift create mode 100644 packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h create mode 100644 packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json create mode 100644 packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt create mode 100644 packages/core/RNSentryCocoaTester/TestAssets/valid.options.json create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java create mode 100644 packages/core/ios/RNSentrySDK.h create mode 100644 packages/core/ios/RNSentrySDK.m create mode 100644 packages/core/ios/RNSentryStart.h create mode 100644 packages/core/ios/RNSentryStart.m create mode 100644 packages/core/plugin/src/logger.ts create mode 100644 packages/core/plugin/src/version.ts create mode 100644 packages/core/src/js/tools/sentryOptionsSerializer.ts create mode 100644 packages/core/test/expo-plugin/modifyAppDelegate.test.ts create mode 100644 packages/core/test/expo-plugin/modifyMainApplication.test.ts create mode 100644 packages/core/test/tools/sentryOptionsSerializer.test.ts create mode 100644 samples/expo/sentry.options.json create mode 100755 samples/react-native-macos/scripts/pod-install.sh create mode 100644 samples/react-native/.detoxrc.js create mode 100644 samples/react-native/android/app/src/androidTest/java/io/sentry/reactnative/sample/DetoxTest.java delete mode 100644 samples/react-native/e2e/captureErrorsScreenTransaction.test.yml create mode 100644 samples/react-native/e2e/jest.config.android.auto.js rename samples/react-native/e2e/{jest.config.android.js => jest.config.android.manual.js} (51%) create mode 100644 samples/react-native/e2e/jest.config.ios.auto.js rename samples/react-native/e2e/{jest.config.ios.js => jest.config.ios.manual.js} (66%) create mode 100644 samples/react-native/e2e/setup.android.auto.ts delete mode 100644 samples/react-native/e2e/setup.android.ts create mode 100644 samples/react-native/e2e/setup.ios.auto.ts delete mode 100644 samples/react-native/e2e/setup.ios.ts create mode 100644 samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.android.manual.ts create mode 100644 samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.android.manual.yml create mode 100644 samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.ts create mode 100644 samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.yml rename samples/react-native/e2e/{ => tests/captureErrorScreenTransaction}/captureErrorsScreenTransaction.test.ts (90%) create mode 100644 samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.yml create mode 100644 samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.android.ts create mode 100644 samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.ios.ts create mode 100644 samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.yml create mode 100644 samples/react-native/e2e/tests/captureMessage/captureMessage.test.android.auto.ts create mode 100644 samples/react-native/e2e/tests/captureMessage/captureMessage.test.android.manual.ts create mode 100644 samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.auto.yml create mode 100644 samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.ts create mode 100644 samples/react-native/e2e/tests/captureMessage/captureMessage.test.yml rename samples/react-native/e2e/{captureSpaceflightNewsScreenTransaction.test.yml => tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ios.auto.yml} (95%) rename samples/react-native/e2e/{ => tests/captureSpaceflightNewsScreenTransaction}/captureSpaceflightNewsScreenTransaction.test.ts (87%) create mode 100644 samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.yml create mode 100755 samples/react-native/scripts/build-android-debug-auto.sh create mode 100755 samples/react-native/scripts/build-android-debug-legacy.sh create mode 100755 samples/react-native/scripts/build-android-debug-manual.sh create mode 100755 samples/react-native/scripts/build-android-debug.sh create mode 100755 samples/react-native/scripts/build-android-release-legacy.sh create mode 100755 samples/react-native/scripts/build-android-release.sh create mode 100755 samples/react-native/scripts/build-android.sh create mode 100755 samples/react-native/scripts/build-ios-debug.sh create mode 100755 samples/react-native/scripts/build-ios-release.sh create mode 100755 samples/react-native/scripts/build-ios.sh create mode 100755 samples/react-native/scripts/pod-install-debug-dynamic-legacy.sh create mode 100755 samples/react-native/scripts/pod-install-debug-dynamic.sh create mode 100755 samples/react-native/scripts/pod-install-debug-static-legacy.sh create mode 100755 samples/react-native/scripts/pod-install-debug-static.sh create mode 100755 samples/react-native/scripts/pod-install-release-dynamic-legacy.sh create mode 100755 samples/react-native/scripts/pod-install-release-dynamic.sh create mode 100755 samples/react-native/scripts/pod-install-release-static-legacy.sh create mode 100755 samples/react-native/scripts/pod-install-release-static.sh create mode 100755 samples/react-native/scripts/pod-install.sh create mode 100755 samples/react-native/scripts/set-dsn-aos.mjs create mode 100755 samples/react-native/scripts/set-dsn-ios.mjs create mode 100644 samples/react-native/scripts/set-dsn.mjs create mode 100755 samples/react-native/scripts/test-android-auto.sh rename samples/react-native/scripts/{test-android.sh => test-android-manual.sh} (88%) rename samples/react-native/scripts/{test-ios.sh => test-ios-auto.sh} (88%) create mode 100755 samples/react-native/scripts/test-ios-manual.sh create mode 100644 samples/react-native/sentry.options.json diff --git a/.github/workflows/sample-application.yml b/.github/workflows/sample-application.yml index 10c77e7256..861ef94487 100644 --- a/.github/workflows/sample-application.yml +++ b/.github/workflows/sample-application.yml @@ -82,7 +82,7 @@ jobs: - uses: ruby/setup-ruby@v1 if: ${{ matrix.platform == 'ios' || matrix.platform == 'macos' }} with: - working-directory: ${{ matrix.platform == 'ios' && env.REACT_NATIVE_SAMPLE_PATH || ' samples/react-native-macos' }} + working-directory: ${{ matrix.platform == 'ios' && env.REACT_NATIVE_SAMPLE_PATH || 'samples/react-native-macos' }} ruby-version: '3.3.0' # based on what is used in the sample bundler-cache: true # runs 'bundle install' and caches installed gems automatically cache-version: 1 # cache the installed gems @@ -112,62 +112,39 @@ jobs: if: ${{ matrix.platform == 'ios' || matrix.platform == 'macos' }} working-directory: samples run: | - [[ "${{ matrix.platform }}" == "ios" ]] && cd react-native/ios - [[ "${{ matrix.platform }}" == "macos" ]] && cd react-native-macos/macos + [[ "${{ matrix.platform }}" == "ios" ]] && cd react-native + [[ "${{ matrix.platform }}" == "macos" ]] && cd react-native-macos - [[ "${{ matrix.build-type }}" == "production" ]] && ENABLE_PROD=1 || ENABLE_PROD=0 - [[ "${{ matrix.rn-architecture }}" == "new" ]] && ENABLE_NEW_ARCH=1 || ENABLE_NEW_ARCH=0 + [[ "${{ matrix.build-type }}" == "production" ]] && export ENABLE_PROD=1 || export ENABLE_PROD=0 + [[ "${{ matrix.rn-architecture }}" == "new" ]] && export ENABLE_NEW_ARCH=1 || export ENABLE_NEW_ARCH=0 [[ "${{ matrix.ios-use-frameworks }}" == "dynamic-frameworks" ]] && export USE_FRAMEWORKS=dynamic - echo "ENABLE_PROD=$ENABLE_PROD" - echo "ENABLE_NEW_ARCH=$ENABLE_NEW_ARCH" - PRODUCTION=$ENABLE_PROD RCT_NEW_ARCH_ENABLED=$ENABLE_NEW_ARCH bundle exec pod install - cat Podfile.lock | grep $RN_SENTRY_POD_NAME + + ./scripts/pod-install.sh - name: Build Android App if: ${{ matrix.platform == 'android' }} - working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/android + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} run: | - if [[ ${{ matrix.rn-architecture }} == 'new' ]]; then - perl -i -pe's/newArchEnabled=false/newArchEnabled=true/g' gradle.properties - echo 'New Architecture enabled' - elif [[ ${{ matrix.rn-architecture }} == 'legacy' ]]; then - perl -i -pe's/newArchEnabled=true/newArchEnabled=false/g' gradle.properties - echo 'Legacy Architecture enabled' - else - echo 'No changes for architecture: ${{ matrix.rn-architecture }}' - fi - [[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug' - echo "Building $CONFIG" - [[ "${{ matrix.build-type }}" == "production" ]] && TEST_TYPE='release' || TEST_TYPE='debug' - echo "Building $TEST_TYPE" + export RN_ARCHITECTURE="${{ matrix.rn-architecture }}" + [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='release' || export CONFIG='debug' - ./gradlew ":app:assemble$CONFIG" -PreactNativeArchitectures=x86 + ./scripts/set-dsn-aos.mjs + ./scripts/build-android.sh -PreactNativeArchitectures=x86 - name: Build iOS App if: ${{ matrix.platform == 'ios' }} - working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/ios + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} run: | - [[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug' - echo "Building $CONFIG" - mkdir -p "DerivedData" - derivedData="$(cd "DerivedData" ; pwd -P)" - set -o pipefail && xcodebuild \ - -workspace sentryreactnativesample.xcworkspace \ - -configuration "$CONFIG" \ - -scheme sentryreactnativesample \ - -sdk 'iphonesimulator' \ - -destination 'generic/platform=iOS Simulator' \ - ONLY_ACTIVE_ARCH=yes \ - -derivedDataPath "$derivedData" \ - build \ - | tee xcodebuild.log \ - | xcbeautify --quieter --is-ci --disable-colored-output + [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='Release' || export CONFIG='Debug' + + ./scripts/set-dsn-ios.mjs + ./scripts/build-ios.sh - name: Build macOS App if: ${{ matrix.platform == 'macos' }} working-directory: samples/react-native-macos/macos run: | - [[ "${{ matrix.build-type }}" == "production" ]] && CONFIG='Release' || CONFIG='Debug' + [[ "${{ matrix.build-type }}" == "production" ]] && export CONFIG='Release' || export CONFIG='Debug' echo "Building $CONFIG" mkdir -p "DerivedData" derivedData="$(cd "DerivedData" ; pwd -P)" @@ -184,8 +161,8 @@ jobs: - name: Archive iOS App if: ${{ matrix.platform == 'ios' && matrix.rn-architecture == 'new' && matrix.build-type == 'production' && matrix.ios-use-frameworks == 'no-frameworks' }} + working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} run: | - cd ${{ env.REACT_NATIVE_SAMPLE_PATH }}/ios/DerivedData/Build/Products/Release-iphonesimulator zip -r \ ${{ github.workspace }}/${{ env.IOS_APP_ARCHIVE_PATH }} \ sentryreactnativesample.app @@ -193,10 +170,10 @@ jobs: - name: Archive Android App if: ${{ matrix.platform == 'android' && matrix.rn-architecture == 'new' && matrix.build-type == 'production' }} run: | - mv ${{ env.REACT_NATIVE_SAMPLE_PATH }}/android/app/build/outputs/apk/release/app-release.apk app.apk zip -j \ ${{ env.ANDROID_APP_ARCHIVE_PATH }} \ - app.apk + ${{ env.REACT_NATIVE_SAMPLE_PATH }}/app.apk \ + ${{ env.REACT_NATIVE_SAMPLE_PATH }}/app-androidTest.apk - name: Upload iOS APP if: ${{ matrix.platform == 'ios' && matrix.rn-architecture == 'new' && matrix.build-type == 'production' && matrix.ios-use-frameworks == 'no-frameworks' }} @@ -272,7 +249,9 @@ jobs: - name: Unzip Android APK if: ${{ matrix.platform == 'android' }} working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }} - run: unzip ${{ env.ANDROID_APP_ARCHIVE_PATH }} + run: | + unzip ${{ env.ANDROID_APP_ARCHIVE_PATH }} + rm app-androidTest.apk - name: Enable Corepack run: npm i -g corepack diff --git a/CHANGELOG.md b/CHANGELOG.md index 37672dc6c8..1ee1f84f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,75 @@ See our [migration docs](https://docs.sentry.io/platforms/react-native/migration - **Sentry Self-Hosted**: ([#5523](https://github.com/getsentry/sentry-react-native/pull/5523)) - Sentry CLI v3 requires self-hosted **25.11.1+** (previously 25.2.0) +### Features + +- Capture App Start errors and crashes by initializing Sentry from `sentry.options.json` ([#4472](https://github.com/getsentry/sentry-react-native/pull/4472)) + + Create `sentry.options.json` in the React Native project root and set options the same as you currently have in `Sentry.init` in JS. + + ```json + { + "dsn": "https://key@example.io/value", + } + ``` + + Initialize Sentry on the native layers by newly provided native methods. + + ```kotlin + import io.sentry.react.RNSentrySDK + + class MainApplication : Application(), ReactApplication { + override fun onCreate() { + super.onCreate() + RNSentrySDK.init(this) + } + } + ``` + + ```obj-c + #import + + @implementation AppDelegate + - (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions + { + [RNSentrySDK start]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; + } + @end + ``` + +- Add RNSentrySDK APIs support to @sentry/react-native/expo plugin ([#4633](https://github.com/getsentry/sentry-react-native/pull/4633)) + - Adds `useNativeInit` option to automatically initialize Sentry natively before JavaScript loads, enabling capture of app start errors + ```json + { + "expo": { + "plugins": [ + [ + "@sentry/react-native/expo", + { + "useNativeInit": true + } + ] + ] + } + } + ``` + +### Changes + +- Load `optionsFile` into the JS bundle during Metro bundle process ([#4476](https://github.com/getsentry/sentry-react-native/pull/4476)) +- Add experimental version of `startWithConfigureOptions` for Apple platforms ([#4444](https://github.com/getsentry/sentry-react-native/pull/4444)) +- Add experimental version of `init` with optional `OptionsConfiguration` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) +- Add initialization using `sentry.options.json` for Apple platforms ([#4447](https://github.com/getsentry/sentry-react-native/pull/4447)) +- Add initialization using `sentry.options.json` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) +- Merge options from file with `Sentry.init` options in JS ([#4510](https://github.com/getsentry/sentry-react-native/pull/4510)) + +### Internal + +- Extract iOS native initialization to standalone structures ([#4442](https://github.com/getsentry/sentry-react-native/pull/4442)) +- Extract Android native initialization to standalone structures ([#4445](https://github.com/getsentry/sentry-react-native/pull/4445)) + ### Dependencies - Bump Cocoa SDK from v8.58.0 to v9.1.0 ([#5356](https://github.com/getsentry/sentry-react-native/pull/5356)) diff --git a/lerna.json b/lerna.json index c211ea86ad..3ad0529dae 100644 --- a/lerna.json +++ b/lerna.json @@ -8,4 +8,4 @@ "performance-tests/*" ], "npmClient": "yarn" -} \ No newline at end of file +} diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index d314d7e938..8ac85664c6 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -42,10 +42,14 @@ Pod::Spec.new do |s| s.preserve_paths = '*.js' s.source_files = 'ios/**/*.{h,m,mm}' - s.public_header_files = 'ios/RNSentry.h' + s.public_header_files = 'ios/RNSentry.h', 'ios/RNSentrySDK.h', 'ios/RNSentryStart.h', 'ios/RNSentryVersion.h', 'ios/RNSentryBreadcrumb.h', 'ios/RNSentryReplay.h', 'ios/RNSentryReplayBreadcrumbConverter.h', 'ios/Replay/RNSentryReplayMask.h', 'ios/Replay/RNSentryReplayUnmask.h', 'ios/RNSentryTimeToDisplay.h' s.compiler_flags = other_cflags + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES' + } + s.dependency 'Sentry/HybridSDK', '9.1.0' if defined? install_modules_dependencies @@ -56,10 +60,10 @@ Pod::Spec.new do |s| if is_new_arch_enabled then # New Architecture on React Native 0.70 and older - s.pod_target_xcconfig = { + s.pod_target_xcconfig.merge!({ "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" - } + }) s.dependency "React-RCTFabric" # Required for Fabric Components (like RCTViewComponentView) s.dependency "React-Codegen" diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json new file mode 100644 index 0000000000..be3bb71111 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json @@ -0,0 +1,3 @@ +{ + "dsn": "invalid-dsn" +} \ No newline at end of file diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt new file mode 100644 index 0000000000..f07bfaea41 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt @@ -0,0 +1 @@ +invalid-options \ No newline at end of file diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json new file mode 100644 index 0000000000..f97a8df3f2 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json @@ -0,0 +1,5 @@ +{ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "enableTracing": true, + "tracesSampleRate": 1.0 +} \ No newline at end of file diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentryJsonConverterTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentryJsonConverterTest.kt new file mode 100644 index 0000000000..e49aa546f8 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentryJsonConverterTest.kt @@ -0,0 +1,103 @@ +package io.sentry.react + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import io.sentry.react.RNSentryJsonConverter.convertToWritable +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RNSentryJsonConverterTest { + @Test + fun testConvertToWritableWithSimpleJsonObject() { + val jsonObject = + JSONObject().apply { + put("floatKey", 12.3f) + put("doubleKey", 12.3) + put("intKey", 123) + put("stringKey", "test") + put("nullKey", JSONObject.NULL) + } + + val result: WritableMap? = convertToWritable(jsonObject) + + assertNotNull(result) + assertEquals(12.3, result!!.getDouble("floatKey"), 0.0001) + assertEquals(12.3, result.getDouble("doubleKey"), 0.0) + assertEquals(123, result.getInt("intKey")) + assertEquals("test", result.getString("stringKey")) + assertNull(result.getString("nullKey")) + } + + @Test + fun testConvertToWritableWithNestedJsonObject() { + val jsonObject = + JSONObject().apply { + put( + "nested", + JSONObject().apply { + put("key", "value") + }, + ) + } + + val result: WritableMap? = convertToWritable(jsonObject) + + assertNotNull(result) + val nestedMap = result!!.getMap("nested") + assertNotNull(nestedMap) + assertEquals("value", nestedMap!!.getString("key")) + } + + @Test + fun testConvertToWritableWithJsonArray() { + val jsonArray = + JSONArray().apply { + put(1) + put(2.5) + put("string") + put(JSONObject.NULL) + } + + val result: WritableArray = convertToWritable(jsonArray) + + assertEquals(1, result.getInt(0)) + assertEquals(2.5, result.getDouble(1), 0.0) + assertEquals("string", result.getString(2)) + assertNull(result.getString(3)) + } + + @Test + fun testConvertToWritableWithNestedJsonArray() { + val jsonObject = + JSONObject().apply { + put( + "array", + JSONArray().apply { + put( + JSONObject().apply { + put("key1", "value1") + }, + ) + put( + JSONObject().apply { + put("key2", "value2") + }, + ) + }, + ) + } + + val result: WritableMap? = convertToWritable(jsonObject) + + val array = result?.getArray("array") + assertEquals("value1", array?.getMap(0)?.getString("key1")) + assertEquals("value2", array?.getMap(1)?.getString("key2")) + } +} diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt new file mode 100644 index 0000000000..bfa1647cbc --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt @@ -0,0 +1,201 @@ +package io.sentry.react + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.facebook.react.common.JavascriptException +import io.sentry.Hint +import io.sentry.ILogger +import io.sentry.Sentry +import io.sentry.Sentry.OptionsConfiguration +import io.sentry.SentryEvent +import io.sentry.android.core.AndroidLogger +import io.sentry.android.core.SentryAndroidOptions +import io.sentry.protocol.SdkVersion +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RNSentrySDKTest { + private val logger: ILogger = AndroidLogger(RNSentrySDKTest::class.java.simpleName) + private lateinit var context: Context + + companion object { + private const val INITIALISATION_ERROR = "Failed to initialize Sentry's React Native SDK" + private const val VALID_OPTIONS = "sentry.options.json" + private const val INVALID_OPTIONS = "invalid.options.json" + private const val INVALID_JSON = "invalid.options.txt" + private const val MISSING = "non-existing-file" + + private val validConfig = + OptionsConfiguration { options -> + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + private val invalidConfig = + OptionsConfiguration { options -> + options.dsn = "invalid-dsn" + } + private val emptyConfig = OptionsConfiguration {} + } + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().context + } + + @After + fun tearDown() { + Sentry.close() + } + + @Test + fun initialisesSuccessfullyWithDefaultValidJsonFile() { // sentry.options.json + RNSentrySDK.init(context) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndDefaultValidJsonFile() { + RNSentrySDK.init(context, validConfig) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndInvalidJsonFile() { + RNSentrySDK.init(context, validConfig, INVALID_OPTIONS, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndMissingJsonFile() { + RNSentrySDK.init(context, validConfig, MISSING, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndErrorInParsingJsonFile() { + RNSentrySDK.init(context, validConfig, INVALID_JSON, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithNoConfigurationAndValidJsonFile() { + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithNoConfigurationAndInvalidJsonFile() { + try { + RNSentrySDK.init(context, emptyConfig, INVALID_OPTIONS, logger) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithInvalidConfigAndInvalidJsonFile() { + try { + RNSentrySDK.init(context, invalidConfig, INVALID_OPTIONS, logger) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithInvalidConfigAndValidJsonFile() { + try { + RNSentrySDK.init(context, invalidConfig, VALID_OPTIONS, logger) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithInvalidConfigurationAndDefaultValidJsonFile() { + try { + RNSentrySDK.init(context, invalidConfig) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } + + @Test + fun defaultsAndFinalsAreSetWithValidJsonFile() { + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + verifyDefaults(actualOptions) + verifyFinals(actualOptions) + // options file + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") + } + + @Test + fun defaultsAndFinalsAreSetWithValidConfiguration() { + RNSentrySDK.init(context, validConfig, MISSING, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + verifyDefaults(actualOptions) + verifyFinals(actualOptions) + // configuration + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") + } + + @Test + fun defaultsOverrideOptionsJsonFile() { + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + assertNull(actualOptions.tracesSampleRate) + // Note: enableTracing property doesn't exist in Sentry Android SDK v7 + // Tracing is disabled by setting tracesSampleRate and tracesSampler to null + } + + @Test + fun configurationOverridesDefaultOptions() { + val validConfig = + OptionsConfiguration { options -> + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.tracesSampleRate = 0.5 + // Note: enableTracing property doesn't exist in Sentry Android SDK v7 + } + RNSentrySDK.init(context, validConfig, MISSING, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + assertEquals(0.5, actualOptions.tracesSampleRate) + // Note: enableTracing property doesn't exist in Sentry Android SDK v7 + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") + } + + private fun verifyDefaults(actualOptions: SentryAndroidOptions) { + assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) + assertEquals(RNSentryVersion.ANDROID_SDK_NAME, actualOptions.sdkVersion?.name) + assertEquals( + io.sentry.android.core.BuildConfig.VERSION_NAME, + actualOptions.sdkVersion?.version, + ) + // Note: In Sentry Android SDK v7, SdkVersion doesn't expose packages as a getter + // The React Native package is added via addPackage() but not accessible via getter + assertNull(actualOptions.tracesSampleRate) + assertNull(actualOptions.tracesSampler) + // Note: enableTracing property doesn't exist in Sentry Android SDK v7 + // Tracing is disabled by setting tracesSampleRate and tracesSampler to null + } + + private fun verifyFinals(actualOptions: SentryAndroidOptions) { + val event = + SentryEvent().apply { sdk = SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, "1.0") } + val result = actualOptions.beforeSend?.execute(event, Hint()) + assertNotNull(result) + assertEquals("android", result?.getTag("event.origin")) + assertEquals("java", result?.getTag("event.environment")) + } +} diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryCompositeOptionsConfigurationTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryCompositeOptionsConfigurationTest.kt new file mode 100644 index 0000000000..699fd81ccb --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryCompositeOptionsConfigurationTest.kt @@ -0,0 +1,50 @@ +package io.sentry.react + +import io.sentry.Sentry.OptionsConfiguration +import io.sentry.android.core.SentryAndroidOptions +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +@RunWith(JUnit4::class) +class RNSentryCompositeOptionsConfigurationTest { + @Test + fun `configure should call base and overriding configurations`() { + val baseConfig: OptionsConfiguration = mock() + val overridingConfig: OptionsConfiguration = mock() + + val compositeConfig = RNSentryCompositeOptionsConfiguration(baseConfig, overridingConfig) + val options = SentryAndroidOptions() + compositeConfig.configure(options) + + verify(baseConfig).configure(options) + verify(overridingConfig).configure(options) + } + + @Test + fun `configure should apply base configuration and override values`() { + val baseConfig = + OptionsConfiguration { options -> + options.dsn = "https://base-dsn@sentry.io" + options.isDebug = false + options.release = "some-release" + } + val overridingConfig = + OptionsConfiguration { options -> + options.dsn = "https://over-dsn@sentry.io" + options.isDebug = true + options.environment = "production" + } + + val compositeConfig = RNSentryCompositeOptionsConfiguration(baseConfig, overridingConfig) + val options = SentryAndroidOptions() + compositeConfig.configure(options) + + assert(options.dsn == "https://over-dsn@sentry.io") // overridden value + assert(options.isDebug) // overridden value + assert(options.release == "some-release") // base value not overridden + assert(options.environment == "production") // overridden value not in base + } +} diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt deleted file mode 100644 index 3f1cc53b75..0000000000 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt +++ /dev/null @@ -1,361 +0,0 @@ -package io.sentry.react - -import com.facebook.react.bridge.JavaOnlyMap -import com.facebook.react.common.JavascriptException -import io.sentry.Breadcrumb -import io.sentry.ILogger -import io.sentry.android.core.SentryAndroidOptions -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.Mockito.mock - -@RunWith(JUnit4::class) -class RNSentryModuleImplTest { - private lateinit var module: RNSentryModuleImpl - private lateinit var logger: ILogger - - @Before - fun setUp() { - logger = mock(ILogger::class.java) - - module = Utils.createRNSentryModuleWithMockedContext() - } - - @Test - fun `when the spotlight option is enabled, the spotlight SentryAndroidOption is set to true and the default url is used`() { - val options = - JavaOnlyMap.of( - "spotlight", - true, - "defaultSidecarUrl", - "http://localhost:8969/teststream", - ) - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, options, logger) - assert(actualOptions.isEnableSpotlight) - assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) - } - - @Test - fun `when the spotlight url is passed, the spotlight is enabled for the given url`() { - val options = JavaOnlyMap.of("spotlight", "http://localhost:8969/teststream") - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, options, logger) - assert(actualOptions.isEnableSpotlight) - assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) - } - - @Test - fun `when the spotlight option is disabled, the spotlight SentryAndroidOption is set to false`() { - val options = JavaOnlyMap.of("spotlight", false) - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, options, logger) - assertFalse(actualOptions.isEnableSpotlight) - } - - @Test - fun `the JavascriptException is added to the ignoredExceptionsForType list on initialisation`() { - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, JavaOnlyMap.of(), logger) - assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) - } - - @Test - fun `beforeBreadcrumb callback filters out Sentry DSN requests breadcrumbs`() { - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "dsn", - "https://abc@def.ingest.sentry.io/1234567", - "devServerUrl", - "http://localhost:8081", - ) - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "https://def.ingest.sentry.io/1234567") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertNull("Breadcrumb should be filtered out", result) - } - - @Test - fun `beforeBreadcrumb callback filters out dev server breadcrumbs`() { - val mockDevServerUrl = "http://localhost:8081" - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "dsn", - "https://abc@def.ingest.sentry.io/1234567", - "devServerUrl", - mockDevServerUrl, - ) - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", mockDevServerUrl) - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertNull("Breadcrumb should be filtered out", result) - } - - @Test - fun `beforeBreadcrumb callback does not filter out non dev server or dsn breadcrumbs`() { - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "dsn", - "https://abc@def.ingest.sentry.io/1234567", - "devServerUrl", - "http://localhost:8081", - ) - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } - - @Test - fun `the breadcrumb is not filtered out when the dev server url and dsn are not passed`() { - val options = SentryAndroidOptions() - module.getSentryAndroidOptions(options, JavaOnlyMap(), logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } - - @Test - fun `the breadcrumb is not filtered out when the dev server url is not passed and the dsn does not match`() { - val options = SentryAndroidOptions() - val rnOptions = JavaOnlyMap.of("dsn", "https://abc@def.ingest.sentry.io/1234567") - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } - - @Test - fun `the breadcrumb is not filtered out when the dev server url does not match and the dsn is not passed`() { - val options = SentryAndroidOptions() - val rnOptions = JavaOnlyMap.of("devServerUrl", "http://localhost:8081") - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } - - @Test - fun `trySetIgnoreErrors sets only regex patterns`() { - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "ignoreErrorsRegex", - com.facebook.react.bridge.JavaOnlyArray - .of("^Foo.*", "Bar$"), - ) - module.trySetIgnoreErrors(options, rnOptions) - assertEquals(listOf("^Foo.*", "Bar$"), options.ignoredErrors!!.map { it.filterString }) - } - - @Test - fun `trySetIgnoreErrors sets only string patterns`() { - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "ignoreErrorsStr", - com.facebook.react.bridge.JavaOnlyArray - .of("ExactError", "AnotherError"), - ) - module.trySetIgnoreErrors(options, rnOptions) - assertEquals(listOf(".*\\QExactError\\E.*", ".*\\QAnotherError\\E.*"), options.ignoredErrors!!.map { it.filterString }) - } - - @Test - fun `trySetIgnoreErrors sets both regex and string patterns`() { - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "ignoreErrorsRegex", - com.facebook.react.bridge.JavaOnlyArray - .of("^Foo.*"), - "ignoreErrorsStr", - com.facebook.react.bridge.JavaOnlyArray - .of("ExactError"), - ) - module.trySetIgnoreErrors(options, rnOptions) - assertEquals(listOf("^Foo.*", ".*\\QExactError\\E.*"), options.ignoredErrors!!.map { it.filterString }) - } - - @Test - fun `trySetIgnoreErrors sets nothing if neither is present`() { - val options = SentryAndroidOptions() - val rnOptions = JavaOnlyMap.of() - module.trySetIgnoreErrors(options, rnOptions) - assertNull(options.ignoredErrors) - } - - @Test - fun `trySetIgnoreErrors with string containing regex special characters should match literally if Pattern_quote is used`() { - val options = SentryAndroidOptions() - val special = "I like chocolate (and tomato)." - val rnOptions = - JavaOnlyMap.of( - "ignoreErrorsStr", - com.facebook.react.bridge.JavaOnlyArray - .of(special), - ) - module.trySetIgnoreErrors(options, rnOptions) - - assertEquals(listOf(".*\\QI like chocolate (and tomato).\\E.*"), options.ignoredErrors!!.map { it.filterString }) - - val regex = Regex(options.ignoredErrors!![0].filterString) - assertTrue(regex.matches("I like chocolate (and tomato).")) - assertTrue(regex.matches(" I like chocolate (and tomato). ")) - assertTrue(regex.matches("I like chocolate (and tomato). And vanilla.")) - } - - @Test - fun `trySetIgnoreErrors with string containing star should not match everything if Pattern_quote is used`() { - val options = SentryAndroidOptions() - val special = "Error*WithStar" - val rnOptions = - JavaOnlyMap.of( - "ignoreErrorsStr", - com.facebook.react.bridge.JavaOnlyArray - .of(special), - ) - module.trySetIgnoreErrors(options, rnOptions) - assertEquals(listOf(".*\\QError*WithStar\\E.*"), options.ignoredErrors!!.map { it.filterString }) - - val regex = Regex(options.ignoredErrors!![0].filterString) - assertTrue(regex.matches("Error*WithStar")) - } - - @Test - fun `setUser with geo data creates user with correct geo properties`() { - val userKeys = - JavaOnlyMap.of( - "id", - "123", - "email", - "test@example.com", - "username", - "testuser", - "geo", - JavaOnlyMap.of( - "city", - "San Francisco", - "country_code", - "US", - "region", - "California", - ), - ) - val userDataKeys = JavaOnlyMap.of("customField", "customValue") - - module.setUser(userKeys, userDataKeys) - } - - @Test - fun `setUser with partial geo data creates user with available geo properties`() { - val userKeys = - JavaOnlyMap.of( - "id", - "123", - "geo", - JavaOnlyMap.of( - "city", - "New York", - "country_code", - "US", - ), - ) - val userDataKeys = JavaOnlyMap.of() - - module.setUser(userKeys, userDataKeys) - } - - @Test - fun `setUser with empty geo data handles empty geo object`() { - val userKeys = - JavaOnlyMap.of( - "id", - "123", - "geo", - JavaOnlyMap.of(), - ) - val userDataKeys = JavaOnlyMap.of() - - module.setUser(userKeys, userDataKeys) - } - - @Test - fun `setUser with null geo data handles null geo gracefully`() { - val userKeys = - JavaOnlyMap.of( - "id", - "123", - "geo", - null, - ) - val userDataKeys = JavaOnlyMap.of() - - module.setUser(userKeys, userDataKeys) - } - - @Test - fun `setUser with invalid geo data handles non-map geo gracefully`() { - val userKeys = - JavaOnlyMap.of( - "id", - "123", - "geo", - "invalid_geo_data", - ) - val userDataKeys = JavaOnlyMap.of() - - module.setUser(userKeys, userDataKeys) - } -} diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt new file mode 100644 index 0000000000..aa91ae46be --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt @@ -0,0 +1,285 @@ +package io.sentry.react + +import android.app.Activity +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.common.JavascriptException +import io.sentry.Breadcrumb +import io.sentry.ILogger +import io.sentry.SentryEvent +import io.sentry.android.core.CurrentActivityHolder +import io.sentry.android.core.SentryAndroidOptions +import io.sentry.protocol.SdkVersion +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +@RunWith(JUnit4::class) +class RNSentryStartTest { + private lateinit var logger: ILogger + + private lateinit var activity: Activity + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + logger = mock(ILogger::class.java) + activity = mock(Activity::class.java) + } + + @Test + fun `when the spotlight option is enabled, the spotlight SentryAndroidOption is set to true and the default url is used`() { + val options = + JavaOnlyMap.of( + "spotlight", + true, + "defaultSidecarUrl", + "http://localhost:8969/teststream", + ) + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger) + assert(actualOptions.isEnableSpotlight) + assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) + } + + @Test + fun `when the spotlight url is passed, the spotlight is enabled for the given url`() { + val options = JavaOnlyMap.of("spotlight", "http://localhost:8969/teststream") + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger) + assert(actualOptions.isEnableSpotlight) + assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) + } + + @Test + fun `when the spotlight option is enabled without defaultSidecarUrl, the spotlight is enabled and does not crash`() { + val options = JavaOnlyMap.of("spotlight", true) + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger) + assert(actualOptions.isEnableSpotlight) + assertNull(actualOptions.spotlightConnectionUrl) + } + + @Test + fun `when the spotlight option is disabled, the spotlight SentryAndroidOption is set to false`() { + val options = JavaOnlyMap.of("spotlight", false) + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger) + assertFalse(actualOptions.isEnableSpotlight) + } + + @Test + fun `beforeBreadcrumb callback filters out Sentry DSN requests breadcrumbs`() { + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "dsn", + "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", + "http://localhost:8081", + ) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "https://def.ingest.sentry.io/1234567") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertNull("Breadcrumb should be filtered out", result) + } + + @Test + fun `beforeBreadcrumb callback filters out dev server breadcrumbs`() { + val mockDevServerUrl = "http://localhost:8081" + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "dsn", + "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", + mockDevServerUrl, + ) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", mockDevServerUrl) + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertNull("Breadcrumb should be filtered out", result) + } + + @Test + fun `beforeBreadcrumb callback does not filter out non dev server or dsn breadcrumbs`() { + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "dsn", + "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", + "http://localhost:8081", + ) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url and dsn are not passed`() { + val options = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(options, JavaOnlyMap(), logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url is not passed and the dsn does not match`() { + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of("dsn", "https://abc@def.ingest.sentry.io/1234567") + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url does not match and the dsn is not passed`() { + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of("devServerUrl", "http://localhost:8081") + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the JavascriptException is added to the ignoredExceptionsForType list on with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) + } + + @Test + fun `the sdk version information is added to the initialisation options with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertEquals(RNSentryVersion.ANDROID_SDK_NAME, actualOptions.sdkVersion?.name) + assertEquals( + io.sentry.android.core.BuildConfig.VERSION_NAME, + actualOptions.sdkVersion?.version, + ) + // Note: In Sentry Android SDK v7, SdkVersion doesn't expose packages as a getter + // The React Native package is added via addPackage() but not accessible via getter + } + + @Test + fun `the tracing options are added to the initialisation options with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertNull(actualOptions.tracesSampleRate) + assertNull(actualOptions.tracesSampler) + // Note: enableTracing property doesn't exist in Sentry Android SDK v7 + // Tracing is disabled by setting tracesSampleRate and tracesSampler to null + } + + @Test + fun `the current activity is added to the initialisation options with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertEquals(activity, CurrentActivityHolder.getInstance().activity) + } + + @Test + fun `beforeSend callback that sets event tags is set with react finals`() { + val options = SentryAndroidOptions() + val event = + SentryEvent().apply { sdk = SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, "1.0") } + + RNSentryStart.updateWithReactFinals(options) + val result = options.beforeSend?.execute(event, mock()) + + assertNotNull(result) + assertEquals("android", result?.getTag("event.origin")) + assertEquals("java", result?.getTag("event.environment")) + } + + @Test + fun `when enableNativeCrashHandling is false, native crash integrations are removed without ConcurrentModificationException`() { + val rnOptions = JavaOnlyMap.of("enableNativeCrashHandling", false) + val options = SentryAndroidOptions() + + // This should not throw ConcurrentModificationException + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) + + // Verify integrations were removed + val integrations = options.getIntegrations() + assertFalse( + "UncaughtExceptionHandlerIntegration should be removed", + integrations.any { it is io.sentry.UncaughtExceptionHandlerIntegration }, + ) + assertFalse( + "AnrIntegration should be removed", + integrations.any { it is io.sentry.android.core.AnrIntegration }, + ) + assertFalse( + "NdkIntegration should be removed", + integrations.any { it is io.sentry.android.core.NdkIntegration }, + ) + } + + @Test + fun `when enableNativeCrashHandling is true, native crash integrations are kept`() { + val rnOptions = JavaOnlyMap.of("enableNativeCrashHandling", true) + val options = SentryAndroidOptions() + + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) + + // When enabled, the default integrations should still be present + // Note: This test verifies that we don't remove integrations when the flag is true + val integrations = options.getIntegrations() + assertNotNull("Integrations list should not be null", integrations) + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h index a00ee1747c..be2572cad8 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h @@ -2,10 +2,15 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // +#import + +#import "../RNSentrySDK+Test.h" #import "RNSentryBreadcrumb.h" #import "RNSentryOnDrawReporter+Test.h" #import "RNSentryReplay.h" #import "RNSentryReplayBreadcrumbConverter.h" #import "RNSentryReplayMask.h" #import "RNSentryReplayUnmask.h" +#import "RNSentryStart.h" #import "RNSentryTimeToDisplay.h" +#import "RNSentryVersion.h" diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h new file mode 100644 index 0000000000..331b27bcb8 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h @@ -0,0 +1,7 @@ +#import "RNSentryStart.h" + +@interface RNSentryStart (Test) + ++ (void)setEventOriginTag:(SentryEvent *)event; + +@end diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift new file mode 100644 index 0000000000..e0269a5961 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift @@ -0,0 +1,115 @@ +import XCTest + +final class RNSentryStartFromFileTests: XCTestCase { + + func testNoThrowOnMissingOptionsFile() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getNonExistingOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNil(actualOptions.parsedDsn) + } + + func testNoThrowOnInvalidFileType() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getInvalidOptionsTypePath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNil(actualOptions.parsedDsn) + } + + func testNoThrowOnInvalidOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getInvalidOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNotNil(actualOptions.parsedDsn) + XCTAssertEqual(actualOptions.environment, "environment-from-invalid-file") + } + + func testLoadValidOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getValidOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNotNil(actualOptions.parsedDsn) + XCTAssertEqual(actualOptions.environment, "environment-from-valid-file") + } + + func testOptionsFromFileInConfigureOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getValidOptionsPath()) { options in + wasConfigurationCalled = true + XCTAssertEqual(options.environment, "environment-from-valid-file") + } + + XCTAssertTrue(wasConfigurationCalled) + } + + func testOptionsOverwrittenInConfigureOptions() { + RNSentrySDK.start(getValidOptionsPath()) { options in + options.environment = "new-environment" + } + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertEqual(actualOptions.environment, "new-environment") + } + + func getNonExistingOptionsPath() -> String { + return "/non-existing.options.json" + } + + func getInvalidOptionsTypePath() -> String { + guard let path = getTestBundle().path(forResource: "invalid.options", ofType: "txt") else { + fatalError("Could not get invalid type options path") + } + return path + } + + func getInvalidOptionsPath() -> String { + guard let path = getTestBundle().path(forResource: "invalid.options", ofType: "json") else { + fatalError("Could not get invalid options path") + } + return path + } + + func getValidOptionsPath() -> String { + guard let path = getTestBundle().path(forResource: "valid.options", ofType: "json") else { + fatalError("Could not get invalid options path") + } + return path + } + + func getTestBundle() -> Bundle { + let maybeBundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix(".xctest") }) + guard let bundle = maybeBundle else { + fatalError("Could not find test bundle") + } + return bundle + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift new file mode 100644 index 0000000000..b9d12200cf --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift @@ -0,0 +1,248 @@ +import XCTest + +final class RNSentryStartTests: XCTestCase { + + func testStartDoesNotThrowWithoutConfigure() { + RNSentrySDK.start(configureOptions: nil) + } + + func assertReactDefaults(_ actualOptions: Options?) { + XCTAssertFalse(actualOptions!.enableCaptureFailedRequests) + XCTAssertNil(actualOptions!.tracesSampleRate) + XCTAssertNil(actualOptions!.tracesSampler) + XCTAssertFalse(actualOptions!.enableTracing) + } + + func testStartSetsReactDeafults() { + var actualOptions: Options? + + RNSentrySDK.start { options in + actualOptions = options + } + + XCTAssertNotNil(actualOptions, "start have not provided default options or have not executed configure callback") + assertReactDefaults(actualOptions) + } + + func testAutoStartSetsReactDefaults() throws { + try startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + + let actualOptions = PrivateSentrySDKOnly.options + assertReactDefaults(actualOptions) + } + + func testStartEnablesHybridTracing() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + }, + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.enableAutoPerformanceTracing = true + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "enableAutoPerformanceTracing": true + ]) + } + ] + + // Test each implementation + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + XCTAssertTrue(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) + XCTAssertTrue(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + } + } + + func testStartDisablesHybridTracing() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.enableAutoPerformanceTracing = false + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "enableAutoPerformanceTracing": false + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + XCTAssertFalse(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) + XCTAssertFalse(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + } + } + + func testStartIgnoresUnhandledJsExceptions() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + let actualEvent = actualOptions.beforeSend!(createUnhandledJsExceptionEvent()) + + XCTAssertNil(actualEvent) + } + } + + func testStartSetsNativeEventOrigin() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + let actualEvent = actualOptions.beforeSend!(createNativeEvent()) + + XCTAssertNotNil(actualEvent) + XCTAssertNotNil(actualEvent!.tags) + XCTAssertEqual(actualEvent!.tags!["event.origin"], "ios") + XCTAssertEqual(actualEvent!.tags!["event.environment"], "native") + } + } + + func testStartDoesNotOverwriteUserBeforeSend() { + var executed = false + + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.beforeSend = { event in + executed = true + return event + } + } + + PrivateSentrySDKOnly.options.beforeSend!(genericEvent()) + + XCTAssertTrue(executed) + } + + func testStartSetsHybridSdkName() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualEvent = captuteTestEvent() + + XCTAssertNotNil(actualEvent) + XCTAssertNotNil(actualEvent!.sdk) + XCTAssertEqual(actualEvent!.sdk!["name"] as! String, NATIVE_SDK_NAME) + + let packages = actualEvent!.sdk!["packages"] as! [[String: String]] + let reactPackage = packages.first { $0["name"] == REACT_NATIVE_SDK_PACKAGE_NAME } + + XCTAssertNotNil(reactPackage) + XCTAssertEqual(reactPackage!["name"], REACT_NATIVE_SDK_PACKAGE_NAME) + XCTAssertEqual(reactPackage!["version"], REACT_NATIVE_SDK_PACKAGE_VERSION) + } + } + + func startFromRN(options: [String: Any]) throws { + var error: NSError? + RNSentryStart.start(options: options, error: &error) + + if let error = error { + throw error + } + } + + func createUnhandledJsExceptionEvent() -> Event { + let event = Event() + event.exceptions = [] + event.exceptions!.append(Exception(value: "Test", type: "Unhandled JS Exception: undefined is not a function")) + return event + } + + func createNativeEvent() -> Event { + let event = Event() + event.sdk = [ + "name": NATIVE_SDK_NAME, + "version": "1.2.3" + ] + return event + } + + func genericEvent() -> Event { + return Event() + } + + func captuteTestEvent() -> Event? { + var actualEvent: Event? + + // This is the closest to the sent event we can get using the actual Sentry start method + let originalBeforeSend = PrivateSentrySDKOnly.options.beforeSend + PrivateSentrySDKOnly.options.beforeSend = { event in + if let originalBeforeSend = originalBeforeSend { + let processedEvent = originalBeforeSend(event) + actualEvent = processedEvent + return processedEvent + } + actualEvent = event + return event + } + + SentrySDK.capture(message: "Test") + + return actualEvent + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.h index 0de744facc..8b19634acb 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.h +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.h @@ -1,5 +1,6 @@ #import #import +#import @class SentryOptions; @class SentryUser; diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m index 211a23f5b9..e8c04115ba 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m @@ -1,8 +1,10 @@ #import "RNSentryTests.h" #import "RNSentryReplay.h" +#import "RNSentryStart+Test.h" #import "SentrySDKWrapper.h" #import #import +#import #import #import @import Sentry; @@ -40,6 +42,8 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties XCTAssertEqual( actualOptions.tracesSampleRate, nil, @"Traces sample rate should not be passed to native"); XCTAssertEqual(actualOptions.tracesSampler, nil, @"Traces sampler should not be passed to native"); +// Note: enableTracing property is deprecated in Sentry Cocoa SDK v7 +// Tracing is disabled by setting tracesSampleRate and tracesSampler to nil } - (void)testCaptureFailedRequestsIsDisabled @@ -1060,4 +1064,342 @@ - (void)testCreateUserWithoutGeoDataDoesNotCreateGeoObject XCTAssertNil(user.geo, @"Geo should be nil when not provided"); } +#pragma mark - RNSentryStart Tests + +- (void)testStartWithDictionaryRemovesPerformanceProperties +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = + @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"beforeSend" : @"will_be_overwritten", + @"tracesSampleRate" : @1, + @"tracesSampler" : ^(SentrySamplingContext *_Nonnull samplingContext) { return @1; +} +, @"enableTracing" : @YES, +} +; +[RNSentryStart startWithOptions:mockedReactNativeDictionary error:&error]; +SentryOptions *actualOptions = PrivateSentrySDKOnly.options; +XCTAssertNotNil(actualOptions, @"Did not create sentry options"); +XCTAssertNil(error, @"Should not pass no error"); +XCTAssertNotNil( + actualOptions.beforeSend, @"Before send is overwriten by the native RNSentry implementation"); +XCTAssertEqual( + actualOptions.tracesSampleRate, nil, @"Traces sample rate should not be passed to native"); +XCTAssertEqual(actualOptions.tracesSampler, nil, @"Traces sampler should not be passed to native"); +// Note: enableTracing property is deprecated in Sentry Cocoa SDK v7 +// Tracing is disabled by setting tracesSampleRate and tracesSampler to nil +} + +- (void)testStartCaptureFailedRequestsIsDisabled +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + }; + [RNSentryStart startWithOptions:mockedReactNativeDictionary error:&error]; + SentryOptions *actualOptions = PrivateSentrySDKOnly.options; + + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertFalse(actualOptions.enableCaptureFailedRequests); +} + +- (void)testStartCreateOptionsWithDictionaryNativeCrashHandlingDefault +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertEqual(actualOptions.enableCrashHandler, YES, @"Did not set native crash handling"); +} + +- (void)testStartCreateOptionsWithDictionaryAutoPerformanceTracingDefault +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertEqual( + actualOptions.enableAutoPerformanceTracing, true, @"Did not set Auto Performance Tracing"); +} + +- (void)testStartCreateOptionsWithDictionaryNativeCrashHandlingEnabled +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"enableNativeCrashHandling" : @YES, + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertEqual(actualOptions.enableCrashHandler, YES, @"Did not set native crash handling"); +} + +- (void)testStartCreateOptionsWithDictionaryAutoPerformanceTracingEnabled +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"enableAutoPerformanceTracing" : @YES, + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertEqual( + actualOptions.enableAutoPerformanceTracing, true, @"Did not set Auto Performance Tracing"); +} + +- (void)testStartCreateOptionsWithDictionaryNativeCrashHandlingDisabled +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"enableNativeCrashHandling" : @NO, + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertEqual(actualOptions.enableCrashHandler, NO, @"Did not disable native crash handling"); +} + +- (void)testStartCreateOptionsWithDictionaryAutoPerformanceTracingDisabled +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"enableAutoPerformanceTracing" : @NO, + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertEqual(actualOptions.enableAutoPerformanceTracing, false, + @"Did not disable Auto Performance Tracing"); +} + +- (void)testStartCreateOptionsWithDictionarySpotlightEnabled +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"spotlight" : @YES, + @"defaultSidecarUrl" : @"http://localhost:8969/teststream", + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); + XCTAssertEqual(actualOptions.spotlightUrl, @"http://localhost:8969/teststream"); +} + +- (void)testStartCreateOptionsWithDictionarySpotlightOne +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"spotlight" : @1, + @"defaultSidecarUrl" : @"http://localhost:8969/teststream", + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); + XCTAssertEqual(actualOptions.spotlightUrl, @"http://localhost:8969/teststream"); +} + +- (void)testStartCreateOptionsWithDictionarySpotlightUrl +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"spotlight" : @"http://localhost:8969/teststream", + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); + XCTAssertEqual(actualOptions.spotlightUrl, @"http://localhost:8969/teststream"); +} + +- (void)testStartCreateOptionsWithDictionarySpotlightDisabled +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"spotlight" : @NO, + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertFalse(actualOptions.enableSpotlight, @"Did not disable spotlight"); +} + +- (void)testStartCreateOptionsWithDictionarySpotlightZero +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"spotlight" : @0, + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + XCTAssertFalse(actualOptions.enableSpotlight, @"Did not disable spotlight"); +} + +- (void)testStartPassesErrorOnWrongDsn +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"not_a_valid_dsn", + }; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + + XCTAssertNil(actualOptions, @"Created invalid sentry options"); + XCTAssertNotNil(error, @"Did not created error on invalid dsn"); +} + +- (void)testStartBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedDictionary = @{ + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", + @"devServerUrl" : @"http://localhost:8081" + }; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; + + SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; + breadcrumb.type = @"http"; + breadcrumb.data = @{ @"url" : @"https://def.ingest.sentry.io/1234567" }; + + SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb); + + XCTAssertNil(result, @"Breadcrumb should be filtered out"); +} + +- (void)testStartBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs +{ + NSError *error = nil; + + NSString *mockDevServer = @"http://localhost:8081"; + + NSDictionary *_Nonnull mockedDictionary = + @{ @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", @"devServerUrl" : mockDevServer }; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; + + SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; + breadcrumb.type = @"http"; + breadcrumb.data = @{ @"url" : mockDevServer }; + + SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb); + + XCTAssertNil(result, @"Breadcrumb should be filtered out"); +} + +- (void)testStartBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBreadcrumbs +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedDictionary = @{ + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", + @"devServerUrl" : @"http://localhost:8081" + }; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; + + SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; + breadcrumb.type = @"http"; + breadcrumb.data = @{ @"url" : @"http://testurl.com/service" }; + + SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb); + + XCTAssertEqual(breadcrumb, result); +} + +- (void) + testStartBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedAndDsnDoesNotMatch +{ + NSError *error = nil; + + NSDictionary *_Nonnull mockedDictionary = @{ // dsn is always validated in SentryOptions initialization + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567" + }; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; + + SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; + breadcrumb.type = @"http"; + breadcrumb.data = @{ @"url" : @"http://testurl.com/service" }; + + SentryBreadcrumb *result = options.beforeBreadcrumb(breadcrumb); + + XCTAssertEqual(breadcrumb, result); +} + +- (void)testStartEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags +{ + SentryEvent *testEvent = [[SentryEvent alloc] init]; + testEvent.sdk = @{ + @"name" : @"sentry.cocoa.react-native", + }; + + [RNSentryStart setEventOriginTag:testEvent]; + + XCTAssertEqual(testEvent.tags[@"event.origin"], @"ios"); + XCTAssertEqual(testEvent.tags[@"event.environment"], @"native"); +} + +- (void)testStartEventFromSentryReactNativeOriginAndEnvironmentTagsAreOverwritten +{ + SentryEvent *testEvent = [[SentryEvent alloc] init]; + testEvent.sdk = @{ + @"name" : @"sentry.cocoa.react-native", + }; + testEvent.tags = @{ + @"event.origin" : @"testEventOriginTag", + @"event.environment" : @"testEventEnvironmentTag", + }; + + [RNSentryStart setEventOriginTag:testEvent]; + + XCTAssertEqual(testEvent.tags[@"event.origin"], @"ios"); + XCTAssertEqual(testEvent.tags[@"event.environment"], @"native"); +} + @end diff --git a/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h b/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h new file mode 100644 index 0000000000..0d3708d110 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h @@ -0,0 +1,9 @@ +#import "RNSentrySDK.h" +@import Sentry; + +@interface RNSentrySDK (Test) + ++ (void)start:(NSString *)path + configureOptions:(void (^)(SentryOptions *_Nonnull options))configureOptions; + +@end diff --git a/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json new file mode 100644 index 0000000000..bf8f2be64c --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json @@ -0,0 +1,5 @@ +{ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "environment": "environment-from-invalid-file", + "invalid-option": 123 +} diff --git a/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt new file mode 100644 index 0000000000..601553b507 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt @@ -0,0 +1 @@ +invalid-options diff --git a/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json b/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json new file mode 100644 index 0000000000..641087d5e8 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json @@ -0,0 +1,4 @@ +{ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "environment": "environment-from-valid-file" +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java new file mode 100644 index 0000000000..0069abb660 --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java @@ -0,0 +1,25 @@ +package io.sentry.react; + +import io.sentry.Sentry.OptionsConfiguration; +import io.sentry.android.core.SentryAndroidOptions; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +class RNSentryCompositeOptionsConfiguration implements OptionsConfiguration { + private final @NotNull List> configurations; + + @SafeVarargs + protected RNSentryCompositeOptionsConfiguration( + @NotNull OptionsConfiguration... configurations) { + this.configurations = List.of(configurations); + } + + @Override + public void configure(@NotNull SentryAndroidOptions options) { + for (OptionsConfiguration configuration : configurations) { + if (configuration != null) { + configuration.configure(options); + } + } + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java new file mode 100644 index 0000000000..44ec324eed --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java @@ -0,0 +1,76 @@ +package io.sentry.react; + +import com.facebook.react.bridge.JavaOnlyArray; +import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import io.sentry.android.core.AndroidLogger; +import java.util.Iterator; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +final class RNSentryJsonConverter { + public static final String NAME = "RNSentry.RNSentryJsonConverter"; + + private static final ILogger logger = new AndroidLogger(NAME); + + private RNSentryJsonConverter() { + throw new AssertionError("Utility class should not be instantiated"); + } + + @Nullable + static WritableMap convertToWritable(@NotNull JSONObject jsonObject) { + try { + WritableMap writableMap = new JavaOnlyMap(); + Iterator iterator = jsonObject.keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = jsonObject.get(key); + if (value instanceof Float || value instanceof Double) { + writableMap.putDouble(key, jsonObject.getDouble(key)); + } else if (value instanceof Number) { + writableMap.putInt(key, jsonObject.getInt(key)); + } else if (value instanceof String) { + writableMap.putString(key, jsonObject.getString(key)); + } else if (value instanceof JSONObject) { + writableMap.putMap(key, convertToWritable(jsonObject.getJSONObject(key))); + } else if (value instanceof JSONArray) { + writableMap.putArray(key, convertToWritable(jsonObject.getJSONArray(key))); + } else if (value == JSONObject.NULL) { + writableMap.putNull(key); + } + } + return writableMap; + } catch (JSONException e) { + logger.log(SentryLevel.ERROR, "Error parsing json object:" + e.getMessage()); + return null; + } + } + + @NotNull + static WritableArray convertToWritable(@NotNull JSONArray jsonArray) throws JSONException { + WritableArray writableArray = new JavaOnlyArray(); + for (int i = 0; i < jsonArray.length(); i++) { + Object value = jsonArray.get(i); + if (value instanceof Float || value instanceof Double) { + writableArray.pushDouble(jsonArray.getDouble(i)); + } else if (value instanceof Number) { + writableArray.pushInt(jsonArray.getInt(i)); + } else if (value instanceof String) { + writableArray.pushString(jsonArray.getString(i)); + } else if (value instanceof JSONObject) { + writableArray.pushMap(convertToWritable(jsonArray.getJSONObject(i))); + } else if (value instanceof JSONArray) { + writableArray.pushArray(convertToWritable(jsonArray.getJSONArray(i))); + } else if (value == JSONObject.NULL) { + writableArray.pushNull(); + } + } + return writableArray; + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java new file mode 100644 index 0000000000..9c7cf5d3ff --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java @@ -0,0 +1,41 @@ +package io.sentry.react; + +import android.content.Context; +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; + +final class RNSentryJsonUtils { + private RNSentryJsonUtils() { + throw new AssertionError("Utility class should not be instantiated"); + } + + static @Nullable JSONObject getOptionsFromConfigurationFile( + @NotNull Context context, @NotNull String fileName, @NotNull ILogger logger) { + try (InputStream inputStream = context.getAssets().open(fileName); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); + } + String configFileContent = stringBuilder.toString(); + return new JSONObject(configFileContent); + + } catch (Exception e) { + logger.log( + SentryLevel.ERROR, + "Failed to read configuration file. Please make sure " + + fileName + + " exists in the root of your project.", + e); + return null; + } + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 11a4b40c53..261a865562 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -23,41 +23,27 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; -import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.bridge.WritableNativeMap; -import com.facebook.react.common.JavascriptException; import io.sentry.Breadcrumb; import io.sentry.ILogger; import io.sentry.IScope; import io.sentry.ISentryExecutorService; import io.sentry.ISerializer; -import io.sentry.Integration; -import io.sentry.ProfileLifecycle; import io.sentry.ScopesAdapter; -import io.sentry.ScreenshotStrategyType; import io.sentry.Sentry; import io.sentry.SentryDate; import io.sentry.SentryDateProvider; -import io.sentry.SentryEvent; import io.sentry.SentryExecutorService; import io.sentry.SentryLevel; import io.sentry.SentryOptions; -import io.sentry.SentryReplayOptions; -import io.sentry.SentryReplayOptions.SentryReplayQuality; -import io.sentry.UncaughtExceptionHandlerIntegration; import io.sentry.android.core.AndroidLogger; import io.sentry.android.core.AndroidProfiler; -import io.sentry.android.core.AnrIntegration; -import io.sentry.android.core.BuildConfig; import io.sentry.android.core.BuildInfoProvider; -import io.sentry.android.core.CurrentActivityHolder; import io.sentry.android.core.InternalSentrySdk; -import io.sentry.android.core.NdkIntegration; -import io.sentry.android.core.SentryAndroid; import io.sentry.android.core.SentryAndroidDateProvider; import io.sentry.android.core.SentryAndroidOptions; import io.sentry.android.core.ViewHierarchyEventProcessor; @@ -67,12 +53,8 @@ import io.sentry.protocol.Geo; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; -import io.sentry.protocol.SentryPackage; import io.sentry.protocol.User; import io.sentry.protocol.ViewHierarchy; -import io.sentry.react.replay.RNSentryReplayFragmentLifecycleTracer; -import io.sentry.react.replay.RNSentryReplayMask; -import io.sentry.react.replay.RNSentryReplayUnmask; import io.sentry.util.DebugMetaPropertiesApplier; import io.sentry.util.FileUtils; import io.sentry.util.JsonSerializationUtils; @@ -93,10 +75,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Properties; -import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.regex.Pattern; import org.jetbrains.annotations.NotNull; @@ -185,30 +165,13 @@ private void initFragmentInitialFrameTracking() { } } - private void initFragmentReplayTracking() { - final RNSentryReplayFragmentLifecycleTracer fragmentLifecycleTracer = - new RNSentryReplayFragmentLifecycleTracer(logger); - - final @Nullable Activity currentActivity = getCurrentActivity(); - if (!(currentActivity instanceof FragmentActivity)) { - return; - } - - final @NotNull FragmentActivity fragmentActivity = (FragmentActivity) currentActivity; - final @Nullable FragmentManager supportFragmentManager = - fragmentActivity.getSupportFragmentManager(); - if (supportFragmentManager != null) { - supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleTracer, true); - } - } - public void initNativeReactNavigationNewFrameTracking(Promise promise) { this.initFragmentInitialFrameTracking(); } public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { - SentryAndroid.init( - getApplicationContext(), options -> getSentryAndroidOptions(options, rnOptions, logger)); + RNSentryStart.startWithOptions( + getApplicationContext(), rnOptions, getCurrentActivity(), logger); promise.resolve(true); } @@ -224,320 +187,6 @@ protected Context getApplicationContext() { return context; } - protected void getSentryAndroidOptions( - @NotNull SentryAndroidOptions options, @NotNull ReadableMap rnOptions, ILogger logger) { - @Nullable SdkVersion sdkVersion = options.getSdkVersion(); - if (sdkVersion == null) { - sdkVersion = new SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, BuildConfig.VERSION_NAME); - } else { - sdkVersion.setName(RNSentryVersion.ANDROID_SDK_NAME); - } - sdkVersion.addPackage( - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); - - options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); - options.setNativeSdkName(RNSentryVersion.NATIVE_SDK_NAME); - options.setSdkVersion(sdkVersion); - - if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) { - options.setDebug(true); - } - if (rnOptions.hasKey("enabled")) { - options.setEnabled(rnOptions.getBoolean("enabled")); - } - if (rnOptions.hasKey("dsn") && rnOptions.getString("dsn") != null) { - String dsn = rnOptions.getString("dsn"); - logger.log(SentryLevel.INFO, String.format("Starting with DSN: '%s'", dsn)); - options.setDsn(dsn); - } else { - // SentryAndroid needs an empty string fallback for the dsn. - options.setDsn(""); - } - if (rnOptions.hasKey("sampleRate")) { - options.setSampleRate(rnOptions.getDouble("sampleRate")); - } - if (rnOptions.hasKey("sendClientReports")) { - options.setSendClientReports(rnOptions.getBoolean("sendClientReports")); - } - if (rnOptions.hasKey("maxBreadcrumbs")) { - options.setMaxBreadcrumbs(rnOptions.getInt("maxBreadcrumbs")); - } - if (rnOptions.hasKey("maxCacheItems")) { - options.setMaxCacheItems(rnOptions.getInt("maxCacheItems")); - } - if (rnOptions.hasKey("environment") && rnOptions.getString("environment") != null) { - options.setEnvironment(rnOptions.getString("environment")); - } - if (rnOptions.hasKey("release") && rnOptions.getString("release") != null) { - options.setRelease(rnOptions.getString("release")); - } - if (rnOptions.hasKey("dist") && rnOptions.getString("dist") != null) { - options.setDist(rnOptions.getString("dist")); - } - if (rnOptions.hasKey("enableAutoSessionTracking")) { - options.setEnableAutoSessionTracking(rnOptions.getBoolean("enableAutoSessionTracking")); - } - if (rnOptions.hasKey("sessionTrackingIntervalMillis")) { - options.setSessionTrackingIntervalMillis(rnOptions.getInt("sessionTrackingIntervalMillis")); - } - if (rnOptions.hasKey("shutdownTimeout")) { - options.setShutdownTimeoutMillis(rnOptions.getInt("shutdownTimeout")); - } - if (rnOptions.hasKey("enableNdkScopeSync")) { - options.setEnableScopeSync(rnOptions.getBoolean("enableNdkScopeSync")); - } - if (rnOptions.hasKey("attachStacktrace")) { - options.setAttachStacktrace(rnOptions.getBoolean("attachStacktrace")); - } - if (rnOptions.hasKey("attachThreads")) { - // JS use top level stacktrace and android attaches Threads which hides them so - // by default we hide. - options.setAttachThreads(rnOptions.getBoolean("attachThreads")); - } - if (rnOptions.hasKey("attachScreenshot")) { - options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot")); - } - if (rnOptions.hasKey("attachViewHierarchy")) { - options.setAttachViewHierarchy(rnOptions.getBoolean("attachViewHierarchy")); - } - if (rnOptions.hasKey("sendDefaultPii")) { - options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii")); - } - if (rnOptions.hasKey("maxQueueSize")) { - options.setMaxQueueSize(rnOptions.getInt("maxQueueSize")); - } - if (rnOptions.hasKey("enableNdk")) { - options.setEnableNdk(rnOptions.getBoolean("enableNdk")); - } - if (rnOptions.hasKey("enableLogs")) { - options.getLogs().setEnabled(rnOptions.getBoolean("enableLogs")); - } - if (rnOptions.hasKey("spotlight")) { - if (rnOptions.getType("spotlight") == ReadableType.Boolean) { - options.setEnableSpotlight(rnOptions.getBoolean("spotlight")); - options.setSpotlightConnectionUrl(rnOptions.getString("defaultSidecarUrl")); - } else if (rnOptions.getType("spotlight") == ReadableType.String) { - options.setEnableSpotlight(true); - options.setSpotlightConnectionUrl(rnOptions.getString("spotlight")); - } - } - - SentryReplayOptions replayOptions = getReplayOptions(rnOptions); - options.setSessionReplay(replayOptions); - // Check if the replay integration is available on the classpath. It's already - // kept from R8 - // shrinking by sentry-android-core - final boolean isReplayAvailable = - loadClass.isClassAvailable("io.sentry.android.replay.ReplayIntegration", logger); - if (isReplayEnabled(replayOptions) && isReplayAvailable) { - options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter()); - initFragmentReplayTracking(); - } - - // Configure Android UI Profiling - configureAndroidProfiling(options, rnOptions); - - // Exclude Dev Server and Sentry Dsn request from Breadcrumbs - String dsn = getURLFromDSN(rnOptions.getString("dsn")); - String devServerUrl = rnOptions.getString("devServerUrl"); - options.setBeforeBreadcrumb( - (breadcrumb, hint) -> { - Object urlObject = breadcrumb.getData("url"); - String url = urlObject instanceof String ? (String) urlObject : ""; - if ("http".equals(breadcrumb.getType()) - && ((dsn != null && url.startsWith(dsn)) - || (devServerUrl != null && url.startsWith(devServerUrl)))) { - return null; - } - return breadcrumb; - }); - - // React native internally throws a JavascriptException. - // we want to ignore it on the native side to avoid sending it twice. - options.addIgnoredExceptionForType(JavascriptException.class); - - trySetIgnoreErrors(options, rnOptions); - - options.setBeforeSend( - (event, hint) -> { - setEventOriginTag(event); - addPackages(event, options.getSdkVersion()); - - return event; - }); - - if (rnOptions.hasKey("enableNativeCrashHandling") - && !rnOptions.getBoolean("enableNativeCrashHandling")) { - final List integrations = options.getIntegrations(); - for (final Integration integration : integrations) { - if (integration instanceof UncaughtExceptionHandlerIntegration - || integration instanceof AnrIntegration - || integration instanceof NdkIntegration) { - integrations.remove(integration); - } - } - } - logger.log( - SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations())); - - final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); - final Activity currentActivity = getCurrentActivity(); - if (currentActivity != null) { - currentActivityHolder.setActivity(currentActivity); - } - } - - private boolean isReplayEnabled(SentryReplayOptions replayOptions) { - return replayOptions.getSessionSampleRate() != null - || replayOptions.getOnErrorSampleRate() != null; - } - - private SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { - final SdkVersion replaySdkVersion = - new SdkVersion( - RNSentryVersion.REACT_NATIVE_SDK_NAME, - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); - @NotNull - final SentryReplayOptions androidReplayOptions = - new SentryReplayOptions(false, replaySdkVersion); - - if (!(rnOptions.hasKey("replaysSessionSampleRate") - || rnOptions.hasKey("replaysOnErrorSampleRate"))) { - return androidReplayOptions; - } - - androidReplayOptions.setSessionSampleRate( - rnOptions.hasKey("replaysSessionSampleRate") - ? rnOptions.getDouble("replaysSessionSampleRate") - : null); - androidReplayOptions.setOnErrorSampleRate( - rnOptions.hasKey("replaysOnErrorSampleRate") - ? rnOptions.getDouble("replaysOnErrorSampleRate") - : null); - - if (rnOptions.hasKey("replaysSessionQuality")) { - final String qualityString = rnOptions.getString("replaysSessionQuality"); - final SentryReplayQuality quality = parseReplayQuality(qualityString); - androidReplayOptions.setQuality(quality); - } - - if (!rnOptions.hasKey("mobileReplayOptions")) { - return androidReplayOptions; - } - @Nullable final ReadableMap rnMobileReplayOptions = rnOptions.getMap("mobileReplayOptions"); - if (rnMobileReplayOptions == null) { - return androidReplayOptions; - } - - androidReplayOptions.setMaskAllText( - !rnMobileReplayOptions.hasKey("maskAllText") - || rnMobileReplayOptions.getBoolean("maskAllText")); - androidReplayOptions.setMaskAllImages( - !rnMobileReplayOptions.hasKey("maskAllImages") - || rnMobileReplayOptions.getBoolean("maskAllImages")); - - final boolean redactVectors = - !rnMobileReplayOptions.hasKey("maskAllVectors") - || rnMobileReplayOptions.getBoolean("maskAllVectors"); - if (redactVectors) { - androidReplayOptions.addMaskViewClass("com.horcrux.svg.SvgView"); // react-native-svg - } - - if (rnMobileReplayOptions.hasKey("screenshotStrategy")) { - final String screenshotStrategyString = rnMobileReplayOptions.getString("screenshotStrategy"); - final ScreenshotStrategyType screenshotStrategy = - parseScreenshotStrategy(screenshotStrategyString); - androidReplayOptions.setScreenshotStrategy(screenshotStrategy); - } - - androidReplayOptions.setMaskViewContainerClass(RNSentryReplayMask.class.getName()); - androidReplayOptions.setUnmaskViewContainerClass(RNSentryReplayUnmask.class.getName()); - - return androidReplayOptions; - } - - private ScreenshotStrategyType parseScreenshotStrategy(@Nullable String strategyString) { - if (strategyString == null) { - return ScreenshotStrategyType.PIXEL_COPY; - } - - switch (strategyString.toLowerCase(Locale.ROOT)) { - case "canvas": - return ScreenshotStrategyType.CANVAS; - default: - return ScreenshotStrategyType.PIXEL_COPY; - } - } - - private SentryReplayQuality parseReplayQuality(@Nullable String qualityString) { - if (qualityString == null) { - return SentryReplayQuality.MEDIUM; - } - - switch (qualityString.toLowerCase(Locale.ROOT)) { - case "low": - return SentryReplayQuality.LOW; - case "medium": - return SentryReplayQuality.MEDIUM; - case "high": - return SentryReplayQuality.HIGH; - default: - return SentryReplayQuality.MEDIUM; - } - } - - private void configureAndroidProfiling( - @NotNull SentryAndroidOptions options, @NotNull ReadableMap rnOptions) { - if (!rnOptions.hasKey("_experiments")) { - return; - } - - @Nullable final ReadableMap experiments = rnOptions.getMap("_experiments"); - if (experiments == null || !experiments.hasKey("androidProfilingOptions")) { - return; - } - - @Nullable - final ReadableMap androidProfilingOptions = experiments.getMap("androidProfilingOptions"); - if (androidProfilingOptions == null) { - return; - } - - // Set profile session sample rate - if (androidProfilingOptions.hasKey("profileSessionSampleRate")) { - final double profileSessionSampleRate = - androidProfilingOptions.getDouble("profileSessionSampleRate"); - options.setProfileSessionSampleRate(profileSessionSampleRate); - logger.log( - SentryLevel.INFO, - String.format( - "Android UI Profiling profileSessionSampleRate set to: %.2f", - profileSessionSampleRate)); - } - - // Set profiling lifecycle mode - if (androidProfilingOptions.hasKey("lifecycle")) { - final String lifecycle = androidProfilingOptions.getString("lifecycle"); - if ("manual".equalsIgnoreCase(lifecycle)) { - options.setProfileLifecycle(ProfileLifecycle.MANUAL); - logger.log(SentryLevel.INFO, "Android UI Profile Lifecycle set to MANUAL"); - } else if ("trace".equalsIgnoreCase(lifecycle)) { - options.setProfileLifecycle(ProfileLifecycle.TRACE); - logger.log(SentryLevel.INFO, "Android UI Profile Lifecycle set to TRACE"); - } - } - - // Set start on app start - if (androidProfilingOptions.hasKey("startOnAppStart")) { - final boolean startOnAppStart = androidProfilingOptions.getBoolean("startOnAppStart"); - options.setStartProfilerOnAppStart(startOnAppStart); - logger.log( - SentryLevel.INFO, - String.format("Android UI Profiling startOnAppStart set to %b", startOnAppStart)); - } - } - public void crash() { throw new RuntimeException("TEST - Sentry Client Crash (only works in release mode)"); } @@ -1276,51 +925,6 @@ public void crashedLastRun(Promise promise) { promise.resolve(Sentry.isCrashedLastRun()); } - private void setEventOriginTag(SentryEvent event) { - // We hardcode native-java as only java events are processed by the Android SDK. - SdkVersion sdk = event.getSdk(); - if (sdk != null) { - switch (sdk.getName()) { - case RNSentryVersion.NATIVE_SDK_NAME: - setEventEnvironmentTag(event, "native"); - break; - case RNSentryVersion.ANDROID_SDK_NAME: - setEventEnvironmentTag(event, "java"); - break; - default: - break; - } - } - } - - private void setEventEnvironmentTag(SentryEvent event, String environment) { - event.setTag("event.origin", "android"); - event.setTag("event.environment", environment); - } - - private void addPackages(SentryEvent event, SdkVersion sdk) { - SdkVersion eventSdk = event.getSdk(); - if (eventSdk != null - && "sentry.javascript.react-native".equals(eventSdk.getName()) - && sdk != null) { - Set sentryPackages = sdk.getPackageSet(); - if (sentryPackages != null) { - for (SentryPackage sentryPackage : sentryPackages) { - eventSdk.addPackage(sentryPackage.getName(), sentryPackage.getVersion()); - } - } - - Set integrations = sdk.getIntegrationSet(); - if (integrations != null) { - for (String integration : integrations) { - eventSdk.addIntegration(integration); - } - } - - event.setSdk(eventSdk); - } - } - private boolean checkAndroidXAvailability() { try { Class.forName("androidx.core.app.FrameMetricsAggregator"); diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java new file mode 100644 index 0000000000..ca219351fe --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -0,0 +1,68 @@ +package io.sentry.react; + +import android.content.Context; +import com.facebook.react.bridge.ReadableMap; +import io.sentry.ILogger; +import io.sentry.Sentry; +import io.sentry.SentryLevel; +import io.sentry.android.core.AndroidLogger; +import io.sentry.android.core.SentryAndroidOptions; +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; + +public final class RNSentrySDK { + private static final String CONFIGURATION_FILE = "sentry.options.json"; + private static final String NAME = "RNSentrySDK"; + + private static final ILogger logger = new AndroidLogger(NAME); + + private RNSentrySDK() { + throw new AssertionError("Utility class should not be instantiated"); + } + + static void init( + @NotNull final Context context, + @NotNull Sentry.OptionsConfiguration configuration, + @NotNull String configurationFile, + @NotNull ILogger logger) { + try { + JSONObject jsonObject = + RNSentryJsonUtils.getOptionsFromConfigurationFile(context, configurationFile, logger); + if (jsonObject == null) { + RNSentryStart.startWithConfiguration(context, configuration); + return; + } + ReadableMap rnOptions = RNSentryJsonConverter.convertToWritable(jsonObject); + if (rnOptions == null) { + RNSentryStart.startWithConfiguration(context, configuration); + return; + } + RNSentryStart.startWithOptions(context, rnOptions, configuration, logger); + } catch (Exception e) { + logger.log( + SentryLevel.ERROR, "Failed to start Sentry with options from configuration file.", e); + throw new RuntimeException("Failed to initialize Sentry's React Native SDK", e); + } + } + + /** + * @experimental Start the Native Android SDK with the provided configuration options. Uses as a + * base configurations the `sentry.options.json` configuration file if it exists. + * @param context Android Context + * @param configuration configuration options + */ + public static void init( + @NotNull final Context context, + @NotNull Sentry.OptionsConfiguration configuration) { + init(context, configuration, CONFIGURATION_FILE, logger); + } + + /** + * @experimental Start the Native Android SDK with options from `sentry.options.json` + * configuration file. + * @param context Android Context + */ + public static void init(@NotNull final Context context) { + init(context, options -> {}, CONFIGURATION_FILE, logger); + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java new file mode 100644 index 0000000000..cbe6bc6b62 --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -0,0 +1,417 @@ +package io.sentry.react; + +import android.app.Activity; +import android.content.Context; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.common.JavascriptException; +import io.sentry.ILogger; +import io.sentry.ProfileLifecycle; +import io.sentry.Sentry; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions.BeforeSendCallback; +import io.sentry.SentryReplayOptions; +import io.sentry.UncaughtExceptionHandlerIntegration; +import io.sentry.android.core.AnrIntegration; +import io.sentry.android.core.BuildConfig; +import io.sentry.android.core.CurrentActivityHolder; +import io.sentry.android.core.NdkIntegration; +import io.sentry.android.core.SentryAndroid; +import io.sentry.android.core.SentryAndroidOptions; +import io.sentry.protocol.SdkVersion; +import io.sentry.react.replay.RNSentryReplayMask; +import io.sentry.react.replay.RNSentryReplayUnmask; +import java.net.URI; +import java.net.URISyntaxException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class RNSentryStart { + + private RNSentryStart() { + throw new AssertionError("Utility class should not be instantiated"); + } + + static void startWithConfiguration( + @NotNull final Context context, + @NotNull Sentry.OptionsConfiguration configuration) { + Sentry.OptionsConfiguration defaults = + options -> updateWithReactDefaults(options, null); + RNSentryCompositeOptionsConfiguration compositeConfiguration = + new RNSentryCompositeOptionsConfiguration( + defaults, configuration, RNSentryStart::updateWithReactFinals); + SentryAndroid.init(context, compositeConfiguration); + } + + static void startWithOptions( + @NotNull final Context context, + @NotNull final ReadableMap rnOptions, + @NotNull Sentry.OptionsConfiguration configuration, + @NotNull ILogger logger) { + Sentry.OptionsConfiguration defaults = + options -> updateWithReactDefaults(options, null); + Sentry.OptionsConfiguration rnConfigurationOptions = + options -> getSentryAndroidOptions(options, rnOptions, logger); + RNSentryCompositeOptionsConfiguration compositeConfiguration = + new RNSentryCompositeOptionsConfiguration( + rnConfigurationOptions, defaults, configuration, RNSentryStart::updateWithReactFinals); + SentryAndroid.init(context, compositeConfiguration); + } + + static void startWithOptions( + @NotNull final Context context, + @NotNull final ReadableMap rnOptions, + @Nullable Activity currentActivity, + @NotNull ILogger logger) { + Sentry.OptionsConfiguration defaults = + options -> updateWithReactDefaults(options, currentActivity); + Sentry.OptionsConfiguration rnConfigurationOptions = + options -> getSentryAndroidOptions(options, rnOptions, logger); + RNSentryCompositeOptionsConfiguration compositeConfiguration = + new RNSentryCompositeOptionsConfiguration( + rnConfigurationOptions, defaults, RNSentryStart::updateWithReactFinals); + SentryAndroid.init(context, compositeConfiguration); + } + + static void getSentryAndroidOptions( + @NotNull SentryAndroidOptions options, + @NotNull ReadableMap rnOptions, + @NotNull ILogger logger) { + if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) { + options.setDebug(true); + } + if (rnOptions.hasKey("dsn") && rnOptions.getString("dsn") != null) { + String dsn = rnOptions.getString("dsn"); + logger.log(SentryLevel.INFO, String.format("Starting with DSN: '%s'", dsn)); + options.setDsn(dsn); + } else { + // SentryAndroid needs an empty string fallback for the dsn. + options.setDsn(""); + } + if (rnOptions.hasKey("sampleRate")) { + options.setSampleRate(rnOptions.getDouble("sampleRate")); + } + if (rnOptions.hasKey("sendClientReports")) { + options.setSendClientReports(rnOptions.getBoolean("sendClientReports")); + } + if (rnOptions.hasKey("maxBreadcrumbs")) { + options.setMaxBreadcrumbs(rnOptions.getInt("maxBreadcrumbs")); + } + if (rnOptions.hasKey("maxCacheItems")) { + options.setMaxCacheItems(rnOptions.getInt("maxCacheItems")); + } + if (rnOptions.hasKey("environment") && rnOptions.getString("environment") != null) { + options.setEnvironment(rnOptions.getString("environment")); + } + if (rnOptions.hasKey("release") && rnOptions.getString("release") != null) { + options.setRelease(rnOptions.getString("release")); + } + if (rnOptions.hasKey("dist") && rnOptions.getString("dist") != null) { + options.setDist(rnOptions.getString("dist")); + } + if (rnOptions.hasKey("enableAutoSessionTracking")) { + options.setEnableAutoSessionTracking(rnOptions.getBoolean("enableAutoSessionTracking")); + } + if (rnOptions.hasKey("sessionTrackingIntervalMillis")) { + options.setSessionTrackingIntervalMillis(rnOptions.getInt("sessionTrackingIntervalMillis")); + } + if (rnOptions.hasKey("shutdownTimeout")) { + options.setShutdownTimeoutMillis(rnOptions.getInt("shutdownTimeout")); + } + if (rnOptions.hasKey("enableNdkScopeSync")) { + options.setEnableScopeSync(rnOptions.getBoolean("enableNdkScopeSync")); + } + if (rnOptions.hasKey("attachStacktrace")) { + options.setAttachStacktrace(rnOptions.getBoolean("attachStacktrace")); + } + if (rnOptions.hasKey("attachThreads")) { + // JS use top level stacktrace and android attaches Threads which hides them so + // by default we hide. + options.setAttachThreads(rnOptions.getBoolean("attachThreads")); + } + if (rnOptions.hasKey("attachScreenshot")) { + options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot")); + } + if (rnOptions.hasKey("attachViewHierarchy")) { + options.setAttachViewHierarchy(rnOptions.getBoolean("attachViewHierarchy")); + } + if (rnOptions.hasKey("sendDefaultPii")) { + options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii")); + } + if (rnOptions.hasKey("maxQueueSize")) { + options.setMaxQueueSize(rnOptions.getInt("maxQueueSize")); + } + if (rnOptions.hasKey("enableNdk")) { + options.setEnableNdk(rnOptions.getBoolean("enableNdk")); + } + if (rnOptions.hasKey("spotlight")) { + if (rnOptions.getType("spotlight") == ReadableType.Boolean) { + options.setEnableSpotlight(rnOptions.getBoolean("spotlight")); + if (rnOptions.hasKey("defaultSidecarUrl")) { + options.setSpotlightConnectionUrl(rnOptions.getString("defaultSidecarUrl")); + } + } else if (rnOptions.getType("spotlight") == ReadableType.String) { + options.setEnableSpotlight(true); + options.setSpotlightConnectionUrl(rnOptions.getString("spotlight")); + } + } + + SentryReplayOptions replayOptions = getReplayOptions(rnOptions); + options.setSessionReplay(replayOptions); + if (isReplayEnabled(replayOptions)) { + options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter()); + } + + // Configure Android UI Profiling + configureAndroidProfiling(options, rnOptions, logger); + + // Exclude Dev Server and Sentry Dsn request from Breadcrumbs + String dsn = rnOptions.hasKey("dsn") ? getURLFromDSN(rnOptions.getString("dsn")) : null; + String devServerUrl = + rnOptions.hasKey("devServerUrl") ? rnOptions.getString("devServerUrl") : null; + options.setBeforeBreadcrumb( + (breadcrumb, hint) -> { + Object urlObject = breadcrumb.getData("url"); + String url = urlObject instanceof String ? (String) urlObject : ""; + if ("http".equals(breadcrumb.getType()) + && ((dsn != null && url.startsWith(dsn)) + || (devServerUrl != null && url.startsWith(devServerUrl)))) { + return null; + } + return breadcrumb; + }); + + if (rnOptions.hasKey("enableNativeCrashHandling") + && !rnOptions.getBoolean("enableNativeCrashHandling")) { + options + .getIntegrations() + .removeIf( + integration -> + integration instanceof UncaughtExceptionHandlerIntegration + || integration instanceof AnrIntegration + || integration instanceof NdkIntegration); + } + logger.log( + SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations())); + } + + private static void configureAndroidProfiling( + @NotNull SentryAndroidOptions options, + @NotNull ReadableMap rnOptions, + @NotNull ILogger logger) { + if (!rnOptions.hasKey("_experiments")) { + return; + } + + @Nullable final ReadableMap experiments = rnOptions.getMap("_experiments"); + if (experiments == null || !experiments.hasKey("androidProfilingOptions")) { + return; + } + + @Nullable + final ReadableMap androidProfilingOptions = experiments.getMap("androidProfilingOptions"); + if (androidProfilingOptions == null) { + return; + } + + // Set profile session sample rate + if (androidProfilingOptions.hasKey("profileSessionSampleRate")) { + if (androidProfilingOptions.getType("profileSessionSampleRate") == ReadableType.Number) { + final double profileSessionSampleRate = + androidProfilingOptions.getDouble("profileSessionSampleRate"); + options.setProfileSessionSampleRate(profileSessionSampleRate); + logger.log( + SentryLevel.INFO, + String.format( + "Android UI Profiling profileSessionSampleRate set to: %.2f", + profileSessionSampleRate)); + } else { + logger.log( + SentryLevel.WARNING, + "Android UI Profiling profileSessionSampleRate must be a number, ignoring invalid" + + " value"); + } + } + + // Set profiling lifecycle mode + if (androidProfilingOptions.hasKey("lifecycle")) { + if (androidProfilingOptions.getType("lifecycle") == ReadableType.String) { + final String lifecycle = androidProfilingOptions.getString("lifecycle"); + if ("manual".equalsIgnoreCase(lifecycle)) { + options.setProfileLifecycle(ProfileLifecycle.MANUAL); + logger.log(SentryLevel.INFO, "Android UI Profile Lifecycle set to MANUAL"); + } else if ("trace".equalsIgnoreCase(lifecycle)) { + options.setProfileLifecycle(ProfileLifecycle.TRACE); + logger.log(SentryLevel.INFO, "Android UI Profile Lifecycle set to TRACE"); + } + } else { + logger.log( + SentryLevel.WARNING, + "Android UI Profiling lifecycle must be a string, ignoring invalid value"); + } + } + + // Set start on app start + if (androidProfilingOptions.hasKey("startOnAppStart")) { + if (androidProfilingOptions.getType("startOnAppStart") == ReadableType.Boolean) { + final boolean startOnAppStart = androidProfilingOptions.getBoolean("startOnAppStart"); + options.setStartProfilerOnAppStart(startOnAppStart); + logger.log( + SentryLevel.INFO, + String.format("Android UI Profiling startOnAppStart set to %b", startOnAppStart)); + } else { + logger.log( + SentryLevel.WARNING, + "Android UI Profiling startOnAppStart must be a boolean, ignoring invalid value"); + } + } + } + + /** + * This function updates the options with RNSentry defaults. These default can be overwritten by + * users during manual native initialization. + */ + static void updateWithReactDefaults( + @NotNull SentryAndroidOptions options, @Nullable Activity currentActivity) { + @Nullable SdkVersion sdkVersion = options.getSdkVersion(); + if (sdkVersion == null) { + sdkVersion = new SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, BuildConfig.VERSION_NAME); + } else { + sdkVersion.setName(RNSentryVersion.ANDROID_SDK_NAME); + } + sdkVersion.addPackage( + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); + + options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); + options.setNativeSdkName(RNSentryVersion.NATIVE_SDK_NAME); + options.setSdkVersion(sdkVersion); + + // Tracing is only enabled in JS to avoid duplicate navigation spans + options.setTracesSampleRate(null); + options.setTracesSampler(null); + + // React native internally throws a JavascriptException. + // we want to ignore it on the native side to avoid sending it twice. + options.addIgnoredExceptionForType(JavascriptException.class); + + setCurrentActivity(currentActivity); + } + + /** + * This function updates options with changes RNSentry users should not change and so this is + * applied after the configureOptions callback during manual native initialization. + */ + static void updateWithReactFinals(@NotNull SentryAndroidOptions options) { + BeforeSendCallback userBeforeSend = options.getBeforeSend(); + options.setBeforeSend( + (event, hint) -> { + setEventOriginTag(event); + // Note: In Sentry Android SDK v7, native SDK packages/integrations are already + // included in the SDK version set during initialization, so no need to copy them here. + if (userBeforeSend != null) { + return userBeforeSend.execute(event, hint); + } + return event; + }); + } + + private static void setCurrentActivity(Activity currentActivity) { + final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); + if (currentActivity != null) { + currentActivityHolder.setActivity(currentActivity); + } + } + + private static boolean isReplayEnabled(SentryReplayOptions replayOptions) { + return replayOptions.getSessionSampleRate() != null + || replayOptions.getOnErrorSampleRate() != null; + } + + private static SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { + final SdkVersion replaySdkVersion = + new SdkVersion( + RNSentryVersion.REACT_NATIVE_SDK_NAME, + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); + @NotNull + final SentryReplayOptions androidReplayOptions = + new SentryReplayOptions(false, replaySdkVersion); + + if (!(rnOptions.hasKey("replaysSessionSampleRate") + || rnOptions.hasKey("replaysOnErrorSampleRate"))) { + return androidReplayOptions; + } + + androidReplayOptions.setSessionSampleRate( + rnOptions.hasKey("replaysSessionSampleRate") + ? rnOptions.getDouble("replaysSessionSampleRate") + : null); + androidReplayOptions.setOnErrorSampleRate( + rnOptions.hasKey("replaysOnErrorSampleRate") + ? rnOptions.getDouble("replaysOnErrorSampleRate") + : null); + + if (!rnOptions.hasKey("mobileReplayOptions")) { + return androidReplayOptions; + } + @Nullable final ReadableMap rnMobileReplayOptions = rnOptions.getMap("mobileReplayOptions"); + if (rnMobileReplayOptions == null) { + return androidReplayOptions; + } + + androidReplayOptions.setMaskAllText( + !rnMobileReplayOptions.hasKey("maskAllText") + || rnMobileReplayOptions.getBoolean("maskAllText")); + androidReplayOptions.setMaskAllImages( + !rnMobileReplayOptions.hasKey("maskAllImages") + || rnMobileReplayOptions.getBoolean("maskAllImages")); + + final boolean redactVectors = + !rnMobileReplayOptions.hasKey("maskAllVectors") + || rnMobileReplayOptions.getBoolean("maskAllVectors"); + if (redactVectors) { + androidReplayOptions.addMaskViewClass("com.horcrux.svg.SvgView"); // react-native-svg + } + + androidReplayOptions.setMaskViewContainerClass(RNSentryReplayMask.class.getName()); + androidReplayOptions.setUnmaskViewContainerClass(RNSentryReplayUnmask.class.getName()); + + return androidReplayOptions; + } + + private static void setEventOriginTag(SentryEvent event) { + // We hardcode native-java as only java events are processed by the Android SDK. + SdkVersion sdk = event.getSdk(); + if (sdk != null) { + switch (sdk.getName()) { + case RNSentryVersion.NATIVE_SDK_NAME: + setEventEnvironmentTag(event, "native"); + break; + case RNSentryVersion.ANDROID_SDK_NAME: + setEventEnvironmentTag(event, "java"); + break; + default: + break; + } + } + } + + private static void setEventEnvironmentTag(SentryEvent event, String environment) { + event.setTag("event.origin", "android"); + event.setTag("event.environment", environment); + } + + private static @Nullable String getURLFromDSN(@Nullable String dsn) { + if (dsn == null) { + return null; + } + URI uri = null; + try { + uri = new URI(dsn); + } catch (URISyntaxException e) { + return null; + } + return uri.getScheme() + "://" + uri.getHost(); + } +} diff --git a/packages/core/ios/RNSentry.h b/packages/core/ios/RNSentry.h index 5b163baca9..73e27678cf 100644 --- a/packages/core/ios/RNSentry.h +++ b/packages/core/ios/RNSentry.h @@ -9,6 +9,9 @@ #import +// This import exposes public RNSentrySDK start +#import "RNSentrySDK.h" + typedef int (*SymbolicateCallbackType)(const void *, Dl_info *); @class SentryOptions; diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 77f1763528..d7e71da2cb 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -51,6 +51,7 @@ #endif #import "RNSentryExperimentalOptions.h" +#import "RNSentryStart.h" #import "RNSentryVersion.h" #import "SentrySDKWrapper.h" #import "SentryScreenFramesWrapper.h" @@ -58,7 +59,6 @@ static bool hasFetchedAppStart; @implementation RNSentry { - bool sentHybridSdkDidBecomeActive; bool hasListeners; RNSentryTimeToDisplay *_timeToDisplay; NSArray *_ignoreErrorPatternsStr; @@ -143,43 +143,17 @@ - (NSMutableDictionary *)prepareOptions:(NSDictionary *)options RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) { NSMutableDictionary *mutableOptions = [self prepareOptions:options]; -#if SENTRY_TARGET_REPLAY_SUPPORTED - BOOL isSessionReplayEnabled = [RNSentryReplay updateOptions:mutableOptions]; -#else - // Defaulting to false for unsupported targets - BOOL isSessionReplayEnabled = NO; -#endif NSError *error = nil; - [SentrySDKWrapper setupWithDictionary:mutableOptions - isSessionReplayEnabled:isSessionReplayEnabled - error:&error]; + [RNSentryStart startWithOptions:mutableOptions error:&error]; if (error != nil) { reject(@"SentryReactNative", error.localizedDescription, error); return; } -#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - BOOL appIsActive = - [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive; -#else - BOOL appIsActive = [[NSApplication sharedApplication] isActive]; -#endif - - // If the app is active/in foreground, and we have not sent the SentryHybridSdkDidBecomeActive - // notification, send it. - if (appIsActive && !sentHybridSdkDidBecomeActive - && ([SentrySDKWrapper enableAutoSessionTracking] || - [SentrySDKWrapper enableWatchdogTerminationTracking])) { - [[NSNotificationCenter defaultCenter] postNotificationName:@"SentryHybridSdkDidBecomeActive" - object:nil]; - - sentHybridSdkDidBecomeActive = true; - } - -#if SENTRY_TARGET_REPLAY_SUPPORTED - [RNSentryReplay postInit]; -#endif - + // RNSentryStart.startWithOptions already handles: + // - Session tracking notification (SentryHybridSdkDidBecomeActive) + // - Replay postInit + // - SDK initialization resolve(@YES); } diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentrySDK.h new file mode 100644 index 0000000000..232071d9bc --- /dev/null +++ b/packages/core/ios/RNSentrySDK.h @@ -0,0 +1,31 @@ +#import + +@interface RNSentrySDK : NSObject +SENTRY_NO_INIT + +/** + * @experimental + * Inits and configures Sentry for React Native applications using `sentry.options.json` + * configuration file. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)start; + +/** + * @experimental + * Inits and configures Sentry for React Native applicationsusing `sentry.options.json` + * configuration file and `configureOptions` callback. + * + * The `configureOptions` callback can overwrite the config file options + * and add non-serializable items to the options object. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)startWithConfigureOptions: + (void (^_Nullable)(SentryOptions *_Nonnull options))configureOptions + NS_SWIFT_NAME(start(configureOptions:)); + +@end diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m new file mode 100644 index 0000000000..2a96188dfa --- /dev/null +++ b/packages/core/ios/RNSentrySDK.m @@ -0,0 +1,78 @@ +#import "RNSentrySDK.h" +#import "RNSentryStart.h" +#import +#import + +static NSString *SENTRY_OPTIONS_RESOURCE_NAME = @"sentry.options"; +static NSString *SENTRY_OPTIONS_RESOURCE_TYPE = @"json"; + +@implementation RNSentrySDK + ++ (void)start +{ + [self startWithConfigureOptions:nil]; +} + ++ (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOptions +{ + NSString *path = [[NSBundle mainBundle] pathForResource:SENTRY_OPTIONS_RESOURCE_NAME + ofType:SENTRY_OPTIONS_RESOURCE_TYPE]; + + [self start:path configureOptions:configureOptions]; +} + ++ (void)start:(NSString *)path configureOptions:(void (^)(SentryOptions *options))configureOptions +{ + NSError *readError = nil; + NSError *parseError = nil; + NSError *optionsError = nil; + + NSData *_Nullable content = nil; + if (path != nil) { + content = [NSData dataWithContentsOfFile:path options:0 error:&readError]; + } + + NSDictionary *dict = nil; + if (content != nil) { + dict = [NSJSONSerialization JSONObjectWithData:content options:0 error:&parseError]; + } + + if (readError != nil) { + NSLog(@"[RNSentry] Failed to load options from %@, with error: %@", path, + readError.localizedDescription); + } + + if (parseError != nil) { + NSLog(@"[RNSentry] Failed to parse JSON from %@, with error: %@", path, + parseError.localizedDescription); + } + + SentryOptions *options = nil; + if (dict != nil) { + options = [RNSentryStart createOptionsWithDictionary:dict error:&optionsError]; + } + + if (optionsError != nil) { + NSLog(@"[RNSentry] Failed to parse options from %@, with error: %@", path, + optionsError.localizedDescription); + } + + if (options == nil) { + // Fallback in case that options file could not be parsed. + NSError *fallbackError = nil; + options = [SentryOptionsInternal initWithDict:@{} didFailWithError:&fallbackError]; + if (fallbackError != nil) { + NSLog(@"[RNSentry] Failed to create fallback options with error: %@", + fallbackError.localizedDescription); + } + } + + [RNSentryStart updateWithReactDefaults:options]; + if (configureOptions != nil) { + configureOptions(options); + } + [RNSentryStart updateWithReactFinals:options]; + [RNSentryStart startWithOptions:options]; +} + +@end diff --git a/packages/core/ios/RNSentryStart.h b/packages/core/ios/RNSentryStart.h new file mode 100644 index 0000000000..13ab44c37c --- /dev/null +++ b/packages/core/ios/RNSentryStart.h @@ -0,0 +1,25 @@ +#import + +@interface RNSentryStart : NSObject +SENTRY_NO_INIT + ++ (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions + error:(NSError *_Nullable *_Nullable)errorPointer; + ++ (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options + error:(NSError *_Nonnull *_Nonnull)errorPointer; + ++ (void)updateWithReactDefaults:(SentryOptions *)options; ++ (void)updateWithReactFinals:(SentryOptions *)options; + +/** + * @experimental + * Inits and configures Sentry for React Native applications. Make sure to + * set a valid DSN. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)); + +@end diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m new file mode 100644 index 0000000000..1514a00d06 --- /dev/null +++ b/packages/core/ios/RNSentryStart.m @@ -0,0 +1,228 @@ +#import "RNSentryStart.h" +#import "RNSentryExperimentalOptions.h" +#import "RNSentryReplay.h" +#import "RNSentryVersion.h" + +#import +#import +@import Sentry; + +@implementation RNSentryStart + ++ (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions + error:(NSError *_Nullable *_Nullable)errorPointer +{ + SentryOptions *options = [self createOptionsWithDictionary:javascriptOptions + error:errorPointer]; + [self updateWithReactDefaults:options]; + [self updateWithReactFinals:options]; + [self startWithOptions:options]; +} + ++ (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) +{ + NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; + [PrivateSentrySDKOnly setSdkName:NATIVE_SDK_NAME andVersionString:sdkVersion]; + [PrivateSentrySDKOnly addSdkPackage:REACT_NATIVE_SDK_PACKAGE_NAME + version:REACT_NATIVE_SDK_PACKAGE_VERSION]; + + [SentrySDK startWithOptions:options]; + +#if SENTRY_TARGET_REPLAY_SUPPORTED + [RNSentryReplay postInit]; +#endif + + [self postDidBecomeActiveNotification]; +} + ++ (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options + error:(NSError *_Nonnull *_Nonnull)errorPointer +{ + NSMutableDictionary *mutableOptions = [options mutableCopy]; + +#if SENTRY_TARGET_REPLAY_SUPPORTED + BOOL isSessionReplayEnabled = [RNSentryReplay updateOptions:mutableOptions]; +#else + BOOL isSessionReplayEnabled = NO; +#endif + + SentryOptions *sentryOptions = [SentryOptionsInternal initWithDict:mutableOptions + didFailWithError:errorPointer]; + if (*errorPointer != nil) { + return nil; + } + + // Exclude Dev Server and Sentry Dsn request from Breadcrumbs + NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; + // TODO: For Auto Init from JS dev server is resolved automatically, for init from options file + // dev server has to be specified manually + NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; + sentryOptions.beforeBreadcrumb + = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) + { + NSString *url = breadcrumb.data[@"url"] ?: @""; + + if ([@"http" isEqualToString:breadcrumb.type] + && ((dsn != nil && [url hasPrefix:dsn]) + || (devServerUrl != nil && [url hasPrefix:devServerUrl]))) { + return nil; + } + return breadcrumb; + }; + + // JS options.enableNativeCrashHandling equals to native options.enableCrashHandler + if ([mutableOptions valueForKey:@"enableNativeCrashHandling"] != nil) { + BOOL enableNativeCrashHandling = [mutableOptions[@"enableNativeCrashHandling"] boolValue]; + + if (!enableNativeCrashHandling) { + sentryOptions.enableCrashHandler = NO; + } + } + + // Set spotlight option + if ([mutableOptions valueForKey:@"spotlight"] != nil) { + id spotlightValue = [mutableOptions valueForKey:@"spotlight"]; + if ([spotlightValue isKindOfClass:[NSString class]]) { + NSLog(@"Using Spotlight on address: %@", spotlightValue); + sentryOptions.enableSpotlight = true; + sentryOptions.spotlightUrl = spotlightValue; + } else if ([spotlightValue isKindOfClass:[NSNumber class]]) { + sentryOptions.enableSpotlight = [spotlightValue boolValue]; + // TODO: For Auto init from JS set automatically for init from options file have to be + // set manually + id defaultSpotlightUrl = [mutableOptions valueForKey:@"defaultSidecarUrl"]; + if (defaultSpotlightUrl != nil) { + sentryOptions.spotlightUrl = defaultSpotlightUrl; + } + } + } + + if (isSessionReplayEnabled) { + [RNSentryExperimentalOptions setEnableSessionReplayInUnreliableEnvironment:YES + sentryOptions:sentryOptions]; + } + + return sentryOptions; +} + +/** + * This function updates the options with RNSentry defaults. These default can be + * overwritten by users during manual native initialization. + */ ++ (void)updateWithReactDefaults:(SentryOptions *)options +{ + // Failed requests are captured only in JS to avoid duplicates + options.enableCaptureFailedRequests = NO; + + // Tracing is only enabled in JS to avoid duplicate navigation spans + options.tracesSampleRate = nil; + options.tracesSampler = nil; + // Note: enableTracing property is deprecated in Sentry Cocoa SDK v7 + // Tracing is disabled by setting tracesSampleRate and tracesSampler to nil +} + +/** + * This function updates options with changes RNSentry users should not change + * and so this is applied after the configureOptions callback during manual native initialization. + */ ++ (void)updateWithReactFinals:(SentryOptions *)options +{ + SentryBeforeSendEventCallback userBeforeSend = options.beforeSend; + options.beforeSend = ^SentryEvent *(SentryEvent *event) { + // Unhandled JS Exception are processed by the SDK on JS layer + // To avoid duplicates we drop them in the native SDKs + if (nil != event.exceptions.firstObject.type && + [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location + != NSNotFound) { + return nil; + } + + [self setEventOriginTag:event]; + if (userBeforeSend == nil) { + return event; + } else { + return userBeforeSend(event); + } + }; + + // App Start Hybrid mode doesn't wait for didFinishLaunchNotification and the + // didBecomeVisibleNotification as they will be missed when auto initializing from JS + // App Start measurements are created right after the tracking starts + PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = options.enableAutoPerformanceTracing; +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + // Frames Tracking Hybrid Mode ensures tracking + // is enabled without tracing enabled in the native SDK + PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode + = options.enableAutoPerformanceTracing; +#endif +} + ++ (void)setEventOriginTag:(SentryEvent *)event +{ + if (event.sdk != nil) { + NSString *sdkName = event.sdk[@"name"]; + + // If the event is from react native, it gets set + // there and we do not handle it here. + if ([sdkName isEqual:NATIVE_SDK_NAME]) { + [self setEventEnvironmentTag:event origin:@"ios" environment:@"native"]; + } + } +} + ++ (void)setEventEnvironmentTag:(SentryEvent *)event + origin:(NSString *)origin + environment:(NSString *)environment +{ + NSMutableDictionary *newTags = [NSMutableDictionary new]; + + if (nil != event.tags && [event.tags count] > 0) { + [newTags addEntriesFromDictionary:event.tags]; + } + if (nil != origin) { + [newTags setValue:origin forKey:@"event.origin"]; + } + if (nil != environment) { + [newTags setValue:environment forKey:@"event.environment"]; + } + + event.tags = newTags; +} + ++ (NSString *_Nullable)getURLFromDSN:(NSString *)dsn +{ + NSURL *url = [NSURL URLWithString:dsn]; + if (!url) { + return nil; + } + return [NSString stringWithFormat:@"%@://%@", url.scheme, url.host]; +} + +static bool sentHybridSdkDidBecomeActive = NO; + ++ (void)postDidBecomeActiveNotification +{ +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + BOOL appIsActive = + [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive; +#else + BOOL appIsActive = [[NSApplication sharedApplication] isActive]; +#endif + + // If the app is active/in foreground, and we have not sent the SentryHybridSdkDidBecomeActive + // notification, send it. + if (appIsActive && !sentHybridSdkDidBecomeActive + && (PrivateSentrySDKOnly.options.enableAutoSessionTracking + || PrivateSentrySDKOnly.options.enableWatchdogTerminationTracking)) { + // Updates Native App State Manager + // https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentryAppStateManager.m#L136 + // Triggers Session Tracker + // https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentrySessionTracker.m#L144 + [[NSNotificationCenter defaultCenter] postNotificationName:@"SentryHybridSdkDidBecomeActive" + object:nil]; + + sentHybridSdkDidBecomeActive = true; + } +} + +@end diff --git a/packages/core/jest.config.tools.js b/packages/core/jest.config.tools.js index 5c5902d8a7..996ad05625 100644 --- a/packages/core/jest.config.tools.js +++ b/packages/core/jest.config.tools.js @@ -1,7 +1,7 @@ module.exports = { collectCoverage: true, preset: 'ts-jest', - setupFilesAfterEnv: ['/test/mockConsole.ts'], + setupFilesAfterEnv: ['jest-extended/all', '/test/mockConsole.ts'], globals: { __DEV__: true, }, 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 2fdee7f063..6cf2de1739 100644 --- a/packages/core/plugin/src/withSentry.ts +++ b/packages/core/plugin/src/withSentry.ts @@ -1,6 +1,7 @@ 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'; @@ -11,6 +12,7 @@ interface PluginProps { project?: string; authToken?: string; url?: string; + useNativeInit?: boolean; experimental_android?: SentryAndroidGradlePluginOptions; } @@ -25,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}`); } @@ -38,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}`); } @@ -79,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 83c92b464b..a782ea2cc9 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -1,18 +1,26 @@ +import type { ExpoConfig } from '@expo/config-types'; import type { ConfigPlugin } from 'expo/config-plugins'; -import { withAppBuildGradle, withDangerousMod } from 'expo/config-plugins'; +import { withAppBuildGradle, withDangerousMod, withMainApplication } from 'expo/config-plugins'; import * as path from 'path'; -import { warnOnce, writeSentryPropertiesTo } from './utils'; +import { warnOnce } from './logger'; +import { writeSentryPropertiesTo } from './utils'; -export const withSentryAndroid: ConfigPlugin = (config, sentryProperties: string) => { - const cfg = withAppBuildGradle(config, appBuildGradle => { - if (appBuildGradle.modResults.language === 'groovy') { - appBuildGradle.modResults.contents = modifyAppBuildGradle(appBuildGradle.modResults.contents); +export const withSentryAndroid: ConfigPlugin<{ sentryProperties: string; useNativeInit: boolean | undefined }> = ( + config, + { sentryProperties, useNativeInit = false }, +) => { + const appBuildGradleCfg = withAppBuildGradle(config, config => { + if (config.modResults.language === 'groovy') { + config.modResults.contents = modifyAppBuildGradle(config.modResults.contents); } else { throw new Error('Cannot configure Sentry in the app gradle because the build.gradle is not groovy'); } - return appBuildGradle; + return config; }); - return withDangerousMod(cfg, [ + + const mainApplicationCfg = useNativeInit ? modifyMainApplication(appBuildGradleCfg) : appBuildGradleCfg; + + return withDangerousMod(mainApplicationCfg, [ 'android', dangerousMod => { writeSentryPropertiesTo(path.resolve(dangerousMod.modRequest.projectRoot, 'android'), sentryProperties); @@ -48,3 +56,59 @@ export function modifyAppBuildGradle(buildGradle: string): string { return buildGradle.replace(pattern, match => `${applyFrom}\n\n${match}`); } + +export function modifyMainApplication(config: ExpoConfig): ExpoConfig { + return withMainApplication(config, config => { + if (!config.modResults?.path) { + warnOnce("Can't add 'RNSentrySDK.init' to Android MainApplication, because the file was not found."); + return config; + } + + const fileName = path.basename(config.modResults.path); + + if (config.modResults.contents.includes('RNSentrySDK.init')) { + warnOnce(`Your '${fileName}' already contains 'RNSentrySDK.init', the native code won't be updated.`); + return config; + } + + if (config.modResults.language === 'java') { + // Add RNSentrySDK.init + const originalContents = config.modResults.contents; + config.modResults.contents = config.modResults.contents.replace( + /(super\.onCreate\(\)[;\n]*)([ \t]*)/, + '$1\n$2RNSentrySDK.init(this);\n$2', + ); + if (config.modResults.contents === originalContents) { + warnOnce(`Failed to insert 'RNSentrySDK.init' in '${fileName}'.`); + } else if (!config.modResults.contents.includes('import io.sentry.react.RNSentrySDK;')) { + // Insert import statement after package declaration + config.modResults.contents = config.modResults.contents.replace( + /(package .*;\n\n?)/, + '$1import io.sentry.react.RNSentrySDK;\n', + ); + } + } else if (config.modResults.language === 'kt') { + // Add RNSentrySDK.init + const originalContents = config.modResults.contents; + config.modResults.contents = config.modResults.contents.replace( + /(super\.onCreate\(\)[;\n]*)([ \t]*)/, + '$1\n$2RNSentrySDK.init(this)\n$2', + ); + if (config.modResults.contents === originalContents) { + warnOnce(`Failed to insert 'RNSentrySDK.init' in '${fileName}'.`); + } else if (!config.modResults.contents.includes('import io.sentry.react.RNSentrySDK')) { + // Insert import statement after package declaration + config.modResults.contents = config.modResults.contents.replace( + /(package .*\n\n?)/, + '$1import io.sentry.react.RNSentrySDK\n', + ); + } + } else { + warnOnce( + `Unsupported language '${config.modResults.language}' detected in '${fileName}', the native code won't be updated.`, + ); + } + + return config; + }); +} diff --git a/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts b/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts index 0d56ec63ad..2114f100a7 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 type { ExpoConfig } from '@expo/config-types'; -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 e10f820282..83682f5f3e 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -1,8 +1,10 @@ +import type { ExpoConfig } from '@expo/config-types'; /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import type { ConfigPlugin, XcodeProject } from 'expo/config-plugins'; -import { withDangerousMod, withXcodeProject } from 'expo/config-plugins'; +import { withAppDelegate, withDangerousMod, withXcodeProject } from 'expo/config-plugins'; import * as path from 'path'; -import { warnOnce, writeSentryPropertiesTo } from './utils'; +import { warnOnce } from './logger'; +import { writeSentryPropertiesTo } from './utils'; type BuildPhase = { shellScript: string }; @@ -11,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( @@ -35,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); @@ -78,3 +85,59 @@ export function addSentryWithBundledScriptsToBundleShellScript(script: string): (match: string) => `/bin/sh ${SENTRY_REACT_NATIVE_XCODE_PATH} ${match}`, ); } + +export function modifyAppDelegate(config: ExpoConfig): ExpoConfig { + return withAppDelegate(config, async config => { + if (!config.modResults?.path) { + warnOnce("Can't add 'RNSentrySDK.start()' to the iOS AppDelegate, because the file was not found."); + return config; + } + + const fileName = path.basename(config.modResults.path); + + if (config.modResults.language === 'swift') { + if (config.modResults.contents.includes('RNSentrySDK.start()')) { + warnOnce(`Your '${fileName}' already contains 'RNSentrySDK.start()'.`); + return config; + } + // Add RNSentrySDK.start() at the beginning of application method + const originalContents = config.modResults.contents; + config.modResults.contents = config.modResults.contents.replace( + /(func application\([^)]*\) -> Bool \{)\s*\n(\s*)/s, + '$1\n$2RNSentrySDK.start()\n$2', + ); + 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 the first import (works for both UIKit and Expo imports) + config.modResults.contents = config.modResults.contents.replace(/(import \S+\n)/, '$1import RNSentry\n'); + } + } else if (['objcpp', 'objc'].includes(config.modResults.language)) { + if (config.modResults.contents.includes('[RNSentrySDK start]')) { + warnOnce(`Your '${fileName}' already contains '[RNSentrySDK start]'.`); + return config; + } + // Add [RNSentrySDK start] at the beginning of application:didFinishLaunchingWithOptions method + const originalContents = config.modResults.contents; + config.modResults.contents = config.modResults.contents.replace( + /(- \(BOOL\)application:[\s\S]*?didFinishLaunchingWithOptions:[\s\S]*?\{\n)(\s*)/s, + '$1$2[RNSentrySDK start];\n$2', + ); + 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 '${config.modResults.language}' detected in '${fileName}', the native code won't be updated.`, + ); + } + + return config; + }); +} diff --git a/packages/core/scripts/sentry-xcode.sh b/packages/core/scripts/sentry-xcode.sh index 73ba666fc2..6a4bcc9f51 100755 --- a/packages/core/scripts/sentry-xcode.sh +++ b/packages/core/scripts/sentry-xcode.sh @@ -73,4 +73,24 @@ if [ -f "$SENTRY_COLLECT_MODULES" ]; then /bin/sh "$SENTRY_COLLECT_MODULES" fi +# sentry.options.json Block +SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX="Skipping options file copy. To disable this behavior, set SENTRY_COPY_OPTIONS_FILE=false in your environment variables." +SENTRY_OPTIONS_FILE_NAME="sentry.options.json" +SENTRY_OPTIONS_FILE_DESTINATION_PATH="$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/$SENTRY_OPTIONS_FILE_NAME" +[ -z "$SENTRY_OPTIONS_FILE_PATH" ] && SENTRY_OPTIONS_FILE_PATH="$RN_PROJECT_ROOT/$SENTRY_OPTIONS_FILE_NAME" +[ -z "$SENTRY_COPY_OPTIONS_FILE" ] && SENTRY_COPY_OPTIONS_FILE=true + +if [ "$SENTRY_COPY_OPTIONS_FILE" = true ]; then + if [[ -z "$CONFIGURATION_BUILD_DIR" ]]; then + echo "[Sentry] CONFIGURATION_BUILD_DIR is not set. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + elif [[ -z "$UNLOCALIZED_RESOURCES_FOLDER_PATH" ]]; then + echo "[Sentry] UNLOCALIZED_RESOURCES_FOLDER_PATH is not set. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + elif [ ! -f "$SENTRY_OPTIONS_FILE_PATH" ]; then + echo "[Sentry] $SENTRY_OPTIONS_FILE_PATH not found. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + else + cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" + echo "[Sentry] Copied $SENTRY_OPTIONS_FILE_PATH to $SENTRY_OPTIONS_FILE_DESTINATION_PATH" + fi +fi + exit $exitCode diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 345be29640..3ab9225241 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -20,8 +20,48 @@ interface InjectedExecOps { ExecOperations getExecOps() } +project.ext.shouldCopySentryOptionsFile = { -> // If not set, default to true + return System.getenv('SENTRY_COPY_OPTIONS_FILE') != 'false' +} + def config = project.hasProperty("sentryCli") ? project.sentryCli : []; +def configFile = "sentry.options.json" // Sentry configuration file +def androidAssetsDir = new File("$rootDir/app/src/main/assets") // Path to Android assets folder + +tasks.register("copySentryJsonConfiguration") { + onlyIf { shouldCopySentryOptionsFile() } + doLast { + def appRoot = project.rootDir.parentFile ?: project.rootDir + def sentryOptionsFile = new File(appRoot, configFile) + if (sentryOptionsFile.exists()) { + if (!androidAssetsDir.exists()) { + androidAssetsDir.mkdirs() + } + + copy { + from sentryOptionsFile + into androidAssetsDir + rename { String fileName -> configFile } + } + logger.lifecycle("Copied ${configFile} to Android assets") + } else { + logger.warn("${configFile} not found in app root (${appRoot})") + } + } +} + +tasks.register("cleanupTemporarySentryJsonConfiguration") { + onlyIf { shouldCopySentryOptionsFile() } + doLast { + def sentryOptionsFile = new File(androidAssetsDir, configFile) + if (sentryOptionsFile.exists()) { + logger.lifecycle("Deleting temporary file: ${sentryOptionsFile.path}") + sentryOptionsFile.delete() + } + } +} + plugins.withId('com.android.application') { def androidComponents = extensions.getByName("androidComponents") @@ -278,6 +318,17 @@ plugins.withId('com.android.application') { // gradle.projectsEvaluated doesn't work with --configure-on-demand // the task are create too late and not executed project.afterEvaluate { + // Add a task that copies the sentry.options.json file before the build starts + tasks.named("preBuild").configure { + dependsOn("copySentryJsonConfiguration") + } + // Cleanup sentry.options.json from assets after the build + tasks.matching { task -> + task.name == "build" || task.name.startsWith("assemble") || task.name.startsWith("install") + }.configureEach { + finalizedBy("cleanupTemporarySentryJsonConfiguration") + } + if (config.flavorAware && config.sentryProperties) { throw new GradleException("Incompatible sentry configuration. " + "You cannot use both `flavorAware` and `sentryProperties`. " + diff --git a/packages/core/src/js/sdk.tsx b/packages/core/src/js/sdk.tsx index 9b49a8bca6..8df0c66255 100644 --- a/packages/core/src/js/sdk.tsx +++ b/packages/core/src/js/sdk.tsx @@ -27,6 +27,7 @@ import { DEFAULT_BUFFER_SIZE, makeNativeTransportFactory } from './transports/na import { getDefaultEnvironment, isExpoGo, isRunningInMetroDevServer, isWeb } from './utils/environment'; import { getDefaultRelease } from './utils/release'; import { safeFactory, safeTracesSampler } from './utils/safe'; +import { RN_GLOBAL_OBJ } from './utils/worldwide'; import { NATIVE } from './wrapper'; const DEFAULT_OPTIONS: ReactNativeOptions = { @@ -56,12 +57,17 @@ export function init(passedOptions: ReactNativeOptions): void { return; } - const maxQueueSize = passedOptions.maxQueueSize + const userOptions = { + ...RN_GLOBAL_OBJ.__SENTRY_OPTIONS__, + ...passedOptions, + }; + + const maxQueueSize = userOptions.maxQueueSize // eslint-disable-next-line deprecation/deprecation - ?? passedOptions.transportOptions?.bufferSize + ?? userOptions.transportOptions?.bufferSize ?? DEFAULT_OPTIONS.maxQueueSize; - const enableNative = passedOptions.enableNative === undefined || passedOptions.enableNative + const enableNative = userOptions.enableNative === undefined || userOptions.enableNative ? NATIVE.isNativeAvailable() : false; @@ -84,11 +90,11 @@ export function init(passedOptions: ReactNativeOptions): void { return `${dsnComponents.protocol}://${dsnComponents.host}${port}`; }; - const userBeforeBreadcrumb = safeFactory(passedOptions.beforeBreadcrumb, { loggerMessage: 'The beforeBreadcrumb threw an error' }); + const userBeforeBreadcrumb = safeFactory(userOptions.beforeBreadcrumb, { loggerMessage: 'The beforeBreadcrumb threw an error' }); // Exclude Dev Server and Sentry Dsn request from Breadcrumbs const devServerUrl = getDevServer()?.url; - const dsn = getURLFromDSN(passedOptions.dsn); + const dsn = getURLFromDSN(userOptions.dsn); const defaultBeforeBreadcrumb = (breadcrumb: Breadcrumb, _hint?: BreadcrumbHint): Breadcrumb | null => { const type = breadcrumb.type || ''; const url = typeof breadcrumb.data?.url === 'string' ? breadcrumb.data.url : ''; @@ -112,27 +118,35 @@ export function init(passedOptions: ReactNativeOptions): void { const options: ReactNativeClientOptions = { ...DEFAULT_OPTIONS, - ...passedOptions, - release: passedOptions.release ?? getDefaultRelease(), + ...userOptions, + release: userOptions.release ?? getDefaultRelease(), enableNative, - enableNativeNagger: shouldEnableNativeNagger(passedOptions.enableNativeNagger), + enableNativeNagger: shouldEnableNativeNagger(userOptions.enableNativeNagger), // If custom transport factory fails the SDK won't initialize - transport: passedOptions.transport + transport: userOptions.transport || makeNativeTransportFactory({ enableNative, }) || makeFetchTransport, transportOptions: { ...DEFAULT_OPTIONS.transportOptions, - ...(passedOptions.transportOptions ?? {}), + ...(userOptions.transportOptions ?? {}), bufferSize: maxQueueSize, }, maxQueueSize, integrations: [], - stackParser: stackParserFromStackParserOptions(passedOptions.stackParser || defaultStackParser), + stackParser: stackParserFromStackParserOptions(userOptions.stackParser || defaultStackParser), beforeBreadcrumb: chainedBeforeBreadcrumb, - initialScope: safeFactory(passedOptions.initialScope, { loggerMessage: 'The initialScope threw an error' }), + initialScope: safeFactory(userOptions.initialScope, { loggerMessage: 'The initialScope threw an error' }), }; + + if (!('autoInitializeNativeSdk' in userOptions) && RN_GLOBAL_OBJ.__SENTRY_OPTIONS__) { + // We expect users to use the file options only in combination with manual native initialization + // eslint-disable-next-line no-console + console.info('Initializing Sentry JS with the options file. Expecting manual native initialization before JS. Native will not be initialized automatically.'); + options.autoInitializeNativeSdk = false; + } + if ('tracesSampler' in options) { options.tracesSampler = safeTracesSampler(options.tracesSampler); } @@ -141,12 +155,12 @@ export function init(passedOptions: ReactNativeOptions): void { options.environment = getDefaultEnvironment(); } - const defaultIntegrations: false | Integration[] = passedOptions.defaultIntegrations === undefined + const defaultIntegrations: false | Integration[] = userOptions.defaultIntegrations === undefined ? getDefaultIntegrations(options) - : passedOptions.defaultIntegrations; + : userOptions.defaultIntegrations; options.integrations = getIntegrationsToSetup({ - integrations: safeFactory(passedOptions.integrations, { loggerMessage: 'The integrations threw an error' }), + integrations: safeFactory(userOptions.integrations, { loggerMessage: 'The integrations threw an error' }), defaultIntegrations, }); initAndBind(ReactNativeClient, options); @@ -155,6 +169,10 @@ export function init(passedOptions: ReactNativeOptions): void { debug.log('Offline caching, native errors features are not available in Expo Go.'); debug.log('Use EAS Build / Native Release Build to test these features.'); } + + if (RN_GLOBAL_OBJ.__SENTRY_OPTIONS__) { + debug.log('Sentry JS initialized with options from the options file.'); + } } /** diff --git a/packages/core/src/js/tools/metroconfig.ts b/packages/core/src/js/tools/metroconfig.ts index 411af5565b..14dbef5d51 100644 --- a/packages/core/src/js/tools/metroconfig.ts +++ b/packages/core/src/js/tools/metroconfig.ts @@ -10,7 +10,9 @@ import { setSentryDefaultBabelTransformerPathEnv, } from './sentryBabelTransformerUtils'; import { createSentryMetroSerializer, unstableBeforeAssetSerializationDebugIdPlugin } from './sentryMetroSerializer'; +import { withSentryOptionsFromFile } from './sentryOptionsSerializer'; import { unstableReleaseConstantsPlugin } from './sentryReleaseInjector'; +import type { MetroCustomSerializer } from './utils'; import type { DefaultConfigOptions } from './vendor/expo/expoconfig'; export * from './sentryMetroSerializer'; @@ -38,6 +40,14 @@ export interface SentryMetroConfigOptions { * @default true */ enableSourceContextInDevelopment?: boolean; + /** + * Load Sentry Options from a file. If `true` it will use the default path. + * If `false` it will not load any options from a file. Only options provided in the code will be used. + * If `string` it will use the provided path. + * + * @default '{projectRoot}/sentry.options.json' + */ + optionsFile?: string | boolean; } export interface SentryExpoConfigOptions { @@ -66,6 +76,7 @@ export function withSentryConfig( annotateReactComponents = false, includeWebReplay = true, enableSourceContextInDevelopment = true, + optionsFile = true, }: SentryMetroConfigOptions = {}, ): MetroConfig { setSentryMetroDevServerEnvFlag(); @@ -83,6 +94,9 @@ export function withSentryConfig( if (enableSourceContextInDevelopment) { newConfig = withSentryMiddleware(newConfig); } + if (optionsFile) { + newConfig = withSentryOptionsFromFile(newConfig, optionsFile); + } return newConfig; } @@ -119,6 +133,10 @@ export function getSentryExpoConfig( newConfig = withSentryMiddleware(newConfig); } + if (options.optionsFile ?? true) { + newConfig = withSentryOptionsFromFile(newConfig, options.optionsFile ?? true); + } + return newConfig; } @@ -180,8 +198,6 @@ export function withSentryBabelTransformer( }; } -type MetroCustomSerializer = Required['serializer']>['customSerializer'] | undefined; - function withSentryDebugId(config: MetroConfig): MetroConfig { const customSerializer = createSentryMetroSerializer( config.serializer?.customSerializer || undefined, diff --git a/packages/core/src/js/tools/sentryMetroSerializer.ts b/packages/core/src/js/tools/sentryMetroSerializer.ts index 5453e44276..e0598362ad 100644 --- a/packages/core/src/js/tools/sentryMetroSerializer.ts +++ b/packages/core/src/js/tools/sentryMetroSerializer.ts @@ -45,6 +45,7 @@ export function unstableBeforeAssetSerializationDebugIdPlugin({ return prependModule(premodules, debugIdModule); } +// TODO: deprecate this and afterwards rename to createSentryDebugIdSerializer /** * Creates a Metro serializer that adds Debug ID module to the plain bundle. * The Debug ID module is a virtual module that provides a debug ID in runtime. diff --git a/packages/core/src/js/tools/sentryOptionsSerializer.ts b/packages/core/src/js/tools/sentryOptionsSerializer.ts new file mode 100644 index 0000000000..750f92eb7f --- /dev/null +++ b/packages/core/src/js/tools/sentryOptionsSerializer.ts @@ -0,0 +1,108 @@ +import { logger } from '@sentry/core'; +import * as fs from 'fs'; +import type { MetroConfig, Module, ReadOnlyGraph, SerializerOptions } from 'metro'; +import * as path from 'path'; +import type { MetroCustomSerializer, VirtualJSOutput } from './utils'; +import { createSet } from './utils'; +// eslint-disable-next-line import/no-extraneous-dependencies +import countLines from './vendor/metro/countLines'; + +const DEFAULT_OPTIONS_FILE_NAME = 'sentry.options.json'; + +/** + * Loads Sentry options from a file in + */ +export function withSentryOptionsFromFile(config: MetroConfig, optionsFile: string | boolean): MetroConfig { + if (optionsFile === false) { + return config; + } + + const { projectRoot } = config; + if (!projectRoot) { + // eslint-disable-next-line no-console + console.error('[@sentry/react-native/metro] Project root is required to load Sentry options from a file'); + return config; + } + + let optionsPath = path.join(projectRoot, DEFAULT_OPTIONS_FILE_NAME); + if (typeof optionsFile === 'string' && path.isAbsolute(optionsFile)) { + optionsPath = optionsFile; + } else if (typeof optionsFile === 'string') { + optionsPath = path.join(projectRoot, optionsFile); + } + + const originalSerializer = config.serializer?.customSerializer; + if (!originalSerializer) { + // It's okay to bail here because we don't expose this for direct usage, but as part of `withSentryConfig` + // If used directly in RN, the user is responsible for providing a custom serializer first, Expo provides serializer in default config + // eslint-disable-next-line no-console + console.error( + '[@sentry/react-native/metro] `config.serializer.customSerializer` is required to load Sentry options from a file', + ); + return config; + } + + const sentryOptionsSerializer: MetroCustomSerializer = ( + entryPoint: string, + preModules: readonly Module[], + graph: ReadOnlyGraph, + options: SerializerOptions, + ) => { + const sentryOptionsModule = createSentryOptionsModule(optionsPath); + if (sentryOptionsModule) { + (preModules as Module[]).push(sentryOptionsModule); + } + return originalSerializer(entryPoint, preModules, graph, options); + }; + + return { + ...config, + serializer: { + ...config.serializer, + customSerializer: sentryOptionsSerializer, + }, + }; +} + +function createSentryOptionsModule(filePath: string): Module | null { + let content: string; + try { + content = fs.readFileSync(filePath, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + logger.debug(`[@sentry/react-native/metro] Sentry options file does not exist at ${filePath}`); + } else { + logger.error(`[@sentry/react-native/metro] Failed to read Sentry options file at ${filePath}`); + } + return null; + } + + let parsedContent: Record; + try { + parsedContent = JSON.parse(content); + } catch (error) { + logger.error(`[@sentry/react-native/metro] Failed to parse Sentry options file at ${filePath}`); + return null; + } + + const minifiedContent = JSON.stringify(parsedContent); + const optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`; + + logger.debug(`[@sentry/react-native/metro] Sentry options added to the bundle from file at ${filePath}`); + return { + dependencies: new Map(), + getSource: () => Buffer.from(optionsCode), + inverseDependencies: createSet(), + path: '__sentry-options__', + output: [ + { + type: 'js/script/virtual', + data: { + code: optionsCode, + lineCount: countLines(optionsCode), + map: [], + }, + }, + ], + }; +} diff --git a/packages/core/src/js/tools/utils.ts b/packages/core/src/js/tools/utils.ts index 9ff23564d2..e7ed5bc849 100644 --- a/packages/core/src/js/tools/utils.ts +++ b/packages/core/src/js/tools/utils.ts @@ -1,9 +1,11 @@ import * as crypto from 'crypto'; // eslint-disable-next-line import/no-extraneous-dependencies -import type { MixedOutput, Module, ReadOnlyGraph, SerializerOptions } from 'metro'; +import type { MetroConfig, MixedOutput, Module, ReadOnlyGraph, SerializerOptions } from 'metro'; import type CountingSet from 'metro/src/lib/CountingSet'; // types are in src but exports are in private import countLines from './vendor/metro/countLines'; +export type MetroCustomSerializer = Required['serializer']>['customSerializer'] | undefined; + // Variant of MixedOutput // https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/DeltaBundler/types.flow.js#L21 export type VirtualJSOutput = { diff --git a/packages/core/src/js/utils/worldwide.ts b/packages/core/src/js/utils/worldwide.ts index 9e1893d441..e4e612704e 100644 --- a/packages/core/src/js/utils/worldwide.ts +++ b/packages/core/src/js/utils/worldwide.ts @@ -1,6 +1,7 @@ import type { InternalGlobal } from '@sentry/core'; import { GLOBAL_OBJ } from '@sentry/core'; import type { ErrorUtils } from 'react-native/types'; +import type { ReactNativeOptions } from '../options'; import type { ExpoGlobalObject } from './expoglobalobject'; export interface HermesPromiseRejectionTrackingOptions { @@ -34,6 +35,7 @@ export interface ReactNativeInternalGlobal extends InternalGlobal { nativePerformanceNow?: () => number; TextEncoder?: TextEncoder; alert?: (message: string) => void; + __SENTRY_OPTIONS__?: ReactNativeOptions; SENTRY_RELEASE?: { /** Used by Sentry Webpack Plugin, not used by RN, only to silence TS */ id?: 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 new file mode 100644 index 0000000000..d4e0e324da --- /dev/null +++ b/packages/core/test/expo-plugin/modifyAppDelegate.test.ts @@ -0,0 +1,206 @@ +import type { ExpoConfig } from '@expo/config-types'; +import { warnOnce } from '../../plugin/src/logger'; +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/logger', () => ({ + warnOnce: jest.fn(), +})); + +interface MockedExpoConfig extends ExpoConfig { + modResults: { + path: string; + contents: string; + language: 'swift' | 'objc' | 'objcpp' | string; + }; +} + +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 RNSentry +import React_RCTAppDelegate +import ReactAppDependencyProvider +import UIKit + +@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 = createMockConfig(); + }); + + it('should skip modification if modResults or path is missing', async () => { + config.modResults.path = undefined; + + const result = await modifyAppDelegate(config); + + expect(warnOnce).toHaveBeenCalledWith( + "Can't add 'RNSentrySDK.start()' to the iOS AppDelegate, because the file was not found.", + ); + 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 '); + expect(result.modResults.contents).toContain('[RNSentrySDK start];'); + 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 {'; + + 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); + }); +}); + +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 new file mode 100644 index 0000000000..65340f22ab --- /dev/null +++ b/packages/core/test/expo-plugin/modifyMainApplication.test.ts @@ -0,0 +1,191 @@ +import type { ExpoConfig } from '@expo/config-types'; +import { warnOnce } from '../../plugin/src/logger'; +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/logger', () => ({ + warnOnce: jest.fn(), +})); + +interface MockedExpoConfig extends ExpoConfig { + modResults: { + path: string; + contents: string; + language: 'java' | 'kt'; + }; +} + +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; + + beforeEach(() => { + jest.clearAllMocks(); + // Reset to a mocked Java config after each test + config = createMockConfig(); + }); + + it('should skip modification if modResults or path is missing', async () => { + config.modResults.path = undefined; + + const result = await modifyMainApplication(config); + + expect(warnOnce).toHaveBeenCalledWith( + "Can't add 'RNSentrySDK.init' to Android MainApplication, because the file was not found.", + ); + 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', the native code won't be updated.", + ); + 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('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 = 'kt'; + 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('RNSentrySDK.init(this)'); + expect(result.modResults.contents).toBe(kotlinExpected); + }); + + 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); + }); +}); + +function createMockConfig(): MockedExpoConfig { + return { + name: 'test', + slug: 'test', + modResults: { + path: '/android/app/src/main/java/com/example/MainApplication.java', + contents: javaContents, + language: 'java', + }, + }; +} 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 bce3cfa916..47eefec124 100644 --- a/packages/core/test/expo-plugin/withSentryAndroidGradlePlugin.test.ts +++ b/packages/core/test/expo-plugin/withSentryAndroidGradlePlugin.test.ts @@ -1,5 +1,5 @@ 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 { sentryAndroidGradlePluginVersion, @@ -11,7 +11,7 @@ jest.mock('@expo/config-plugins', () => ({ withAppBuildGradle: jest.fn(), })); -jest.mock('../../plugin/src/utils', () => ({ +jest.mock('../../plugin/src/logger', () => ({ warnOnce: jest.fn(), })); diff --git a/packages/core/test/sdk.test.ts b/packages/core/test/sdk.test.ts index 017a0a9780..66cf4a927b 100644 --- a/packages/core/test/sdk.test.ts +++ b/packages/core/test/sdk.test.ts @@ -1,7 +1,8 @@ -import type { BaseTransportOptions, Breadcrumb, BreadcrumbHint, ClientOptions, Integration, Scope } from '@sentry/core'; +import type { Breadcrumb, BreadcrumbHint, Integration, Scope } from '@sentry/core'; import { debug, initAndBind } from '@sentry/core'; import { makeFetchTransport } from '@sentry/react'; import { getDevServer } from '../src/js/integrations/debugsymbolicatorutils'; +import type { ReactNativeClientOptions } from '../src/js/options'; import { init, withScope } from '../src/js/sdk'; import type { ReactNativeTracingIntegration } from '../src/js/tracing'; import { REACT_NATIVE_TRACING_INTEGRATION_NAME, reactNativeTracingIntegration } from '../src/js/tracing'; @@ -109,6 +110,60 @@ describe('Tests the SDK functionality', () => { }); }); + describe('initialization from sentry.options.json', () => { + it('initializes without __SENTRY_OPTIONS__', () => { + delete RN_GLOBAL_OBJ.__SENTRY_OPTIONS__; + init({}); + expect(initAndBind).toHaveBeenCalledOnce(); + }); + + it('adds options from __SENTRY_OPTIONS__', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = { + dsn: 'https://key@example.io/value', + }; + init({}); + expect(usedOptions()?.dsn).toBe('https://key@example.io/value'); + }); + + it('options init override options from __SENTRY_OPTIONS__', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = { + dsn: 'https://key@example.io/file', + }; + init({ + dsn: 'https://key@example.io/code', + }); + expect(usedOptions()?.dsn).toBe('https://key@example.io/code'); + }); + + it('initializing with __SENTRY_OPTIONS__ disabled native auto initialization', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = {}; + init({}); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(false); + }); + + it('initializing without __SENTRY_OPTIONS__ enables native auto initialization', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = undefined; + init({}); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(true); + }); + + it('initializing with __SENTRY_OPTIONS__ keeps native auto initialization true if set', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = {}; + init({ + autoInitializeNativeSdk: true, + }); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(true); + }); + + it('initializing with __SENTRY_OPTIONS__ keeps native auto initialization false if set', () => { + RN_GLOBAL_OBJ.__SENTRY_OPTIONS__ = {}; + init({ + autoInitializeNativeSdk: false, + }); + expect(usedOptions()?.autoInitializeNativeSdk).toBe(false); + }); + }); + describe('environment', () => { it('detect development environment', () => { (getDefaultEnvironment as jest.Mock).mockImplementation(() => 'development'); @@ -1207,7 +1262,7 @@ function createMockedIntegration({ name }: { name?: string } = {}): Integration }; } -function usedOptions(): ClientOptions | undefined { +function usedOptions(): ReactNativeClientOptions | undefined { return (initAndBind as jest.MockedFunction).mock.calls[0]?.[secondArg]; } diff --git a/packages/core/test/tools/sentryOptionsSerializer.test.ts b/packages/core/test/tools/sentryOptionsSerializer.test.ts new file mode 100644 index 0000000000..2e855fffd3 --- /dev/null +++ b/packages/core/test/tools/sentryOptionsSerializer.test.ts @@ -0,0 +1,208 @@ +import { logger } from '@sentry/core'; +import * as fs from 'fs'; +import type { Graph, Module, SerializerOptions } from 'metro'; +import { withSentryOptionsFromFile } from '../../src/js/tools/sentryOptionsSerializer'; +import { createSet } from '../../src/js/tools/utils'; + +jest.mock('fs', () => ({ + readFileSync: jest.fn(), +})); + +const consoleErrorSpy = jest.spyOn(console, 'error'); +const loggerDebugSpy = jest.spyOn(logger, 'debug'); +const loggerErrorSpy = jest.spyOn(logger, 'error'); + +const customSerializerMock = jest.fn(); +let mockedPreModules: Module[] = []; + +describe('Sentry Options Serializer', () => { + beforeEach(() => { + jest.resetAllMocks(); + mockedPreModules = createMockedPreModules(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test('returns original config if optionsFile is false', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + const result = withSentryOptionsFromFile(config(), false); + expect(result).toEqual(config()); + }); + + test('logs error and returns original config if projectRoot is missing', () => { + const config = () => ({ + serializer: { + customSerializer: customSerializerMock, + }, + }); + + const result = withSentryOptionsFromFile(config(), true); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Project root is required')); + expect(result).toEqual(config()); + }); + + test('logs error and returns original config if customSerializer is missing', () => { + const config = () => ({ + projectRoot: '/test', + serializer: {}, + }); + const consoleErrorSpy = jest.spyOn(console, 'error'); + + const result = withSentryOptionsFromFile(config(), true); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('`config.serializer.customSerializer` is required'), + ); + expect(result).toEqual(config()); + }); + + test('adds sentry options module when file exists and is valid JSON', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + const mockOptions = { test: 'value' }; + (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockOptions)); + + const actualConfig = withSentryOptionsFromFile(config(), true); + actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null); + + expect(mockedPreModules).toHaveLength(2); + expect(mockedPreModules.at(-1)).toEqual( + expect.objectContaining({ + getSource: expect.any(Function), + path: '__sentry-options__', + output: [ + { + type: 'js/script/virtual', + data: { + code: 'var __SENTRY_OPTIONS__={"test":"value"};', + lineCount: 1, + map: [], + }, + }, + ], + }), + ); + expect(mockedPreModules.at(-1).getSource().toString()).toEqual(mockedPreModules.at(-1).output[0].data.code); + expect(loggerDebugSpy).toHaveBeenCalledWith(expect.stringContaining('options added to the bundle')); + }); + + test('logs error and does not add module when file does not exist', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + (fs.readFileSync as jest.Mock).mockImplementation(() => { + throw { code: 'ENOENT' }; + }); + + const actualConfig = withSentryOptionsFromFile(config(), true); + actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null); + + expect(loggerDebugSpy).toHaveBeenCalledWith(expect.stringContaining('options file does not exist')); + expect(mockedPreModules).toMatchObject(createMockedPreModules()); + }); + + test('logs error and does not add module when file contains invalid JSON', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + (fs.readFileSync as jest.Mock).mockReturnValue('invalid json'); + + const actualConfig = withSentryOptionsFromFile(config(), true); + actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null); + + expect(loggerErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to parse Sentry options file')); + expect(mockedPreModules).toMatchObject(createMockedPreModules()); + }); + + test('calls original serializer with correct arguments and returns its result', () => { + const mockedEntryPoint = 'entryPoint'; + const mockedGraph: Graph = jest.fn() as unknown as Graph; + const mockedOptions: SerializerOptions = jest.fn() as unknown as SerializerOptions; + const mockedResult = {}; + const originalSerializer = jest.fn().mockReturnValue(mockedResult); + + const actualConfig = withSentryOptionsFromFile( + { + projectRoot: '/test', + serializer: { + customSerializer: originalSerializer, + }, + }, + true, + ); + const actualResult = actualConfig.serializer?.customSerializer( + mockedEntryPoint, + mockedPreModules, + mockedGraph, + mockedOptions, + ); + + expect(originalSerializer).toHaveBeenCalledWith(mockedEntryPoint, mockedPreModules, mockedGraph, mockedOptions); + expect(actualResult).toEqual(mockedResult); + }); + + test('uses custom file path when optionsFile is a string', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + withSentryOptionsFromFile(config(), 'custom/path.json').serializer?.customSerializer( + null, + mockedPreModules, + null, + null, + ); + withSentryOptionsFromFile(config(), '/absolute/path.json').serializer?.customSerializer( + null, + mockedPreModules, + null, + null, + ); + + expect(fs.readFileSync).toHaveBeenCalledWith('/test/custom/path.json', expect.anything()); + expect(fs.readFileSync).toHaveBeenCalledWith('/absolute/path.json', expect.anything()); + }); +}); + +function createMockedPreModules(): Module[] { + return [createMinimalModule()]; +} + +function createMinimalModule(): Module { + return { + dependencies: new Map(), + getSource: getEmptySource, + inverseDependencies: createSet(), + path: '__sentry-options__', + output: [], + }; +} + +function getEmptySource(): Buffer { + return Buffer.from(''); +} diff --git a/samples/expo/app.json b/samples/expo/app.json index c3eb7ef323..196f689b12 100644 --- a/samples/expo/app.json +++ b/samples/expo/app.json @@ -46,6 +46,7 @@ "url": "https://sentry.io/", "project": "sentry-react-native", "organization": "sentry-sdks", + "useNativeInit": true, "experimental_android": { "enableAndroidGradlePlugin": true, "autoUploadProguardMapping": true, diff --git a/samples/expo/sentry.options.json b/samples/expo/sentry.options.json new file mode 100644 index 0000000000..53ae525bc0 --- /dev/null +++ b/samples/expo/sentry.options.json @@ -0,0 +1,18 @@ +{ + "dsn": "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", + "debug": true, + "environment": "dev", + "enableUserInteractionTracing": true, + "enableAutoSessionTracking": true, + "sessionTrackingIntervalMillis": 30000, + "enableTracing": true, + "tracesSampleRate": 1.0, + "attachStacktrace": true, + "attachScreenshot": true, + "attachViewHierarchy": true, + "enableCaptureFailedRequests": true, + "profilesSampleRate": 1.0, + "replaysSessionSampleRate": 1.0, + "replaysOnErrorSampleRate": 1.0, + "spotlight": true +} diff --git a/samples/react-native-macos/scripts/pod-install.sh b/samples/react-native-macos/scripts/pod-install.sh new file mode 100755 index 0000000000..a923f8c32a --- /dev/null +++ b/samples/react-native-macos/scripts/pod-install.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +echo "USE_FRAMEWORKS=$USE_FRAMEWORKS" +echo "ENABLE_PROD=$ENABLE_PROD" +echo "ENABLE_NEW_ARCH=$ENABLE_NEW_ARCH" + +cd "${thisFilePath}/.." +bundle install + +cd macos +PRODUCTION=$ENABLE_PROD RCT_NEW_ARCH_ENABLED=$ENABLE_NEW_ARCH bundle exec pod update + +cat Podfile.lock | grep $RN_SENTRY_POD_NAME diff --git a/samples/react-native/.detoxrc.js b/samples/react-native/.detoxrc.js new file mode 100644 index 0000000000..86e992abae --- /dev/null +++ b/samples/react-native/.detoxrc.js @@ -0,0 +1,74 @@ +const process = require('process'); + +/** @type {Detox.DetoxConfig} */ +module.exports = { + testRunner: {}, + apps: { + 'ci.android': { + type: 'android.apk', + binaryPath: 'app.apk', + testBinaryPath: 'app-androidTest.apk', + }, + 'ci.ios': { + type: 'ios.app', + binaryPath: 'sentryreactnativesample.app', + }, + }, + devices: { + 'ci.emulator': { + type: process.env.ANDROID_TYPE?.trim(), + device: { + avdName: process.env.ANDROID_AVD_NAME?.trim(), + adbName: process.env.ANDROID_ADB_NAME?.trim(), + }, + }, + 'ci.simulator': { + type: 'ios.simulator', + device: { + type: process.env.IOS_DEVICE?.trim(), + os: process.env.IOS_VERSION?.trim(), + }, + }, + }, + configurations: { + 'ci.android': { + device: 'ci.emulator', + app: 'ci.android', + testRunner: { + args: { + $0: 'jest', + config: 'e2e-detox/jest.config.android.js', + }, + jest: { + setupTimeout: 120000, + }, + }, + }, + 'ci.sim.auto': { + device: 'ci.simulator', + app: 'ci.ios', + testRunner: { + args: { + $0: 'jest', + config: 'e2e-detox/jest.config.ios.auto.js', + }, + jest: { + setupTimeout: 120000, + }, + }, + }, + 'ci.sim.manual': { + device: 'ci.simulator', + app: 'ci.ios', + testRunner: { + args: { + $0: 'jest', + config: 'e2e-detox/jest.config.ios.manual.js', + }, + jest: { + setupTimeout: 120000, + }, + }, + }, + }, +}; diff --git a/samples/react-native/.gitignore b/samples/react-native/.gitignore index f2dfeb669b..692281f514 100644 --- a/samples/react-native/.gitignore +++ b/samples/react-native/.gitignore @@ -64,3 +64,5 @@ yarn-error.log .metro-health-check* *.xcarchive +*.apk +**/*.app diff --git a/samples/react-native/android/app/build.gradle b/samples/react-native/android/app/build.gradle index 430af53f69..3082436e65 100644 --- a/samples/react-native/android/app/build.gradle +++ b/samples/react-native/android/app/build.gradle @@ -132,6 +132,10 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion versionCode 71 versionName "7.11.0" + buildConfigField "boolean", "SENTRY_DISABLE_NATIVE_START", System.getenv('SENTRY_DISABLE_NATIVE_START') ?: String.valueOf(sentryDisableNativeStart) + + testBuildType System.getProperty('testBuildType', 'debug') + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } signingConfigs { @@ -186,11 +190,15 @@ android { signingConfig signingConfigs.debug minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro" } } } dependencies { + androidTestImplementation('com.wix:detox:+') + implementation 'androidx.appcompat:appcompat:1.7.0' + // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") diff --git a/samples/react-native/android/app/proguard-rules.pro b/samples/react-native/android/app/proguard-rules.pro index 11b025724a..f4ada6b5a1 100644 --- a/samples/react-native/android/app/proguard-rules.pro +++ b/samples/react-native/android/app/proguard-rules.pro @@ -8,3 +8,7 @@ # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: + +# Detox Release tests were failing on missing kotlin.Result +# It should be covered by node_modules/detox/android/detox/proguard-rules-app.pro but it seems missing +-keep class kotlin.** { *; } diff --git a/samples/react-native/android/app/src/androidTest/java/io/sentry/reactnative/sample/DetoxTest.java b/samples/react-native/android/app/src/androidTest/java/io/sentry/reactnative/sample/DetoxTest.java new file mode 100644 index 0000000000..28b9b28d1c --- /dev/null +++ b/samples/react-native/android/app/src/androidTest/java/io/sentry/reactnative/sample/DetoxTest.java @@ -0,0 +1,28 @@ +package io.sentry.reactnative.sample; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; +import com.wix.detox.Detox; +import com.wix.detox.config.DetoxConfig; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class DetoxTest { + @Rule + public ActivityTestRule mActivityRule = + new ActivityTestRule<>(MainActivity.class, false, false); + + @Test + public void runDetoxTests() { + DetoxConfig detoxConfig = new DetoxConfig(); + detoxConfig.idlePolicyConfig.masterTimeoutSec = 90; + detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60; + detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60); + + Detox.runTests(mActivityRule, detoxConfig); + } +} diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index 07747f085c..424572aa38 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -11,10 +11,7 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader -import io.sentry.Hint -import io.sentry.SentryEvent -import io.sentry.SentryOptions.BeforeSendCallback -import io.sentry.android.core.SentryAndroid +import io.sentry.react.RNSentrySDK class MainApplication : Application(), @@ -40,9 +37,15 @@ class MainApplication : override fun onCreate() { super.onCreate() - // When the native init is enabled the `autoInitializeNativeSdk` - // in JS has to be set to `false` - // this.initializeSentry() + if (!BuildConfig.SENTRY_DISABLE_NATIVE_START) { + RNSentrySDK.init(this) + } + + // Check for crash-on-start intent for testing + if (shouldCrashOnStart()) { + throw RuntimeException("This was intentional test crash before JS started.") + } + 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. @@ -50,29 +53,15 @@ class MainApplication : } } - private fun initializeSentry() { - SentryAndroid.init(this) { options -> - // Only options set here will apply to the Android SDK - // Options from JS are not passed to the Android SDK when initialized manually - options.dsn = "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561" - options.isDebug = true - - options.beforeSend = - BeforeSendCallback { event: SentryEvent, hint: Hint? -> - // React native internally throws a JavascriptException - // Since we catch it before that, we don't want to send this one - // because we would send it twice - try { - val ex = event.exceptions!![0] - if (null != ex && ex.type!!.contains("JavascriptException")) { - return@BeforeSendCallback null - } - } catch (ignored: Throwable) { - // We do nothing - } - - event - } + private fun shouldCrashOnStart(): Boolean { + // Check if crash flag file exists (for E2E testing) + val crashFile = getFileStreamPath(".sentry_crash_on_start") + if (crashFile.exists()) { + // Delete the flag immediately so we only crash once + // This allows the next launch to succeed and send the crash report + crashFile.delete() + return true } + return false } } diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/SamplePackage.java b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/SamplePackage.java index 8dfbd44c8b..434bc8927a 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/SamplePackage.java +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/SamplePackage.java @@ -77,6 +77,38 @@ private void crashNow() { } }); + modules.add( + new ReactContextBaseJavaModule(reactContext) { + @Override + public String getName() { + return "TestControlModule"; + } + + @ReactMethod + public void enableCrashOnStart(Promise promise) { + try { + // Create flag file to trigger crash on next app start + getReactApplicationContext() + .openFileOutput(".sentry_crash_on_start", ReactApplicationContext.MODE_PRIVATE) + .close(); + promise.resolve(true); + } catch (Exception e) { + promise.reject("ERROR", "Failed to enable crash on start", e); + } + } + + @ReactMethod + public void disableCrashOnStart(Promise promise) { + try { + // Delete flag file + getReactApplicationContext().deleteFile(".sentry_crash_on_start"); + promise.resolve(true); + } catch (Exception e) { + promise.reject("ERROR", "Failed to disable crash on start", e); + } + } + }); + return modules; } } diff --git a/samples/react-native/android/build.gradle b/samples/react-native/android/build.gradle index 7f6c897368..02e00245a4 100644 --- a/samples/react-native/android/build.gradle +++ b/samples/react-native/android/build.gradle @@ -20,4 +20,12 @@ buildscript { } } +allprojects { + repositories { + maven { + url("$rootDir/../node_modules/detox/Detox-android") + } + } +} + apply plugin: "com.facebook.react.rootproject" diff --git a/samples/react-native/android/gradle.properties b/samples/react-native/android/gradle.properties index 600fea4b77..d71a974450 100644 --- a/samples/react-native/android/gradle.properties +++ b/samples/react-native/android/gradle.properties @@ -38,3 +38,8 @@ newArchEnabled=true # Use this property to enable or disable the Hermes JS engine. # If set to false, you will be using JSC instead. hermesEnabled=true + +# Only implemented in this sample project. +# It's used for testing the native SDK auto-start feature. +# true means manual native start is disabled and JS auto initializes native SDK. +sentryDisableNativeStart=false diff --git a/samples/react-native/e2e/captureErrorsScreenTransaction.test.yml b/samples/react-native/e2e/captureErrorsScreenTransaction.test.yml deleted file mode 100644 index 9f59b1155a..0000000000 --- a/samples/react-native/e2e/captureErrorsScreenTransaction.test.yml +++ /dev/null @@ -1,13 +0,0 @@ -appId: io.sentry.reactnative.sample ---- -- launchApp: - # We expect cold start - clearState: true - stopApp: true - arguments: - isE2ETest: true - -# For unknown reasons tapOn: "Performance" does not work on iOS -- tapOn: - id: "performance-tab-icon" -- tapOn: "Auto Tracing Example" diff --git a/samples/react-native/e2e/jest.config.android.auto.js b/samples/react-native/e2e/jest.config.android.auto.js new file mode 100644 index 0000000000..db4de3a8fd --- /dev/null +++ b/samples/react-native/e2e/jest.config.android.auto.js @@ -0,0 +1,13 @@ +const path = require('path'); +const baseConfig = require('./jest.config.base'); + +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + ...baseConfig, + globalSetup: path.resolve(__dirname, 'setup.android.auto.ts'), + testMatch: [ + ...baseConfig.testMatch, + '/e2e/**/*.test.android.ts', + '/e2e/**/*.test.android.auto.ts', + ], +}; diff --git a/samples/react-native/e2e/jest.config.android.js b/samples/react-native/e2e/jest.config.android.manual.js similarity index 51% rename from samples/react-native/e2e/jest.config.android.js rename to samples/react-native/e2e/jest.config.android.manual.js index d84363325d..89d80d3fc7 100644 --- a/samples/react-native/e2e/jest.config.android.js +++ b/samples/react-native/e2e/jest.config.android.manual.js @@ -1,8 +1,11 @@ -const path = require('path'); const baseConfig = require('./jest.config.base'); /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { ...baseConfig, - globalSetup: path.resolve(__dirname, 'setup.android.ts'), + testMatch: [ + ...baseConfig.testMatch, + '/e2e/**/*.test.android.ts', + '/e2e/**/*.test.android.manual.ts', + ], }; diff --git a/samples/react-native/e2e/jest.config.ios.auto.js b/samples/react-native/e2e/jest.config.ios.auto.js new file mode 100644 index 0000000000..8d04d86887 --- /dev/null +++ b/samples/react-native/e2e/jest.config.ios.auto.js @@ -0,0 +1,13 @@ +const path = require('path'); +const baseConfig = require('./jest.config.base'); + +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + ...baseConfig, + globalSetup: path.resolve(__dirname, 'setup.ios.auto.ts'), + testMatch: [ + ...baseConfig.testMatch, + '/e2e/**/*.test.ios.ts', + '/e2e/**/*.test.ios.auto.ts', + ], +}; diff --git a/samples/react-native/e2e/jest.config.ios.js b/samples/react-native/e2e/jest.config.ios.manual.js similarity index 66% rename from samples/react-native/e2e/jest.config.ios.js rename to samples/react-native/e2e/jest.config.ios.manual.js index 5060c26e3f..df5a5b8e95 100644 --- a/samples/react-native/e2e/jest.config.ios.js +++ b/samples/react-native/e2e/jest.config.ios.manual.js @@ -1,9 +1,14 @@ -const path = require('path'); const baseConfig = require('./jest.config.base'); +const path = require('path'); /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { ...baseConfig, + testMatch: [ + ...baseConfig.testMatch, + '/e2e/**/*.test.ios.ts', + '/e2e/**/*.test.ios.manual.ts', + ], globalSetup: path.resolve(__dirname, 'setup.ios.ts'), testTimeout: 300000, }; diff --git a/samples/react-native/e2e/setup.android.auto.ts b/samples/react-native/e2e/setup.android.auto.ts new file mode 100644 index 0000000000..4a840f16a0 --- /dev/null +++ b/samples/react-native/e2e/setup.android.auto.ts @@ -0,0 +1,7 @@ +import { setAutoInitTest } from './utils/environment'; + +function setupAuto() { + setAutoInitTest(); +} + +export default setupAuto; diff --git a/samples/react-native/e2e/setup.android.ts b/samples/react-native/e2e/setup.android.ts deleted file mode 100644 index 91c0dfec95..0000000000 --- a/samples/react-native/e2e/setup.android.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { setAndroid } from './utils/environment'; - -function setupAndroid() { - setAndroid(); -} - -export default setupAndroid; diff --git a/samples/react-native/e2e/setup.ios.auto.ts b/samples/react-native/e2e/setup.ios.auto.ts new file mode 100644 index 0000000000..4a840f16a0 --- /dev/null +++ b/samples/react-native/e2e/setup.ios.auto.ts @@ -0,0 +1,7 @@ +import { setAutoInitTest } from './utils/environment'; + +function setupAuto() { + setAutoInitTest(); +} + +export default setupAuto; diff --git a/samples/react-native/e2e/setup.ios.ts b/samples/react-native/e2e/setup.ios.ts deleted file mode 100644 index b3f6a69385..0000000000 --- a/samples/react-native/e2e/setup.ios.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { setIOS } from './utils/environment'; - -function setupIOS() { - setIOS(); -} - -export default setupIOS; diff --git a/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.android.manual.ts b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.android.manual.ts new file mode 100644 index 0000000000..6faae67695 --- /dev/null +++ b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.android.manual.ts @@ -0,0 +1,118 @@ +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; +import { Envelope, EventItem } from '@sentry/core'; + +import { + createSentryServer, + containingEvent, +} from '../../utils/mockedSentryServer'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; + +describe('Capture app start crash (Android)', () => { + let sentryServer = createSentryServer(); + + let envelope: Envelope; + + beforeAll(async () => { + await sentryServer.start(); + + const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + + await maestro('tests/captureAppStartCrash/captureAppStartCrash.test.android.manual.yml'); + + envelope = await envelopePromise; + }, 300000); // 5 minutes timeout for crash handling + + afterAll(async () => { + await sentryServer.close(); + }); + + it('envelope contains sdk metadata', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item).toEqual([ + { + content_type: 'application/json', + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + platform: 'java', + sdk: expect.objectContaining({ + name: 'sentry.java.android.react-native', + packages: expect.arrayContaining([ + expect.objectContaining({ + name: 'maven:io.sentry:sentry-android-core', + }), + expect.objectContaining({ + name: 'npm:@sentry/react-native', + }), + ]), + }), + }), + ]); + }); + + it('captures app start crash exception', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + // Android wraps onCreate exceptions, so check that at least one exception + // contains our intentional crash message + const exceptions = item?.[1]?.exception?.values; + expect(exceptions).toBeDefined(); + + const hasIntentionalCrash = exceptions?.some( + (ex: any) => + ex.type === 'RuntimeException' && + ex.value?.includes('This was intentional test crash before JS started.') + ); + + expect(hasIntentionalCrash).toBe(true); + + // Verify at least one exception has UncaughtExceptionHandler mechanism + const hasUncaughtHandler = exceptions?.some( + (ex: any) => ex.mechanism?.type === 'UncaughtExceptionHandler' + ); + + expect(hasUncaughtHandler).toBe(true); + }); + + it('crash happened before JS was loaded', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + // Verify this is a native crash, not from JavaScript + expect(item?.[1]).toEqual( + expect.objectContaining({ + platform: 'java', + }), + ); + + // Should not have JavaScript context since JS wasn't loaded yet + expect(item?.[1]?.contexts?.react_native_context).toBeUndefined(); + }); + + it('contains device and app context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + device: expect.objectContaining({ + brand: expect.any(String), + manufacturer: expect.any(String), + model: expect.any(String), + }), + app: expect.objectContaining({ + app_identifier: 'io.sentry.reactnative.sample', + app_name: expect.any(String), + app_version: expect.any(String), + }), + os: expect.objectContaining({ + name: 'Android', + version: expect.any(String), + }), + }), + }), + ); + }); +}); diff --git a/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.android.manual.yml b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.android.manual.yml new file mode 100644 index 0000000000..a58cfbb457 --- /dev/null +++ b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.android.manual.yml @@ -0,0 +1,30 @@ +appId: io.sentry.reactnative.sample +--- +# First launch: Enable crash flag and exit gracefully +- launchApp: + clearState: true + stopApp: true + +# App launches on ErrorsTab by default, wait for screen to load +- waitForAnimationToEnd: + timeout: 2000 + +# Scroll down to find the "Enable Crash on Start" button (Android only) +- scrollUntilVisible: + element: "Enable Crash on Start" + timeout: 10000 + direction: DOWN + +- tapOn: "Enable Crash on Start" +- stopApp + +# Second launch: App crashes on start +# The crash flag auto-deletes when read, so app only crashes once +- launchApp: + clearState: false + stopApp: false + +# Third launch: App starts normally and sends the crash event +- launchApp: + clearState: false + stopApp: false diff --git a/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.ts b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.ts new file mode 100644 index 0000000000..c00efb7ad2 --- /dev/null +++ b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.ts @@ -0,0 +1,117 @@ +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; +import { Envelope, EventItem } from '@sentry/core'; + +import { + createSentryServer, + containingEvent, +} from '../../utils/mockedSentryServer'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; + +describe('Capture app start crash', () => { + let sentryServer = createSentryServer(); + + let envelope: Envelope; + + beforeAll(async () => { + await sentryServer.start(); + + const envelopePromise = sentryServer.waitForEnvelope(containingEvent); + + await maestro('tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.yml'); + + envelope = await envelopePromise; + }); + + afterAll(async () => { + await sentryServer.close(); + }); + + it('envelope contains sdk metadata', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item).toEqual([ + { + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + platform: 'cocoa', + sdk: { + features: ['experimentalViewRenderer', 'dataSwizzling'], + integrations: [ + 'SessionReplay', + // FIXME: Why are these not included? + // 'WatchdogTerminationTracking', + // 'Screenshot', + // 'Crash', + // 'ANRTracking', + // 'ViewHierarchy', + // 'AutoBreadcrumbTracking', + // 'AutoSessionTracking', + // 'NetworkTracking', + // 'AppStartTracking', + // 'FramesTracking', + ], + name: 'sentry.cocoa.react-native', + packages: [ + { + name: 'cocoapods:getsentry/sentry.cocoa.react-native', + version: expect.any(String), + }, + { + name: 'npm:@sentry/react-native', + version: expect.any(String), + }, + ], + version: expect.any(String), + }, + tags: { + 'event.environment': 'native', + 'event.origin': 'ios', + }, + }), + ]); + }); + + it('envelope contains the expected exception', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item).toEqual([ + { + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + exception: { + values: expect.arrayContaining([ + expect.objectContaining({ + mechanism: expect.objectContaining({ + handled: false, + meta: { + mach_exception: { + code: 0, + exception: 10, + name: 'EXC_CRASH', + subcode: 0, + }, + signal: { + code: 0, + name: 'SIGABRT', + number: 6, + }, + }, + type: 'nsexception', + }), + stacktrace: expect.objectContaining({ + frames: expect.any(Array), + }), + type: 'CrashOnStart', + value: 'This was intentional test crash before JS started.', + }), + ]), + }, + }), + ]); + }); +}); diff --git a/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.yml b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.yml new file mode 100644 index 0000000000..ffba05663d --- /dev/null +++ b/samples/react-native/e2e/tests/captureAppStartCrash/captureAppStartCrash.test.ios.manual.yml @@ -0,0 +1,16 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + clearState: true + stopApp: true + arguments: + sentryCrashOnStart: true + isE2ETest: true + +# This launch sends the crash event before the app crashes again +- launchApp: + clearState: false + stopApp: false + arguments: + sentryCrashOnStart: true + isE2ETest: true diff --git a/samples/react-native/e2e/captureErrorsScreenTransaction.test.ts b/samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.ts similarity index 90% rename from samples/react-native/e2e/captureErrorsScreenTransaction.test.ts rename to samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.ts index 3c91e49322..653c9ceef8 100644 --- a/samples/react-native/e2e/captureErrorsScreenTransaction.test.ts +++ b/samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.ts @@ -3,28 +3,28 @@ import { EventItem } from '@sentry/core'; import { createSentryServer, containingTransactionWithName, -} from './utils/mockedSentryServer'; +} from '../../utils/mockedSentryServer'; -import { getItemOfTypeFrom } from './utils/event'; -import { maestro } from './utils/maestro'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; describe('Capture Errors Screen Transaction', () => { let sentryServer = createSentryServer(); const getErrorsEnvelope = () => - sentryServer.getEnvelope(containingTransactionWithName('Errors')); + sentryServer.getEnvelope(containingTransactionWithName('ErrorsScreen')); beforeAll(async () => { await sentryServer.start(); const waitForErrorsTx = sentryServer.waitForEnvelope( - containingTransactionWithName('Errors'), // The last created and sent transaction + containingTransactionWithName('ErrorsScreen'), // The last created and sent transaction ); - await maestro('captureErrorsScreenTransaction.test.yml'); + await maestro('tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.yml'); await waitForErrorsTx; - }); + }, 240000); // 240 seconds timeout for iOS event delivery afterAll(async () => { await sentryServer.close(); diff --git a/samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.yml b/samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.yml new file mode 100644 index 0000000000..1bfb4b50c7 --- /dev/null +++ b/samples/react-native/e2e/tests/captureErrorScreenTransaction/captureErrorsScreenTransaction.test.yml @@ -0,0 +1,17 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + # We expect cold start + clearState: true + stopApp: true + arguments: + isE2ETest: true + +# The app should launch on the ErrorsScreen (first tab) +# Wait for the screen to be ready and for the app start transaction to complete +- assertVisible: "Capture message" + +# Perform a scroll to ensure some interaction happens, then wait +# This gives the transaction time to finish (idleTimeoutMs: 5000) +- scroll +- scroll diff --git a/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.android.ts b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.android.ts new file mode 100644 index 0000000000..5c246dee71 --- /dev/null +++ b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.android.ts @@ -0,0 +1,65 @@ +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; +import { Envelope } from '@sentry/core'; + +import { + createSentryServer, + containingEventWithAndroidMessage, +} from '../../utils/mockedSentryServer'; +import { HEADER } from '../../utils/consts'; +import { maestro } from '../../utils/maestro'; + +describe('Capture message', () => { + let sentryServer = createSentryServer(); + + let envelope: Envelope; + + beforeAll(async () => { + await sentryServer.start(); + + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithAndroidMessage('Captured message'), + ); + + await maestro('tests/captureHeader/envelopeHeader.test.yml'); + + envelope = await envelopePromise; + }); + + afterAll(async () => { + await sentryServer.close(); + }); + + it('contains event_id and sent_at in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + event_id: expect.any(String), + sent_at: expect.any(String), + }), + ); + }); + + it('contains sdk info in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + sdk: expect.objectContaining({ + name: 'sentry.javascript.react-native', + version: expect.any(String), + }), + sent_at: expect.any(String), + }), + ); + }); + + it('contains trace info in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + trace: expect.objectContaining({ + environment: expect.any(String), + public_key: expect.any(String), + replay_id: expect.any(String), + trace_id: expect.any(String), + }), + }), + ); + }); +}); diff --git a/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.ios.ts b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.ios.ts new file mode 100644 index 0000000000..0f0790c47a --- /dev/null +++ b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.ios.ts @@ -0,0 +1,71 @@ +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; +import { Envelope } from '@sentry/core'; + +import { + createSentryServer, + containingEventWithMessage, +} from '../../utils/mockedSentryServer'; +import { HEADER } from '../../utils/consts'; +import { maestro } from '../../utils/maestro'; + +describe('Capture message', () => { + let sentryServer = createSentryServer(); + + let envelope: Envelope; + + beforeAll(async () => { + await sentryServer.start(); + + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithMessage('Captured message'), + ); + + await maestro('tests/captureHeader/envelopeHeader.test.yml'); + + envelope = await envelopePromise; + }, 240000); // 240 seconds timeout for iOS event delivery + + afterAll(async () => { + await sentryServer.close(); + }); + + it('contains event_id and sent_at in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + event_id: expect.any(String), + sent_at: expect.any(String), + }), + ); + }); + + it('contains sdk info in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + sdk: expect.objectContaining({ + features: [], + integrations: [], + name: 'sentry.javascript.react-native', + packages: [], + version: expect.any(String), + }), + sent_at: expect.any(String), + }), + ); + }); + + it('contains trace info in the envelope header', async () => { + expect(envelope[HEADER]).toEqual( + expect.objectContaining({ + trace: expect.objectContaining({ + environment: expect.any(String), + public_key: expect.any(String), + replay_id: expect.any(String), + sample_rate: '1', + sampled: '1', + trace_id: expect.any(String), + transaction: 'ErrorsScreen', + }), + }), + ); + }); +}); diff --git a/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.yml b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.yml new file mode 100644 index 0000000000..ddb62a923c --- /dev/null +++ b/samples/react-native/e2e/tests/captureHeader/envelopeHeader.test.yml @@ -0,0 +1,9 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + clearState: true + stopApp: true + arguments: + isE2ETest: true + +- tapOn: "Capture message" diff --git a/samples/react-native/e2e/tests/captureMessage/captureMessage.test.android.auto.ts b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.android.auto.ts new file mode 100644 index 0000000000..2c7bfa0809 --- /dev/null +++ b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.android.auto.ts @@ -0,0 +1,99 @@ +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; +import { Envelope, EventItem } from '@sentry/core'; + +import { + createSentryServer, + containingEventWithAndroidMessage, +} from '../../utils/mockedSentryServer'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; + +describe('Capture message (auto init from JS)', () => { + let sentryServer = createSentryServer(); + + let envelope: Envelope; + + beforeAll(async () => { + await sentryServer.start(); + + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithAndroidMessage('Captured message'), + ); + + await maestro('tests/captureMessage/captureMessage.test.yml'); + + envelope = await envelopePromise; + }, 240000); // 240 seconds timeout + + afterAll(async () => { + await sentryServer.close(); + }); + + it('envelope contains message event', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item).toEqual([ + { + content_type: 'application/json', + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + level: 'info', + message: { + message: 'Captured message', + }, + platform: 'javascript', + }), + ]); + }); + + it('contains device context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + device: expect.objectContaining({ + battery_level: expect.any(Number), + brand: expect.any(String), + family: expect.any(String), + manufacturer: expect.any(String), + model: expect.any(String), + simulator: expect.any(Boolean), + }), + }), + }), + ); + }); + + it('contains app context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + app: expect.objectContaining({ + app_identifier: expect.any(String), + app_name: expect.any(String), + app_version: expect.any(String), + }), + }), + }), + ); + }); + + it('SDK initialized from JavaScript (auto init)', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + // Verify that native SDK was NOT initialized before JS + // When auto init, the SDK is initialized from JavaScript + expect(item?.[1]).toEqual( + expect.objectContaining({ + sdk: expect.objectContaining({ + name: 'sentry.javascript.react-native', + }), + }), + ); + }); +}); diff --git a/samples/react-native/e2e/tests/captureMessage/captureMessage.test.android.manual.ts b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.android.manual.ts new file mode 100644 index 0000000000..95c1bf3c6d --- /dev/null +++ b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.android.manual.ts @@ -0,0 +1,148 @@ +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; +import { Envelope, EventItem } from '@sentry/core'; + +import { + createSentryServer, + containingEventWithAndroidMessage, +} from '../../utils/mockedSentryServer'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; + +describe('Capture message (manual native init)', () => { + let sentryServer = createSentryServer(); + + let envelope: Envelope; + + beforeAll(async () => { + await sentryServer.start(); + + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithAndroidMessage('Captured message'), + ); + + await maestro('tests/captureMessage/captureMessage.test.yml'); + + envelope = await envelopePromise; + }, 240000); // 240 seconds timeout + + afterAll(async () => { + await sentryServer.close(); + }); + + it('envelope contains message event', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item).toEqual([ + { + content_type: 'application/json', + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + level: 'info', + message: { + message: 'Captured message', + }, + platform: 'javascript', + }), + ]); + }); + + it('contains device context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + device: expect.objectContaining({ + battery_level: expect.any(Number), + battery_temperature: expect.any(Number), + boot_time: expect.any(String), + brand: expect.any(String), + charging: expect.any(Boolean), + connection_type: expect.any(String), + family: expect.any(String), + free_memory: expect.any(Number), + free_storage: expect.any(Number), + id: expect.any(String), + locale: expect.any(String), + low_memory: expect.any(Boolean), + manufacturer: expect.any(String), + memory_size: expect.any(Number), + model: expect.any(String), + model_id: expect.any(String), + online: expect.any(Boolean), + orientation: expect.any(String), + processor_count: expect.any(Number), + processor_frequency: expect.any(Number), + screen_density: expect.any(Number), + screen_dpi: expect.any(Number), + screen_height_pixels: expect.any(Number), + screen_width_pixels: expect.any(Number), + simulator: expect.any(Boolean), + storage_size: expect.any(Number), + timezone: expect.any(String), + }), + }), + }), + ); + }); + + it('contains app context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + app: expect.objectContaining({ + app_build: expect.any(String), + app_identifier: expect.any(String), + app_name: expect.any(String), + app_start_time: expect.any(String), + app_version: expect.any(String), + in_foreground: expect.any(Boolean), + view_names: ['ErrorsScreen'], + }), + }), + }), + ); + }); + + it('contains os context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + os: { + build: expect.any(String), + kernel_version: expect.any(String), + name: 'Android', + rooted: expect.any(Boolean), + version: expect.any(String), + }, + }), + }), + ); + }); + + it('contains react native context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + react_native_context: { + expo: false, + fabric: expect.any(Boolean), + hermes_debug_info: expect.any(Boolean), + hermes_version: expect.any(String), + js_engine: 'hermes', + react_native_version: expect.any(String), + turbo_module: expect.any(Boolean), + }, + }), + }), + ); + }); +}); diff --git a/samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.auto.yml b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.auto.yml new file mode 100644 index 0000000000..b60a4de2b4 --- /dev/null +++ b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.auto.yml @@ -0,0 +1,10 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + clearState: true + stopApp: true + arguments: + sentryDisableNativeStart: true + isE2ETest: true + +- tapOn: "Capture message" diff --git a/samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.ts b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.ts new file mode 100644 index 0000000000..7672dc8412 --- /dev/null +++ b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.ios.ts @@ -0,0 +1,134 @@ +import { describe, it, beforeAll, expect, afterAll } from '@jest/globals'; +import { Envelope, EventItem } from '@sentry/core'; + +import { + createSentryServer, + containingEventWithMessage, +} from '../../utils/mockedSentryServer'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; +import { isAutoInitTest } from '../../utils/environment'; + +describe('Capture message', () => { + let sentryServer = createSentryServer(); + + let envelope: Envelope; + + beforeAll(async () => { + await sentryServer.start(); + + const envelopePromise = sentryServer.waitForEnvelope( + containingEventWithMessage('Captured message'), + ); + + if (isAutoInitTest()) { + await maestro('tests/captureMessage/captureMessage.test.ios.auto.yml'); + } else { + await maestro('tests/captureMessage/captureMessage.test.yml'); + } + + envelope = await envelopePromise; + }, 240000); // 240 seconds timeout for iOS event delivery + + afterAll(async () => { + await sentryServer.close(); + }); + + it('envelope contains message event', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item).toEqual([ + { + length: expect.any(Number), + type: 'event', + }, + expect.objectContaining({ + level: 'info', + message: 'Captured message', + platform: 'javascript', + }), + ]); + }); + + it('contains device context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + device: expect.objectContaining({ + arch: expect.any(String), + family: expect.any(String), + free_memory: expect.any(Number), + locale: expect.any(String), + memory_size: expect.any(Number), + model: expect.any(String), + model_id: expect.any(String), + processor_count: expect.any(Number), + simulator: expect.any(Boolean), + thermal_state: expect.any(String), + usable_memory: expect.any(Number), + }), + }), + }), + ); + }); + + it('contains app context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + app: expect.objectContaining({ + app_build: expect.any(String), + app_identifier: expect.any(String), + app_name: expect.any(String), + app_start_time: expect.any(String), + app_version: expect.any(String), + in_foreground: expect.any(Boolean), + // view_names: ['ErrorsScreen-jn5qquvH9Nz'], // TODO: fix this generated hash should not be part of the name + }), + }), + }), + ); + }); + + it('contains os context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + os: { + build: expect.any(String), + kernel_version: expect.any(String), + name: 'iOS', + rooted: expect.any(Boolean), + version: expect.any(String), + }, + }), + }), + ); + }); + + it('contains react native context', async () => { + const item = getItemOfTypeFrom(envelope, 'event'); + + expect(item?.[1]).toEqual( + expect.objectContaining({ + contexts: expect.objectContaining({ + react_native_context: { + expo: false, + fabric: expect.any(Boolean), + hermes_debug_info: expect.any(Boolean), + hermes_version: expect.any(String), + js_engine: 'hermes', + react_native_version: expect.any(String), + turbo_module: expect.any(Boolean), + }, + }), + }), + ); + }); +}); diff --git a/samples/react-native/e2e/tests/captureMessage/captureMessage.test.yml b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.yml new file mode 100644 index 0000000000..33179939c6 --- /dev/null +++ b/samples/react-native/e2e/tests/captureMessage/captureMessage.test.yml @@ -0,0 +1,19 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + clearState: true + stopApp: true + arguments: + isE2ETest: true + +# App launches on ErrorsTab (first tab) +# Wait a moment for the screen to fully load +- waitForAnimationToEnd: + timeout: 2000 + +# Button might be off-screen, scroll to find it +- scrollUntilVisible: + element: "Capture message" + timeout: 10000 + direction: DOWN +- tapOn: "Capture message" diff --git a/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.yml b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ios.auto.yml similarity index 95% rename from samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.yml rename to samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ios.auto.yml index b453035f52..f5326278fe 100644 --- a/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.yml +++ b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ios.auto.yml @@ -5,6 +5,7 @@ appId: io.sentry.reactnative.sample clearState: true stopApp: true arguments: + sentryDisableNativeStart: true isE2ETest: true # For unknown reasons tapOn: "Performance" does not work on iOS diff --git a/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.ts b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ts similarity index 87% rename from samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.ts rename to samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ts index 6e9d6be8b5..5f8637de7c 100644 --- a/samples/react-native/e2e/captureSpaceflightNewsScreenTransaction.test.ts +++ b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ts @@ -5,10 +5,11 @@ import { containingTransactionWithName, takeSecond, containingTransaction, -} from './utils/mockedSentryServer'; +} from '../../utils/mockedSentryServer'; -import { getItemOfTypeFrom } from './utils/event'; -import { maestro } from './utils/maestro'; +import { getItemOfTypeFrom } from '../../utils/event'; +import { maestro } from '../../utils/maestro'; +import { isAutoInitTest } from '../../utils/environment'; describe('Capture Spaceflight News Screen Transaction', () => { let sentryServer = createSentryServer(); @@ -32,7 +33,11 @@ describe('Capture Spaceflight News Screen Transaction', () => { takeSecond(containingNewsScreen), ); - await maestro('captureSpaceflightNewsScreenTransaction.test.yml'); + if (isAutoInitTest()) { + await maestro('tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.ios.auto.yml'); + } else { + await maestro('tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.yml'); + } await waitForSpaceflightNewsTx; @@ -40,7 +45,7 @@ describe('Capture Spaceflight News Screen Transaction', () => { allTransactionEnvelopes = sentryServer.getAllEnvelopes( containingTransaction, ); - }); + }, 240000); // 240 seconds timeout for iOS event delivery afterAll(async () => { await sentryServer.close(); @@ -123,8 +128,6 @@ describe('Capture Spaceflight News Screen Transaction', () => { const item = getFirstNewsEventItem(); const spans = item?.[1].spans; - console.log(spans); - const httpSpans = spans?.filter( span => span.data?.['sentry.op'] === 'http.client', ); diff --git a/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.yml b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.yml new file mode 100644 index 0000000000..6f901f93ad --- /dev/null +++ b/samples/react-native/e2e/tests/captureSpaceflightNewsScreenTransaction/captureSpaceflightNewsScreenTransaction.test.yml @@ -0,0 +1,53 @@ +appId: io.sentry.reactnative.sample +--- +- launchApp: + # We expect cold start + clearState: true + stopApp: true + arguments: + isE2ETest: true + +# For unknown reasons tapOn: "Performance" does not work on iOS +- tapOn: + id: "performance-tab-icon" +- tapOn: "Open Spaceflight News" + +# Wait for the screen to load articles and initial data to appear +- waitForAnimationToEnd: + timeout: 5000 + +# Scroll down multiple times to trigger auto-load (AUTO_LOAD_LIMIT = 1) +# This will trigger handleEndReached which increments autoLoadCount +# After autoLoadCount >= 1, the "Load More Articles" button will appear +- scroll +- scroll +- scroll + +# Wait a bit for the auto-load to complete and button to appear +- waitForAnimationToEnd: + timeout: 3000 + +# Now the "Load More Articles" button should be visible +- scrollUntilVisible: + element: "Load More Articles" + timeout: 10000 +# On iOS the visibility is resolved when the button only peaks from the bottom tabs +# this causes Maestro to click the bottom tab instead of the button +# thus the extra scroll is needed to make the button visible +- scroll +- tapOn: "Load More Articles" + +# Wait for more articles to load after manual tap +- waitForAnimationToEnd: + timeout: 3000 +- scroll +- scrollUntilVisible: + element: "Load More Articles" + timeout: 10000 + +- tapOn: + id: "errors-tab-icon" + +# The tab keeps News Screen open, but the data are updated on the next visit +- tapOn: + id: "performance-tab-icon" diff --git a/samples/react-native/e2e/utils/environment.ts b/samples/react-native/e2e/utils/environment.ts index cde97ea350..37bf0c6620 100644 --- a/samples/react-native/e2e/utils/environment.ts +++ b/samples/react-native/e2e/utils/environment.ts @@ -1,23 +1,15 @@ type TestGlobal = typeof globalThis & { - E2E_TEST_PLATFORM: 'android' | 'ios'; + E2E_TEST_INIT_TYPE: 'auto' | 'manual'; }; function getTestGlobal(): TestGlobal { return globalThis as TestGlobal; } -export function setAndroid(): void { - getTestGlobal().E2E_TEST_PLATFORM = 'android'; +export function setAutoInitTest(): void { + getTestGlobal().E2E_TEST_INIT_TYPE = 'auto'; } -export function setIOS(): void { - getTestGlobal().E2E_TEST_PLATFORM = 'ios'; -} - -export function isAndroid(): boolean { - return getTestGlobal().E2E_TEST_PLATFORM === 'android'; -} - -export function isIOS(): boolean { - return getTestGlobal().E2E_TEST_PLATFORM === 'ios'; +export function isAutoInitTest(): boolean { + return getTestGlobal().E2E_TEST_INIT_TYPE === 'auto'; } diff --git a/samples/react-native/e2e/utils/mockedSentryServer.ts b/samples/react-native/e2e/utils/mockedSentryServer.ts index 5b791f7b62..34baaeacb1 100644 --- a/samples/react-native/e2e/utils/mockedSentryServer.ts +++ b/samples/react-native/e2e/utils/mockedSentryServer.ts @@ -66,7 +66,6 @@ export function createSentryServer({ port = 8961 } = {}): { start: () => { return new Promise((resolve, _reject) => { server.listen(port, () => { - console.log(`Sentry server listening on port ${port}`); resolve(); }); }); diff --git a/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj b/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj index e98d264df9..c66b30e41e 100644 --- a/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj +++ b/samples/react-native/ios/sentryreactnativesample.xcodeproj/project.pbxproj @@ -282,14 +282,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-frameworks.sh\"\n"; @@ -317,14 +313,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-resources.sh\"\n"; @@ -382,14 +374,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample-sentryreactnativesampleTests/Pods-sentryreactnativesample-sentryreactnativesampleTests-frameworks.sh\"\n"; @@ -403,14 +391,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-sentryreactnativesample/Pods-sentryreactnativesample-resources.sh\"\n"; @@ -501,7 +485,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U; @@ -523,7 +507,7 @@ PRODUCT_NAME = sentryreactnativesample; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore io.sentry.reactnative.sample"; - RCT_NEW_ARCH_ENABLED = 1; + RCT_NEW_ARCH_ENABLED = 1; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -538,7 +522,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 97JCY7859U; @@ -559,7 +543,7 @@ PRODUCT_NAME = sentryreactnativesample; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match AppStore io.sentry.reactnative.sample"; - RCT_NEW_ARCH_ENABLED = 1; + RCT_NEW_ARCH_ENABLED = 1; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; @@ -640,7 +624,10 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -713,7 +700,10 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme b/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme index 15d942042b..a6e129394a 100644 --- a/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme +++ b/samples/react-native/ios/sentryreactnativesample.xcodeproj/xcshareddata/xcschemes/sentryreactnativesample.xcscheme @@ -41,7 +41,7 @@ + + + + + + +#import +#import @interface AppDelegate () { } @@ -24,6 +27,16 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + if ([self shouldStartSentry]) { + [RNSentrySDK start]; + } + + if ([self shouldCrashOnStart]) { + @throw [NSException exceptionWithName:@"CrashOnStart" + reason:@"This was intentional test crash before JS started." + userInfo:nil]; + } + // When the native init is enabled the `autoInitializeNativeSdk` // in JS has to be set to `false` // [SentryNativeInitializer initializeSentry]; @@ -71,4 +84,16 @@ - (NSURL *)bundleURL return [super getTurboModule:name jsInvoker:jsInvoker]; } +- (BOOL)shouldStartSentry +{ + NSArray *arguments = [[NSProcessInfo processInfo] arguments]; + return ![arguments containsObject:@"sentryDisableNativeStart"]; +} + +- (BOOL)shouldCrashOnStart +{ + NSArray *arguments = [[NSProcessInfo processInfo] arguments]; + return [arguments containsObject:@"sentryCrashOnStart"]; +} + @end diff --git a/samples/react-native/package.json b/samples/react-native/package.json index 0d2964878b..e9668ae07a 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -6,15 +6,33 @@ "android": "react-native run-android", "ios": "react-native run-ios", "start": "react-native start", + "build-android-release": "scripts/build-android-release.sh", + "build-android-release-legacy": "scripts/build-android-release-legacy.sh", + "build-android-debug": "scripts/build-android-debug.sh", + "build-android-debug-legacy": "scripts/build-android-debug-legacy.sh", + "build-android-debug-auto": "scripts/build-android-debug-auto.sh", + "build-android-debug-manual": "scripts/build-android-debug-manual.sh", + "build-ios-release": "scripts/build-ios-release.sh", + "build-ios-debug": "scripts/build-ios-debug.sh", "test": "jest", - "test-android": "scripts/test-android.sh", - "test-ios": "scripts/test-ios.sh", + "test-android": "scripts/test-android-manual.sh", + "test-ios": "scripts/test-ios-auto.sh", + "set-test-dsn-android": "scripts/set-dsn-aos.mjs", + "set-test-dsn-ios": "scripts/set-dsn-ios.mjs", + "test-android-manual": "scripts/test-android-manual.sh", + "test-android-auto": "scripts/test-android-auto.sh", + "test-ios-manual": "scripts/test-ios-manual.sh", + "test-ios-auto": "scripts/test-ios-auto.sh", "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx", "fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", - "pod-install": "cd ios; RCT_NEW_ARCH_ENABLED=1 bundle exec pod update; cd ..", - "pod-install-production": "cd ios; PRODUCTION=1 RCT_NEW_ARCH_ENABLED=1 bundle exec pod update; cd ..", - "pod-install-legacy": "cd ios; bundle exec pod update; cd ..", - "pod-install-legacy-production": "cd ios; PRODUCTION=1 bundle exec pod update; cd ..", + "pod-install-debug-static": "scripts/pod-install-debug-static.sh", + "pod-install-debug-static-legacy": "scripts/pod-install-debug-static-legacy.sh", + "pod-install-debug-dynamic": "scripts/pod-install-debug-dynamic.sh", + "pod-install-debug-dynamic-legacy": "scripts/pod-install-debug-dynamic-legacy.sh", + "pod-install-release-static": "scripts/pod-install-release-static.sh", + "pod-install-release-static-legacy": "scripts/pod-install-release-static-legacy.sh", + "pod-install-release-dynamic": "scripts/pod-install-release-dynamic.sh", + "pod-install-release-dynamic-legacy": "scripts/pod-install-release-dynamic-legacy.sh", "clean-ios": "cd ios; rm -rf Podfile.lock Pods build; cd ..", "clean-watchman": "watchman watch-del-all", "set-build-number": "npx react-native-version --skip-tag --never-amend --set-build", @@ -36,6 +54,7 @@ "delay": "^6.0.0", "react": "19.1.0", "react-native": "0.80.2", + "react-native-build-config": "^0.3.2", "react-native-gesture-handler": "^2.28.0", "react-native-image-picker": "^8.2.1", "react-native-launch-arguments": "^4.1.0", @@ -68,6 +87,7 @@ "@typescript-eslint/parser": "^7.18.0", "babel-jest": "^29.6.3", "babel-plugin-module-resolver": "^5.0.0", + "detox": "^20.33.0", "eslint": "^8.19.0", "eslint-plugin-ft-flow": "^3.0.11", "jest": "^29.6.3", diff --git a/samples/react-native/scripts/build-android-debug-auto.sh b/samples/react-native/scripts/build-android-debug-auto.sh new file mode 100755 index 0000000000..a4e50a4d1f --- /dev/null +++ b/samples/react-native/scripts/build-android-debug-auto.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export RN_ARCHITECTURE="new" +export CONFIG="debug" +export SENTRY_DISABLE_NATIVE_START="true" + +echo "Building Android with SENTRY_DISABLE_NATIVE_START=${SENTRY_DISABLE_NATIVE_START}" +echo "This build will initialize Sentry from JavaScript (auto init)" + +"${thisFilePath}/build-android.sh" + +# Rename the output APK to distinguish it from manual build +cd "${thisFilePath}/.." +if [ -f "app.apk" ]; then + mv app.apk app-auto.apk + echo "Build complete: app-auto.apk" +fi diff --git a/samples/react-native/scripts/build-android-debug-legacy.sh b/samples/react-native/scripts/build-android-debug-legacy.sh new file mode 100755 index 0000000000..ac0952892d --- /dev/null +++ b/samples/react-native/scripts/build-android-debug-legacy.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export RN_ARCHITECTURE="legacy" +export CONFIG="debug" + +"${thisFilePath}/build-android.sh" diff --git a/samples/react-native/scripts/build-android-debug-manual.sh b/samples/react-native/scripts/build-android-debug-manual.sh new file mode 100755 index 0000000000..7bb4754496 --- /dev/null +++ b/samples/react-native/scripts/build-android-debug-manual.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export RN_ARCHITECTURE="new" +export CONFIG="debug" +export SENTRY_DISABLE_NATIVE_START="false" + +echo "Building Android with SENTRY_DISABLE_NATIVE_START=${SENTRY_DISABLE_NATIVE_START}" +echo "This build will initialize Sentry natively before JS (manual init)" + +"${thisFilePath}/build-android.sh" + +# Rename the output APK to distinguish it from auto build +cd "${thisFilePath}/.." +if [ -f "app.apk" ]; then + mv app.apk app-manual.apk + echo "Build complete: app-manual.apk" +fi diff --git a/samples/react-native/scripts/build-android-debug.sh b/samples/react-native/scripts/build-android-debug.sh new file mode 100755 index 0000000000..89f9ae626c --- /dev/null +++ b/samples/react-native/scripts/build-android-debug.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export RN_ARCHITECTURE="new" +export CONFIG="debug" + +"${thisFilePath}/build-android.sh" diff --git a/samples/react-native/scripts/build-android-release-legacy.sh b/samples/react-native/scripts/build-android-release-legacy.sh new file mode 100755 index 0000000000..cf853c15cc --- /dev/null +++ b/samples/react-native/scripts/build-android-release-legacy.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export RN_ARCHITECTURE="legacy" +export CONFIG="release" + +"${thisFilePath}/build-android.sh" diff --git a/samples/react-native/scripts/build-android-release.sh b/samples/react-native/scripts/build-android-release.sh new file mode 100755 index 0000000000..3403a3c1bb --- /dev/null +++ b/samples/react-native/scripts/build-android-release.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export RN_ARCHITECTURE="new" +export CONFIG="release" + +"${thisFilePath}/build-android.sh" diff --git a/samples/react-native/scripts/build-android.sh b/samples/react-native/scripts/build-android.sh new file mode 100755 index 0000000000..3ada91cfd2 --- /dev/null +++ b/samples/react-native/scripts/build-android.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +cd "${thisFilePath}/../android" + +rm -rf ../app.apk ../app-androidTest.apk + +if [[ "${RN_ARCHITECTURE}" == 'new' ]]; then + perl -i -pe's/newArchEnabled=false/newArchEnabled=true/g' gradle.properties + echo 'New Architecture enabled' +elif [[ "${RN_ARCHITECTURE}" == 'legacy' ]]; then + perl -i -pe's/newArchEnabled=true/newArchEnabled=false/g' gradle.properties + echo 'Legacy Architecture enabled' +else + echo "No changes for architecture: ${RN_ARCHITECTURE}" +fi + +echo "Building $CONFIG" + +assembleConfig=$(python3 -c "print(\"${CONFIG}\".capitalize())") + +./gradlew ":app:assemble${assembleConfig}" app:assembleAndroidTest -DtestBuildType=$CONFIG "$@" + +cp "app/build/outputs/apk/${CONFIG}/app-${CONFIG}.apk" ../app.apk +cp "app/build/outputs/apk/androidTest/${CONFIG}/app-${CONFIG}-androidTest.apk" ../app-androidTest.apk diff --git a/samples/react-native/scripts/build-ios-debug.sh b/samples/react-native/scripts/build-ios-debug.sh new file mode 100755 index 0000000000..290fd20d94 --- /dev/null +++ b/samples/react-native/scripts/build-ios-debug.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export CONFIG='Debug' + +"${thisFilePath}/build-ios.sh" diff --git a/samples/react-native/scripts/build-ios-release.sh b/samples/react-native/scripts/build-ios-release.sh new file mode 100755 index 0000000000..f5de18b73e --- /dev/null +++ b/samples/react-native/scripts/build-ios-release.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +export CONFIG='Release' + +"${thisFilePath}/build-ios.sh" diff --git a/samples/react-native/scripts/build-ios.sh b/samples/react-native/scripts/build-ios.sh new file mode 100755 index 0000000000..3a8cfefe4a --- /dev/null +++ b/samples/react-native/scripts/build-ios.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +cd "${thisFilePath}/../ios" + +rm -rf ../sentryreactnativesample.app + +echo "Building $CONFIG" + +rm -rf xcodebuild.log + +mkdir -p "DerivedData" +derivedData="$(cd "DerivedData" ; pwd -P)" +set -o pipefail && xcodebuild \ + -workspace sentryreactnativesample.xcworkspace \ + -configuration "$CONFIG" \ + -scheme sentryreactnativesample \ + -sdk 'iphonesimulator' \ + -destination 'generic/platform=iOS Simulator' \ + ONLY_ACTIVE_ARCH=yes \ + -derivedDataPath "$derivedData" \ + build \ + | tee xcodebuild.log \ + | if [ "$CI" = "true" ]; then xcbeautify --quieter --is-ci --disable-colored-output; else xcbeautify; fi + +cp -r "DerivedData/Build/Products/${CONFIG}-iphonesimulator/sentryreactnativesample.app" ../sentryreactnativesample.app diff --git a/samples/react-native/scripts/pod-install-debug-dynamic-legacy.sh b/samples/react-native/scripts/pod-install-debug-dynamic-legacy.sh new file mode 100755 index 0000000000..cea9690bb0 --- /dev/null +++ b/samples/react-native/scripts/pod-install-debug-dynamic-legacy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=0 +export ENABLE_NEW_ARCH=0 +export USE_FRAMEWORKS=dynamic + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-debug-dynamic.sh b/samples/react-native/scripts/pod-install-debug-dynamic.sh new file mode 100755 index 0000000000..ed3acafb8b --- /dev/null +++ b/samples/react-native/scripts/pod-install-debug-dynamic.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=0 +export ENABLE_NEW_ARCH=1 +export USE_FRAMEWORKS=dynamic + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-debug-static-legacy.sh b/samples/react-native/scripts/pod-install-debug-static-legacy.sh new file mode 100755 index 0000000000..52b80ba450 --- /dev/null +++ b/samples/react-native/scripts/pod-install-debug-static-legacy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=0 +export ENABLE_NEW_ARCH=0 +export USE_FRAMEWORKS=static + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-debug-static.sh b/samples/react-native/scripts/pod-install-debug-static.sh new file mode 100755 index 0000000000..86049e4425 --- /dev/null +++ b/samples/react-native/scripts/pod-install-debug-static.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=0 +export ENABLE_NEW_ARCH=1 +export USE_FRAMEWORKS=static + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-release-dynamic-legacy.sh b/samples/react-native/scripts/pod-install-release-dynamic-legacy.sh new file mode 100755 index 0000000000..d6f7449abc --- /dev/null +++ b/samples/react-native/scripts/pod-install-release-dynamic-legacy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=1 +export ENABLE_NEW_ARCH=0 +export USE_FRAMEWORKS=dynamic + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-release-dynamic.sh b/samples/react-native/scripts/pod-install-release-dynamic.sh new file mode 100755 index 0000000000..9207b45dfe --- /dev/null +++ b/samples/react-native/scripts/pod-install-release-dynamic.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=1 +export ENABLE_NEW_ARCH=1 +export USE_FRAMEWORKS=dynamic + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-release-static-legacy.sh b/samples/react-native/scripts/pod-install-release-static-legacy.sh new file mode 100755 index 0000000000..9742caa73a --- /dev/null +++ b/samples/react-native/scripts/pod-install-release-static-legacy.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=1 +export ENABLE_NEW_ARCH=0 +export USE_FRAMEWORKS=static + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install-release-static.sh b/samples/react-native/scripts/pod-install-release-static.sh new file mode 100755 index 0000000000..8de5b13a61 --- /dev/null +++ b/samples/react-native/scripts/pod-install-release-static.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Exit on error +set -e + +export ENABLE_PROD=1 +export ENABLE_NEW_ARCH=1 +export USE_FRAMEWORKS=static + +thisFilePath=$(dirname "$0") + +"${thisFilePath}/pod-install.sh" diff --git a/samples/react-native/scripts/pod-install.sh b/samples/react-native/scripts/pod-install.sh new file mode 100755 index 0000000000..5d1ada6789 --- /dev/null +++ b/samples/react-native/scripts/pod-install.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Exit on error +set -e + +thisFilePath=$(dirname "$0") + +echo "USE_FRAMEWORKS=$USE_FRAMEWORKS" +echo "ENABLE_PROD=$ENABLE_PROD" +echo "ENABLE_NEW_ARCH=$ENABLE_NEW_ARCH" + +cd "${thisFilePath}/.." +bundle install + +cd ios +PRODUCTION=$ENABLE_PROD RCT_NEW_ARCH_ENABLED=$ENABLE_NEW_ARCH bundle exec pod update + +cat Podfile.lock | grep $RN_SENTRY_POD_NAME diff --git a/samples/react-native/scripts/set-dsn-aos.mjs b/samples/react-native/scripts/set-dsn-aos.mjs new file mode 100755 index 0000000000..01bed2c984 --- /dev/null +++ b/samples/react-native/scripts/set-dsn-aos.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { setAndroidDsn } from './set-dsn.mjs'; + +setAndroidDsn(); diff --git a/samples/react-native/scripts/set-dsn-ios.mjs b/samples/react-native/scripts/set-dsn-ios.mjs new file mode 100755 index 0000000000..7757c1e7b7 --- /dev/null +++ b/samples/react-native/scripts/set-dsn-ios.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { setIosDsn } from './set-dsn.mjs'; + +setIosDsn(); diff --git a/samples/react-native/scripts/set-dsn.mjs b/samples/react-native/scripts/set-dsn.mjs new file mode 100644 index 0000000000..da2153f203 --- /dev/null +++ b/samples/react-native/scripts/set-dsn.mjs @@ -0,0 +1,24 @@ +import { fileURLToPath } from 'node:url'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export function setIosDsn() { + setDsn('http://key@localhost:8961/123456'); +} + +export function setAndroidDsn() { + setDsn('http://key@10.0.2.2:8961/123456'); +} + +function setDsn(dsn) { + const sentryOptionsPath = path.join(__dirname, '../sentry.options.json'); + const sentryOptions = JSON.parse(fs.readFileSync(sentryOptionsPath, 'utf8')); + sentryOptions.dsn = dsn; + fs.writeFileSync( + sentryOptionsPath, + JSON.stringify(sentryOptions, null, 2) + '\n', + ); + console.log('Dsn set to: ', dsn); +} diff --git a/samples/react-native/scripts/test-android-auto.sh b/samples/react-native/scripts/test-android-auto.sh new file mode 100755 index 0000000000..5a27fe3457 --- /dev/null +++ b/samples/react-native/scripts/test-android-auto.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e # exit on error + +# Get current directory +thisFileDirPath=$(dirname "$0") +reactProjectRootPath="$(cd "$thisFileDirPath/.." && pwd)" + +maybeApkPath=$(find "${reactProjectRootPath}" -maxdepth 1 -name "*-auto.apk") + +# Check if any APK files exist +apk_count=$(echo "$maybeApkPath" | wc -l) + +if [ -n "$maybeApkPath" ] && [ $apk_count -eq 1 ]; then + # Force install single APK using adb + apk_file="${maybeApkPath}" + echo "Installing $apk_file..." + adb install -r "$apk_file" +elif [ $apk_count -gt 1 ]; then + echo "Error: Multiple APK files found. Expected only one APK file." + exit 1 +else + echo "No APK files found, continuing without install" +fi + +# Run the tests +npx jest --config e2e/jest.config.android.auto.js diff --git a/samples/react-native/scripts/test-android.sh b/samples/react-native/scripts/test-android-manual.sh similarity index 88% rename from samples/react-native/scripts/test-android.sh rename to samples/react-native/scripts/test-android-manual.sh index de4b5d5e87..694c64dd8b 100755 --- a/samples/react-native/scripts/test-android.sh +++ b/samples/react-native/scripts/test-android-manual.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e -x # exit on error, print commands +set -e # exit on error # Get current directory thisFileDirPath=$(dirname "$0") @@ -24,4 +24,4 @@ else fi # Run the tests -npx jest --config e2e/jest.config.android.js +npx jest --config e2e/jest.config.android.manual.js diff --git a/samples/react-native/scripts/test-ios.sh b/samples/react-native/scripts/test-ios-auto.sh similarity index 88% rename from samples/react-native/scripts/test-ios.sh rename to samples/react-native/scripts/test-ios-auto.sh index e242dde917..3c85059436 100755 --- a/samples/react-native/scripts/test-ios.sh +++ b/samples/react-native/scripts/test-ios-auto.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e -x # exit on error, print commands +set -e # exit on error # Get current directory thisFileDirPath=$(dirname "$0") @@ -23,4 +23,4 @@ else fi # Run the tests -npx jest --config e2e/jest.config.ios.js +npx jest --config e2e/jest.config.ios.auto.js diff --git a/samples/react-native/scripts/test-ios-manual.sh b/samples/react-native/scripts/test-ios-manual.sh new file mode 100755 index 0000000000..f2e6098440 --- /dev/null +++ b/samples/react-native/scripts/test-ios-manual.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -e # exit on error + +# Get current directory +thisFileDirPath=$(dirname "$0") +reactProjectRootPath="$(cd "$thisFileDirPath/.." && pwd)" + +maybeAppPath=$(find "${reactProjectRootPath}" -maxdepth 1 -name "*.app") + +# Check if any APP files exist +app_count=$(echo "$maybeAppPath" | wc -l) + +if [ -n "$maybeAppPath" ] && [ $app_count -eq 1 ]; then + app_file="${maybeAppPath}" + echo "Installing $app_file..." + xcrun simctl install booted "$app_file" +elif [ $app_count -gt 1 ]; then + echo "Error: Multiple APP files found. Expected only one APP file." + exit 1 +else + echo "No APP files found, continuing without install" +fi + +# Run the tests +npx jest --config e2e/jest.config.ios.manual.js diff --git a/samples/react-native/sentry.options.json b/samples/react-native/sentry.options.json new file mode 100644 index 0000000000..58425c35d5 --- /dev/null +++ b/samples/react-native/sentry.options.json @@ -0,0 +1,18 @@ +{ + "dsn": "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", + "debug": true, + "environment": "dev", + "enableUserInteractionTracing": true, + "enableAutoSessionTracking": true, + "sessionTrackingIntervalMillis": 30000, + "enableTracing": true, + "tracesSampleRate": 1, + "attachStacktrace": true, + "attachScreenshot": true, + "attachViewHierarchy": true, + "enableCaptureFailedRequests": true, + "profilesSampleRate": 1, + "replaysSessionSampleRate": 1, + "replaysOnErrorSampleRate": 1, + "spotlight": true +} diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index f9436b8ed2..814c675965 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -19,7 +19,7 @@ import WebviewScreen from './Screens/WebviewScreen'; import getErrorsTab from './tabs/ErrorsTab'; import getPerformanceTab from './tabs/PerformanceTab'; import getPlaygroundTab from './tabs/PlaygroundTab'; -import { getDsn, logWithoutTracing } from './utils'; +import { logWithoutTracing, shouldUseAutoStart } from './utils'; LogBox.ignoreAllLogs(); const isMobileOs = Platform.OS === 'android' || Platform.OS === 'ios'; @@ -45,10 +45,6 @@ const StackNavigator: TypedNavigator = isMobileOs const BottomTabNavigator = createBottomTabNavigator(); Sentry.init({ - // Replace the example DSN below with your own DSN: - dsn: getDsn(), - debug: true, - environment: 'dev', beforeSend: (event: Sentry.ErrorEvent) => { logWithoutTracing('Event beforeSend:', event.event_id); return event; @@ -170,7 +166,7 @@ Sentry.init({ spotlight: false, // This should be disabled when manually initializing the native SDK // Note that options from JS are not passed to the native SDKs when initialized manually - autoInitializeNativeSdk: true, + autoInitializeNativeSdk: shouldUseAutoStart(), enableMetrics: true, }); diff --git a/samples/react-native/src/Screens/ErrorsScreen.tsx b/samples/react-native/src/Screens/ErrorsScreen.tsx index a03022a351..d1b2052ed1 100644 --- a/samples/react-native/src/Screens/ErrorsScreen.tsx +++ b/samples/react-native/src/Screens/ErrorsScreen.tsx @@ -22,7 +22,7 @@ import { setScopeProperties } from '../setScopeProperties'; import { TimeToFullDisplay } from '../utils'; import type { Event as SentryEvent } from '@sentry/core'; -const { AssetsModule, CppModule, CrashModule } = NativeModules; +const { AssetsModule, CppModule, CrashModule, TestControlModule } = NativeModules; interface Props { navigation: StackNavigationProp; @@ -207,6 +207,30 @@ const ErrorsScreen = (_props: Props) => { }); }} /> +