Skip to content

feat: Capture app start errors before JS #4472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
00e9040
ref(ios): Extract Cocoa SDK init into standalone file (#4442)
krystofwoldrich Jan 21, 2025
4c0b8f4
Merge branch 'main' into capture-app-start-errors
krystofwoldrich Jan 21, 2025
2cb7eb2
ref(android): Extracts Android native initialization to standalone st…
antonis Jan 22, 2025
7144a64
feat(experimental): Add native `startWithConfigureOptions` for Apple …
krystofwoldrich Jan 22, 2025
7850677
feat: Read `sentry.options.json` during cocoa init (#4447)
krystofwoldrich Jan 22, 2025
1e5dbde
Adds utility class for converting `JsonObject` to `WritableMap` (#4479)
antonis Jan 23, 2025
22a5f81
feat: Automatically load `sentry.options.json` file (#4476)
krystofwoldrich Feb 3, 2025
a1cb36d
feat(experimental): Initialize Android SDK from json configuration (#…
antonis Feb 4, 2025
95c36ef
Merge branch 'main' into capture-app-start-errors
krystofwoldrich Feb 5, 2025
14fe05d
misc: Add `sentry.options.json` example to the changelog (#4509)
krystofwoldrich Feb 5, 2025
15a7e6d
feat(init): Load options from `sentry.options.json` in JS (#4510)
krystofwoldrich Feb 6, 2025
b9ec093
release: 6.7.0-alpha.0
getsentry-bot Feb 6, 2025
dbdd4b5
Merge branch 'release/6.7.0-alpha.0' into capture-app-start-errors
Feb 6, 2025
b947d7f
misc(sample): Change RN Sample to use native file init by default (#4…
krystofwoldrich Feb 13, 2025
6b08b9a
chore(sample-rn): Remove duplicate init options from code (#4532)
krystofwoldrich Feb 13, 2025
a7ffa1f
chore(sample-rn): Always use fhe file option (including auto init) (#…
krystofwoldrich Feb 13, 2025
28cf7b4
internal(sample-rn): Add Detox for integration/e2e tests of the rn sa…
krystofwoldrich Feb 14, 2025
cf00d4d
internal(sample-rn): Add header and message envelope tests (#4536)
krystofwoldrich Feb 17, 2025
b754aa3
Merge remote-tracking branch 'origin/main' into capture-app-start-errors
krystofwoldrich Feb 18, 2025
0bf6636
fix(sample-e2e): Fix type errors missing sentry/core and afterAll (#4…
krystofwoldrich Feb 20, 2025
0f5cd7d
Merge remote-tracking branch 'origin/main' into capture-app-start-errors
krystofwoldrich Feb 20, 2025
e935360
chore(samples): Add package scripts for native builds, dsn and testin…
krystofwoldrich Feb 21, 2025
952dd05
Merge branch 'main' into capture-app-start-errors
krystofwoldrich Feb 24, 2025
ae342a3
test(e2e): Verify captured Errors Screen transaction (#4584)
krystofwoldrich Feb 25, 2025
770f9fb
test(e2e): Add auto init from JS tests (#4588)
krystofwoldrich Feb 25, 2025
cbb85b2
test(e2e): Add app start crash test for iOS (#4593)
krystofwoldrich Feb 25, 2025
b4ee16b
test(e2e): Avoid race conditions when waiting for captured message (#…
krystofwoldrich Feb 26, 2025
ee11f58
chore(sample-e2e): Move Detox related files to e2e-detox dir
krystofwoldrich Jun 11, 2025
588ba6d
Merge branch 'main' into capture-app-start-errors
krystofwoldrich Jun 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 155 additions & 44 deletions .github/workflows/sample-application.yml
Original file line number Diff line number Diff line change
@@ -98,62 +98,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/detox/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/detox/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)"
@@ -170,19 +147,19 @@ 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

- 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' }}
@@ -207,6 +184,138 @@ jobs:
name: build-sample-${{ matrix.rn-architecture }}-${{ matrix.platform }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-logs
path: ${{ env.REACT_NATIVE_SAMPLE_PATH }}/${{ matrix.platform }}/*.log

test-detox:
name: ${{ matrix.job-name }}
runs-on: ${{ matrix.runs-on }}
needs: [diff_check, build]
if: ${{ needs.diff_check.outputs.skip_ci != 'true' }}
strategy:
# we want that the matrix keeps running, default is to cancel them if it fails.
fail-fast: false
matrix:
include:
- job-name: 'Test iOS Release Auto Init'
platform: ios
runs-on: macos-15
rn-architecture: 'new'
ios-use-frameworks: 'no-frameworks'
build-type: 'production'
test-command: 'yarn test-ios-auto' # tests native auto init from JS

- job-name: 'Test iOS Release Manual Init'
platform: ios
runs-on: macos-15
rn-architecture: 'new'
ios-use-frameworks: 'no-frameworks'
build-type: 'production'
test-command: 'yarn test-ios-manual'

- job-name: 'Test Android Release Manual Init'
platform: android
runs-on: ubuntu-latest
rn-architecture: 'new'
build-type: 'production'
test-command: 'yarn test-android-manual'

steps:
- uses: actions/checkout@v4

- name: Download iOS App Archive
if: ${{ matrix.platform == 'ios' }}
uses: actions/download-artifact@v4
with:
name: sample-rn-${{ matrix.rn-architecture }}-${{ matrix.build-type }}-${{ matrix.ios-use-frameworks}}-${{ matrix.platform }}
path: ${{ env.REACT_NATIVE_SAMPLE_PATH }}

- name: Download Android APK
if: ${{ matrix.platform == 'android' }}
uses: actions/download-artifact@v4
with:
name: sample-rn-${{ matrix.rn-architecture }}-${{ matrix.build-type }}-${{ matrix.platform }}
path: ${{ env.REACT_NATIVE_SAMPLE_PATH }}

- name: Unzip iOS App Archive
if: ${{ matrix.platform == 'ios' }}
working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}
run: unzip ${{ env.IOS_APP_ARCHIVE_PATH }}

- name: Unzip Android APK
if: ${{ matrix.platform == 'android' }}
working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}
run: unzip ${{ env.ANDROID_APP_ARCHIVE_PATH }}

- name: Enable Corepack
run: |
npm install -g [email protected]
corepack enable
- uses: actions/setup-node@v4
with:
node-version: 18
cache: 'yarn'
cache-dependency-path: yarn.lock

- name: Install JS Dependencies
run: yarn install

- name: Install Detox
run: npm install -g [email protected]

- name: Install Apple Simulator Utilities
if: ${{ matrix.platform == 'ios' }}
run: |
brew tap wix/brew
brew install applesimutils

- name: Setup KVM
if: ${{ matrix.platform == 'android' }}
shell: bash
run: |
# check if virtualization is supported...
sudo apt install -y --no-install-recommends cpu-checker coreutils && echo "CPUs=$(nproc --all)" && kvm-ok
# allow access to KVM to run the emulator
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- uses: futureware-tech/simulator-action@dab10d813144ef59b48d401cd95da151222ef8cd # pin@v4
if: ${{ matrix.platform == 'ios' }}
with:
# the same envs are used by Detox ci.sim configuration
model: ${{ env.IOS_DEVICE }}
os_version: ${{ env.IOS_VERSION }}

- name: Run Detox iOS Tests
if: ${{ matrix.platform == 'ios' }}
working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}
run: ${{ matrix.test-command }}

- name: Run tests on Android
if: ${{ matrix.platform == 'android' }}
env:
# used by Detox ci.android configuration
ANDROID_AVD_NAME: 'test' # test is default reactivecircus/android-emulator-runner name
ANDROID_TYPE: 'android.emulator'
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # [email protected]
with:
api-level: ${{ env.ANDROID_API_LEVEL }}
force-avd-creation: false
disable-animations: true
disable-spellchecker: true
target: 'aosp_atd'
channel: canary # Necessary for ATDs
emulator-options: >
-no-window
-no-snapshot-save
-gpu swiftshader_indirect
-noaudio
-no-boot-anim
-camera-back none
-camera-front none
-timezone US/Pacific
working-directory: ${{ env.REACT_NATIVE_SAMPLE_PATH }}
script: ${{ matrix.test-command }}

test:
name: Test ${{ matrix.platform }} ${{ matrix.build-type }} REV2
runs-on: ${{ matrix.runs-on }}
@@ -258,7 +367,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
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -364,6 +364,61 @@
- [changelog](https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/CHANGELOG.md#320)
- [diff](https://github.com/getsentry/sentry-javascript-bundler-plugins/compare/3.1.2...3.2.0)


## 6.7.0-alpha.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://[email protected]/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 <RNSentry/RNSentry.h>

@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[RNSentrySDK start];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
```

### 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<SentryAndroidOptions>` 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))

## 6.7.0

> [!WARNING]
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -8,4 +8,4 @@
"performance-tests/*"
],
"npmClient": "yarn"
}
}
2 changes: 1 addition & 1 deletion packages/core/RNSentry.podspec
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ 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'

s.compiler_flags = other_cflags

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"dsn": "invalid-dsn"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invalid-options
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dsn": "https://[email protected]/123456",
"enableTracing": true,
"tracesSampleRate": 1.0
}
Original file line number Diff line number Diff line change
@@ -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"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
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<SentryAndroidOptions> { options ->
options.dsn = "https://abcd@efgh.ingest.sentry.io/123456"
}
private val invalidConfig =
OptionsConfiguration<SentryAndroidOptions> { options ->
options.dsn = "invalid-dsn"
}
private val emptyConfig = OptionsConfiguration<SentryAndroidOptions> {}
}

@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)
assertEquals(false, actualOptions.enableTracing)
}

@Test
fun configurationOverridesDefaultOptions() {
val validConfig =
OptionsConfiguration<SentryAndroidOptions> { options ->
options.dsn = "https://abcd@efgh.ingest.sentry.io/123456"
options.tracesSampleRate = 0.5
options.enableTracing = true
}
RNSentrySDK.init(context, validConfig, MISSING, logger)
val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions
assertEquals(0.5, actualOptions.tracesSampleRate)
assertEquals(true, actualOptions.enableTracing)
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,
)
val pack = actualOptions.sdkVersion?.packages?.first { it.name == RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME }
assertNotNull(pack)
assertEquals(RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION, pack?.version)
assertNull(actualOptions.tracesSampleRate)
assertNull(actualOptions.tracesSampler)
assertEquals(false, actualOptions.enableTracing)
}

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"))
}
}
Original file line number Diff line number Diff line change
@@ -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<SentryAndroidOptions> = mock()
val overridingConfig: OptionsConfiguration<SentryAndroidOptions> = 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<SentryAndroidOptions> { options ->
options.dsn = "https://base-dsn@sentry.io"
options.isDebug = false
options.release = "some-release"
}
val overridingConfig =
OptionsConfiguration<SentryAndroidOptions> { 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
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
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 RNSentryModuleImplTest {
private lateinit var module: RNSentryModuleImpl
class RNSentryStartTest {
private lateinit var logger: ILogger

private lateinit var activity: Activity

@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
logger = mock(ILogger::class.java)

module = Utils.createRNSentryModuleWithMockedContext()
activity = mock(Activity::class.java)
}

@Test
@@ -37,7 +44,7 @@ class RNSentryModuleImplTest {
"http://localhost:8969/teststream",
)
val actualOptions = SentryAndroidOptions()
module.getSentryAndroidOptions(actualOptions, options, logger)
RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger)
assert(actualOptions.isEnableSpotlight)
assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl)
}
@@ -46,7 +53,7 @@ class RNSentryModuleImplTest {
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)
RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger)
assert(actualOptions.isEnableSpotlight)
assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl)
}
@@ -55,17 +62,10 @@ class RNSentryModuleImplTest {
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)
RNSentryStart.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()
@@ -76,7 +76,7 @@ class RNSentryModuleImplTest {
"devServerUrl",
"http://localhost:8081",
)
module.getSentryAndroidOptions(options, rnOptions, logger)
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)

val breadcrumb =
Breadcrumb().apply {
@@ -100,7 +100,7 @@ class RNSentryModuleImplTest {
"devServerUrl",
mockDevServerUrl,
)
module.getSentryAndroidOptions(options, rnOptions, logger)
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)

val breadcrumb =
Breadcrumb().apply {
@@ -123,7 +123,7 @@ class RNSentryModuleImplTest {
"devServerUrl",
"http://localhost:8081",
)
module.getSentryAndroidOptions(options, rnOptions, logger)
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)

val breadcrumb =
Breadcrumb().apply {
@@ -139,7 +139,7 @@ class RNSentryModuleImplTest {
@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)
RNSentryStart.getSentryAndroidOptions(options, JavaOnlyMap(), logger)

val breadcrumb =
Breadcrumb().apply {
@@ -156,7 +156,7 @@ class RNSentryModuleImplTest {
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)
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)

val breadcrumb =
Breadcrumb().apply {
@@ -173,7 +173,7 @@ class RNSentryModuleImplTest {
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)
RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger)

val breadcrumb =
Breadcrumb().apply {
@@ -185,4 +185,67 @@ class RNSentryModuleImplTest {

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,
)
assertEquals(true, actualOptions.sdkVersion?.packages?.isNotEmpty())
assertEquals(
RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME,
actualOptions.sdkVersion
?.packages
?.last()
?.name,
)
assertEquals(
RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION,
actualOptions.sdkVersion
?.packages
?.last()
?.version,
)
}

@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)
assertEquals(false, actualOptions.enableTracing)
}

@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"))
}
}
Original file line number Diff line number Diff line change
@@ -11,6 +11,12 @@
3339C4812D6625570088EB3A /* RNSentryUserTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3339C4802D6625570088EB3A /* RNSentryUserTests.mm */; };
336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */; };
3380C6C42CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */; };
33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */; };
339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */; };
339C6C422D3FD3AE00CA72ED /* RNSentryStartFromFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */; };
339C6C482D3FD9A700CA72ED /* invalid.options.json in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C462D3FD91900CA72ED /* invalid.options.json */; };
339C6C492D3FD9A700CA72ED /* invalid.options.txt in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C452D3FD90200CA72ED /* invalid.options.txt */; };
339C6C4B2D3FD9B200CA72ED /* valid.options.json in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */; };
33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; };
33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; };
33DEDFEA2D8DBE67006066E4 /* RNSentryOnDrawReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33DEDFE92D8DBE5B006066E4 /* RNSentryOnDrawReporterTests.swift */; };
@@ -30,6 +36,9 @@
332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentryCocoaTesterTests-Bridging-Header.h"; sourceTree = "<group>"; };
3339C47F2D6625260088EB3A /* RNSentry+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentry+Test.h"; sourceTree = "<group>"; };
3339C4802D6625570088EB3A /* RNSentryUserTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RNSentryUserTests.mm; sourceTree = "<group>"; };
333B58A82D35BA93000F8D04 /* RNSentryStart.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryStart.h; path = ../ios/RNSentryStart.h; sourceTree = SOURCE_ROOT; };
333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "RNSentryStart+Test.h"; path = "RNSentryCocoaTesterTests/RNSentryStart+Test.h"; sourceTree = SOURCE_ROOT; };
333B58AF2D36A7FD000F8D04 /* RNSentrySDK.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentrySDK.h; path = ../ios/RNSentrySDK.h; sourceTree = SOURCE_ROOT; };
336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNSentryReplayBreadcrumbConverterTests.swift; sourceTree = "<group>"; };
3360843A2C32E3A8008CC412 /* RNSentryReplayBreadcrumbConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryReplayBreadcrumbConverter.h; path = ../ios/RNSentryReplayBreadcrumbConverter.h; sourceTree = "<group>"; };
3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryBreadcrumbTests.swift; sourceTree = "<group>"; };
@@ -39,6 +48,14 @@
3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryReplayPostInitTests.swift; sourceTree = "<group>"; };
338739072A7D7D2800950DDD /* RNSentryReplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryReplay.h; path = ../ios/RNSentryReplay.h; sourceTree = "<group>"; };
33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryOnDrawReporter.h; path = ../ios/RNSentryOnDrawReporter.h; sourceTree = "<group>"; };
33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryOnDrawReporterTests.m; sourceTree = "<group>"; };
339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartTests.swift; sourceTree = "<group>"; };
339C6C3D2D3FA04D00CA72ED /* RNSentryVersion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryVersion.h; path = ../ios/RNSentryVersion.h; sourceTree = SOURCE_ROOT; };
339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartFromFileTests.swift; sourceTree = "<group>"; };
339C6C442D3FD62D00CA72ED /* RNSentrySDK+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentrySDK+Test.h"; sourceTree = "<group>"; };
339C6C452D3FD90200CA72ED /* invalid.options.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = invalid.options.txt; sourceTree = "<group>"; };
339C6C462D3FD91900CA72ED /* invalid.options.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = invalid.options.json; sourceTree = "<group>"; };
339C6C4A2D3FD9AB00CA72ED /* valid.options.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = valid.options.json; sourceTree = "<group>"; };
33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryFramesTrackerListenerTests.m; sourceTree = "<group>"; };
33AFDFEE2B8D14C200AAB120 /* RNSentryFramesTrackerListenerTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryFramesTrackerListenerTests.h; sourceTree = "<group>"; };
33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryDependencyContainerTests.m; sourceTree = "<group>"; };
@@ -78,6 +95,7 @@
3360896929524163007C7730 = {
isa = PBXGroup;
children = (
339C6C432D3FD41C00CA72ED /* TestAssets */,
33AFE0122B8F319000AAB120 /* RNSentry */,
3360899029524164007C7730 /* RNSentryCocoaTesterTests */,
3360897329524163007C7730 /* Products */,
@@ -97,6 +115,8 @@
3360899029524164007C7730 /* RNSentryCocoaTesterTests */ = {
isa = PBXGroup;
children = (
339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */,
339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */,
33DEDFEF2D9185E3006066E4 /* RNSentryTimeToDisplayTests.swift */,
33DEDFEC2D8DC820006066E4 /* RNSentryOnDrawReporter+Test.mm */,
33DEDFEB2D8DC800006066E4 /* RNSentryOnDrawReporter+Test.h */,
@@ -127,9 +147,24 @@
path = Replay;
sourceTree = "<group>";
};
339C6C432D3FD41C00CA72ED /* TestAssets */ = {
isa = PBXGroup;
children = (
339C6C4A2D3FD9AB00CA72ED /* valid.options.json */,
339C6C462D3FD91900CA72ED /* invalid.options.json */,
339C6C452D3FD90200CA72ED /* invalid.options.txt */,
);
path = TestAssets;
sourceTree = "<group>";
};
33AFE0122B8F319000AAB120 /* RNSentry */ = {
isa = PBXGroup;
children = (
339C6C442D3FD62D00CA72ED /* RNSentrySDK+Test.h */,
339C6C3D2D3FA04D00CA72ED /* RNSentryVersion.h */,
333B58AF2D36A7FD000F8D04 /* RNSentrySDK.h */,
333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */,
333B58A82D35BA93000F8D04 /* RNSentryStart.h */,
33DEDFEE2D8DD431006066E4 /* RNSentryTimeToDisplay.h */,
3380C6C02CDEC56B0018B9B6 /* Replay */,
332D33482CDBDC7300547D76 /* RNSentry.h */,
@@ -161,6 +196,7 @@
3360898929524164007C7730 /* Sources */,
BB7D14838753E6599863899B /* Frameworks */,
CC7959F3721CB3AD7CB6A047 /* [CP] Copy Pods Resources */,
339C6C472D3FD99900CA72ED /* Resources */,
);
buildRules = (
);
@@ -205,6 +241,19 @@
};
/* End PBXProject section */

/* Begin PBXResourcesBuildPhase section */
339C6C472D3FD99900CA72ED /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
339C6C482D3FD9A700CA72ED /* invalid.options.json in Resources */,
339C6C4B2D3FD9B200CA72ED /* valid.options.json in Resources */,
339C6C492D3FD9A700CA72ED /* invalid.options.txt in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
30F19D4E16BEEFEC68733838 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
@@ -254,8 +303,10 @@
files = (
AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */,
332D33472CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift in Sources */,
339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */,
33DEDFEA2D8DBE67006066E4 /* RNSentryOnDrawReporterTests.swift in Sources */,
33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */,
339C6C422D3FD3AE00CA72ED /* RNSentryStartFromFileTests.swift in Sources */,
336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */,
33DEDFED2D8DC825006066E4 /* RNSentryOnDrawReporter+Test.mm in Sources */,
33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */,
Original file line number Diff line number Diff line change
@@ -8,4 +8,7 @@
#import "RNSentryReplayBreadcrumbConverter.h"
#import "RNSentryReplayMask.h"
#import "RNSentryReplayUnmask.h"
#import "RNSentrySDK+Test.h"
#import "RNSentryStart.h"
#import "RNSentryTimeToDisplay.h"
#import "RNSentryVersion.h"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#import "RNSentryStart.h"

@interface
RNSentryStart (Test)

+ (void)setEventOriginTag:(SentryEvent *)event;

@end
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#import "RNSentrySDK.h"

@interface
RNSentrySDK (Test)

+ (void)start:(NSString *)path
configureOptions:(void (^)(SentryOptions *_Nonnull options))configureOptions;

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dsn": "https://abcd@efgh.ingest.sentry.io/123456",
"environment": "environment-from-invalid-file",
"invalid-option": 123
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invalid-options
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"dsn": "https://abcd@efgh.ingest.sentry.io/123456",
"environment": "environment-from-valid-file"
}
Original file line number Diff line number Diff line change
@@ -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<SentryAndroidOptions> {
private final @NotNull List<OptionsConfiguration<SentryAndroidOptions>> configurations;

@SafeVarargs
protected RNSentryCompositeOptionsConfiguration(
@NotNull OptionsConfiguration<SentryAndroidOptions>... configurations) {
this.configurations = List.of(configurations);
}

@Override
public void configure(@NotNull SentryAndroidOptions options) {
for (OptionsConfiguration<SentryAndroidOptions> configuration : configurations) {
if (configuration != null) {
configuration.configure(options);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -23,38 +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.HubAdapter;
import io.sentry.ILogger;
import io.sentry.IScope;
import io.sentry.ISentryExecutorService;
import io.sentry.ISerializer;
import io.sentry.Integration;
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.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;
@@ -63,11 +52,8 @@
import io.sentry.android.core.performance.AppStartMetrics;
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.RNSentryReplayMask;
import io.sentry.react.replay.RNSentryReplayUnmask;
import io.sentry.util.DebugMetaPropertiesApplier;
import io.sentry.util.FileUtils;
import io.sentry.util.JsonSerializationUtils;
@@ -80,8 +66,6 @@
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
@@ -178,8 +162,8 @@ public void initNativeReactNavigationNewFrameTracking(Promise promise) {
}

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);
}
@@ -195,209 +179,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("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"));
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());
}

// 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);

options.setBeforeSend(
(event, hint) -> {
setEventOriginTag(event);
addPackages(event, options.getSdkVersion());

return event;
});

if (rnOptions.hasKey("enableNativeCrashHandling")
&& !rnOptions.getBoolean("enableNativeCrashHandling")) {
final List<Integration> 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("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;
}

public void crash() {
throw new RuntimeException("TEST - Sentry Client Crash (only works in release mode)");
}
@@ -1052,51 +833,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) {
List<SentryPackage> sentryPackages = sdk.getPackages();
if (sentryPackages != null) {
for (SentryPackage sentryPackage : sentryPackages) {
eventSdk.addPackage(sentryPackage.getName(), sentryPackage.getVersion());
}
}

List<String> integrations = sdk.getIntegrations();
if (integrations != null) {
for (String integration : integrations) {
eventSdk.addIntegration(integration);
}
}

event.setSdk(eventSdk);
}
}

private boolean checkAndroidXAvailability() {
try {
Class.forName("androidx.core.app.FrameMetricsAggregator");
@@ -1110,17 +846,4 @@ private boolean checkAndroidXAvailability() {
private boolean isFrameMetricsAggregatorAvailable() {
return androidXAvailable && frameMetricsAggregator != null;
}

public 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<SentryAndroidOptions> 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<SentryAndroidOptions> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
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.Integration;
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.protocol.SentryPackage;
import io.sentry.react.replay.RNSentryReplayMask;
import io.sentry.react.replay.RNSentryReplayUnmask;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
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<SentryAndroidOptions> configuration) {
Sentry.OptionsConfiguration<SentryAndroidOptions> 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<SentryAndroidOptions> configuration,
@NotNull ILogger logger) {
Sentry.OptionsConfiguration<SentryAndroidOptions> defaults =
options -> updateWithReactDefaults(options, null);
Sentry.OptionsConfiguration<SentryAndroidOptions> 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<SentryAndroidOptions> defaults =
options -> updateWithReactDefaults(options, currentActivity);
Sentry.OptionsConfiguration<SentryAndroidOptions> 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"));
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());
}

// 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;
});

if (rnOptions.hasKey("enableNativeCrashHandling")
&& !rnOptions.getBoolean("enableNativeCrashHandling")) {
final List<Integration> 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()));
}

/**
* 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);
options.setEnableTracing(false);

// 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);
addPackages(event, options.getSdkVersion());
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 void addPackages(SentryEvent event, SdkVersion sdk) {
SdkVersion eventSdk = event.getSdk();
if (eventSdk != null
&& "sentry.javascript.react-native".equals(eventSdk.getName())
&& sdk != null) {
List<SentryPackage> sentryPackages = sdk.getPackages();
if (sentryPackages != null) {
for (SentryPackage sentryPackage : sentryPackages) {
eventSdk.addPackage(sentryPackage.getName(), sentryPackage.getVersion());
}
}

List<String> integrations = sdk.getIntegrations();
if (integrations != null) {
for (String integration : integrations) {
eventSdk.addIntegration(integration);
}
}

event.setSdk(eventSdk);
}
}

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();
}
}
8 changes: 3 additions & 5 deletions packages/core/ios/RNSentry.h
Original file line number Diff line number Diff line change
@@ -11,6 +11,9 @@
#import <Sentry/SentryDebugImageProvider.h>
#import <Sentry/SentryOptions.h>

// This import exposes public RNSentrySDK start
#import "RNSentrySDK.h"

typedef int (*SymbolicateCallbackType)(const void *, Dl_info *);

@interface
@@ -20,11 +23,6 @@ SentrySDK (Private)

@interface RNSentry : RCTEventEmitter <RCTBridgeModule>

- (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options
error:(NSError *_Nullable *_Nonnull)errorPointer;

- (void)setEventOriginTag:(SentryEvent *)event;

- (NSDictionary *_Nonnull)fetchNativeStackFramesBy:(NSArray<NSNumber *> *)instructionsAddr
symbolicate:(SymbolicateCallbackType)symbolicate;

171 changes: 2 additions & 169 deletions packages/core/ios/RNSentry.mm
Original file line number Diff line number Diff line change
@@ -49,6 +49,7 @@
# import "RNSentryRNSScreen.h"
#endif

#import "RNSentryStart.h"
#import "RNSentryVersion.h"

@interface
@@ -63,7 +64,6 @@ + (void)storeEnvelope:(SentryEnvelope *)envelope;
static bool hasFetchedAppStart;

@implementation RNSentry {
bool sentHybridSdkDidBecomeActive;
bool hasListeners;
RNSentryTimeToDisplay *_timeToDisplay;
}
@@ -94,181 +94,14 @@ - (instancetype)init
: (RCTPromiseRejectBlock)reject)
{
NSError *error = nil;
SentryOptions *sentryOptions = [self createOptionsWithDictionary:options error:&error];
[RNSentryStart startWithOptions:options error:&error];
if (error != nil) {
reject(@"SentryReactNative", error.localizedDescription, error);
return;
}

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:sentryOptions];

#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)) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"SentryHybridSdkDidBecomeActive"
object:nil];

sentHybridSdkDidBecomeActive = true;
}

#if SENTRY_TARGET_REPLAY_SUPPORTED
[RNSentryReplay postInit];
#endif

resolve(@YES);
}

- (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options
error:(NSError *_Nonnull *_Nonnull)errorPointer
{
SentryBeforeSendEventCallback beforeSend = ^SentryEvent *(SentryEvent *event)
{
// We don't want to send an event after startup that came from a Unhandled JS Exception of
// react native Because we sent it already before the app crashed.
if (nil != event.exceptions.firstObject.type &&
[event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location
!= NSNotFound) {
return nil;
}

[self setEventOriginTag:event];

return event;
};

NSMutableDictionary *mutableOptions = [options mutableCopy];
[mutableOptions setValue:beforeSend forKey:@"beforeSend"];

// remove performance traces sample rate and traces sampler since we don't want to synchronize
// these configurations to the Native SDKs. The user could tho initialize the SDK manually and
// set themselves.
[mutableOptions removeObjectForKey:@"tracesSampleRate"];
[mutableOptions removeObjectForKey:@"tracesSampler"];
[mutableOptions removeObjectForKey:@"enableTracing"];

#if SENTRY_TARGET_REPLAY_SUPPORTED
[RNSentryReplay updateOptions:mutableOptions];
#endif

SentryOptions *sentryOptions = [[SentryOptions alloc] 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"]];
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;
};

if ([mutableOptions valueForKey:@"enableNativeCrashHandling"] != nil) {
BOOL enableNativeCrashHandling = [mutableOptions[@"enableNativeCrashHandling"] boolValue];

if (!enableNativeCrashHandling) {
NSMutableArray *integrations = sentryOptions.integrations.mutableCopy;
[integrations removeObject:@"SentryCrashIntegration"];
sentryOptions.integrations = integrations;
}
}

// 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];
id defaultSpotlightUrl = [mutableOptions valueForKey:@"defaultSidecarUrl"];
if (defaultSpotlightUrl != nil) {
sentryOptions.spotlightUrl = defaultSpotlightUrl;
}
}
}

// Enable the App start and Frames tracking measurements
if ([mutableOptions valueForKey:@"enableAutoPerformanceTracing"] != nil) {
BOOL enableAutoPerformanceTracing =
[mutableOptions[@"enableAutoPerformanceTracing"] boolValue];
PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = enableAutoPerformanceTracing;
#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST
PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode = enableAutoPerformanceTracing;
#endif
}

// Failed requests can only be enabled in one SDK to avoid duplicates
sentryOptions.enableCaptureFailedRequests = NO;

return sentryOptions;
}

- (NSString *_Nullable)getURLFromDSN:(NSString *)dsn
{
NSURL *url = [NSURL URLWithString:dsn];
if (!url) {
return nil;
}
return [NSString stringWithFormat:@"%@://%@", url.scheme, url.host];
}

- (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;
}

RCT_EXPORT_METHOD(initNativeReactNavigationNewFrameTracking
: (RCTPromiseResolveBlock)resolve rejecter
: (RCTPromiseRejectBlock)reject)
31 changes: 31 additions & 0 deletions packages/core/ios/RNSentrySDK.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#import <Sentry/Sentry.h>

@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
71 changes: 71 additions & 0 deletions packages/core/ios/RNSentrySDK.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#import "RNSentrySDK.h"
#import "RNSentryStart.h"

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.
options = [[SentryOptions alloc] init];
}

[RNSentryStart updateWithReactDefaults:options];
if (configureOptions != nil) {
configureOptions(options);
}
[RNSentryStart updateWithReactFinals:options];
[RNSentryStart startWithOptions:options];
}

@end
26 changes: 26 additions & 0 deletions packages/core/ios/RNSentryStart.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#import <Sentry/SentryDefines.h>
#import <Sentry/SentryOptions.h>

@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
Loading