Skip to content

Commit

Permalink
Merge e0264ef into 1e5dbde
Browse files Browse the repository at this point in the history
  • Loading branch information
antonis authored Jan 24, 2025
2 parents 1e5dbde + e0264ef commit 66c8b49
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 37 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- 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))
- Add initialization using `sentry.options.json` 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
@@ -0,0 +1,50 @@
package io.sentry.react

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

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

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

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

@Test
fun `configure should apply base configuration and override values`() {
val baseConfig =
OptionsConfiguration<SentryAndroidOptions> { options ->
options.dsn = "https://[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,25 @@
package io.sentry.react;

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

class RNSentryCompositeOptionsConfiguration implements OptionsConfiguration<SentryAndroidOptions> {
private final @NotNull List<OptionsConfiguration<SentryAndroidOptions>> configurations;

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

@Override
public void configure(@NotNull SentryAndroidOptions options) {
for (OptionsConfiguration<SentryAndroidOptions> configuration : configurations) {
if (configuration != null) {
configuration.configure(options);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.sentry.react;

import android.content.Context;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;

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

static @Nullable JSONObject getOptionsFromConfigurationFile(
@NotNull Context context, @NotNull String fileName, @NotNull ILogger logger) {
try (InputStream inputStream = context.getAssets().open(fileName);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {

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

} catch (Exception e) {
logger.log(
SentryLevel.ERROR,
"Failed to read configuration file. Please make sure "
+ fileName
+ " exists in the root of your project.",
e);
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.sentry.react;

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

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

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

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

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

/**
* 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) {
init(context, configuration, logger);
}

/**
* 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 -> {}, logger);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import com.facebook.react.common.JavascriptException;
import io.sentry.ILogger;
import io.sentry.Integration;
import io.sentry.Sentry;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions.BeforeSendCallback;
import io.sentry.SentryReplayOptions;
import io.sentry.UncaughtExceptionHandlerIntegration;
import io.sentry.android.core.AnrIntegration;
Expand All @@ -27,19 +29,52 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class RNSentryStart {
final class RNSentryStart {

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

public static void startWithOptions(
static void startWithConfiguration(
@NotNull final Context context,
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration) {
RNSentryCompositeOptionsConfiguration compositeConfiguration =
new RNSentryCompositeOptionsConfiguration(
RNSentryStart::updateWithReactDefaults,
configuration,
RNSentryStart::updateWithReactFinals);
SentryAndroid.init(context, compositeConfiguration);
}

static void startWithOptions(
@NotNull final Context context,
@NotNull final ReadableMap rnOptions,
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration,
@NotNull ILogger logger) {
Sentry.OptionsConfiguration<SentryAndroidOptions> rnConfigurationOptions =
options -> getSentryAndroidOptions(options, rnOptions, null, logger);
RNSentryCompositeOptionsConfiguration compositeConfiguration =
new RNSentryCompositeOptionsConfiguration(
RNSentryStart::updateWithReactDefaults,
rnConfigurationOptions,
configuration,
RNSentryStart::updateWithReactFinals);
SentryAndroid.init(context, compositeConfiguration);
}

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));
Sentry.OptionsConfiguration<SentryAndroidOptions> rnConfigurationOptions =
options -> getSentryAndroidOptions(options, rnOptions, currentActivity, logger);
RNSentryCompositeOptionsConfiguration compositeConfiguration =
new RNSentryCompositeOptionsConfiguration(
RNSentryStart::updateWithReactDefaults,
rnConfigurationOptions,
RNSentryStart::updateWithReactFinals);
SentryAndroid.init(context, compositeConfiguration);
}

static void getSentryAndroidOptions(
Expand Down Expand Up @@ -163,14 +198,6 @@ static void getSentryAndroidOptions(
// we want to ignore it on the native side to avoid sending it twice.
options.addIgnoredExceptionForType(JavascriptException.class);

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

return event;
});

if (rnOptions.hasKey("enableNativeCrashHandling")
&& !rnOptions.getBoolean("enableNativeCrashHandling")) {
final List<Integration> integrations = options.getIntegrations();
Expand All @@ -188,6 +215,42 @@ static void getSentryAndroidOptions(
setCurrentActivity(currentActivity);
}

/**
* This function updates the options with RNSentry defaults. These default can be overwritten by
* users during manual native initialization.
*/
static void updateWithReactDefaults(@NotNull SentryAndroidOptions options) {
// Tracing is only enabled in JS to avoid duplicate navigation spans
options.setTracesSampleRate(null);
options.setTracesSampler(null);
options.setEnableTracing(false);
}

/**
* This function updates options with changes RNSentry users should not change and so this is
* applied after the configureOptions callback during manual native initialization.
*/
static void updateWithReactFinals(@NotNull SentryAndroidOptions options) {
BeforeSendCallback userBeforeSend = options.getBeforeSend();
options.setBeforeSend(
(event, hint) -> {
// Unhandled JS Exception are processed by the SDK on JS layer
// To avoid duplicates we drop them in the native SDKs
if (event.getExceptions() != null && !event.getExceptions().isEmpty()) {
String exType = event.getExceptions().get(0).getType();
if (exType != null && exType.contains("Unhandled JS Exception")) {
return null; // Skip sending this event
}
}
setEventOriginTag(event);
addPackages(event, options.getSdkVersion());
if (userBeforeSend != null) {
return userBeforeSend.execute(event, hint);
}
return event;
});
}

private static void setCurrentActivity(Activity currentActivity) {
final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance();
if (currentActivity != null) {
Expand Down
53 changes: 52 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 @@ -15,9 +15,60 @@ project.ext.shouldSentryAutoUpload = { ->
return shouldSentryAutoUploadGeneral() && shouldSentryAutoUploadNative()
}

project.ext.shouldCopySentryOptionsFile = { -> // If not set, default to true
return System.getenv('SENTRY_COPY_OPTIONS_FILE') != 'false'
}

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

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

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

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

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

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
Loading

0 comments on commit 66c8b49

Please sign in to comment.