diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b5890013..8a93d03bba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +22,14 @@ - Rename `navigation.processing` span to more expressive `Navigation dispatch to screen A mounted/navigation cancelled` ([#4423](https://github.com/getsentry/sentry-react-native/pull/4423)) - Add RN SDK package to `sdk.packages` for Cocoa ([#4381](https://github.com/getsentry/sentry-react-native/pull/4381)) +- Add experimental version of `startWithConfigureOptions` for Apple platforms ([#4444](https://github.com/getsentry/sentry-react-native/pull/4444)) +- Add initialization using `sentry.options.json` for Apple platforms ([#4447](https://github.com/getsentry/sentry-react-native/pull/4447)) ### Internal - Initialize `RNSentryTimeToDisplay` during native module `init` on iOS ([#4443](https://github.com/getsentry/sentry-react-native/pull/4443)) +- Extract iOS native initialization to standalone structures ([#4442](https://github.com/getsentry/sentry-react-native/pull/4442)) +- Extract Android native initialization to standalone structures ([#4445](https://github.com/getsentry/sentry-react-native/pull/4445)) ### Dependencies diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 97c4fd315c..3c595c08eb 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -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 diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentryJsonConverterTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentryJsonConverterTest.kt new file mode 100644 index 0000000000..e49aa546f8 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentryJsonConverterTest.kt @@ -0,0 +1,103 @@ +package io.sentry.react + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import io.sentry.react.RNSentryJsonConverter.convertToWritable +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RNSentryJsonConverterTest { + @Test + fun testConvertToWritableWithSimpleJsonObject() { + val jsonObject = + JSONObject().apply { + put("floatKey", 12.3f) + put("doubleKey", 12.3) + put("intKey", 123) + put("stringKey", "test") + put("nullKey", JSONObject.NULL) + } + + val result: WritableMap? = convertToWritable(jsonObject) + + assertNotNull(result) + assertEquals(12.3, result!!.getDouble("floatKey"), 0.0001) + assertEquals(12.3, result.getDouble("doubleKey"), 0.0) + assertEquals(123, result.getInt("intKey")) + assertEquals("test", result.getString("stringKey")) + assertNull(result.getString("nullKey")) + } + + @Test + fun testConvertToWritableWithNestedJsonObject() { + val jsonObject = + JSONObject().apply { + put( + "nested", + JSONObject().apply { + put("key", "value") + }, + ) + } + + val result: WritableMap? = convertToWritable(jsonObject) + + assertNotNull(result) + val nestedMap = result!!.getMap("nested") + assertNotNull(nestedMap) + assertEquals("value", nestedMap!!.getString("key")) + } + + @Test + fun testConvertToWritableWithJsonArray() { + val jsonArray = + JSONArray().apply { + put(1) + put(2.5) + put("string") + put(JSONObject.NULL) + } + + val result: WritableArray = convertToWritable(jsonArray) + + assertEquals(1, result.getInt(0)) + assertEquals(2.5, result.getDouble(1), 0.0) + assertEquals("string", result.getString(2)) + assertNull(result.getString(3)) + } + + @Test + fun testConvertToWritableWithNestedJsonArray() { + val jsonObject = + JSONObject().apply { + put( + "array", + JSONArray().apply { + put( + JSONObject().apply { + put("key1", "value1") + }, + ) + put( + JSONObject().apply { + put("key2", "value2") + }, + ) + }, + ) + } + + val result: WritableMap? = convertToWritable(jsonObject) + + val array = result?.getArray("array") + assertEquals("value1", array?.getMap(0)?.getString("key1")) + assertEquals("value2", array?.getMap(1)?.getString("key2")) + } +} diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt index adffbf78ad..34af996a76 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt @@ -3,20 +3,13 @@ package io.sentry.react import android.content.pm.PackageInfo import android.content.pm.PackageManager import com.facebook.react.bridge.Arguments -import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableMap -import com.facebook.react.common.JavascriptException -import io.sentry.Breadcrumb import io.sentry.ILogger import io.sentry.SentryLevel -import io.sentry.android.core.SentryAndroidOptions import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -103,163 +96,4 @@ class RNSentryModuleImplTest { val capturedMap = writableMapCaptor.value assertEquals(false, capturedMap.getBoolean("has_fetched")) } - - @Test - fun `when the spotlight option is enabled, the spotlight SentryAndroidOption is set to true and the default url is used`() { - val options = - JavaOnlyMap.of( - "spotlight", - true, - "defaultSidecarUrl", - "http://localhost:8969/teststream", - ) - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, options, logger) - assert(actualOptions.isEnableSpotlight) - assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) - } - - @Test - fun `when the spotlight url is passed, the spotlight is enabled for the given url`() { - val options = JavaOnlyMap.of("spotlight", "http://localhost:8969/teststream") - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, options, logger) - assert(actualOptions.isEnableSpotlight) - assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) - } - - @Test - fun `when the spotlight option is disabled, the spotlight SentryAndroidOption is set to false`() { - val options = JavaOnlyMap.of("spotlight", false) - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, options, logger) - assertFalse(actualOptions.isEnableSpotlight) - } - - @Test - fun `the JavascriptException is added to the ignoredExceptionsForType list on initialisation`() { - val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, JavaOnlyMap.of(), logger) - assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) - } - - @Test - fun `beforeBreadcrumb callback filters out Sentry DSN requests breadcrumbs`() { - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "dsn", - "https://abc@def.ingest.sentry.io/1234567", - "devServerUrl", - "http://localhost:8081", - ) - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "https://def.ingest.sentry.io/1234567") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertNull("Breadcrumb should be filtered out", result) - } - - @Test - fun `beforeBreadcrumb callback filters out dev server breadcrumbs`() { - val mockDevServerUrl = "http://localhost:8081" - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "dsn", - "https://abc@def.ingest.sentry.io/1234567", - "devServerUrl", - mockDevServerUrl, - ) - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", mockDevServerUrl) - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertNull("Breadcrumb should be filtered out", result) - } - - @Test - fun `beforeBreadcrumb callback does not filter out non dev server or dsn breadcrumbs`() { - val options = SentryAndroidOptions() - val rnOptions = - JavaOnlyMap.of( - "dsn", - "https://abc@def.ingest.sentry.io/1234567", - "devServerUrl", - "http://localhost:8081", - ) - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } - - @Test - fun `the breadcrumb is not filtered out when the dev server url and dsn are not passed`() { - val options = SentryAndroidOptions() - module.getSentryAndroidOptions(options, JavaOnlyMap(), logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } - - @Test - fun `the breadcrumb is not filtered out when the dev server url is not passed and the dsn does not match`() { - val options = SentryAndroidOptions() - val rnOptions = JavaOnlyMap.of("dsn", "https://abc@def.ingest.sentry.io/1234567") - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } - - @Test - fun `the breadcrumb is not filtered out when the dev server url does not match and the dsn is not passed`() { - val options = SentryAndroidOptions() - val rnOptions = JavaOnlyMap.of("devServerUrl", "http://localhost:8081") - module.getSentryAndroidOptions(options, rnOptions, logger) - - val breadcrumb = - Breadcrumb().apply { - type = "http" - setData("url", "http://testurl.com/service") - } - - val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) - - assertEquals(breadcrumb, result) - } } diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt new file mode 100644 index 0000000000..c2ee6f1d88 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt @@ -0,0 +1,191 @@ +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.android.core.SentryAndroidOptions +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +@RunWith(JUnit4::class) +class RNSentryStartTest { + private lateinit var logger: ILogger + + private lateinit var activity: Activity + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + logger = mock(ILogger::class.java) + activity = mock(Activity::class.java) + } + + @Test + fun `when the spotlight option is enabled, the spotlight SentryAndroidOption is set to true and the default url is used`() { + val options = + JavaOnlyMap.of( + "spotlight", + true, + "defaultSidecarUrl", + "http://localhost:8969/teststream", + ) + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) + assert(actualOptions.isEnableSpotlight) + assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) + } + + @Test + fun `when the spotlight url is passed, the spotlight is enabled for the given url`() { + val options = JavaOnlyMap.of("spotlight", "http://localhost:8969/teststream") + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) + assert(actualOptions.isEnableSpotlight) + assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) + } + + @Test + fun `when the spotlight option is disabled, the spotlight SentryAndroidOption is set to false`() { + val options = JavaOnlyMap.of("spotlight", false) + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) + assertFalse(actualOptions.isEnableSpotlight) + } + + @Test + fun `the JavascriptException is added to the ignoredExceptionsForType list on initialisation`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(actualOptions, JavaOnlyMap.of(), activity, logger) + assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) + } + + @Test + fun `beforeBreadcrumb callback filters out Sentry DSN requests breadcrumbs`() { + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "dsn", + "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", + "http://localhost:8081", + ) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "https://def.ingest.sentry.io/1234567") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertNull("Breadcrumb should be filtered out", result) + } + + @Test + fun `beforeBreadcrumb callback filters out dev server breadcrumbs`() { + val mockDevServerUrl = "http://localhost:8081" + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "dsn", + "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", + mockDevServerUrl, + ) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", mockDevServerUrl) + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertNull("Breadcrumb should be filtered out", result) + } + + @Test + fun `beforeBreadcrumb callback does not filter out non dev server or dsn breadcrumbs`() { + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "dsn", + "https://abc@def.ingest.sentry.io/1234567", + "devServerUrl", + "http://localhost:8081", + ) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url and dsn are not passed`() { + val options = SentryAndroidOptions() + RNSentryStart.getSentryAndroidOptions(options, JavaOnlyMap(), activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url is not passed and the dsn does not match`() { + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of("dsn", "https://abc@def.ingest.sentry.io/1234567") + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } + + @Test + fun `the breadcrumb is not filtered out when the dev server url does not match and the dsn is not passed`() { + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of("devServerUrl", "http://localhost:8081") + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + + val breadcrumb = + Breadcrumb().apply { + type = "http" + setData("url", "http://testurl.com/service") + } + + val result = options.beforeBreadcrumb?.execute(breadcrumb, mock()) + + assertEquals(breadcrumb, result) + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index f78b1be0e0..0d82e39ef1 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -11,6 +11,11 @@ 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 */; }; 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.mm */; }; @@ -25,6 +30,9 @@ 332D33482CDBDC7300547D76 /* RNSentry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentry.h; path = ../ios/RNSentry.h; sourceTree = SOURCE_ROOT; }; 332D33492CDCC8E100547D76 /* RNSentryTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryTests.h; sourceTree = ""; }; 332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentryCocoaTesterTests-Bridging-Header.h"; sourceTree = ""; }; + 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 = ""; }; 3360843A2C32E3A8008CC412 /* RNSentryReplayBreadcrumbConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryReplayBreadcrumbConverter.h; path = ../ios/RNSentryReplayBreadcrumbConverter.h; sourceTree = ""; }; 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryBreadcrumbTests.swift; sourceTree = ""; }; @@ -35,6 +43,13 @@ 338739072A7D7D2800950DDD /* RNSentryReplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryReplay.h; path = ../ios/RNSentryReplay.h; sourceTree = ""; }; 33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryOnDrawReporter.h; path = ../ios/RNSentryOnDrawReporter.h; sourceTree = ""; }; 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryOnDrawReporterTests.m; sourceTree = ""; }; + 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartTests.swift; sourceTree = ""; }; + 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 = ""; }; + 339C6C442D3FD62D00CA72ED /* RNSentrySDK+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentrySDK+Test.h"; sourceTree = ""; }; + 339C6C452D3FD90200CA72ED /* invalid.options.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = invalid.options.txt; sourceTree = ""; }; + 339C6C462D3FD91900CA72ED /* invalid.options.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = invalid.options.json; sourceTree = ""; }; + 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = valid.options.json; sourceTree = ""; }; 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryFramesTrackerListenerTests.m; sourceTree = ""; }; 33AFDFEE2B8D14C200AAB120 /* RNSentryFramesTrackerListenerTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryFramesTrackerListenerTests.h; sourceTree = ""; }; 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryDependencyContainerTests.m; sourceTree = ""; }; @@ -69,6 +84,7 @@ 3360896929524163007C7730 = { isa = PBXGroup; children = ( + 339C6C432D3FD41C00CA72ED /* TestAssets */, 33AFE0122B8F319000AAB120 /* RNSentry */, 3360899029524164007C7730 /* RNSentryCocoaTesterTests */, 3360897329524163007C7730 /* Products */, @@ -88,6 +104,8 @@ 3360899029524164007C7730 /* RNSentryCocoaTesterTests */ = { isa = PBXGroup; children = ( + 339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */, + 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */, 332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */, 332D33492CDCC8E100547D76 /* RNSentryTests.h */, 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */, @@ -113,9 +131,24 @@ path = Replay; sourceTree = ""; }; + 339C6C432D3FD41C00CA72ED /* TestAssets */ = { + isa = PBXGroup; + children = ( + 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */, + 339C6C462D3FD91900CA72ED /* invalid.options.json */, + 339C6C452D3FD90200CA72ED /* invalid.options.txt */, + ); + path = TestAssets; + sourceTree = ""; + }; 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( + 339C6C442D3FD62D00CA72ED /* RNSentrySDK+Test.h */, + 339C6C3D2D3FA04D00CA72ED /* RNSentryVersion.h */, + 333B58AF2D36A7FD000F8D04 /* RNSentrySDK.h */, + 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */, + 333B58A82D35BA93000F8D04 /* RNSentryStart.h */, 3380C6C02CDEC56B0018B9B6 /* Replay */, 332D33482CDBDC7300547D76 /* RNSentry.h */, 3360843A2C32E3A8008CC412 /* RNSentryReplayBreadcrumbConverter.h */, @@ -146,6 +179,7 @@ 3360898929524164007C7730 /* Sources */, BB7D14838753E6599863899B /* Frameworks */, CC7959F3721CB3AD7CB6A047 /* [CP] Copy Pods Resources */, + 339C6C472D3FD99900CA72ED /* Resources */, ); buildRules = ( ); @@ -190,6 +224,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; @@ -239,7 +286,9 @@ files = ( AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */, 332D33472CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift in Sources */, + 339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */, 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */, + 339C6C422D3FD3AE00CA72ED /* RNSentryStartFromFileTests.swift in Sources */, 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */, 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */, 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */, diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h index bc2bdd0304..08fddcbf8e 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h @@ -7,3 +7,6 @@ #import "RNSentryReplayBreadcrumbConverter.h" #import "RNSentryReplayMask.h" #import "RNSentryReplayUnmask.h" +#import "RNSentrySDK+Test.h" +#import "RNSentryStart.h" +#import "RNSentryVersion.h" diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h new file mode 100644 index 0000000000..fcdfe7872b --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h @@ -0,0 +1,8 @@ +#import "RNSentryStart.h" + +@interface +RNSentryStart (Test) + ++ (void)setEventOriginTag:(SentryEvent *)event; + +@end diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift new file mode 100644 index 0000000000..e0269a5961 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift @@ -0,0 +1,115 @@ +import XCTest + +final class RNSentryStartFromFileTests: XCTestCase { + + func testNoThrowOnMissingOptionsFile() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getNonExistingOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNil(actualOptions.parsedDsn) + } + + func testNoThrowOnInvalidFileType() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getInvalidOptionsTypePath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNil(actualOptions.parsedDsn) + } + + func testNoThrowOnInvalidOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getInvalidOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNotNil(actualOptions.parsedDsn) + XCTAssertEqual(actualOptions.environment, "environment-from-invalid-file") + } + + func testLoadValidOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getValidOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNotNil(actualOptions.parsedDsn) + XCTAssertEqual(actualOptions.environment, "environment-from-valid-file") + } + + func testOptionsFromFileInConfigureOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getValidOptionsPath()) { options in + wasConfigurationCalled = true + XCTAssertEqual(options.environment, "environment-from-valid-file") + } + + XCTAssertTrue(wasConfigurationCalled) + } + + func testOptionsOverwrittenInConfigureOptions() { + RNSentrySDK.start(getValidOptionsPath()) { options in + options.environment = "new-environment" + } + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertEqual(actualOptions.environment, "new-environment") + } + + func getNonExistingOptionsPath() -> String { + return "/non-existing.options.json" + } + + func getInvalidOptionsTypePath() -> String { + guard let path = getTestBundle().path(forResource: "invalid.options", ofType: "txt") else { + fatalError("Could not get invalid type options path") + } + return path + } + + func getInvalidOptionsPath() -> String { + guard let path = getTestBundle().path(forResource: "invalid.options", ofType: "json") else { + fatalError("Could not get invalid options path") + } + return path + } + + func getValidOptionsPath() -> String { + guard let path = getTestBundle().path(forResource: "valid.options", ofType: "json") else { + fatalError("Could not get invalid options path") + } + return path + } + + func getTestBundle() -> Bundle { + let maybeBundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix(".xctest") }) + guard let bundle = maybeBundle else { + fatalError("Could not find test bundle") + } + return bundle + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift new file mode 100644 index 0000000000..b9d12200cf --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift @@ -0,0 +1,248 @@ +import XCTest + +final class RNSentryStartTests: XCTestCase { + + func testStartDoesNotThrowWithoutConfigure() { + RNSentrySDK.start(configureOptions: nil) + } + + func assertReactDefaults(_ actualOptions: Options?) { + XCTAssertFalse(actualOptions!.enableCaptureFailedRequests) + XCTAssertNil(actualOptions!.tracesSampleRate) + XCTAssertNil(actualOptions!.tracesSampler) + XCTAssertFalse(actualOptions!.enableTracing) + } + + func testStartSetsReactDeafults() { + var actualOptions: Options? + + RNSentrySDK.start { options in + actualOptions = options + } + + XCTAssertNotNil(actualOptions, "start have not provided default options or have not executed configure callback") + assertReactDefaults(actualOptions) + } + + func testAutoStartSetsReactDefaults() throws { + try startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + + let actualOptions = PrivateSentrySDKOnly.options + assertReactDefaults(actualOptions) + } + + func testStartEnablesHybridTracing() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + }, + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.enableAutoPerformanceTracing = true + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "enableAutoPerformanceTracing": true + ]) + } + ] + + // Test each implementation + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + XCTAssertTrue(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) + XCTAssertTrue(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + } + } + + func testStartDisablesHybridTracing() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.enableAutoPerformanceTracing = false + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "enableAutoPerformanceTracing": false + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + XCTAssertFalse(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) + XCTAssertFalse(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + } + } + + func testStartIgnoresUnhandledJsExceptions() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + let actualEvent = actualOptions.beforeSend!(createUnhandledJsExceptionEvent()) + + XCTAssertNil(actualEvent) + } + } + + func testStartSetsNativeEventOrigin() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + let actualEvent = actualOptions.beforeSend!(createNativeEvent()) + + XCTAssertNotNil(actualEvent) + XCTAssertNotNil(actualEvent!.tags) + XCTAssertEqual(actualEvent!.tags!["event.origin"], "ios") + XCTAssertEqual(actualEvent!.tags!["event.environment"], "native") + } + } + + func testStartDoesNotOverwriteUserBeforeSend() { + var executed = false + + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.beforeSend = { event in + executed = true + return event + } + } + + PrivateSentrySDKOnly.options.beforeSend!(genericEvent()) + + XCTAssertTrue(executed) + } + + func testStartSetsHybridSdkName() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualEvent = captuteTestEvent() + + XCTAssertNotNil(actualEvent) + XCTAssertNotNil(actualEvent!.sdk) + XCTAssertEqual(actualEvent!.sdk!["name"] as! String, NATIVE_SDK_NAME) + + let packages = actualEvent!.sdk!["packages"] as! [[String: String]] + let reactPackage = packages.first { $0["name"] == REACT_NATIVE_SDK_PACKAGE_NAME } + + XCTAssertNotNil(reactPackage) + XCTAssertEqual(reactPackage!["name"], REACT_NATIVE_SDK_PACKAGE_NAME) + XCTAssertEqual(reactPackage!["version"], REACT_NATIVE_SDK_PACKAGE_VERSION) + } + } + + func startFromRN(options: [String: Any]) throws { + var error: NSError? + RNSentryStart.start(options: options, error: &error) + + if let error = error { + throw error + } + } + + func createUnhandledJsExceptionEvent() -> Event { + let event = Event() + event.exceptions = [] + event.exceptions!.append(Exception(value: "Test", type: "Unhandled JS Exception: undefined is not a function")) + return event + } + + func createNativeEvent() -> Event { + let event = Event() + event.sdk = [ + "name": NATIVE_SDK_NAME, + "version": "1.2.3" + ] + return event + } + + func genericEvent() -> Event { + return Event() + } + + func captuteTestEvent() -> Event? { + var actualEvent: Event? + + // This is the closest to the sent event we can get using the actual Sentry start method + let originalBeforeSend = PrivateSentrySDKOnly.options.beforeSend + PrivateSentrySDKOnly.options.beforeSend = { event in + if let originalBeforeSend = originalBeforeSend { + let processedEvent = originalBeforeSend(event) + actualEvent = processedEvent + return processedEvent + } + actualEvent = event + return event + } + + SentrySDK.capture(message: "Test") + + return actualEvent + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm index 6e63793b85..abe2ae70ce 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm @@ -1,6 +1,8 @@ #import "RNSentryTests.h" +#import "RNSentryStart+Test.h" #import #import +#import #import #import #import @@ -11,9 +13,8 @@ @interface RNSentryInitNativeSdkTests : XCTestCase @implementation RNSentryInitNativeSdkTests -- (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties +- (void)testStartWithDictionaryRemovesPerformanceProperties { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @@ -25,9 +26,8 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties , @"enableTracing" : @YES, } ; -SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; - +[RNSentryStart startWithOptions:mockedReactNativeDictionary error:&error]; +SentryOptions *actualOptions = PrivateSentrySDKOnly.options; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertNotNil( @@ -40,14 +40,13 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties - (void)testCaptureFailedRequestsIsDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + [RNSentryStart startWithOptions:mockedReactNativeDictionary error:&error]; + SentryOptions *actualOptions = PrivateSentrySDKOnly.options; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); @@ -56,14 +55,13 @@ - (void)testCaptureFailedRequestsIsDisabled - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDefault { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual([actualOptions.integrations containsObject:@"SentryCrashIntegration"], true, @@ -72,14 +70,13 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDefault - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDefault { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual( @@ -88,15 +85,14 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDefault - (void)testCreateOptionsWithDictionaryNativeCrashHandlingEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableNativeCrashHandling" : @YES, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual([actualOptions.integrations containsObject:@"SentryCrashIntegration"], true, @@ -105,15 +101,14 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingEnabled - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableAutoPerformanceTracing" : @YES, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual( @@ -122,15 +117,14 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingEnabled - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableNativeCrashHandling" : @NO, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual([actualOptions.integrations containsObject:@"SentryCrashIntegration"], false, @@ -139,15 +133,14 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDisabled - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableAutoPerformanceTracing" : @NO, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual(actualOptions.enableAutoPerformanceTracing, false, @@ -156,7 +149,6 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDisabled - (void)testCreateOptionsWithDictionarySpotlightEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -164,8 +156,8 @@ - (void)testCreateOptionsWithDictionarySpotlightEnabled @"spotlight" : @YES, @"defaultSidecarUrl" : @"http://localhost:8969/teststream", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); @@ -174,7 +166,6 @@ - (void)testCreateOptionsWithDictionarySpotlightEnabled - (void)testCreateOptionsWithDictionarySpotlightOne { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -182,8 +173,8 @@ - (void)testCreateOptionsWithDictionarySpotlightOne @"spotlight" : @1, @"defaultSidecarUrl" : @"http://localhost:8969/teststream", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); @@ -192,15 +183,14 @@ - (void)testCreateOptionsWithDictionarySpotlightOne - (void)testCreateOptionsWithDictionarySpotlightUrl { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"spotlight" : @"http://localhost:8969/teststream", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); @@ -209,15 +199,14 @@ - (void)testCreateOptionsWithDictionarySpotlightUrl - (void)testCreateOptionsWithDictionarySpotlightDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"spotlight" : @NO, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertFalse(actualOptions.enableSpotlight, @"Did not disable spotlight"); @@ -225,15 +214,14 @@ - (void)testCreateOptionsWithDictionarySpotlightDisabled - (void)testCreateOptionsWithDictionarySpotlightZero { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"spotlight" : @0, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertFalse(actualOptions.enableSpotlight, @"Did not disable spotlight"); @@ -241,14 +229,13 @@ - (void)testCreateOptionsWithDictionarySpotlightZero - (void)testPassesErrorOnWrongDsn { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"not_a_valid_dsn", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNil(actualOptions, @"Created invalid sentry options"); XCTAssertNotNil(error, @"Did not created error on invalid dsn"); @@ -256,14 +243,14 @@ - (void)testPassesErrorOnWrongDsn - (void)testBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", @"devServerUrl" : @"http://localhost:8081" }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -276,14 +263,14 @@ - (void)testBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs - (void)testBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSString *mockDevServer = @"http://localhost:8081"; NSDictionary *_Nonnull mockedDictionary = @{ @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", @"devServerUrl" : mockDevServer }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -296,14 +283,14 @@ - (void)testBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs - (void)testBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", @"devServerUrl" : @"http://localhost:8081" }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -316,13 +303,13 @@ - (void)testBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBr - (void)testBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedAndDsnDoesNotMatch { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ // dsn is always validated in SentryOptions initialization @"dsn" : @"https://abc@def.ingest.sentry.io/1234567" }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -335,13 +322,12 @@ - (void)testBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedA - (void)testEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags { - RNSentry *rnSentry = [[RNSentry alloc] init]; SentryEvent *testEvent = [[SentryEvent alloc] init]; testEvent.sdk = @{ @"name" : @"sentry.cocoa.react-native", }; - [rnSentry setEventOriginTag:testEvent]; + [RNSentryStart setEventOriginTag:testEvent]; XCTAssertEqual(testEvent.tags[@"event.origin"], @"ios"); XCTAssertEqual(testEvent.tags[@"event.environment"], @"native"); @@ -349,7 +335,6 @@ - (void)testEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags - (void)testEventFromSentryReactNativeOriginAndEnvironmentTagsAreOverwritten { - RNSentry *rnSentry = [[RNSentry alloc] init]; SentryEvent *testEvent = [[SentryEvent alloc] init]; testEvent.sdk = @{ @"name" : @"sentry.cocoa.react-native", @@ -359,7 +344,7 @@ - (void)testEventFromSentryReactNativeOriginAndEnvironmentTagsAreOverwritten @"event.environment" : @"testEventEnvironmentTag", }; - [rnSentry setEventOriginTag:testEvent]; + [RNSentryStart setEventOriginTag:testEvent]; XCTAssertEqual(testEvent.tags[@"event.origin"], @"ios"); XCTAssertEqual(testEvent.tags[@"event.environment"], @"native"); diff --git a/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h b/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h new file mode 100644 index 0000000000..06da31b42d --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h @@ -0,0 +1,9 @@ +#import "RNSentrySDK.h" + +@interface +RNSentrySDK (Test) + ++ (void)start:(NSString *)path + configureOptions:(void (^)(SentryOptions *_Nonnull options))configureOptions; + +@end diff --git a/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json new file mode 100644 index 0000000000..bf8f2be64c --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json @@ -0,0 +1,5 @@ +{ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "environment": "environment-from-invalid-file", + "invalid-option": 123 +} diff --git a/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt new file mode 100644 index 0000000000..601553b507 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt @@ -0,0 +1 @@ +invalid-options diff --git a/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json b/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json new file mode 100644 index 0000000000..641087d5e8 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json @@ -0,0 +1,4 @@ +{ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "environment": "environment-from-valid-file" +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java new file mode 100644 index 0000000000..44ec324eed --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonConverter.java @@ -0,0 +1,76 @@ +package io.sentry.react; + +import com.facebook.react.bridge.JavaOnlyArray; +import com.facebook.react.bridge.JavaOnlyMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import io.sentry.android.core.AndroidLogger; +import java.util.Iterator; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +final class RNSentryJsonConverter { + public static final String NAME = "RNSentry.RNSentryJsonConverter"; + + private static final ILogger logger = new AndroidLogger(NAME); + + private RNSentryJsonConverter() { + throw new AssertionError("Utility class should not be instantiated"); + } + + @Nullable + static WritableMap convertToWritable(@NotNull JSONObject jsonObject) { + try { + WritableMap writableMap = new JavaOnlyMap(); + Iterator iterator = jsonObject.keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = jsonObject.get(key); + if (value instanceof Float || value instanceof Double) { + writableMap.putDouble(key, jsonObject.getDouble(key)); + } else if (value instanceof Number) { + writableMap.putInt(key, jsonObject.getInt(key)); + } else if (value instanceof String) { + writableMap.putString(key, jsonObject.getString(key)); + } else if (value instanceof JSONObject) { + writableMap.putMap(key, convertToWritable(jsonObject.getJSONObject(key))); + } else if (value instanceof JSONArray) { + writableMap.putArray(key, convertToWritable(jsonObject.getJSONArray(key))); + } else if (value == JSONObject.NULL) { + writableMap.putNull(key); + } + } + return writableMap; + } catch (JSONException e) { + logger.log(SentryLevel.ERROR, "Error parsing json object:" + e.getMessage()); + return null; + } + } + + @NotNull + static WritableArray convertToWritable(@NotNull JSONArray jsonArray) throws JSONException { + WritableArray writableArray = new JavaOnlyArray(); + for (int i = 0; i < jsonArray.length(); i++) { + Object value = jsonArray.get(i); + if (value instanceof Float || value instanceof Double) { + writableArray.pushDouble(jsonArray.getDouble(i)); + } else if (value instanceof Number) { + writableArray.pushInt(jsonArray.getInt(i)); + } else if (value instanceof String) { + writableArray.pushString(jsonArray.getString(i)); + } else if (value instanceof JSONObject) { + writableArray.pushMap(convertToWritable(jsonArray.getJSONObject(i))); + } else if (value instanceof JSONArray) { + writableArray.pushArray(convertToWritable(jsonArray.getJSONArray(i))); + } else if (value == JSONObject.NULL) { + writableArray.pushNull(); + } + } + return writableArray; + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 486ae72c48..3ffc3eb617 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -20,13 +20,11 @@ import com.facebook.react.bridge.ReactApplicationContext; 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 com.facebook.react.modules.core.DeviceEventManagerModule; import io.sentry.Breadcrumb; import io.sentry.HubAdapter; @@ -34,25 +32,16 @@ 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; @@ -61,11 +50,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; @@ -77,8 +63,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,216 +162,12 @@ public void initNativeReactNavigationNewFrameTracking(Promise promise) { } public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { - SentryAndroid.init( - this.getReactApplicationContext(), - options -> getSentryAndroidOptions(options, rnOptions, logger)); + RNSentryStart.startWithOptions( + this.getReactApplicationContext(), rnOptions, getCurrentActivity(), logger); promise.resolve(true); } - 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 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)"); } @@ -974,51 +754,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 sentryPackages = sdk.getPackages(); - if (sentryPackages != null) { - for (SentryPackage sentryPackage : sentryPackages) { - eventSdk.addPackage(sentryPackage.getName(), sentryPackage.getVersion()); - } - } - - List 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"); @@ -1032,17 +767,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(); - } } diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java new file mode 100644 index 0000000000..263633c4a8 --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -0,0 +1,311 @@ +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.SentryEvent; +import io.sentry.SentryLevel; +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; + +public final class RNSentryStart { + + private RNSentryStart() { + throw new AssertionError("Utility class should not be instantiated"); + } + + public static void startWithOptions( + @NotNull final Context context, + @NotNull final ReadableMap rnOptions, + @Nullable Activity currentActivity, + @NotNull ILogger logger) { + SentryAndroid.init( + context, options -> getSentryAndroidOptions(options, rnOptions, currentActivity, logger)); + } + + static void getSentryAndroidOptions( + @NotNull SentryAndroidOptions options, + @NotNull ReadableMap rnOptions, + @Nullable Activity currentActivity, + 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 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())); + + setCurrentActivity(currentActivity); + } + + 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 sentryPackages = sdk.getPackages(); + if (sentryPackages != null) { + for (SentryPackage sentryPackage : sentryPackages) { + eventSdk.addPackage(sentryPackage.getName(), sentryPackage.getVersion()); + } + } + + List 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(); + } +} diff --git a/packages/core/ios/RNSentry.h b/packages/core/ios/RNSentry.h index cfd0b74b28..c7fb93e0ea 100644 --- a/packages/core/ios/RNSentry.h +++ b/packages/core/ios/RNSentry.h @@ -11,6 +11,9 @@ #import #import +// 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 -- (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options - error:(NSError *_Nullable *_Nonnull)errorPointer; - -- (void)setEventOriginTag:(SentryEvent *)event; - - (NSDictionary *_Nonnull)fetchNativeStackFramesBy:(NSArray *)instructionsAddr symbolicate:(SymbolicateCallbackType)symbolicate; diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 79ff76d0ae..69fd287403 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -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) diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentrySDK.h new file mode 100644 index 0000000000..232071d9bc --- /dev/null +++ b/packages/core/ios/RNSentrySDK.h @@ -0,0 +1,31 @@ +#import + +@interface RNSentrySDK : NSObject +SENTRY_NO_INIT + +/** + * @experimental + * Inits and configures Sentry for React Native applications using `sentry.options.json` + * configuration file. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)start; + +/** + * @experimental + * Inits and configures Sentry for React Native applicationsusing `sentry.options.json` + * configuration file and `configureOptions` callback. + * + * The `configureOptions` callback can overwrite the config file options + * and add non-serializable items to the options object. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)startWithConfigureOptions: + (void (^_Nullable)(SentryOptions *_Nonnull options))configureOptions + NS_SWIFT_NAME(start(configureOptions:)); + +@end diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m new file mode 100644 index 0000000000..7d7f4cf9b3 --- /dev/null +++ b/packages/core/ios/RNSentrySDK.m @@ -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 diff --git a/packages/core/ios/RNSentryStart.h b/packages/core/ios/RNSentryStart.h new file mode 100644 index 0000000000..01a0617148 --- /dev/null +++ b/packages/core/ios/RNSentryStart.h @@ -0,0 +1,26 @@ +#import +#import + +@interface RNSentryStart : NSObject +SENTRY_NO_INIT + ++ (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions + error:(NSError *_Nullable *_Nullable)errorPointer; + ++ (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options + error:(NSError *_Nonnull *_Nonnull)errorPointer; + ++ (void)updateWithReactDefaults:(SentryOptions *)options; ++ (void)updateWithReactFinals:(SentryOptions *)options; + +/** + * @experimental + * Inits and configures Sentry for React Native applications. Make sure to + * set a valid DSN. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)); + +@end diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m new file mode 100644 index 0000000000..84e2d83b02 --- /dev/null +++ b/packages/core/ios/RNSentryStart.m @@ -0,0 +1,222 @@ +#import "RNSentryStart.h" +#import "RNSentryReplay.h" +#import "RNSentryVersion.h" + +#import +#import +#import + +@implementation RNSentryStart + ++ (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions + error:(NSError *_Nullable *_Nullable)errorPointer +{ + SentryOptions *options = [self createOptionsWithDictionary:javascriptOptions + error:errorPointer]; + [self updateWithReactDefaults:options]; + [self updateWithReactFinals:options]; + [self startWithOptions:options]; +} + ++ (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) +{ + NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; + [PrivateSentrySDKOnly setSdkName:NATIVE_SDK_NAME andVersionString:sdkVersion]; + [PrivateSentrySDKOnly addSdkPackage:REACT_NATIVE_SDK_PACKAGE_NAME + version:REACT_NATIVE_SDK_PACKAGE_VERSION]; + + [SentrySDK startWithOptions:options]; + +#if SENTRY_TARGET_REPLAY_SUPPORTED + [RNSentryReplay postInit]; +#endif + + [self postDidBecomeActiveNotification]; +} + ++ (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options + error:(NSError *_Nonnull *_Nonnull)errorPointer +{ + NSMutableDictionary *mutableOptions = [options mutableCopy]; + +#if SENTRY_TARGET_REPLAY_SUPPORTED + [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"]]; + // TODO: For Auto Init from JS dev server is resolved automatically, for init from options file + // dev server has to be specified manually + NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; + sentryOptions.beforeBreadcrumb + = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) + { + NSString *url = breadcrumb.data[@"url"] ?: @""; + + if ([@"http" isEqualToString:breadcrumb.type] + && ((dsn != nil && [url hasPrefix:dsn]) + || (devServerUrl != nil && [url hasPrefix:devServerUrl]))) { + return nil; + } + return breadcrumb; + }; + + // JS options.enableNativeCrashHandling equals to native options.enableCrashHandler + if ([mutableOptions valueForKey:@"enableNativeCrashHandling"] != nil) { + BOOL enableNativeCrashHandling = [mutableOptions[@"enableNativeCrashHandling"] boolValue]; + + if (!enableNativeCrashHandling) { + 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]; + // TODO: For Auto init from JS set automatically for init from options file have to be + // set manually + id defaultSpotlightUrl = [mutableOptions valueForKey:@"defaultSidecarUrl"]; + if (defaultSpotlightUrl != nil) { + sentryOptions.spotlightUrl = defaultSpotlightUrl; + } + } + } + + return sentryOptions; +} + +/** + * This function updates the options with RNSentry defaults. These default can be + * overwritten by users during manual native initialization. + */ ++ (void)updateWithReactDefaults:(SentryOptions *)options +{ + // Failed requests are captured only in JS to avoid duplicates + options.enableCaptureFailedRequests = NO; + + // Tracing is only enabled in JS to avoid duplicate navigation spans + options.tracesSampleRate = nil; + options.tracesSampler = nil; + options.enableTracing = NO; +} + +/** + * This function updates options with changes RNSentry users should not change + * and so this is applied after the configureOptions callback during manual native initialization. + */ ++ (void)updateWithReactFinals:(SentryOptions *)options +{ + SentryBeforeSendEventCallback userBeforeSend = options.beforeSend; + options.beforeSend = ^SentryEvent *(SentryEvent *event) + { + // Unhandled JS Exception are processed by the SDK on JS layer + // To avoid duplicates we drop them in the native SDKs + if (nil != event.exceptions.firstObject.type && + [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location + != NSNotFound) { + return nil; + } + + [self setEventOriginTag:event]; + if (userBeforeSend == nil) { + return event; + } else { + return userBeforeSend(event); + } + }; + + // App Start Hybrid mode doesn't wait for didFinishLaunchNotification and the + // didBecomeVisibleNotification as they will be missed when auto initializing from JS + // App Start measurements are created right after the tracking starts + PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = options.enableAutoPerformanceTracing; +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + // Frames Tracking Hybrid Mode ensures tracking + // is enabled without tracing enabled in the native SDK + PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode + = options.enableAutoPerformanceTracing; +#endif +} + ++ (void)setEventOriginTag:(SentryEvent *)event +{ + if (event.sdk != nil) { + NSString *sdkName = event.sdk[@"name"]; + + // If the event is from react native, it gets set + // there and we do not handle it here. + if ([sdkName isEqual:NATIVE_SDK_NAME]) { + [self setEventEnvironmentTag:event origin:@"ios" environment:@"native"]; + } + } +} + ++ (void)setEventEnvironmentTag:(SentryEvent *)event + origin:(NSString *)origin + environment:(NSString *)environment +{ + NSMutableDictionary *newTags = [NSMutableDictionary new]; + + if (nil != event.tags && [event.tags count] > 0) { + [newTags addEntriesFromDictionary:event.tags]; + } + if (nil != origin) { + [newTags setValue:origin forKey:@"event.origin"]; + } + if (nil != environment) { + [newTags setValue:environment forKey:@"event.environment"]; + } + + event.tags = newTags; +} + ++ (NSString *_Nullable)getURLFromDSN:(NSString *)dsn +{ + NSURL *url = [NSURL URLWithString:dsn]; + if (!url) { + return nil; + } + return [NSString stringWithFormat:@"%@://%@", url.scheme, url.host]; +} + +static bool sentHybridSdkDidBecomeActive = NO; + ++ (void)postDidBecomeActiveNotification +{ +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + BOOL appIsActive = + [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive; +#else + BOOL appIsActive = [[NSApplication sharedApplication] isActive]; +#endif + + // If the app is active/in foreground, and we have not sent the SentryHybridSdkDidBecomeActive + // notification, send it. + if (appIsActive && !sentHybridSdkDidBecomeActive + && (PrivateSentrySDKOnly.options.enableAutoSessionTracking + || PrivateSentrySDKOnly.options.enableWatchdogTerminationTracking)) { + // Updates Native App State Manager + // https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentryAppStateManager.m#L136 + // Triggers Session Tracker + // https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentrySessionTracker.m#L144 + [[NSNotificationCenter defaultCenter] postNotificationName:@"SentryHybridSdkDidBecomeActive" + object:nil]; + + sentHybridSdkDidBecomeActive = true; + } +} + +@end diff --git a/packages/core/scripts/sentry-xcode.sh b/packages/core/scripts/sentry-xcode.sh index 78970c4c60..336d393220 100755 --- a/packages/core/scripts/sentry-xcode.sh +++ b/packages/core/scripts/sentry-xcode.sh @@ -51,3 +51,22 @@ fi if [ -f "$SENTRY_COLLECT_MODULES" ]; then /bin/sh "$SENTRY_COLLECT_MODULES" fi + +SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX="Skipping options file copy. To disable this behavior, set SENTRY_COPY_OPTIONS_FILE=false in your environment variables." +SENTRY_OPTIONS_FILE_NAME="sentry.options.json" +SENTRY_OPTIONS_FILE_DESTINATION_PATH="$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/$SENTRY_OPTIONS_FILE_NAME" +[ -z "$SENTRY_OPTIONS_FILE_PATH" ] && SENTRY_OPTIONS_FILE_PATH="$RN_PROJECT_ROOT/$SENTRY_OPTIONS_FILE_NAME" +[ -z "$SENTRY_COPY_OPTIONS_FILE" ] && SENTRY_COPY_OPTIONS_FILE=true + +if [ "$SENTRY_COPY_OPTIONS_FILE" = true ]; then + if [[ -z "$CONFIGURATION_BUILD_DIR" ]]; then + echo "[Sentry] CONFIGURATION_BUILD_DIR is not set. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + elif [[ -z "$UNLOCALIZED_RESOURCES_FOLDER_PATH" ]]; then + echo "[Sentry] UNLOCALIZED_RESOURCES_FOLDER_PATH is not set. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + elif [ ! -f "$SENTRY_OPTIONS_FILE_PATH" ]; then + echo "[Sentry] $SENTRY_OPTIONS_FILE_PATH not found. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + else + cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" + echo "[Sentry] Copied $SENTRY_OPTIONS_FILE_PATH to $SENTRY_OPTIONS_FILE_DESTINATION_PATH" + fi +fi diff --git a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm index 71a62884ac..fe0893c5b6 100644 --- a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm +++ b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm @@ -9,6 +9,7 @@ # import #endif +#import #import #import @@ -19,44 +20,12 @@ @implementation AppDelegate -- (void)initializeSentry -{ - [SentrySDK startWithConfigureOptions:^(SentryOptions *options) { - // Only options set here will apply to the iOS SDK - // Options from JS are not passed to the iOS SDK when initialized manually - options.dsn = @"https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561"; - options.debug = YES; // Enabled debug when first installing is always helpful - - options.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) { - NSLog(@"Unhandled JS Exception"); - return nil; - } - - return event; - }; - - // Enable the App start and Frames tracking measurements - // If this is disabled the app start and frames tracking - // won't be passed from native to JS transactions - PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = true; -#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode = true; -#endif - }]; -} - - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // When the native init is enabled the `autoInitializeNativeSdk` // in JS has to be set to `false` - // [self initializeSentry]; + // [RNSentrySDK start]; self.moduleName = @"sentry-react-native-sample"; // You can add your custom initial props in the dictionary below. diff --git a/samples/react-native/sentry.options.json b/samples/react-native/sentry.options.json new file mode 100644 index 0000000000..f6465b7923 --- /dev/null +++ b/samples/react-native/sentry.options.json @@ -0,0 +1,20 @@ +{ + "dsn": "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", + "debug": true, + "environment": "dev", + "enableUserInteractionTracing": true, + "enableAutoSessionTracking": true, + "sessionTrackingIntervalMillis": 30000, + "enableTracing": true, + "tracesSampleRate": 1.0, + "attachStacktrace": true, + "attachScreenshot": true, + "attachViewHierarchy": true, + "enableCaptureFailedRequests": true, + "_release": "myapp@1.2.3+1", + "_dist": 1, + "profilesSampleRate": 1.0, + "replaysSessionSampleRate": 1.0, + "replaysOnErrorSampleRate": 1.0, + "spotlight": true +}