Skip to content

Commit

Permalink
Merge cedd24a into 99410e9
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Jan 21, 2025
2 parents 99410e9 + cedd24a commit ee5a152
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

- 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 initialization using `sentry.options.json` and `RNSentrySDK.startWithOptions` method for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451))

### Internal

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.JavaOnlyMap
import com.facebook.soloader.SoLoader
import io.sentry.react.RNSentryMapConverter
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -359,4 +363,40 @@ class MapConverterTest {

assertEquals(actual, expectedMap1)
}

@Test
fun testJsonObjectToReadableMap() {
val json =
JSONObject().apply {
put("stringKey", "stringValue")
put("booleanKey", true)
put("intKey", 123)
}

val result = RNSentryMapConverter.jsonObjectToReadableMap(json)

assertNotNull(result)
assertTrue(result is JavaOnlyMap)
assertEquals("stringValue", result.getString("stringKey"))
assertEquals(true, result.getBoolean("booleanKey"))
assertEquals(123, result.getInt("intKey"))
}

@Test
fun testMapToReadableMap() {
val map =
mapOf(
"stringKey" to "stringValue",
"booleanKey" to true,
"intKey" to 123,
)

val result = RNSentryMapConverter.mapToReadableMap(map)

assertNotNull(result)
assertTrue(result is JavaOnlyMap)
assertEquals("stringValue", result.getString("stringKey"))
assertEquals(true, result.getBoolean("booleanKey"))
assertEquals(123, result.getInt("intKey"))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.sentry.rnsentryandroidtester

import io.sentry.Sentry.OptionsConfiguration
import io.sentry.android.core.SentryAndroidOptions
import io.sentry.react.RNSentryCompositeOptionsConfiguration
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify

@RunWith(JUnit4::class)
class RNSentryCompositeOptionsConfigurationTest {
@Test
fun `configure should call base and overriding configurations`() {
val baseConfig: OptionsConfiguration<SentryAndroidOptions> = mock()
val overridingConfig: OptionsConfiguration<SentryAndroidOptions> = mock()

val compositeConfig = RNSentryCompositeOptionsConfiguration(baseConfig, overridingConfig)
val options = SentryAndroidOptions()
compositeConfig.configure(options)

verify(baseConfig).configure(options)
verify(overridingConfig).configure(options)
}

@Test
fun `configure should apply base configuration and override values`() {
val baseConfig =
OptionsConfiguration<SentryAndroidOptions> { options ->
options.dsn = "https://[email protected]"
options.isDebug = false
options.release = "some-release"
}
val overridingConfig =
OptionsConfiguration<SentryAndroidOptions> { options ->
options.dsn = "https://[email protected]"
options.isDebug = true
options.environment = "production"
}

val compositeConfig = RNSentryCompositeOptionsConfiguration(baseConfig, overridingConfig)
val options = SentryAndroidOptions()
compositeConfig.configure(options)

assert(options.dsn == "https://[email protected]") // overridden value
assert(options.isDebug) // overridden value
assert(options.release == "some-release") // base value not overridden
assert(options.environment == "production") // overridden value not in base
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.sentry.react;

import io.sentry.Sentry.OptionsConfiguration;
import io.sentry.android.core.SentryAndroidOptions;
import org.jetbrains.annotations.NotNull;

public class RNSentryCompositeOptionsConfiguration
implements OptionsConfiguration<SentryAndroidOptions> {
private final OptionsConfiguration<SentryAndroidOptions> baseConfiguration;
private final OptionsConfiguration<SentryAndroidOptions> overridingConfiguration;

public RNSentryCompositeOptionsConfiguration(
OptionsConfiguration<SentryAndroidOptions> baseConfiguration,
OptionsConfiguration<SentryAndroidOptions> overridingConfiguration) {
this.baseConfiguration = baseConfiguration;
this.overridingConfiguration = overridingConfiguration;
}

@Override
public void configure(@NotNull SentryAndroidOptions options) {
baseConfiguration.configure(options);
overridingConfiguration.configure(options);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.sentry.react;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.JavaOnlyMap;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
Expand All @@ -10,9 +11,13 @@
import io.sentry.android.core.AndroidLogger;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import org.json.JSONException;
import org.json.JSONObject;

public final class RNSentryMapConverter {
public static final String NAME = "RNSentry.MapConverter";
Expand Down Expand Up @@ -131,4 +136,37 @@ private static void addValueToWritableMap(WritableMap writableMap, String key, O
logger.log(SentryLevel.ERROR, "Could not convert object" + value);
}
}

public static ReadableMap jsonObjectToReadableMap(JSONObject jsonObject) {
Map<String, Object> map = jsonObjectToMap(jsonObject);
return mapToReadableMap(map);
}

public static ReadableMap mapToReadableMap(Map<String, Object> map) {
// We are not directly using `convertToWritable` since `Arguments.createArray()`
// fails before bridge initialisation
Object[] keysAndValues = new Object[map.size() * 2];
int index = 0;
for (Map.Entry<String, Object> entry : map.entrySet()) {
keysAndValues[index++] = entry.getKey();
keysAndValues[index++] = entry.getValue();
}
return JavaOnlyMap.of(keysAndValues);
}

private static Map<String, Object> jsonObjectToMap(JSONObject jsonObject) {
Map<String, Object> map = new HashMap<>();
Iterator<String> keys = jsonObject.keys();
while (keys.hasNext()) {
String key = keys.next();
Object value = null;
try {
value = jsonObject.get(key);
} catch (JSONException e) {
throw new RuntimeException(e);
}
map.put(key, value);
}
return map;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.sentry.react;

import android.content.Context;
import com.facebook.react.bridge.ReadableMap;
import io.sentry.ILogger;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import io.sentry.android.core.AndroidLogger;
import io.sentry.android.core.SentryAndroidOptions;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.jetbrains.annotations.NotNull;
import org.json.JSONObject;

public final class RNSentrySDK {
private static final String CONFIGURATION_FILE = "sentry.options.json";
private static final String NAME = "RNSentrySDK";

private static final ILogger logger = new AndroidLogger(NAME);

private RNSentrySDK() {
throw new AssertionError("Utility class should not be instantiated");
}

/**
* Start the Native Android SDK with the provided options
*
* @param context Android Context
* @param configuration configuration options
*/
public static void init(
@NotNull final Context context,
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration) {
try {
JSONObject jsonObject = getOptionsFromConfigurationFile(context);
ReadableMap rnOptions = RNSentryMapConverter.jsonObjectToReadableMap(jsonObject);
RNSentryStart.startWithOptions(context, rnOptions, configuration, null, logger);
} catch (Exception e) {
logger.log(
SentryLevel.ERROR, "Failed to start Sentry with options from configuration file.", e);
throw new RuntimeException(e);
}
}

/**
* Start the Native Android SDK with options from `sentry.options.json` configuration file
*
* @param context Android Context
*/
public static void init(@NotNull final Context context) {
init(context, options -> {});
}

private static JSONObject getOptionsFromConfigurationFile(Context context) {
try (InputStream inputStream = context.getAssets().open(CONFIGURATION_FILE);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {

StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
String configFileContent = stringBuilder.toString();
return new JSONObject(configFileContent);

} catch (Exception e) {
logger.log(
SentryLevel.ERROR,
"Failed to read configuration file. Please make sure "
+ CONFIGURATION_FILE
+ " exists in the root of your project.",
e);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.facebook.react.common.JavascriptException;
import io.sentry.ILogger;
import io.sentry.Integration;
import io.sentry.Sentry;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.SentryReplayOptions;
Expand All @@ -33,6 +34,21 @@ private RNSentryStart() {
throw new AssertionError("Utility class should not be instantiated");
}

public static void startWithOptions(
@NotNull final Context context,
@NotNull final ReadableMap rnOptions,
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration,
@Nullable Activity currentActivity,
@NotNull ILogger logger) {
Sentry.OptionsConfiguration<SentryAndroidOptions> rnConfigurationOptions =
options -> getSentryAndroidOptions(options, rnOptions, currentActivity, logger);

RNSentryCompositeOptionsConfiguration compositeConfiguration =
new RNSentryCompositeOptionsConfiguration(rnConfigurationOptions, configuration);

SentryAndroid.init(context, compositeConfiguration);
}

public static void startWithOptions(
@NotNull final Context context,
@NotNull final ReadableMap rnOptions,
Expand Down
47 changes: 46 additions & 1 deletion packages/core/sentry.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import org.apache.tools.ant.taskdefs.condition.Os
import java.util.regex.Matcher
import java.util.regex.Pattern

project.ext.shouldSentryAutoUploadNative = { ->
project.ext.shouldSentryAutoUploadNative = { ->
return System.getenv('SENTRY_DISABLE_NATIVE_DEBUG_UPLOAD') != 'true'
}

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

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

def configFile = "sentry.options.json" // Sentry condiguration file
def androidAssetsDir = new File("$rootDir/app/src/main/assets") // Path to Android assets folder

tasks.register("copySentryJsonConfiguration") {
doLast {
def appRoot = project.rootDir.parentFile ?: project.rootDir
def sentryOptionsFile = new File(appRoot, configFile)
if (sentryOptionsFile.exists()) {
if (!androidAssetsDir.exists()) {
androidAssetsDir.mkdirs()
}

copy {
from sentryOptionsFile
into androidAssetsDir
rename { String fileName -> configFile }
}
logger.lifecycle("Copied ${configFile} to Android assets")
} else {
logger.warn("${configFile} not found in app root (${appRoot})")
}
}
}

tasks.register("cleanupTemporarySentryJsonConfiguration") {
doLast {
def sentryOptionsFile = new File(androidAssetsDir, configFile)
if (sentryOptionsFile.exists()) {
logger.lifecycle("Deleting temporary file: ${sentryOptionsFile.path}")
sentryOptionsFile.delete()
}
}
}

gradle.projectsEvaluated {
// Add a task that copies the sentry.options.json file before the build starts
tasks.named("preBuild").configure {
dependsOn("copySentryJsonConfiguration")
}
// Cleanup sentry.options.json from assets after the build
tasks.matching { task ->
task.name == "build" || task.name.startsWith("assemble") || task.name.startsWith("install")
}.configureEach {
finalizedBy("cleanupTemporarySentryJsonConfiguration")
}

def releases = extractReleasesInfo()

if (config.flavorAware && config.sentryProperties) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import com.facebook.soloader.SoLoader
import io.sentry.Hint
import io.sentry.SentryEvent
import io.sentry.SentryOptions.BeforeSendCallback
import io.sentry.android.core.SentryAndroid
import io.sentry.react.RNSentrySDK

class MainApplication :
Application(),
Expand Down Expand Up @@ -51,9 +51,8 @@ class MainApplication :
}

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

Expand All @@ -74,5 +73,7 @@ class MainApplication :
event
}
}

// RNSentrySDK.init(this)
}
}
Loading

0 comments on commit ee5a152

Please sign in to comment.