Skip to content

Commit 236a90a

Browse files
authored
Merge d909ed4 into 3d41a98
2 parents 3d41a98 + d909ed4 commit 236a90a

File tree

10 files changed

+318
-5
lines changed

10 files changed

+318
-5
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
- 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))
1919
- Add RN SDK package to `sdk.packages` for Cocoa ([#4381](https://github.com/getsentry/sentry-react-native/pull/4381))
20+
- Add experimental initialization using `sentry.options.json` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451))
2021

2122
### Internal
2223

packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/rnsentryandroidtester/RNSentryMapConverterTest.kt

+40
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import android.content.Context
44
import androidx.test.ext.junit.runners.AndroidJUnit4
55
import androidx.test.platform.app.InstrumentationRegistry
66
import com.facebook.react.bridge.Arguments
7+
import com.facebook.react.bridge.JavaOnlyMap
78
import com.facebook.soloader.SoLoader
89
import io.sentry.react.RNSentryMapConverter
10+
import org.json.JSONObject
911
import org.junit.Assert.assertEquals
12+
import org.junit.Assert.assertNotNull
1013
import org.junit.Assert.assertNull
14+
import org.junit.Assert.assertTrue
1115
import org.junit.Before
1216
import org.junit.Test
1317
import org.junit.runner.RunWith
@@ -359,4 +363,40 @@ class MapConverterTest {
359363

360364
assertEquals(actual, expectedMap1)
361365
}
366+
367+
@Test
368+
fun testJsonObjectToReadableMap() {
369+
val json =
370+
JSONObject().apply {
371+
put("stringKey", "stringValue")
372+
put("booleanKey", true)
373+
put("intKey", 123)
374+
}
375+
376+
val result = RNSentryMapConverter.jsonObjectToReadableMap(json)
377+
378+
assertNotNull(result)
379+
assertTrue(result is JavaOnlyMap)
380+
assertEquals("stringValue", result.getString("stringKey"))
381+
assertEquals(true, result.getBoolean("booleanKey"))
382+
assertEquals(123, result.getInt("intKey"))
383+
}
384+
385+
@Test
386+
fun testMapToReadableMap() {
387+
val map =
388+
mapOf(
389+
"stringKey" to "stringValue",
390+
"booleanKey" to true,
391+
"intKey" to 123,
392+
)
393+
394+
val result = RNSentryMapConverter.mapToReadableMap(map)
395+
396+
assertNotNull(result)
397+
assertTrue(result is JavaOnlyMap)
398+
assertEquals("stringValue", result.getString("stringKey"))
399+
assertEquals(true, result.getBoolean("booleanKey"))
400+
assertEquals(123, result.getInt("intKey"))
401+
}
362402
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.sentry.rnsentryandroidtester
2+
3+
import io.sentry.Sentry.OptionsConfiguration
4+
import io.sentry.android.core.SentryAndroidOptions
5+
import io.sentry.react.RNSentryCompositeOptionsConfiguration
6+
import org.junit.Test
7+
import org.junit.runner.RunWith
8+
import org.junit.runners.JUnit4
9+
import org.mockito.kotlin.mock
10+
import org.mockito.kotlin.verify
11+
12+
@RunWith(JUnit4::class)
13+
class RNSentryCompositeOptionsConfigurationTest {
14+
@Test
15+
fun `configure should call base and overriding configurations`() {
16+
val baseConfig: OptionsConfiguration<SentryAndroidOptions> = mock()
17+
val overridingConfig: OptionsConfiguration<SentryAndroidOptions> = mock()
18+
19+
val compositeConfig = RNSentryCompositeOptionsConfiguration(baseConfig, overridingConfig)
20+
val options = SentryAndroidOptions()
21+
compositeConfig.configure(options)
22+
23+
verify(baseConfig).configure(options)
24+
verify(overridingConfig).configure(options)
25+
}
26+
27+
@Test
28+
fun `configure should apply base configuration and override values`() {
29+
val baseConfig =
30+
OptionsConfiguration<SentryAndroidOptions> { options ->
31+
options.dsn = "https://[email protected]"
32+
options.isDebug = false
33+
options.release = "some-release"
34+
}
35+
val overridingConfig =
36+
OptionsConfiguration<SentryAndroidOptions> { options ->
37+
options.dsn = "https://[email protected]"
38+
options.isDebug = true
39+
options.environment = "production"
40+
}
41+
42+
val compositeConfig = RNSentryCompositeOptionsConfiguration(baseConfig, overridingConfig)
43+
val options = SentryAndroidOptions()
44+
compositeConfig.configure(options)
45+
46+
assert(options.dsn == "https://[email protected]") // overridden value
47+
assert(options.isDebug) // overridden value
48+
assert(options.release == "some-release") // base value not overridden
49+
assert(options.environment == "production") // overridden value not in base
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.sentry.react;
2+
3+
import io.sentry.Sentry.OptionsConfiguration;
4+
import io.sentry.android.core.SentryAndroidOptions;
5+
import org.jetbrains.annotations.NotNull;
6+
7+
public class RNSentryCompositeOptionsConfiguration
8+
implements OptionsConfiguration<SentryAndroidOptions> {
9+
private final OptionsConfiguration<SentryAndroidOptions> baseConfiguration;
10+
private final OptionsConfiguration<SentryAndroidOptions> overridingConfiguration;
11+
12+
public RNSentryCompositeOptionsConfiguration(
13+
OptionsConfiguration<SentryAndroidOptions> baseConfiguration,
14+
OptionsConfiguration<SentryAndroidOptions> overridingConfiguration) {
15+
this.baseConfiguration = baseConfiguration;
16+
this.overridingConfiguration = overridingConfiguration;
17+
}
18+
19+
@Override
20+
public void configure(@NotNull SentryAndroidOptions options) {
21+
baseConfiguration.configure(options);
22+
overridingConfiguration.configure(options);
23+
}
24+
}

packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java

+38
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.sentry.react;
22

33
import com.facebook.react.bridge.Arguments;
4+
import com.facebook.react.bridge.JavaOnlyMap;
45
import com.facebook.react.bridge.ReadableArray;
56
import com.facebook.react.bridge.ReadableMap;
67
import com.facebook.react.bridge.WritableArray;
@@ -10,9 +11,13 @@
1011
import io.sentry.android.core.AndroidLogger;
1112
import java.math.BigDecimal;
1213
import java.math.BigInteger;
14+
import java.util.HashMap;
15+
import java.util.Iterator;
1316
import java.util.List;
1417
import java.util.Map;
1518
import org.jetbrains.annotations.Nullable;
19+
import org.json.JSONException;
20+
import org.json.JSONObject;
1621

1722
public final class RNSentryMapConverter {
1823
public static final String NAME = "RNSentry.MapConverter";
@@ -131,4 +136,37 @@ private static void addValueToWritableMap(WritableMap writableMap, String key, O
131136
logger.log(SentryLevel.ERROR, "Could not convert object" + value);
132137
}
133138
}
139+
140+
public static ReadableMap jsonObjectToReadableMap(JSONObject jsonObject) {
141+
Map<String, Object> map = jsonObjectToMap(jsonObject);
142+
return mapToReadableMap(map);
143+
}
144+
145+
public static ReadableMap mapToReadableMap(Map<String, Object> map) {
146+
// We are not directly using `convertToWritable` since `Arguments.createArray()`
147+
// fails before bridge initialisation
148+
Object[] keysAndValues = new Object[map.size() * 2];
149+
int index = 0;
150+
for (Map.Entry<String, Object> entry : map.entrySet()) {
151+
keysAndValues[index++] = entry.getKey();
152+
keysAndValues[index++] = entry.getValue();
153+
}
154+
return JavaOnlyMap.of(keysAndValues);
155+
}
156+
157+
private static Map<String, Object> jsonObjectToMap(JSONObject jsonObject) {
158+
Map<String, Object> map = new HashMap<>();
159+
Iterator<String> keys = jsonObject.keys();
160+
while (keys.hasNext()) {
161+
String key = keys.next();
162+
Object value = null;
163+
try {
164+
value = jsonObject.get(key);
165+
} catch (JSONException e) {
166+
throw new RuntimeException(e);
167+
}
168+
map.put(key, value);
169+
}
170+
return map;
171+
}
134172
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package io.sentry.react;
2+
3+
import android.content.Context;
4+
import com.facebook.react.bridge.ReadableMap;
5+
import io.sentry.ILogger;
6+
import io.sentry.Sentry;
7+
import io.sentry.SentryLevel;
8+
import io.sentry.android.core.AndroidLogger;
9+
import io.sentry.android.core.SentryAndroidOptions;
10+
import java.io.BufferedReader;
11+
import java.io.InputStream;
12+
import java.io.InputStreamReader;
13+
import org.jetbrains.annotations.NotNull;
14+
import org.json.JSONObject;
15+
16+
public final class RNSentrySDK {
17+
private static final String CONFIGURATION_FILE = "sentry.options.json";
18+
private static final String NAME = "RNSentrySDK";
19+
20+
private static final ILogger logger = new AndroidLogger(NAME);
21+
22+
private RNSentrySDK() {
23+
throw new AssertionError("Utility class should not be instantiated");
24+
}
25+
26+
/**
27+
* Start the Native Android SDK with the provided options
28+
*
29+
* @param context Android Context
30+
* @param configuration configuration options
31+
*/
32+
public static void init(
33+
@NotNull final Context context,
34+
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration) {
35+
try {
36+
JSONObject jsonObject = getOptionsFromConfigurationFile(context);
37+
ReadableMap rnOptions = RNSentryMapConverter.jsonObjectToReadableMap(jsonObject);
38+
RNSentryStart.startWithOptions(context, rnOptions, configuration, null, logger);
39+
} catch (Exception e) {
40+
logger.log(
41+
SentryLevel.ERROR, "Failed to start Sentry with options from configuration file.", e);
42+
throw new RuntimeException(e);
43+
}
44+
}
45+
46+
/**
47+
* Start the Native Android SDK with options from `sentry.options.json` configuration file
48+
*
49+
* @param context Android Context
50+
*/
51+
public static void init(@NotNull final Context context) {
52+
init(context, options -> {});
53+
}
54+
55+
private static JSONObject getOptionsFromConfigurationFile(Context context) {
56+
try (InputStream inputStream = context.getAssets().open(CONFIGURATION_FILE);
57+
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
58+
59+
StringBuilder stringBuilder = new StringBuilder();
60+
String line;
61+
while ((line = reader.readLine()) != null) {
62+
stringBuilder.append(line);
63+
}
64+
String configFileContent = stringBuilder.toString();
65+
return new JSONObject(configFileContent);
66+
67+
} catch (Exception e) {
68+
logger.log(
69+
SentryLevel.ERROR,
70+
"Failed to read configuration file. Please make sure "
71+
+ CONFIGURATION_FILE
72+
+ " exists in the root of your project.",
73+
e);
74+
return null;
75+
}
76+
}
77+
}

packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java

+16
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.facebook.react.common.JavascriptException;
88
import io.sentry.ILogger;
99
import io.sentry.Integration;
10+
import io.sentry.Sentry;
1011
import io.sentry.SentryEvent;
1112
import io.sentry.SentryLevel;
1213
import io.sentry.SentryReplayOptions;
@@ -33,6 +34,21 @@ private RNSentryStart() {
3334
throw new AssertionError("Utility class should not be instantiated");
3435
}
3536

37+
public static void startWithOptions(
38+
@NotNull final Context context,
39+
@NotNull final ReadableMap rnOptions,
40+
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration,
41+
@Nullable Activity currentActivity,
42+
@NotNull ILogger logger) {
43+
Sentry.OptionsConfiguration<SentryAndroidOptions> rnConfigurationOptions =
44+
options -> getSentryAndroidOptions(options, rnOptions, currentActivity, logger);
45+
46+
RNSentryCompositeOptionsConfiguration compositeConfiguration =
47+
new RNSentryCompositeOptionsConfiguration(rnConfigurationOptions, configuration);
48+
49+
SentryAndroid.init(context, compositeConfiguration);
50+
}
51+
3652
public static void startWithOptions(
3753
@NotNull final Context context,
3854
@NotNull final ReadableMap rnOptions,

packages/core/sentry.gradle

+46-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import org.apache.tools.ant.taskdefs.condition.Os
33
import java.util.regex.Matcher
44
import java.util.regex.Pattern
55

6-
project.ext.shouldSentryAutoUploadNative = { ->
6+
project.ext.shouldSentryAutoUploadNative = { ->
77
return System.getenv('SENTRY_DISABLE_NATIVE_DEBUG_UPLOAD') != 'true'
88
}
99

@@ -17,7 +17,52 @@ project.ext.shouldSentryAutoUpload = { ->
1717

1818
def config = project.hasProperty("sentryCli") ? project.sentryCli : [];
1919

20+
def configFile = "sentry.options.json" // Sentry condiguration file
21+
def androidAssetsDir = new File("$rootDir/app/src/main/assets") // Path to Android assets folder
22+
23+
tasks.register("copySentryJsonConfiguration") {
24+
doLast {
25+
def appRoot = project.rootDir.parentFile ?: project.rootDir
26+
def sentryOptionsFile = new File(appRoot, configFile)
27+
if (sentryOptionsFile.exists()) {
28+
if (!androidAssetsDir.exists()) {
29+
androidAssetsDir.mkdirs()
30+
}
31+
32+
copy {
33+
from sentryOptionsFile
34+
into androidAssetsDir
35+
rename { String fileName -> configFile }
36+
}
37+
logger.lifecycle("Copied ${configFile} to Android assets")
38+
} else {
39+
logger.warn("${configFile} not found in app root (${appRoot})")
40+
}
41+
}
42+
}
43+
44+
tasks.register("cleanupTemporarySentryJsonConfiguration") {
45+
doLast {
46+
def sentryOptionsFile = new File(androidAssetsDir, configFile)
47+
if (sentryOptionsFile.exists()) {
48+
logger.lifecycle("Deleting temporary file: ${sentryOptionsFile.path}")
49+
sentryOptionsFile.delete()
50+
}
51+
}
52+
}
53+
2054
gradle.projectsEvaluated {
55+
// Add a task that copies the sentry.options.json file before the build starts
56+
tasks.named("preBuild").configure {
57+
dependsOn("copySentryJsonConfiguration")
58+
}
59+
// Cleanup sentry.options.json from assets after the build
60+
tasks.matching { task ->
61+
task.name == "build" || task.name.startsWith("assemble") || task.name.startsWith("install")
62+
}.configureEach {
63+
finalizedBy("cleanupTemporarySentryJsonConfiguration")
64+
}
65+
2166
def releases = extractReleasesInfo()
2267

2368
if (config.flavorAware && config.sentryProperties) {

samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt

+5-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import com.facebook.soloader.SoLoader
1414
import io.sentry.Hint
1515
import io.sentry.SentryEvent
1616
import io.sentry.SentryOptions.BeforeSendCallback
17-
import io.sentry.android.core.SentryAndroid
17+
import io.sentry.react.RNSentrySDK
1818

1919
class MainApplication :
2020
Application(),
@@ -51,9 +51,8 @@ class MainApplication :
5151
}
5252

5353
private fun initializeSentry() {
54-
SentryAndroid.init(this) { options ->
55-
// Only options set here will apply to the Android SDK
56-
// Options from JS are not passed to the Android SDK when initialized manually
54+
RNSentrySDK.init(this) { options ->
55+
// Options set here will apply to the Android SDK overriding the ones from `sentry.options.json`
5756
options.dsn = "https://[email protected]/5428561"
5857
options.isDebug = true
5958

@@ -74,5 +73,7 @@ class MainApplication :
7473
event
7574
}
7675
}
76+
77+
// RNSentrySDK.init(this)
7778
}
7879
}

0 commit comments

Comments
 (0)