From bf7c762121edab595d7bd6cdadbf3fdaf8fed01f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 14 Jan 2025 19:22:34 +0200 Subject: [PATCH 01/43] Extract Android SDK Init --- .../io/sentry/react/RNSentryModuleImpl.java | 284 +--------------- .../java/io/sentry/react/RNSentryStart.java | 306 ++++++++++++++++++ 2 files changed, 310 insertions(+), 280 deletions(-) create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java 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..cb8b99d9e7 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; @@ -133,11 +117,14 @@ public class RNSentryModuleImpl { private final @NotNull SentryDateProvider dateProvider; + private final @NotNull RNSentryStart sdk; + public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) { packageInfo = getPackageInfo(reactApplicationContext); this.reactApplicationContext = reactApplicationContext; this.emitNewFrameEvent = createEmitNewFrameEvent(); this.dateProvider = new SentryAndroidDateProvider(); + this.sdk = new RNSentryStart(); } private ReactApplicationContext getReactApplicationContext() { @@ -178,216 +165,11 @@ public void initNativeReactNavigationNewFrameTracking(Promise promise) { } public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { - SentryAndroid.init( - this.getReactApplicationContext(), - options -> getSentryAndroidOptions(options, rnOptions, logger)); + sdk.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 +756,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 +769,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..4f04c29493 --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -0,0 +1,306 @@ +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 org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +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; + +public class RNSentryStart { + + public 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)); + } + + protected 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 void setCurrentActivity(Activity currentActivity) { + final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); + 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; + } + + 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 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(); + } +} From 897d91805ad07759ba32cfa08a05aca44e1cc288 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 14 Jan 2025 19:22:44 +0200 Subject: [PATCH 02/43] Update tests --- .../io/sentry/react/RNSentryModuleImplTest.kt | 166 --------------- .../java/io/sentry/react/RNSentryStartTest.kt | 193 ++++++++++++++++++ 2 files changed, 193 insertions(+), 166 deletions(-) create mode 100644 packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt 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..e49e49f349 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryStartTest.kt @@ -0,0 +1,193 @@ +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 module: RNSentryStart + 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) + module = RNSentryStart() + } + + @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, 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() + module.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() + module.getSentryAndroidOptions(actualOptions, options, activity, logger) + assertFalse(actualOptions.isEnableSpotlight) + } + + @Test + fun `the JavascriptException is added to the ignoredExceptionsForType list on initialisation`() { + val actualOptions = SentryAndroidOptions() + module.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", + ) + module.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, + ) + module.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", + ) + module.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() + module.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") + module.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") + module.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) + } +} From e06e6bbe07df52cfe31c1c995dcbdfa7d36e4c4f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 14 Jan 2025 19:25:39 +0200 Subject: [PATCH 03/43] Adds changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dd716d64a..68f1a9f5e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#8480) - [diff](https://github.com/getsentry/sentry-javascript/compare/8.47.0...8.48.0) +### Internal + +- Extract Android native initialization to standalone structures ([#4445](https://github.com/getsentry/sentry-react-native/pull/4445)) + ## 6.5.0 ### Features From 903fc5542ebe74edfe2c1d22b0ac6067342c0c8a Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 14 Jan 2025 19:31:56 +0200 Subject: [PATCH 04/43] Fix lint issues --- .../io/sentry/react/RNSentryModuleImpl.java | 3 +- .../java/io/sentry/react/RNSentryStart.java | 513 +++++++++--------- 2 files changed, 259 insertions(+), 257 deletions(-) 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 cb8b99d9e7..6f655e3c2c 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 @@ -165,7 +165,8 @@ public void initNativeReactNavigationNewFrameTracking(Promise promise) { } public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { - sdk.startWithOptions(this.getReactApplicationContext(), rnOptions, getCurrentActivity(), logger); + sdk.startWithOptions( + this.getReactApplicationContext(), rnOptions, getCurrentActivity(), logger); promise.resolve(true); } 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 index 4f04c29493..e839502471 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -2,18 +2,9 @@ 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 org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; - import io.sentry.ILogger; import io.sentry.Integration; import io.sentry.SentryEvent; @@ -30,277 +21,287 @@ 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 class RNSentryStart { - public 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)); + public 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)); + } + + protected 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); - protected 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; - }); + options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); + options.setNativeSdkName(RNSentryVersion.NATIVE_SDK_NAME); + options.setSdkVersion(sdkVersion); - 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); + if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) { + options.setDebug(true); } - - private void setCurrentActivity(Activity currentActivity) { - final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); - if (currentActivity != null) { - currentActivityHolder.setActivity(currentActivity); - } + 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(""); } - - private boolean isReplayEnabled(SentryReplayOptions replayOptions) { - return replayOptions.getSessionSampleRate() != null - || replayOptions.getOnErrorSampleRate() != null; + 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")); + } } - 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; - } + SentryReplayOptions replayOptions = getReplayOptions(rnOptions); + options.setSessionReplay(replayOptions); + if (isReplayEnabled(replayOptions)) { + options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter()); + } - androidReplayOptions.setMaskAllText( - !rnMobileReplayOptions.hasKey("maskAllText") - || rnMobileReplayOptions.getBoolean("maskAllText")); - androidReplayOptions.setMaskAllImages( - !rnMobileReplayOptions.hasKey("maskAllImages") - || rnMobileReplayOptions.getBoolean("maskAllImages")); + // 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 boolean redactVectors = - !rnMobileReplayOptions.hasKey("maskAllVectors") - || rnMobileReplayOptions.getBoolean("maskAllVectors"); - if (redactVectors) { - androidReplayOptions.addMaskViewClass("com.horcrux.svg.SvgView"); // react-native-svg - } + setCurrentActivity(currentActivity); + } - androidReplayOptions.setMaskViewContainerClass(RNSentryReplayMask.class.getName()); - androidReplayOptions.setUnmaskViewContainerClass(RNSentryReplayUnmask.class.getName()); + private void setCurrentActivity(Activity currentActivity) { + final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); + 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; + } - 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; } - 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; - } - } + 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 } - private void setEventEnvironmentTag(SentryEvent event, String environment) { - event.setTag("event.origin", "android"); - event.setTag("event.environment", environment); + androidReplayOptions.setMaskViewContainerClass(RNSentryReplayMask.class.getName()); + androidReplayOptions.setUnmaskViewContainerClass(RNSentryReplayUnmask.class.getName()); + + return androidReplayOptions; + } + + 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 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()); - } - } + private void setEventEnvironmentTag(SentryEvent event, String environment) { + event.setTag("event.origin", "android"); + event.setTag("event.environment", environment); + } - List integrations = sdk.getIntegrations(); - if (integrations != null) { - for (String integration : integrations) { - eventSdk.addIntegration(integration); - } - } + 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()); + } + } - event.setSdk(eventSdk); + 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(); + 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(); + } } From 739ad307b9022fa97f5b95b0cf102f9c34cfec3c Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 15 Jan 2025 15:33:49 +0200 Subject: [PATCH 05/43] Rename RNSentryStart instance for clarity --- .../src/main/java/io/sentry/react/RNSentryModuleImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 6f655e3c2c..92acbe4613 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 @@ -117,14 +117,14 @@ public class RNSentryModuleImpl { private final @NotNull SentryDateProvider dateProvider; - private final @NotNull RNSentryStart sdk; + private final @NotNull RNSentryStart startSdk; public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) { packageInfo = getPackageInfo(reactApplicationContext); this.reactApplicationContext = reactApplicationContext; this.emitNewFrameEvent = createEmitNewFrameEvent(); this.dateProvider = new SentryAndroidDateProvider(); - this.sdk = new RNSentryStart(); + this.startSdk = new RNSentryStart(); } private ReactApplicationContext getReactApplicationContext() { @@ -165,7 +165,7 @@ public void initNativeReactNavigationNewFrameTracking(Promise promise) { } public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { - sdk.startWithOptions( + startSdk.startWithOptions( this.getReactApplicationContext(), rnOptions, getCurrentActivity(), logger); promise.resolve(true); From 0f062665cc716db5c8e077218820438c64d440b6 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 15 Jan 2025 16:52:50 +0200 Subject: [PATCH 06/43] WIP: Init with hardcoded json string --- .../io/sentry/react/RNSentryMapConverter.java | 34 +++++++++++++++++ .../java/io/sentry/react/RNSentrySDK.java | 37 +++++++++++++++++++ .../reactnative/sample/MainApplication.kt | 3 ++ samples/react-native/sentry.options.json | 20 ++++++++++ 4 files changed, 94 insertions(+) create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java create mode 100644 samples/react-native/sentry.options.json diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java index 467ffcd90f..6f8927a473 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java @@ -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; @@ -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"; @@ -131,4 +136,33 @@ private static void addValueToWritableMap(WritableMap writableMap, String key, O logger.log(SentryLevel.ERROR, "Could not convert object" + value); } } + + public static ReadableMap jsonObjectToReadableMap(JSONObject jsonObject) { + // We are not directly using `convertToWritable` since `Arguments.createArray()` + // fails before bridge initialisation + Map map = jsonObjectToMap(jsonObject); + Object[] keysAndValues = new Object[map.size() * 2]; + int index = 0; + for (Map.Entry entry : map.entrySet()) { + keysAndValues[index++] = entry.getKey(); + keysAndValues[index++] = entry.getValue(); + } + return JavaOnlyMap.of(keysAndValues); + } + + private static Map jsonObjectToMap(JSONObject jsonObject) { + Map map = new HashMap<>(); + Iterator 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; + } } diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java new file mode 100644 index 0000000000..e14f71930f --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -0,0 +1,37 @@ +package io.sentry.react; + +import android.content.Context; +import com.facebook.react.bridge.ReadableMap; +import io.sentry.ILogger; +import io.sentry.android.core.AndroidLogger; +import org.jetbrains.annotations.NotNull; +import org.json.JSONException; +import org.json.JSONObject; + +public 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 RNSentryStart startSdk; + + public RNSentrySDK() { + startSdk = new RNSentryStart(); + } + + public void startWithOptions( + @NotNull final Context context, @NotNull final ReadableMap rnOptions) { + startSdk.startWithOptions(context, rnOptions, null, logger); + } + + public void start(@NotNull final Context context) { + String json = + "{\"dsn\": \"https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561\"}"; + try { + ReadableMap rnOptions = RNSentryMapConverter.jsonObjectToReadableMap(new JSONObject(json)); + startWithOptions(context, rnOptions); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } +} diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index 07747f085c..985f506525 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -15,6 +15,7 @@ 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(), @@ -43,6 +44,8 @@ class MainApplication : // When the native init is enabled the `autoInitializeNativeSdk` // in JS has to be set to `false` // this.initializeSentry() + val sdk = RNSentrySDK() + sdk.start(this) SoLoader.init(this, OpenSourceMergedSoMapping) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. 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 +} From 42c32ecd5f5f988fe17d116e990d2ea61628243e Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 15 Jan 2025 16:58:26 +0200 Subject: [PATCH 07/43] Adds changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f1a9f5e7..d2f3e5ae61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,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 initialization using `sentry.options.json` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) ### Dependencies From a98bd88a40341a063c41910da5f0c97b96b6c062 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 15 Jan 2025 17:26:11 +0200 Subject: [PATCH 08/43] Expose only a Java Map parameter in the startWithOptions --- CHANGELOG.md | 2 +- .../io/sentry/react/RNSentryMapConverter.java | 6 +++++- .../main/java/io/sentry/react/RNSentrySDK.java | 18 +++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f3e5ae61..3165a85535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,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 initialization using `sentry.options.json` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) +- Add experimental initialization using `sentry.options.json` and `RNSentrySDK.startWithOptions` method for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) ### Dependencies diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java index 6f8927a473..c4c6867e42 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java @@ -138,9 +138,13 @@ private static void addValueToWritableMap(WritableMap writableMap, String key, O } public static ReadableMap jsonObjectToReadableMap(JSONObject jsonObject) { + Map map = jsonObjectToMap(jsonObject); + return mapToReadableMap(map); + } + + public static ReadableMap mapToReadableMap(Map map) { // We are not directly using `convertToWritable` since `Arguments.createArray()` // fails before bridge initialisation - Map map = jsonObjectToMap(jsonObject); Object[] keysAndValues = new Object[map.size() * 2]; int index = 0; for (Map.Entry entry : map.entrySet()) { diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index e14f71930f..6d1afc597e 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -4,6 +4,7 @@ import com.facebook.react.bridge.ReadableMap; import io.sentry.ILogger; import io.sentry.android.core.AndroidLogger; +import java.util.Map; import org.jetbrains.annotations.NotNull; import org.json.JSONException; import org.json.JSONObject; @@ -19,11 +20,26 @@ public RNSentrySDK() { startSdk = new RNSentryStart(); } - public void startWithOptions( + private void startWithOptions( @NotNull final Context context, @NotNull final ReadableMap rnOptions) { startSdk.startWithOptions(context, rnOptions, null, logger); } + /** + * Start the Native Android SDK with the provided options + * @param context Android Context + * @param options Map with options + */ + public void startWithOptions( + @NotNull final Context context, @NotNull final Map options) { + ReadableMap rnOptions = RNSentryMapConverter.mapToReadableMap(options); + startWithOptions(context, rnOptions); + } + + /** + * Start the Native Android SDK with options from `sentry.options.json` configuration file + * @param context Android Context + */ public void start(@NotNull final Context context) { String json = "{\"dsn\": \"https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561\"}"; From 1da8ca38d2c049fcfd1693273030b5e62b431e5f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 15 Jan 2025 17:30:07 +0200 Subject: [PATCH 09/43] Fix lint issue --- .../core/android/src/main/java/io/sentry/react/RNSentrySDK.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index 6d1afc597e..66fdb967a3 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -27,6 +27,7 @@ private void startWithOptions( /** * Start the Native Android SDK with the provided options + * * @param context Android Context * @param options Map with options */ @@ -38,6 +39,7 @@ public void startWithOptions( /** * Start the Native Android SDK with options from `sentry.options.json` configuration file + * * @param context Android Context */ public void start(@NotNull final Context context) { From be6b815bf3c3c572bde775d0a4b9458f5b5c1cd3 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 15 Jan 2025 17:48:11 +0200 Subject: [PATCH 10/43] Convert RNSentrySDK to utility class --- .../src/main/java/io/sentry/react/RNSentrySDK.java | 14 +++++++------- .../sentry/reactnative/sample/MainApplication.kt | 13 +++++++++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index 66fdb967a3..129f8c04d6 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -9,18 +9,18 @@ import org.json.JSONException; import org.json.JSONObject; -public class RNSentrySDK { +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 RNSentryStart startSdk; + private static final RNSentryStart startSdk = new RNSentryStart(); - public RNSentrySDK() { - startSdk = new RNSentryStart(); + private RNSentrySDK() { + throw new AssertionError("Utility class should not be instantiated"); } - private void startWithOptions( + private static void startWithOptions( @NotNull final Context context, @NotNull final ReadableMap rnOptions) { startSdk.startWithOptions(context, rnOptions, null, logger); } @@ -31,7 +31,7 @@ private void startWithOptions( * @param context Android Context * @param options Map with options */ - public void startWithOptions( + public static void startWithOptions( @NotNull final Context context, @NotNull final Map options) { ReadableMap rnOptions = RNSentryMapConverter.mapToReadableMap(options); startWithOptions(context, rnOptions); @@ -42,7 +42,7 @@ public void startWithOptions( * * @param context Android Context */ - public void start(@NotNull final Context context) { + public static void start(@NotNull final Context context) { String json = "{\"dsn\": \"https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561\"}"; try { diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index 985f506525..a0be2b3ff8 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -44,8 +44,17 @@ class MainApplication : // When the native init is enabled the `autoInitializeNativeSdk` // in JS has to be set to `false` // this.initializeSentry() - val sdk = RNSentrySDK() - sdk.start(this) + +// RNSentrySDK.startWithOptions( +// this, +// mapOf( +// "dsn" to "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", +// "debug" to true, +// ), +// ) + + RNSentrySDK.start(this) + SoLoader.init(this, OpenSourceMergedSoMapping) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. From 21638840b20a7756c5cd3b2719b9c35357b1d0a5 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 15 Jan 2025 19:04:47 +0200 Subject: [PATCH 11/43] Initialise from assets json file --- .../java/io/sentry/react/RNSentrySDK.java | 39 +++++++++++++--- .../reactnative/sample/MainApplication.kt | 44 ++++--------------- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index 129f8c04d6..1c1dd09946 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -3,14 +3,17 @@ import android.content.Context; import com.facebook.react.bridge.ReadableMap; import io.sentry.ILogger; +import io.sentry.SentryLevel; import io.sentry.android.core.AndroidLogger; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; import java.util.Map; import org.jetbrains.annotations.NotNull; -import org.json.JSONException; import org.json.JSONObject; public final class RNSentrySDK { - // private static final String CONFIGURATION_FILE = "sentry.options.json"; + private static final String CONFIGURATION_FILE = "sentry.options.json"; private static final String NAME = "RNSentrySDK"; private static final ILogger logger = new AndroidLogger(NAME); @@ -43,13 +46,37 @@ public static void startWithOptions( * @param context Android Context */ public static void start(@NotNull final Context context) { - String json = - "{\"dsn\": \"https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561\"}"; try { - ReadableMap rnOptions = RNSentryMapConverter.jsonObjectToReadableMap(new JSONObject(json)); + JSONObject jsonObject = getOptionsFromConfigurationFile(context); + ReadableMap rnOptions = RNSentryMapConverter.jsonObjectToReadableMap(jsonObject); startWithOptions(context, rnOptions); - } catch (JSONException e) { + } catch (Exception e) { + logger.log( + SentryLevel.ERROR, "Failed to start Sentry with options from configuration file.", e); throw new RuntimeException(e); } } + + 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; + } + } } diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index a0be2b3ff8..2066421c6f 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -11,10 +11,6 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.soloader.OpenSourceMergedSoMapping 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 : @@ -45,16 +41,6 @@ class MainApplication : // in JS has to be set to `false` // this.initializeSentry() -// RNSentrySDK.startWithOptions( -// this, -// mapOf( -// "dsn" to "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", -// "debug" to true, -// ), -// ) - - RNSentrySDK.start(this) - SoLoader.init(this, OpenSourceMergedSoMapping) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. @@ -63,28 +49,14 @@ 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 - options.dsn = "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561" - options.isDebug = true - - options.beforeSend = - BeforeSendCallback { event: SentryEvent, hint: Hint? -> - // React native internally throws a JavascriptException - // Since we catch it before that, we don't want to send this one - // because we would send it twice - try { - val ex = event.exceptions!![0] - if (null != ex && ex.type!!.contains("JavascriptException")) { - return@BeforeSendCallback null - } - } catch (ignored: Throwable) { - // We do nothing - } +// RNSentrySDK.startWithOptions( +// this, +// mapOf( +// "dsn" to "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", +// "debug" to true, +// ), +// ) - event - } - } + RNSentrySDK.start(this) } } From 84547277d310b1b67bf68de47a7224c03bf483d9 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 16 Jan 2025 10:59:51 +0200 Subject: [PATCH 12/43] Copy configuration json in the android assets folder before building --- packages/core/sentry.gradle | 29 +++++++++++++++++++ .../reactnative/sample/MainApplication.kt | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index fbbf567412..2853970800 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -17,7 +17,36 @@ project.ext.shouldSentryAutoUpload = { -> def config = project.hasProperty("sentryCli") ? project.sentryCli : []; +tasks.register("copySentryJsonConfiguration") { + doLast { + def configFile = "sentry.options.json" + def appRoot = project.rootDir.parentFile ?: project.rootDir + def sentryOptionsFile = new File(appRoot, configFile) + def androidAssetsDir = new File("$rootDir/app/src/main/assets") // Path to Android assets folder + + 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})") + } + } +} + gradle.projectsEvaluated { + // Add a task that copies the sentry.options.json file before the build starts + tasks.named("preBuild").configure { + dependsOn("copySentryJsonConfiguration") + } + def releases = extractReleasesInfo() if (config.flavorAware && config.sentryProperties) { diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index 2066421c6f..c78c88d663 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -39,7 +39,7 @@ class MainApplication : super.onCreate() // When the native init is enabled the `autoInitializeNativeSdk` // in JS has to be set to `false` - // this.initializeSentry() + this.initializeSentry() SoLoader.init(this, OpenSourceMergedSoMapping) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { From 1c31e2171077d44039b86cd13ce88037d03ff6aa Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 16 Jan 2025 12:25:50 +0200 Subject: [PATCH 13/43] Adds map converter tests --- .../RNSentryMapConverterTest.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/rnsentryandroidtester/RNSentryMapConverterTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/rnsentryandroidtester/RNSentryMapConverterTest.kt index 6424399bdf..96a4f98385 100644 --- a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/rnsentryandroidtester/RNSentryMapConverterTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/rnsentryandroidtester/RNSentryMapConverterTest.kt @@ -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 @@ -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")) + } } From ff20ed9238668824f53725457f3ecc928883744f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 16 Jan 2025 12:27:32 +0200 Subject: [PATCH 14/43] Disable by default in the sample --- .../main/java/io/sentry/reactnative/sample/MainApplication.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index c78c88d663..9daa12031e 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -39,8 +39,7 @@ class MainApplication : super.onCreate() // When the native init is enabled the `autoInitializeNativeSdk` // in JS has to be set to `false` - this.initializeSentry() - + // this.initializeSentry() SoLoader.init(this, OpenSourceMergedSoMapping) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. From 6509ada94463a780dc92ce1ab88c319c42897f4c Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 16 Jan 2025 18:19:58 +0200 Subject: [PATCH 15/43] Cleanup sentry.options.json from assets after the build --- packages/core/sentry.gradle | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 2853970800..999440b4d9 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -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' } @@ -17,13 +17,13 @@ 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 configFile = "sentry.options.json" def appRoot = project.rootDir.parentFile ?: project.rootDir def sentryOptionsFile = new File(appRoot, configFile) - def androidAssetsDir = new File("$rootDir/app/src/main/assets") // Path to Android assets folder - if (sentryOptionsFile.exists()) { if (!androidAssetsDir.exists()) { androidAssetsDir.mkdirs() @@ -41,11 +41,27 @@ tasks.register("copySentryJsonConfiguration") { } } +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() From dcb7e2e4a96291b455fd2bc162b6f21961306388 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 20 Jan 2025 16:41:23 +0200 Subject: [PATCH 16/43] Converts RNSentryStart to utility class --- .../java/io/sentry/react/RNSentryStartTest.kt | 22 +++++++++---------- .../io/sentry/react/RNSentryModuleImpl.java | 5 +---- .../java/io/sentry/react/RNSentryStart.java | 22 +++++++++++-------- 3 files changed, 24 insertions(+), 25 deletions(-) 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 index e49e49f349..c2ee6f1d88 100644 --- 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 @@ -19,7 +19,6 @@ import org.mockito.MockitoAnnotations @RunWith(JUnit4::class) class RNSentryStartTest { - private lateinit var module: RNSentryStart private lateinit var logger: ILogger private lateinit var activity: Activity @@ -29,7 +28,6 @@ class RNSentryStartTest { MockitoAnnotations.openMocks(this) logger = mock(ILogger::class.java) activity = mock(Activity::class.java) - module = RNSentryStart() } @Test @@ -42,7 +40,7 @@ class RNSentryStartTest { "http://localhost:8969/teststream", ) val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, options, activity, logger) + RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) assert(actualOptions.isEnableSpotlight) assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) } @@ -51,7 +49,7 @@ class RNSentryStartTest { 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, activity, logger) + RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) assert(actualOptions.isEnableSpotlight) assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) } @@ -60,14 +58,14 @@ class RNSentryStartTest { 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, activity, logger) + RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) assertFalse(actualOptions.isEnableSpotlight) } @Test fun `the JavascriptException is added to the ignoredExceptionsForType list on initialisation`() { val actualOptions = SentryAndroidOptions() - module.getSentryAndroidOptions(actualOptions, JavaOnlyMap.of(), activity, logger) + RNSentryStart.getSentryAndroidOptions(actualOptions, JavaOnlyMap.of(), activity, logger) assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) } @@ -81,7 +79,7 @@ class RNSentryStartTest { "devServerUrl", "http://localhost:8081", ) - module.getSentryAndroidOptions(options, rnOptions, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) val breadcrumb = Breadcrumb().apply { @@ -105,7 +103,7 @@ class RNSentryStartTest { "devServerUrl", mockDevServerUrl, ) - module.getSentryAndroidOptions(options, rnOptions, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) val breadcrumb = Breadcrumb().apply { @@ -128,7 +126,7 @@ class RNSentryStartTest { "devServerUrl", "http://localhost:8081", ) - module.getSentryAndroidOptions(options, rnOptions, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) val breadcrumb = Breadcrumb().apply { @@ -144,7 +142,7 @@ class RNSentryStartTest { @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(), activity, logger) + RNSentryStart.getSentryAndroidOptions(options, JavaOnlyMap(), activity, logger) val breadcrumb = Breadcrumb().apply { @@ -161,7 +159,7 @@ class RNSentryStartTest { 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, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) val breadcrumb = Breadcrumb().apply { @@ -178,7 +176,7 @@ class RNSentryStartTest { 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, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) val breadcrumb = Breadcrumb().apply { 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 92acbe4613..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 @@ -117,14 +117,11 @@ public class RNSentryModuleImpl { private final @NotNull SentryDateProvider dateProvider; - private final @NotNull RNSentryStart startSdk; - public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) { packageInfo = getPackageInfo(reactApplicationContext); this.reactApplicationContext = reactApplicationContext; this.emitNewFrameEvent = createEmitNewFrameEvent(); this.dateProvider = new SentryAndroidDateProvider(); - this.startSdk = new RNSentryStart(); } private ReactApplicationContext getReactApplicationContext() { @@ -165,7 +162,7 @@ public void initNativeReactNavigationNewFrameTracking(Promise promise) { } public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { - startSdk.startWithOptions( + RNSentryStart.startWithOptions( this.getReactApplicationContext(), rnOptions, getCurrentActivity(), logger); promise.resolve(true); 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 index e839502471..263633c4a8 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -27,9 +27,13 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class RNSentryStart { +public final class RNSentryStart { - public void startWithOptions( + 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, @@ -38,7 +42,7 @@ public void startWithOptions( context, options -> getSentryAndroidOptions(options, rnOptions, currentActivity, logger)); } - protected void getSentryAndroidOptions( + static void getSentryAndroidOptions( @NotNull SentryAndroidOptions options, @NotNull ReadableMap rnOptions, @Nullable Activity currentActivity, @@ -184,19 +188,19 @@ protected void getSentryAndroidOptions( setCurrentActivity(currentActivity); } - private void setCurrentActivity(Activity currentActivity) { + private static void setCurrentActivity(Activity currentActivity) { final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); if (currentActivity != null) { currentActivityHolder.setActivity(currentActivity); } } - private boolean isReplayEnabled(SentryReplayOptions replayOptions) { + private static boolean isReplayEnabled(SentryReplayOptions replayOptions) { return replayOptions.getSessionSampleRate() != null || replayOptions.getOnErrorSampleRate() != null; } - private SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { + private static SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { final SdkVersion replaySdkVersion = new SdkVersion( RNSentryVersion.REACT_NATIVE_SDK_NAME, @@ -247,7 +251,7 @@ private SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { return androidReplayOptions; } - private void setEventOriginTag(SentryEvent event) { + 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) { @@ -264,12 +268,12 @@ private void setEventOriginTag(SentryEvent event) { } } - private void setEventEnvironmentTag(SentryEvent event, String environment) { + private static void setEventEnvironmentTag(SentryEvent event, String environment) { event.setTag("event.origin", "android"); event.setTag("event.environment", environment); } - private void addPackages(SentryEvent event, SdkVersion sdk) { + private static void addPackages(SentryEvent event, SdkVersion sdk) { SdkVersion eventSdk = event.getSdk(); if (eventSdk != null && "sentry.javascript.react-native".equals(eventSdk.getName()) From f96e2d1c284223cd0ee07aef7f13d50592cd81e5 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 20 Jan 2025 17:09:37 +0200 Subject: [PATCH 17/43] Align with merged convertion of RNSentryStart to utility class --- .../android/src/main/java/io/sentry/react/RNSentrySDK.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index 1c1dd09946..eddceab791 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -17,7 +17,6 @@ public final class RNSentrySDK { private static final String NAME = "RNSentrySDK"; private static final ILogger logger = new AndroidLogger(NAME); - private static final RNSentryStart startSdk = new RNSentryStart(); private RNSentrySDK() { throw new AssertionError("Utility class should not be instantiated"); @@ -25,7 +24,7 @@ private RNSentrySDK() { private static void startWithOptions( @NotNull final Context context, @NotNull final ReadableMap rnOptions) { - startSdk.startWithOptions(context, rnOptions, null, logger); + RNSentryStart.startWithOptions(context, rnOptions, null, logger); } /** From 7788b81730b579fe8ebe2013abec4c01f31ee347 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 20 Jan 2025 18:33:14 +0200 Subject: [PATCH 18/43] Align with Sentry Android naming --- .../src/main/java/io/sentry/react/RNSentrySDK.java | 13 ++++--------- .../io/sentry/reactnative/sample/MainApplication.kt | 4 ++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index eddceab791..b29862bc53 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -22,21 +22,16 @@ private RNSentrySDK() { throw new AssertionError("Utility class should not be instantiated"); } - private static void startWithOptions( - @NotNull final Context context, @NotNull final ReadableMap rnOptions) { - RNSentryStart.startWithOptions(context, rnOptions, null, logger); - } - /** * Start the Native Android SDK with the provided options * * @param context Android Context * @param options Map with options */ - public static void startWithOptions( + public static void init( @NotNull final Context context, @NotNull final Map options) { ReadableMap rnOptions = RNSentryMapConverter.mapToReadableMap(options); - startWithOptions(context, rnOptions); + RNSentryStart.startWithOptions(context, rnOptions, null, logger); } /** @@ -44,11 +39,11 @@ public static void startWithOptions( * * @param context Android Context */ - public static void start(@NotNull final Context context) { + public static void init(@NotNull final Context context) { try { JSONObject jsonObject = getOptionsFromConfigurationFile(context); ReadableMap rnOptions = RNSentryMapConverter.jsonObjectToReadableMap(jsonObject); - startWithOptions(context, rnOptions); + RNSentryStart.startWithOptions(context, rnOptions, null, logger); } catch (Exception e) { logger.log( SentryLevel.ERROR, "Failed to start Sentry with options from configuration file.", e); diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index 9daa12031e..142c767911 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -48,7 +48,7 @@ class MainApplication : } private fun initializeSentry() { -// RNSentrySDK.startWithOptions( +// RNSentrySDK.init( // this, // mapOf( // "dsn" to "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", @@ -56,6 +56,6 @@ class MainApplication : // ), // ) - RNSentrySDK.start(this) + RNSentrySDK.init(this) } } From 25d142a3ca9653c5da2cb4a8f98c7a29bab48309 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 20 Jan 2025 20:27:41 +0200 Subject: [PATCH 19/43] Override json configuration with passed Android configuration --- ...SentryCompositeOptionsConfigurationTest.kt | 51 +++++++++++++++++++ ...RNSentryCompositeOptionsConfiguration.java | 24 +++++++++ .../java/io/sentry/react/RNSentrySDK.java | 29 ++++++----- .../java/io/sentry/react/RNSentryStart.java | 16 ++++++ .../reactnative/sample/MainApplication.kt | 37 ++++++++++---- 5 files changed, 134 insertions(+), 23 deletions(-) create mode 100644 packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryCompositeOptionsConfigurationTest.kt create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryCompositeOptionsConfigurationTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryCompositeOptionsConfigurationTest.kt new file mode 100644 index 0000000000..a834024ca1 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryCompositeOptionsConfigurationTest.kt @@ -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 = mock() + val overridingConfig: OptionsConfiguration = 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 { options -> + options.dsn = "https://base-dsn@sentry.io" + options.isDebug = false + options.release = "some-release" + } + val overridingConfig = + OptionsConfiguration { options -> + options.dsn = "https://over-dsn@sentry.io" + options.isDebug = true + options.environment = "production" + } + + val compositeConfig = RNSentryCompositeOptionsConfiguration(baseConfig, overridingConfig) + val options = SentryAndroidOptions() + compositeConfig.configure(options) + + assert(options.dsn == "https://over-dsn@sentry.io") // overridden value + assert(options.isDebug) // overridden value + assert(options.release == "some-release") // base value not overridden + assert(options.environment == "production") // overridden value not in base + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java new file mode 100644 index 0000000000..1a82323cb7 --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java @@ -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 { + private final OptionsConfiguration baseConfiguration; + private final OptionsConfiguration overridingConfiguration; + + public RNSentryCompositeOptionsConfiguration( + OptionsConfiguration baseConfiguration, + OptionsConfiguration overridingConfiguration) { + this.baseConfiguration = baseConfiguration; + this.overridingConfiguration = overridingConfiguration; + } + + @Override + public void configure(@NotNull SentryAndroidOptions options) { + baseConfiguration.configure(options); + overridingConfiguration.configure(options); + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index b29862bc53..84b46138e2 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -3,12 +3,13 @@ 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 java.util.Map; import org.jetbrains.annotations.NotNull; import org.json.JSONObject; @@ -26,24 +27,15 @@ private RNSentrySDK() { * Start the Native Android SDK with the provided options * * @param context Android Context - * @param options Map with options + * @param configuration configuration options */ public static void init( - @NotNull final Context context, @NotNull final Map options) { - ReadableMap rnOptions = RNSentryMapConverter.mapToReadableMap(options); - RNSentryStart.startWithOptions(context, rnOptions, null, 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) { + @NotNull final Context context, + @NotNull Sentry.OptionsConfiguration configuration) { try { JSONObject jsonObject = getOptionsFromConfigurationFile(context); ReadableMap rnOptions = RNSentryMapConverter.jsonObjectToReadableMap(jsonObject); - RNSentryStart.startWithOptions(context, rnOptions, null, logger); + RNSentryStart.startWithOptions(context, rnOptions, configuration, null, logger); } catch (Exception e) { logger.log( SentryLevel.ERROR, "Failed to start Sentry with options from configuration file.", e); @@ -51,6 +43,15 @@ public static void init(@NotNull final Context context) { } } + /** + * 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))) { 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 index 263633c4a8..6af6b44a7f 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -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; @@ -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 configuration, + @Nullable Activity currentActivity, + @NotNull ILogger logger) { + Sentry.OptionsConfiguration 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, diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index 142c767911..c6e5b834a5 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -11,6 +11,9 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader +import io.sentry.Hint +import io.sentry.SentryEvent +import io.sentry.SentryOptions.BeforeSendCallback import io.sentry.react.RNSentrySDK class MainApplication : @@ -48,14 +51,30 @@ class MainApplication : } private fun initializeSentry() { -// RNSentrySDK.init( -// this, -// mapOf( -// "dsn" to "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", -// "debug" to true, -// ), -// ) - - RNSentrySDK.init(this) + RNSentrySDK.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 + options.dsn = "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561" + options.isDebug = true + + options.beforeSend = + BeforeSendCallback { event: SentryEvent, hint: Hint? -> + // React native internally throws a JavascriptException + // Since we catch it before that, we don't want to send this one + // because we would send it twice + try { + val ex = event.exceptions!![0] + if (null != ex && ex.type!!.contains("JavascriptException")) { + return@BeforeSendCallback null + } + } catch (ignored: Throwable) { + // We do nothing + } + + event + } + } + + // RNSentrySDK.init(this) } } From cedd24acc4ed412eea5e9ead1d8ada7173f6de90 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 20 Jan 2025 20:33:39 +0200 Subject: [PATCH 20/43] Fix comment --- .../main/java/io/sentry/reactnative/sample/MainApplication.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index c6e5b834a5..77647b9301 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -52,8 +52,7 @@ class MainApplication : private fun initializeSentry() { RNSentrySDK.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 + // Options set here will apply to the Android SDK overriding the ones from `sentry.options.json` options.dsn = "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561" options.isDebug = true From d909ed459c8150277f21514dc4f7f66108ba9d77 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 21 Jan 2025 13:02:16 +0200 Subject: [PATCH 21/43] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f7331e6d9..ba7ff4eb1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +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)) +- Add experimental initialization using `sentry.options.json` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) ### Internal From dfb12d04f2dff1855440341747d7b17cb285c7b5 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 22 Jan 2025 10:20:11 +0200 Subject: [PATCH 22/43] Fix changelog --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcf79f76d2..5622b67e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,10 +38,6 @@ - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#7201) - [diff](https://github.com/getsentry/sentry-java/compare/7.20.0...7.20.1) -### Internal - -- Extract Android native initialization to standalone structures ([#4445](https://github.com/getsentry/sentry-react-native/pull/4445)) - ## 6.5.0 ### Features From 710f1a0f1e5cc9b01918ebbc762c921ff89eb654 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 23 Jan 2025 16:43:24 +0200 Subject: [PATCH 23/43] Use RNSentryJsonConverter.convertToWritable --- .../RNSentryMapConverterTest.kt | 40 ------------------- .../io/sentry/react/RNSentryMapConverter.java | 38 ------------------ .../java/io/sentry/react/RNSentrySDK.java | 2 +- 3 files changed, 1 insertion(+), 79 deletions(-) diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/rnsentryandroidtester/RNSentryMapConverterTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/rnsentryandroidtester/RNSentryMapConverterTest.kt index 96a4f98385..6424399bdf 100644 --- a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/rnsentryandroidtester/RNSentryMapConverterTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/rnsentryandroidtester/RNSentryMapConverterTest.kt @@ -4,14 +4,10 @@ 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 @@ -363,40 +359,4 @@ 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")) - } } diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java index c4c6867e42..467ffcd90f 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryMapConverter.java @@ -1,7 +1,6 @@ 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; @@ -11,13 +10,9 @@ 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"; @@ -136,37 +131,4 @@ 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 map = jsonObjectToMap(jsonObject); - return mapToReadableMap(map); - } - - public static ReadableMap mapToReadableMap(Map 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 entry : map.entrySet()) { - keysAndValues[index++] = entry.getKey(); - keysAndValues[index++] = entry.getValue(); - } - return JavaOnlyMap.of(keysAndValues); - } - - private static Map jsonObjectToMap(JSONObject jsonObject) { - Map map = new HashMap<>(); - Iterator 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; - } } diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index 84b46138e2..7daf6196e2 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -34,7 +34,7 @@ public static void init( @NotNull Sentry.OptionsConfiguration configuration) { try { JSONObject jsonObject = getOptionsFromConfigurationFile(context); - ReadableMap rnOptions = RNSentryMapConverter.jsonObjectToReadableMap(jsonObject); + ReadableMap rnOptions = RNSentryJsonConverter.convertToWritable(jsonObject); RNSentryStart.startWithOptions(context, rnOptions, configuration, null, logger); } catch (Exception e) { logger.log( From 7469dc814dac8cfbd5b46a2f4be9d48358397341 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 23 Jan 2025 17:29:29 +0200 Subject: [PATCH 24/43] Move getOptionsFromConfigurationFile in a utility class --- .../io/sentry/react/RNSentryJsonUtils.java | 41 +++++++++++++++++++ .../java/io/sentry/react/RNSentrySDK.java | 29 +------------ 2 files changed, 43 insertions(+), 27 deletions(-) create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java new file mode 100644 index 0000000000..9c7cf5d3ff --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryJsonUtils.java @@ -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; + } + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index 7daf6196e2..269e3ae7e5 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -7,9 +7,6 @@ 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; @@ -33,7 +30,8 @@ public static void init( @NotNull final Context context, @NotNull Sentry.OptionsConfiguration configuration) { try { - JSONObject jsonObject = getOptionsFromConfigurationFile(context); + JSONObject jsonObject = + RNSentryJsonUtils.getOptionsFromConfigurationFile(context, CONFIGURATION_FILE, logger); ReadableMap rnOptions = RNSentryJsonConverter.convertToWritable(jsonObject); RNSentryStart.startWithOptions(context, rnOptions, configuration, null, logger); } catch (Exception e) { @@ -51,27 +49,4 @@ public static void init( 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; - } - } } From e266ae1e07c18a07c59e201dd7ecdc42736102aa Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 23 Jan 2025 17:38:02 +0200 Subject: [PATCH 25/43] Handle initialisation errors --- .../java/io/sentry/react/RNSentrySDK.java | 28 +++++++++++++++++-- .../java/io/sentry/react/RNSentryStart.java | 6 ++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index 269e3ae7e5..5d1f635464 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -25,28 +25,50 @@ private RNSentrySDK() { * * @param context Android Context * @param configuration configuration options + * @param logger logger */ public static void init( @NotNull final Context context, - @NotNull Sentry.OptionsConfiguration configuration) { + @NotNull Sentry.OptionsConfiguration 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, null, logger); } catch (Exception e) { logger.log( SentryLevel.ERROR, "Failed to start Sentry with options from configuration file.", e); - throw new RuntimeException(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 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 -> {}); + init(context, options -> {}, logger); } } 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 index 6af6b44a7f..ae3468d6ef 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -34,6 +34,12 @@ private RNSentryStart() { throw new AssertionError("Utility class should not be instantiated"); } + public static void startWithConfiguration( + @NotNull final Context context, + @NotNull Sentry.OptionsConfiguration configuration) { + SentryAndroid.init(context, configuration); + } + public static void startWithOptions( @NotNull final Context context, @NotNull final ReadableMap rnOptions, From 7522fcebe114a06f11a9421a96539904e4edda2b Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 24 Jan 2025 10:29:37 +0200 Subject: [PATCH 26/43] Remove beforeSend initialisation option --- .../reactnative/sample/MainApplication.kt | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt index 77647b9301..6546ca8b10 100644 --- a/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt +++ b/samples/react-native/android/app/src/main/java/io/sentry/reactnative/sample/MainApplication.kt @@ -11,9 +11,6 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.soloader.SoLoader -import io.sentry.Hint -import io.sentry.SentryEvent -import io.sentry.SentryOptions.BeforeSendCallback import io.sentry.react.RNSentrySDK class MainApplication : @@ -55,23 +52,6 @@ class MainApplication : // Options set here will apply to the Android SDK overriding the ones from `sentry.options.json` options.dsn = "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561" options.isDebug = true - - options.beforeSend = - BeforeSendCallback { event: SentryEvent, hint: Hint? -> - // React native internally throws a JavascriptException - // Since we catch it before that, we don't want to send this one - // because we would send it twice - try { - val ex = event.exceptions!![0] - if (null != ex && ex.type!!.contains("JavascriptException")) { - return@BeforeSendCallback null - } - } catch (ignored: Throwable) { - // We do nothing - } - - event - } } // RNSentrySDK.init(this) From 5ebe8fb28e9ef7ea208bb8e330114895f5aaaec4 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 24 Jan 2025 10:56:13 +0200 Subject: [PATCH 27/43] Lower access level were possible --- .../RNSentryCompositeOptionsConfigurationTest.kt | 3 +-- .../react/RNSentryCompositeOptionsConfiguration.java | 5 ++--- .../src/main/java/io/sentry/react/RNSentrySDK.java | 2 +- .../src/main/java/io/sentry/react/RNSentryStart.java | 11 +++++------ 4 files changed, 9 insertions(+), 12 deletions(-) rename packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/{rnsentryandroidtester => react}/RNSentryCompositeOptionsConfigurationTest.kt (95%) diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryCompositeOptionsConfigurationTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryCompositeOptionsConfigurationTest.kt similarity index 95% rename from packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryCompositeOptionsConfigurationTest.kt rename to packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryCompositeOptionsConfigurationTest.kt index a834024ca1..699fd81ccb 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/rnsentryandroidtester/RNSentryCompositeOptionsConfigurationTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryCompositeOptionsConfigurationTest.kt @@ -1,8 +1,7 @@ -package io.sentry.rnsentryandroidtester +package io.sentry.react 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 diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java index 1a82323cb7..d49fe823a1 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java @@ -4,12 +4,11 @@ import io.sentry.android.core.SentryAndroidOptions; import org.jetbrains.annotations.NotNull; -public class RNSentryCompositeOptionsConfiguration - implements OptionsConfiguration { +class RNSentryCompositeOptionsConfiguration implements OptionsConfiguration { private final OptionsConfiguration baseConfiguration; private final OptionsConfiguration overridingConfiguration; - public RNSentryCompositeOptionsConfiguration( + RNSentryCompositeOptionsConfiguration( OptionsConfiguration baseConfiguration, OptionsConfiguration overridingConfiguration) { this.baseConfiguration = baseConfiguration; diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index 5d1f635464..c1d15c713c 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -43,7 +43,7 @@ public static void init( RNSentryStart.startWithConfiguration(context, configuration); return; } - RNSentryStart.startWithOptions(context, rnOptions, configuration, null, logger); + RNSentryStart.startWithOptions(context, rnOptions, configuration, logger); } catch (Exception e) { logger.log( SentryLevel.ERROR, "Failed to start Sentry with options from configuration file.", e); 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 index ae3468d6ef..15bd7b28ec 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -28,26 +28,25 @@ 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 startWithConfiguration( + static void startWithConfiguration( @NotNull final Context context, @NotNull Sentry.OptionsConfiguration configuration) { SentryAndroid.init(context, configuration); } - public static void startWithOptions( + static void startWithOptions( @NotNull final Context context, @NotNull final ReadableMap rnOptions, @NotNull Sentry.OptionsConfiguration configuration, - @Nullable Activity currentActivity, @NotNull ILogger logger) { Sentry.OptionsConfiguration rnConfigurationOptions = - options -> getSentryAndroidOptions(options, rnOptions, currentActivity, logger); + options -> getSentryAndroidOptions(options, rnOptions, null, logger); RNSentryCompositeOptionsConfiguration compositeConfiguration = new RNSentryCompositeOptionsConfiguration(rnConfigurationOptions, configuration); @@ -55,7 +54,7 @@ public static void startWithOptions( SentryAndroid.init(context, compositeConfiguration); } - public static void startWithOptions( + static void startWithOptions( @NotNull final Context context, @NotNull final ReadableMap rnOptions, @Nullable Activity currentActivity, From b6a36a1954d471f8d1942f7dc498ed9482aa07a0 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 24 Jan 2025 12:17:45 +0200 Subject: [PATCH 28/43] Allow combining more than two configurations --- ...RNSentryCompositeOptionsConfiguration.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java index d49fe823a1..0069abb660 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryCompositeOptionsConfiguration.java @@ -2,22 +2,24 @@ import io.sentry.Sentry.OptionsConfiguration; import io.sentry.android.core.SentryAndroidOptions; +import java.util.List; import org.jetbrains.annotations.NotNull; class RNSentryCompositeOptionsConfiguration implements OptionsConfiguration { - private final OptionsConfiguration baseConfiguration; - private final OptionsConfiguration overridingConfiguration; + private final @NotNull List> configurations; - RNSentryCompositeOptionsConfiguration( - OptionsConfiguration baseConfiguration, - OptionsConfiguration overridingConfiguration) { - this.baseConfiguration = baseConfiguration; - this.overridingConfiguration = overridingConfiguration; + @SafeVarargs + protected RNSentryCompositeOptionsConfiguration( + @NotNull OptionsConfiguration... configurations) { + this.configurations = List.of(configurations); } @Override public void configure(@NotNull SentryAndroidOptions options) { - baseConfiguration.configure(options); - overridingConfiguration.configure(options); + for (OptionsConfiguration configuration : configurations) { + if (configuration != null) { + configuration.configure(options); + } + } } } From 8e87539f6cc99572dc4765e4f5a9cd646f44ec7b Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 24 Jan 2025 12:18:13 +0200 Subject: [PATCH 29/43] Add default and final configuration --- .../java/io/sentry/react/RNSentryStart.java | 70 +++++++++++++++---- 1 file changed, 56 insertions(+), 14 deletions(-) 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 index 15bd7b28ec..fba830f062 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -10,6 +10,7 @@ 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; @@ -37,7 +38,12 @@ private RNSentryStart() { static void startWithConfiguration( @NotNull final Context context, @NotNull Sentry.OptionsConfiguration configuration) { - SentryAndroid.init(context, configuration); + RNSentryCompositeOptionsConfiguration compositeConfiguration = + new RNSentryCompositeOptionsConfiguration( + RNSentryStart::updateWithReactDefaults, + configuration, + RNSentryStart::updateWithReactFinals); + SentryAndroid.init(context, compositeConfiguration); } static void startWithOptions( @@ -47,10 +53,12 @@ static void startWithOptions( @NotNull ILogger logger) { Sentry.OptionsConfiguration rnConfigurationOptions = options -> getSentryAndroidOptions(options, rnOptions, null, logger); - RNSentryCompositeOptionsConfiguration compositeConfiguration = - new RNSentryCompositeOptionsConfiguration(rnConfigurationOptions, configuration); - + new RNSentryCompositeOptionsConfiguration( + RNSentryStart::updateWithReactDefaults, + rnConfigurationOptions, + configuration, + RNSentryStart::updateWithReactFinals); SentryAndroid.init(context, compositeConfiguration); } @@ -59,8 +67,14 @@ static void startWithOptions( @NotNull final ReadableMap rnOptions, @Nullable Activity currentActivity, @NotNull ILogger logger) { - SentryAndroid.init( - context, options -> getSentryAndroidOptions(options, rnOptions, currentActivity, logger)); + Sentry.OptionsConfiguration rnConfigurationOptions = + options -> getSentryAndroidOptions(options, rnOptions, currentActivity, logger); + RNSentryCompositeOptionsConfiguration compositeConfiguration = + new RNSentryCompositeOptionsConfiguration( + RNSentryStart::updateWithReactDefaults, + rnConfigurationOptions, + RNSentryStart::updateWithReactFinals); + SentryAndroid.init(context, compositeConfiguration); } static void getSentryAndroidOptions( @@ -184,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 integrations = options.getIntegrations(); @@ -209,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) { From e0264ef25670629fb35d73d6650f9364449779f1 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 24 Jan 2025 12:34:38 +0200 Subject: [PATCH 30/43] Add a flag to disable reading from the file --- packages/core/sentry.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/sentry.gradle b/packages/core/sentry.gradle index 999440b4d9..990703527f 100644 --- a/packages/core/sentry.gradle +++ b/packages/core/sentry.gradle @@ -15,12 +15,17 @@ 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) @@ -42,6 +47,7 @@ tasks.register("copySentryJsonConfiguration") { } tasks.register("cleanupTemporarySentryJsonConfiguration") { + onlyIf { shouldCopySentryOptionsFile() } doLast { def sentryOptionsFile = new File(androidAssetsDir, configFile) if (sentryOptionsFile.exists()) { From 927a0039af91205c06e6d174a93d7840dc0b19d8 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 24 Jan 2025 16:17:49 +0200 Subject: [PATCH 31/43] Adds RNSentrySDK tests --- .../androidTest/assets/invalid.options.json | 3 + .../androidTest/assets/invalid.options.txt | 1 + .../androidTest/assets/sentry.options.json | 3 + .../java/io/sentry/react/RNSentrySDKTest.kt | 127 ++++++++++++++++++ .../java/io/sentry/react/RNSentrySDK.java | 16 +-- 5 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json create mode 100644 packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json new file mode 100644 index 0000000000..be3bb71111 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.json @@ -0,0 +1,3 @@ +{ + "dsn": "invalid-dsn" +} \ No newline at end of file diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt new file mode 100644 index 0000000000..f07bfaea41 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/invalid.options.txt @@ -0,0 +1 @@ +invalid-options \ No newline at end of file diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json new file mode 100644 index 0000000000..2a2e1344b9 --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json @@ -0,0 +1,3 @@ +{ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" +} \ No newline at end of file diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt new file mode 100644 index 0000000000..c9c89c073f --- /dev/null +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt @@ -0,0 +1,127 @@ +package io.sentry.react + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import io.sentry.ILogger +import io.sentry.Sentry +import io.sentry.Sentry.OptionsConfiguration +import io.sentry.android.core.AndroidLogger +import io.sentry.android.core.SentryAndroidOptions +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class RNSentrySDKTest { + private val logger: ILogger = AndroidLogger(RNSentrySDKTest::class.java.simpleName) + private lateinit var context: Context + + companion object { + private const val INITIALISATION_ERROR = "Failed to initialize Sentry's React Native SDK" + private const val VALID_OPTIONS = "sentry.options.json" + private const val INVALID_OPTIONS = "invalid.options.json" + private const val INVALID_JSON = "invalid.options.txt" + private const val MISSING = "non-existing-file" + + private val validConfig = + OptionsConfiguration { options -> + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + private val invalidConfig = + OptionsConfiguration { options -> + options.dsn = "invalid-dsn" + } + private val emptyConfig = OptionsConfiguration {} + } + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().context + } + + @After + fun tearDown() { + Sentry.close() + } + + @Test + fun initialisesSuccessfullyWithDefaultValidJsonFile() { // sentry.options.json + RNSentrySDK.init(context) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndDefaultValidJsonFile() { + RNSentrySDK.init(context, validConfig) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndInvalidJsonFile() { + RNSentrySDK.init(context, validConfig, INVALID_OPTIONS, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndMissingJsonFile() { + RNSentrySDK.init(context, validConfig, MISSING, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithValidConfigurationAndErrorInParsingJsonFile() { + RNSentrySDK.init(context, validConfig, INVALID_JSON, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun initialisesSuccessfullyWithNoConfigurationAndValidJsonFile() { + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) + assertTrue(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithNoConfigurationAndInvalidJsonFile() { + try { + RNSentrySDK.init(context, emptyConfig, INVALID_OPTIONS, logger) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithInvalidConfigAndInvalidJsonFile() { + try { + RNSentrySDK.init(context, invalidConfig, INVALID_OPTIONS, logger) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithInvalidConfigAndValidJsonFile() { + try { + RNSentrySDK.init(context, invalidConfig, VALID_OPTIONS, logger) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } + + @Test + fun failsToInitialiseWithInvalidConfigurationAndDefaultValidJsonFile() { + try{ + RNSentrySDK.init(context, invalidConfig) + } catch (e: Exception) { + assertEquals(INITIALISATION_ERROR, e.message) + } + assertFalse(Sentry.isEnabled()) + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index c1d15c713c..c2cbcadd0b 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -20,20 +20,14 @@ 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( + static void init( @NotNull final Context context, @NotNull Sentry.OptionsConfiguration configuration, + @NotNull String configurationFile, @NotNull ILogger logger) { try { JSONObject jsonObject = - RNSentryJsonUtils.getOptionsFromConfigurationFile(context, CONFIGURATION_FILE, logger); + RNSentryJsonUtils.getOptionsFromConfigurationFile(context, configurationFile, logger); if (jsonObject == null) { RNSentryStart.startWithConfiguration(context, configuration); return; @@ -60,7 +54,7 @@ public static void init( public static void init( @NotNull final Context context, @NotNull Sentry.OptionsConfiguration configuration) { - init(context, configuration, logger); + init(context, configuration, CONFIGURATION_FILE, logger); } /** @@ -69,6 +63,6 @@ public static void init( * @param context Android Context */ public static void init(@NotNull final Context context) { - init(context, options -> {}, logger); + init(context, options -> {}, CONFIGURATION_FILE, logger); } } From 007a9d9fc17b0eea5a49cbe9493a977093d41e3c Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 24 Jan 2025 16:41:23 +0200 Subject: [PATCH 32/43] Fix lint issue --- .../src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt index c9c89c073f..bbabb43d69 100644 --- a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt @@ -117,8 +117,8 @@ class RNSentrySDKTest { @Test fun failsToInitialiseWithInvalidConfigurationAndDefaultValidJsonFile() { - try{ - RNSentrySDK.init(context, invalidConfig) + try { + RNSentrySDK.init(context, invalidConfig) } catch (e: Exception) { assertEquals(INITIALISATION_ERROR, e.message) } From 32bc0a71e61777d2b3fe6b1cbba55b41570d0510 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 24 Jan 2025 16:41:35 +0200 Subject: [PATCH 33/43] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dabe8f119b..027b0055e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,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 version of `startWithConfigureOptions` for Apple platforms ([#4444](https://github.com/getsentry/sentry-react-native/pull/4444)) +- Add experimental version of `init` with optional `OptionsConfiguration` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) - Add initialization using `sentry.options.json` for Apple platforms ([#4447](https://github.com/getsentry/sentry-react-native/pull/4447)) - Add initialization using `sentry.options.json` for Android ([#4451](https://github.com/getsentry/sentry-react-native/pull/4451)) From 8fc973f089f062a2cda32ec2f5ffe9b93c04197a Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 24 Jan 2025 16:42:07 +0200 Subject: [PATCH 34/43] Updates javadoc --- .../src/main/java/io/sentry/react/RNSentrySDK.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index c2cbcadd0b..ca219351fe 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -46,8 +46,8 @@ static void init( } /** - * Start the Native Android SDK with the provided options - * + * @experimental Start the Native Android SDK with the provided configuration options. Uses as a + * base configurations the `sentry.options.json` configuration file if it exists. * @param context Android Context * @param configuration configuration options */ @@ -58,8 +58,8 @@ public static void init( } /** - * Start the Native Android SDK with options from `sentry.options.json` configuration file - * + * @experimental Start the Native Android SDK with options from `sentry.options.json` + * configuration file. * @param context Android Context */ public static void init(@NotNull final Context context) { From 821fef94291860ba463269804a57016e3a3dec3f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 3 Feb 2025 15:21:42 +0200 Subject: [PATCH 35/43] Ignore JavascriptException by default --- .../java/io/sentry/react/RNSentryStartTest.kt | 4 ++-- .../main/java/io/sentry/react/RNSentryStart.java | 16 ++++------------ 2 files changed, 6 insertions(+), 14 deletions(-) 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 index c2ee6f1d88..a49112f421 100644 --- 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 @@ -63,9 +63,9 @@ class RNSentryStartTest { } @Test - fun `the JavascriptException is added to the ignoredExceptionsForType list on initialisation`() { + fun `the JavascriptException is added to the ignoredExceptionsForType list on with react defaults`() { val actualOptions = SentryAndroidOptions() - RNSentryStart.getSentryAndroidOptions(actualOptions, JavaOnlyMap.of(), activity, logger) + RNSentryStart.updateWithReactDefaults(actualOptions) assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) } 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 index fba830f062..0fa6f5dc11 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -194,10 +194,6 @@ static void getSentryAndroidOptions( 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); - if (rnOptions.hasKey("enableNativeCrashHandling") && !rnOptions.getBoolean("enableNativeCrashHandling")) { final List integrations = options.getIntegrations(); @@ -224,6 +220,10 @@ static void updateWithReactDefaults(@NotNull SentryAndroidOptions options) { options.setTracesSampleRate(null); options.setTracesSampler(null); options.setEnableTracing(false); + + // React native internally throws a JavascriptException. + // we want to ignore it on the native side to avoid sending it twice. + options.addIgnoredExceptionForType(JavascriptException.class); } /** @@ -234,14 +234,6 @@ 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) { From 43b26db87198ec2e7dbd6c60046b6783d4230eea Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 3 Feb 2025 18:31:25 +0200 Subject: [PATCH 36/43] Move SDK name and version to react defaults --- .../java/io/sentry/react/RNSentryStart.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) 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 index 0fa6f5dc11..d8c767339c 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -82,20 +82,6 @@ static void getSentryAndroidOptions( @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); } @@ -216,6 +202,20 @@ static void getSentryAndroidOptions( * users during manual native initialization. */ static void updateWithReactDefaults(@NotNull SentryAndroidOptions options) { + @Nullable SdkVersion sdkVersion = options.getSdkVersion(); + if (sdkVersion == null) { + sdkVersion = new SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, BuildConfig.VERSION_NAME); + } else { + sdkVersion.setName(RNSentryVersion.ANDROID_SDK_NAME); + } + sdkVersion.addPackage( + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); + + options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); + options.setNativeSdkName(RNSentryVersion.NATIVE_SDK_NAME); + options.setSdkVersion(sdkVersion); + // Tracing is only enabled in JS to avoid duplicate navigation spans options.setTracesSampleRate(null); options.setTracesSampler(null); From d2fd58fe76b3143d723dd4867969fea22532e1a8 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 3 Feb 2025 18:41:31 +0200 Subject: [PATCH 37/43] Set current activity in defaults --- .../java/io/sentry/react/RNSentryStartTest.kt | 20 +++++----- .../java/io/sentry/react/RNSentryStart.java | 37 +++++++++---------- 2 files changed, 28 insertions(+), 29 deletions(-) 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 index a49112f421..5e6eb3107f 100644 --- 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 @@ -40,7 +40,7 @@ class RNSentryStartTest { "http://localhost:8969/teststream", ) val actualOptions = SentryAndroidOptions() - RNSentryStart.getSentryAndroidOptions(actualOptions, options, activity, logger) + RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger) assert(actualOptions.isEnableSpotlight) assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) } @@ -49,7 +49,7 @@ class RNSentryStartTest { 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) + RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger) assert(actualOptions.isEnableSpotlight) assertEquals("http://localhost:8969/teststream", actualOptions.spotlightConnectionUrl) } @@ -58,14 +58,14 @@ class RNSentryStartTest { 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) + RNSentryStart.getSentryAndroidOptions(actualOptions, options, logger) assertFalse(actualOptions.isEnableSpotlight) } @Test fun `the JavascriptException is added to the ignoredExceptionsForType list on with react defaults`() { val actualOptions = SentryAndroidOptions() - RNSentryStart.updateWithReactDefaults(actualOptions) + RNSentryStart.updateWithReactDefaults(actualOptions, activity) assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) } @@ -79,7 +79,7 @@ class RNSentryStartTest { "devServerUrl", "http://localhost:8081", ) - RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) val breadcrumb = Breadcrumb().apply { @@ -103,7 +103,7 @@ class RNSentryStartTest { "devServerUrl", mockDevServerUrl, ) - RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) val breadcrumb = Breadcrumb().apply { @@ -126,7 +126,7 @@ class RNSentryStartTest { "devServerUrl", "http://localhost:8081", ) - RNSentryStart.getSentryAndroidOptions(options, rnOptions, activity, logger) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) val breadcrumb = Breadcrumb().apply { @@ -142,7 +142,7 @@ class RNSentryStartTest { @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) + RNSentryStart.getSentryAndroidOptions(options, JavaOnlyMap(), logger) val breadcrumb = Breadcrumb().apply { @@ -159,7 +159,7 @@ class RNSentryStartTest { 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) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) val breadcrumb = Breadcrumb().apply { @@ -176,7 +176,7 @@ class RNSentryStartTest { 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) + RNSentryStart.getSentryAndroidOptions(options, rnOptions, logger) val breadcrumb = Breadcrumb().apply { 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 index d8c767339c..b92d2c5b2c 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -38,11 +38,11 @@ private RNSentryStart() { static void startWithConfiguration( @NotNull final Context context, @NotNull Sentry.OptionsConfiguration configuration) { + Sentry.OptionsConfiguration defaults = + options -> updateWithReactDefaults(options, null); RNSentryCompositeOptionsConfiguration compositeConfiguration = new RNSentryCompositeOptionsConfiguration( - RNSentryStart::updateWithReactDefaults, - configuration, - RNSentryStart::updateWithReactFinals); + defaults, configuration, RNSentryStart::updateWithReactFinals); SentryAndroid.init(context, compositeConfiguration); } @@ -51,14 +51,13 @@ static void startWithOptions( @NotNull final ReadableMap rnOptions, @NotNull Sentry.OptionsConfiguration configuration, @NotNull ILogger logger) { + Sentry.OptionsConfiguration defaults = + options -> updateWithReactDefaults(options, null); Sentry.OptionsConfiguration rnConfigurationOptions = - options -> getSentryAndroidOptions(options, rnOptions, null, logger); + options -> getSentryAndroidOptions(options, rnOptions, logger); RNSentryCompositeOptionsConfiguration compositeConfiguration = new RNSentryCompositeOptionsConfiguration( - RNSentryStart::updateWithReactDefaults, - rnConfigurationOptions, - configuration, - RNSentryStart::updateWithReactFinals); + defaults, rnConfigurationOptions, configuration, RNSentryStart::updateWithReactFinals); SentryAndroid.init(context, compositeConfiguration); } @@ -67,21 +66,20 @@ static void startWithOptions( @NotNull final ReadableMap rnOptions, @Nullable Activity currentActivity, @NotNull ILogger logger) { + Sentry.OptionsConfiguration defaults = + options -> updateWithReactDefaults(options, currentActivity); Sentry.OptionsConfiguration rnConfigurationOptions = - options -> getSentryAndroidOptions(options, rnOptions, currentActivity, logger); + options -> getSentryAndroidOptions(options, rnOptions, logger); RNSentryCompositeOptionsConfiguration compositeConfiguration = new RNSentryCompositeOptionsConfiguration( - RNSentryStart::updateWithReactDefaults, - rnConfigurationOptions, - RNSentryStart::updateWithReactFinals); + defaults, rnConfigurationOptions, RNSentryStart::updateWithReactFinals); SentryAndroid.init(context, compositeConfiguration); } static void getSentryAndroidOptions( @NotNull SentryAndroidOptions options, @NotNull ReadableMap rnOptions, - @Nullable Activity currentActivity, - ILogger logger) { + @NotNull ILogger logger) { if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) { options.setDebug(true); } @@ -193,15 +191,14 @@ static void getSentryAndroidOptions( } logger.log( SentryLevel.INFO, String.format("Native Integrations '%s'", options.getIntegrations())); - - 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) { + static void updateWithReactDefaults( + @NotNull SentryAndroidOptions options, @Nullable Activity currentActivity) { @Nullable SdkVersion sdkVersion = options.getSdkVersion(); if (sdkVersion == null) { sdkVersion = new SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, BuildConfig.VERSION_NAME); @@ -209,8 +206,8 @@ static void updateWithReactDefaults(@NotNull SentryAndroidOptions options) { sdkVersion.setName(RNSentryVersion.ANDROID_SDK_NAME); } sdkVersion.addPackage( - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, - RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); options.setNativeSdkName(RNSentryVersion.NATIVE_SDK_NAME); @@ -224,6 +221,8 @@ static void updateWithReactDefaults(@NotNull SentryAndroidOptions options) { // React native internally throws a JavascriptException. // we want to ignore it on the native side to avoid sending it twice. options.addIgnoredExceptionForType(JavascriptException.class); + + setCurrentActivity(currentActivity); } /** From 5b9665aefe861f4b4ac8782b4a8e80b210c6c6c3 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 3 Feb 2025 18:49:11 +0200 Subject: [PATCH 38/43] Defaults override file/rn configuration --- .../android/src/main/java/io/sentry/react/RNSentryStart.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index b92d2c5b2c..86699ced05 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -57,7 +57,7 @@ static void startWithOptions( options -> getSentryAndroidOptions(options, rnOptions, logger); RNSentryCompositeOptionsConfiguration compositeConfiguration = new RNSentryCompositeOptionsConfiguration( - defaults, rnConfigurationOptions, configuration, RNSentryStart::updateWithReactFinals); + rnConfigurationOptions, defaults, configuration, RNSentryStart::updateWithReactFinals); SentryAndroid.init(context, compositeConfiguration); } @@ -72,7 +72,7 @@ static void startWithOptions( options -> getSentryAndroidOptions(options, rnOptions, logger); RNSentryCompositeOptionsConfiguration compositeConfiguration = new RNSentryCompositeOptionsConfiguration( - defaults, rnConfigurationOptions, RNSentryStart::updateWithReactFinals); + rnConfigurationOptions, defaults, RNSentryStart::updateWithReactFinals); SentryAndroid.init(context, compositeConfiguration); } From 2b7a319efc391f4c403e7482ccaedb728306861f Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 4 Feb 2025 10:59:40 +0200 Subject: [PATCH 39/43] Add tests for defaults and finals --- .../java/io/sentry/react/RNSentryStartTest.kt | 74 +++++++++++++++++-- 1 file changed, 67 insertions(+), 7 deletions(-) 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 index 5e6eb3107f..fa177159e5 100644 --- 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 @@ -5,9 +5,13 @@ import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.common.JavascriptException import io.sentry.Breadcrumb import io.sentry.ILogger +import io.sentry.SentryEvent +import io.sentry.android.core.CurrentActivityHolder import io.sentry.android.core.SentryAndroidOptions +import io.sentry.protocol.SdkVersion import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before @@ -62,13 +66,6 @@ class RNSentryStartTest { assertFalse(actualOptions.isEnableSpotlight) } - @Test - fun `the JavascriptException is added to the ignoredExceptionsForType list on with react defaults`() { - val actualOptions = SentryAndroidOptions() - RNSentryStart.updateWithReactDefaults(actualOptions, activity) - assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) - } - @Test fun `beforeBreadcrumb callback filters out Sentry DSN requests breadcrumbs`() { val options = SentryAndroidOptions() @@ -188,4 +185,67 @@ class RNSentryStartTest { assertEquals(breadcrumb, result) } + + @Test + fun `the JavascriptException is added to the ignoredExceptionsForType list on with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) + } + + @Test + fun `the sdk version information is added to the initialisation options with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertEquals(RNSentryVersion.ANDROID_SDK_NAME, actualOptions.sdkVersion?.name) + assertEquals( + io.sentry.android.core.BuildConfig.VERSION_NAME, + actualOptions.sdkVersion?.version, + ) + assertEquals(true, actualOptions.sdkVersion?.packages?.isNotEmpty()) + assertEquals( + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, + actualOptions.sdkVersion + ?.packages + ?.last() + ?.name, + ) + assertEquals( + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION, + actualOptions.sdkVersion + ?.packages + ?.last() + ?.version, + ) + } + + @Test + fun `the tracing options are added to the initialisation options with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertNull(actualOptions.tracesSampleRate) + assertNull(actualOptions.tracesSampler) + assertEquals(false, actualOptions.enableTracing) + } + + @Test + fun `the current activity is added to the initialisation options with react defaults`() { + val actualOptions = SentryAndroidOptions() + RNSentryStart.updateWithReactDefaults(actualOptions, activity) + assertEquals(activity, CurrentActivityHolder.getInstance().activity) + } + + @Test + fun `beforeSend callback that sets event tags is set with react finals`() { + val options = SentryAndroidOptions() + val event = + SentryEvent().apply { sdk = SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, "1.0") } + + RNSentryStart.updateWithReactFinals(options) + val result = options.beforeSend?.execute(event, mock()) + + assertNotNull(result) + assertEquals("android", result?.getTag("event.origin")) + assertEquals("java", result?.getTag("event.environment")) + } } From d236c82480960c22e96deac2da4bfa8ac6c49588 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 4 Feb 2025 14:20:00 +0200 Subject: [PATCH 40/43] Allow passing sdk init function --- .../java/io/sentry/react/RNSentrySDK.java | 19 +++++++++++++------ .../java/io/sentry/react/RNSentryStart.java | 18 ++++++++++++++---- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index ca219351fe..0c19a20389 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -8,6 +8,7 @@ import io.sentry.android.core.AndroidLogger; import io.sentry.android.core.SentryAndroidOptions; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.json.JSONObject; public final class RNSentrySDK { @@ -20,24 +21,30 @@ private RNSentrySDK() { throw new AssertionError("Utility class should not be instantiated"); } + /** Passing a custom SDK initializer is intended for internal testing use only. */ + interface SdkInit { + void init(Context context, Sentry.OptionsConfiguration config); + } + static void init( @NotNull final Context context, @NotNull Sentry.OptionsConfiguration configuration, @NotNull String configurationFile, - @NotNull ILogger logger) { + @NotNull ILogger logger, + @Nullable SdkInit sdkInit) { try { JSONObject jsonObject = RNSentryJsonUtils.getOptionsFromConfigurationFile(context, configurationFile, logger); if (jsonObject == null) { - RNSentryStart.startWithConfiguration(context, configuration); + RNSentryStart.startWithConfiguration(context, configuration, sdkInit); return; } ReadableMap rnOptions = RNSentryJsonConverter.convertToWritable(jsonObject); if (rnOptions == null) { - RNSentryStart.startWithConfiguration(context, configuration); + RNSentryStart.startWithConfiguration(context, configuration, sdkInit); return; } - RNSentryStart.startWithOptions(context, rnOptions, configuration, logger); + RNSentryStart.startWithOptions(context, rnOptions, configuration, logger, sdkInit); } catch (Exception e) { logger.log( SentryLevel.ERROR, "Failed to start Sentry with options from configuration file.", e); @@ -54,7 +61,7 @@ static void init( public static void init( @NotNull final Context context, @NotNull Sentry.OptionsConfiguration configuration) { - init(context, configuration, CONFIGURATION_FILE, logger); + init(context, configuration, CONFIGURATION_FILE, logger, null); } /** @@ -63,6 +70,6 @@ public static void init( * @param context Android Context */ public static void init(@NotNull final Context context) { - init(context, options -> {}, CONFIGURATION_FILE, logger); + init(context, options -> {}, CONFIGURATION_FILE, logger, null); } } 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 index 86699ced05..00ad3d31ce 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -37,20 +37,26 @@ private RNSentryStart() { static void startWithConfiguration( @NotNull final Context context, - @NotNull Sentry.OptionsConfiguration configuration) { + @NotNull Sentry.OptionsConfiguration configuration, + @Nullable RNSentrySDK.SdkInit sdkInit) { Sentry.OptionsConfiguration defaults = options -> updateWithReactDefaults(options, null); RNSentryCompositeOptionsConfiguration compositeConfiguration = new RNSentryCompositeOptionsConfiguration( defaults, configuration, RNSentryStart::updateWithReactFinals); - SentryAndroid.init(context, compositeConfiguration); + if (sdkInit != null) { + sdkInit.init(context, compositeConfiguration); + } else { + SentryAndroid.init(context, compositeConfiguration); + } } static void startWithOptions( @NotNull final Context context, @NotNull final ReadableMap rnOptions, @NotNull Sentry.OptionsConfiguration configuration, - @NotNull ILogger logger) { + @NotNull ILogger logger, + @Nullable RNSentrySDK.SdkInit sdkInit) { Sentry.OptionsConfiguration defaults = options -> updateWithReactDefaults(options, null); Sentry.OptionsConfiguration rnConfigurationOptions = @@ -58,7 +64,11 @@ static void startWithOptions( RNSentryCompositeOptionsConfiguration compositeConfiguration = new RNSentryCompositeOptionsConfiguration( rnConfigurationOptions, defaults, configuration, RNSentryStart::updateWithReactFinals); - SentryAndroid.init(context, compositeConfiguration); + if (sdkInit != null) { + sdkInit.init(context, compositeConfiguration); + } else { + SentryAndroid.init(context, compositeConfiguration); + } } static void startWithOptions( From ced915d5593eae951f38779c9dfde22abcb58305 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 4 Feb 2025 14:20:29 +0200 Subject: [PATCH 41/43] Add tests verifying defaults, finals and overriding behavior --- .../androidTest/assets/sentry.options.json | 4 +- .../java/io/sentry/react/RNSentrySDKTest.kt | 95 +++++++++++++++++-- 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json index 2a2e1344b9..f97a8df3f2 100644 --- a/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/assets/sentry.options.json @@ -1,3 +1,5 @@ { - "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "enableTracing": true, + "tracesSampleRate": 1.0 } \ No newline at end of file diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt index bbabb43d69..a35cf3c87e 100644 --- a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt @@ -3,14 +3,20 @@ package io.sentry.react import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.facebook.react.common.JavascriptException +import io.sentry.Hint import io.sentry.ILogger import io.sentry.Sentry import io.sentry.Sentry.OptionsConfiguration +import io.sentry.SentryEvent import io.sentry.android.core.AndroidLogger import io.sentry.android.core.SentryAndroidOptions +import io.sentry.protocol.SdkVersion import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -63,32 +69,32 @@ class RNSentrySDKTest { @Test fun initialisesSuccessfullyWithValidConfigurationAndInvalidJsonFile() { - RNSentrySDK.init(context, validConfig, INVALID_OPTIONS, logger) + RNSentrySDK.init(context, validConfig, INVALID_OPTIONS, logger, null) assertTrue(Sentry.isEnabled()) } @Test fun initialisesSuccessfullyWithValidConfigurationAndMissingJsonFile() { - RNSentrySDK.init(context, validConfig, MISSING, logger) + RNSentrySDK.init(context, validConfig, MISSING, logger, null) assertTrue(Sentry.isEnabled()) } @Test fun initialisesSuccessfullyWithValidConfigurationAndErrorInParsingJsonFile() { - RNSentrySDK.init(context, validConfig, INVALID_JSON, logger) + RNSentrySDK.init(context, validConfig, INVALID_JSON, logger, null) assertTrue(Sentry.isEnabled()) } @Test fun initialisesSuccessfullyWithNoConfigurationAndValidJsonFile() { - RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger, null) assertTrue(Sentry.isEnabled()) } @Test fun failsToInitialiseWithNoConfigurationAndInvalidJsonFile() { try { - RNSentrySDK.init(context, emptyConfig, INVALID_OPTIONS, logger) + RNSentrySDK.init(context, emptyConfig, INVALID_OPTIONS, logger, null) } catch (e: Exception) { assertEquals(INITIALISATION_ERROR, e.message) } @@ -98,7 +104,7 @@ class RNSentrySDKTest { @Test fun failsToInitialiseWithInvalidConfigAndInvalidJsonFile() { try { - RNSentrySDK.init(context, invalidConfig, INVALID_OPTIONS, logger) + RNSentrySDK.init(context, invalidConfig, INVALID_OPTIONS, logger, null) } catch (e: Exception) { assertEquals(INITIALISATION_ERROR, e.message) } @@ -108,7 +114,7 @@ class RNSentrySDKTest { @Test fun failsToInitialiseWithInvalidConfigAndValidJsonFile() { try { - RNSentrySDK.init(context, invalidConfig, VALID_OPTIONS, logger) + RNSentrySDK.init(context, invalidConfig, VALID_OPTIONS, logger, null) } catch (e: Exception) { assertEquals(INITIALISATION_ERROR, e.message) } @@ -124,4 +130,79 @@ class RNSentrySDKTest { } assertFalse(Sentry.isEnabled()) } + + @Test + fun defaultsAndFinalsAreSetWithValidJsonFile() { + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) { _, config -> + val actualOptions = SentryAndroidOptions() + config.configure(actualOptions) + verifyDefaults(actualOptions) + verifyFinals(actualOptions) + // options file + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") + } + } + + @Test + fun defaultsAndFinalsAreSetWithValidConfiguration() { + RNSentrySDK.init(context, validConfig, MISSING, logger) { _, config -> + val actualOptions = SentryAndroidOptions() + config.configure(actualOptions) + verifyDefaults(actualOptions) + verifyFinals(actualOptions) + // configuration + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") + } + } + + @Test + fun defaultsOverrideOptionsJsonFile() { + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) { _, config -> + val actualOptions = SentryAndroidOptions() + config.configure(actualOptions) + assertNull(actualOptions.tracesSampleRate) + assertEquals(false, actualOptions.enableTracing) + } + } + + @Test + fun configurationOverridesDefaultOptions() { + val validConfig = + OptionsConfiguration { options -> + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.tracesSampleRate = 0.5 + options.enableTracing = true + } + RNSentrySDK.init(context, validConfig, MISSING, logger) { _, config -> + val actualOptions = SentryAndroidOptions() + config.configure(actualOptions) + assertEquals(0.5, actualOptions.tracesSampleRate) + assertEquals(true, actualOptions.enableTracing) + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") + } + } + + private fun verifyDefaults(actualOptions: SentryAndroidOptions) { + assertTrue(actualOptions.ignoredExceptionsForType.contains(JavascriptException::class.java)) + assertEquals(RNSentryVersion.ANDROID_SDK_NAME, actualOptions.sdkVersion?.name) + assertEquals( + io.sentry.android.core.BuildConfig.VERSION_NAME, + actualOptions.sdkVersion?.version, + ) + val pack = actualOptions.sdkVersion?.packages?.first { it.name == RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME } + assertNotNull(pack) + assertEquals(RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION, pack?.version) + assertNull(actualOptions.tracesSampleRate) + assertNull(actualOptions.tracesSampler) + assertEquals(false, actualOptions.enableTracing) + } + + private fun verifyFinals(actualOptions: SentryAndroidOptions) { + val event = + SentryEvent().apply { sdk = SdkVersion(RNSentryVersion.ANDROID_SDK_NAME, "1.0") } + val result = actualOptions.beforeSend?.execute(event, Hint()) + assertNotNull(result) + assertEquals("android", result?.getTag("event.origin")) + assertEquals("java", result?.getTag("event.environment")) + } } From 1c96a27e5d8dc001c6d80210a7dbf4545958fcbd Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 4 Feb 2025 16:07:55 +0200 Subject: [PATCH 42/43] Revert "Allow passing sdk init function" This reverts commit d236c82480960c22e96deac2da4bfa8ac6c49588. --- .../java/io/sentry/react/RNSentrySDK.java | 19 ++++++------------- .../java/io/sentry/react/RNSentryStart.java | 18 ++++-------------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java index 0c19a20389..ca219351fe 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentrySDK.java @@ -8,7 +8,6 @@ import io.sentry.android.core.AndroidLogger; import io.sentry.android.core.SentryAndroidOptions; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.json.JSONObject; public final class RNSentrySDK { @@ -21,30 +20,24 @@ private RNSentrySDK() { throw new AssertionError("Utility class should not be instantiated"); } - /** Passing a custom SDK initializer is intended for internal testing use only. */ - interface SdkInit { - void init(Context context, Sentry.OptionsConfiguration config); - } - static void init( @NotNull final Context context, @NotNull Sentry.OptionsConfiguration configuration, @NotNull String configurationFile, - @NotNull ILogger logger, - @Nullable SdkInit sdkInit) { + @NotNull ILogger logger) { try { JSONObject jsonObject = RNSentryJsonUtils.getOptionsFromConfigurationFile(context, configurationFile, logger); if (jsonObject == null) { - RNSentryStart.startWithConfiguration(context, configuration, sdkInit); + RNSentryStart.startWithConfiguration(context, configuration); return; } ReadableMap rnOptions = RNSentryJsonConverter.convertToWritable(jsonObject); if (rnOptions == null) { - RNSentryStart.startWithConfiguration(context, configuration, sdkInit); + RNSentryStart.startWithConfiguration(context, configuration); return; } - RNSentryStart.startWithOptions(context, rnOptions, configuration, logger, sdkInit); + RNSentryStart.startWithOptions(context, rnOptions, configuration, logger); } catch (Exception e) { logger.log( SentryLevel.ERROR, "Failed to start Sentry with options from configuration file.", e); @@ -61,7 +54,7 @@ static void init( public static void init( @NotNull final Context context, @NotNull Sentry.OptionsConfiguration configuration) { - init(context, configuration, CONFIGURATION_FILE, logger, null); + init(context, configuration, CONFIGURATION_FILE, logger); } /** @@ -70,6 +63,6 @@ public static void init( * @param context Android Context */ public static void init(@NotNull final Context context) { - init(context, options -> {}, CONFIGURATION_FILE, logger, null); + init(context, options -> {}, CONFIGURATION_FILE, logger); } } 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 index 00ad3d31ce..86699ced05 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryStart.java @@ -37,26 +37,20 @@ private RNSentryStart() { static void startWithConfiguration( @NotNull final Context context, - @NotNull Sentry.OptionsConfiguration configuration, - @Nullable RNSentrySDK.SdkInit sdkInit) { + @NotNull Sentry.OptionsConfiguration configuration) { Sentry.OptionsConfiguration defaults = options -> updateWithReactDefaults(options, null); RNSentryCompositeOptionsConfiguration compositeConfiguration = new RNSentryCompositeOptionsConfiguration( defaults, configuration, RNSentryStart::updateWithReactFinals); - if (sdkInit != null) { - sdkInit.init(context, compositeConfiguration); - } else { - SentryAndroid.init(context, compositeConfiguration); - } + SentryAndroid.init(context, compositeConfiguration); } static void startWithOptions( @NotNull final Context context, @NotNull final ReadableMap rnOptions, @NotNull Sentry.OptionsConfiguration configuration, - @NotNull ILogger logger, - @Nullable RNSentrySDK.SdkInit sdkInit) { + @NotNull ILogger logger) { Sentry.OptionsConfiguration defaults = options -> updateWithReactDefaults(options, null); Sentry.OptionsConfiguration rnConfigurationOptions = @@ -64,11 +58,7 @@ static void startWithOptions( RNSentryCompositeOptionsConfiguration compositeConfiguration = new RNSentryCompositeOptionsConfiguration( rnConfigurationOptions, defaults, configuration, RNSentryStart::updateWithReactFinals); - if (sdkInit != null) { - sdkInit.init(context, compositeConfiguration); - } else { - SentryAndroid.init(context, compositeConfiguration); - } + SentryAndroid.init(context, compositeConfiguration); } static void startWithOptions( From 8a875658a2a07d855d3bb70896c75a8e195bdb64 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Tue, 4 Feb 2025 16:16:38 +0200 Subject: [PATCH 43/43] Use Sentry.getCurrentHub().options to check the initialised options --- .../java/io/sentry/react/RNSentrySDKTest.kt | 64 ++++++++----------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt index a35cf3c87e..3b95742e55 100644 --- a/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/androidTest/java/io/sentry/react/RNSentrySDKTest.kt @@ -69,32 +69,32 @@ class RNSentrySDKTest { @Test fun initialisesSuccessfullyWithValidConfigurationAndInvalidJsonFile() { - RNSentrySDK.init(context, validConfig, INVALID_OPTIONS, logger, null) + RNSentrySDK.init(context, validConfig, INVALID_OPTIONS, logger) assertTrue(Sentry.isEnabled()) } @Test fun initialisesSuccessfullyWithValidConfigurationAndMissingJsonFile() { - RNSentrySDK.init(context, validConfig, MISSING, logger, null) + RNSentrySDK.init(context, validConfig, MISSING, logger) assertTrue(Sentry.isEnabled()) } @Test fun initialisesSuccessfullyWithValidConfigurationAndErrorInParsingJsonFile() { - RNSentrySDK.init(context, validConfig, INVALID_JSON, logger, null) + RNSentrySDK.init(context, validConfig, INVALID_JSON, logger) assertTrue(Sentry.isEnabled()) } @Test fun initialisesSuccessfullyWithNoConfigurationAndValidJsonFile() { - RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger, null) + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) assertTrue(Sentry.isEnabled()) } @Test fun failsToInitialiseWithNoConfigurationAndInvalidJsonFile() { try { - RNSentrySDK.init(context, emptyConfig, INVALID_OPTIONS, logger, null) + RNSentrySDK.init(context, emptyConfig, INVALID_OPTIONS, logger) } catch (e: Exception) { assertEquals(INITIALISATION_ERROR, e.message) } @@ -104,7 +104,7 @@ class RNSentrySDKTest { @Test fun failsToInitialiseWithInvalidConfigAndInvalidJsonFile() { try { - RNSentrySDK.init(context, invalidConfig, INVALID_OPTIONS, logger, null) + RNSentrySDK.init(context, invalidConfig, INVALID_OPTIONS, logger) } catch (e: Exception) { assertEquals(INITIALISATION_ERROR, e.message) } @@ -114,7 +114,7 @@ class RNSentrySDKTest { @Test fun failsToInitialiseWithInvalidConfigAndValidJsonFile() { try { - RNSentrySDK.init(context, invalidConfig, VALID_OPTIONS, logger, null) + RNSentrySDK.init(context, invalidConfig, VALID_OPTIONS, logger) } catch (e: Exception) { assertEquals(INITIALISATION_ERROR, e.message) } @@ -133,36 +133,30 @@ class RNSentrySDKTest { @Test fun defaultsAndFinalsAreSetWithValidJsonFile() { - RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) { _, config -> - val actualOptions = SentryAndroidOptions() - config.configure(actualOptions) - verifyDefaults(actualOptions) - verifyFinals(actualOptions) - // options file - assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") - } + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + verifyDefaults(actualOptions) + verifyFinals(actualOptions) + // options file + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") } @Test fun defaultsAndFinalsAreSetWithValidConfiguration() { - RNSentrySDK.init(context, validConfig, MISSING, logger) { _, config -> - val actualOptions = SentryAndroidOptions() - config.configure(actualOptions) - verifyDefaults(actualOptions) - verifyFinals(actualOptions) - // configuration - assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") - } + RNSentrySDK.init(context, validConfig, MISSING, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + verifyDefaults(actualOptions) + verifyFinals(actualOptions) + // configuration + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") } @Test fun defaultsOverrideOptionsJsonFile() { - RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) { _, config -> - val actualOptions = SentryAndroidOptions() - config.configure(actualOptions) - assertNull(actualOptions.tracesSampleRate) - assertEquals(false, actualOptions.enableTracing) - } + RNSentrySDK.init(context, emptyConfig, VALID_OPTIONS, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + assertNull(actualOptions.tracesSampleRate) + assertEquals(false, actualOptions.enableTracing) } @Test @@ -173,13 +167,11 @@ class RNSentrySDKTest { options.tracesSampleRate = 0.5 options.enableTracing = true } - RNSentrySDK.init(context, validConfig, MISSING, logger) { _, config -> - val actualOptions = SentryAndroidOptions() - config.configure(actualOptions) - assertEquals(0.5, actualOptions.tracesSampleRate) - assertEquals(true, actualOptions.enableTracing) - assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") - } + RNSentrySDK.init(context, validConfig, MISSING, logger) + val actualOptions = Sentry.getCurrentHub().options as SentryAndroidOptions + assertEquals(0.5, actualOptions.tracesSampleRate) + assertEquals(true, actualOptions.enableTracing) + assert(actualOptions.dsn == "https://abcd@efgh.ingest.sentry.io/123456") } private fun verifyDefaults(actualOptions: SentryAndroidOptions) {