From b1e7b97311499bb97264e065fd446cc2657cc4bb Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 2 Mar 2026 13:50:31 +0100 Subject: [PATCH 1/5] fix(init): Perform less allocation/bytecode instructions in Sentry.init --- ...DuplicateEventDetectionEventProcessor.java | 3 +- .../java/io/sentry/MainEventProcessor.java | 14 +--------- .../io/sentry/SentryExceptionFactory.java | 4 +-- .../main/java/io/sentry/SentryOptions.java | 28 ++++++++++++------- .../java/io/sentry/SentryThreadFactory.java | 4 +-- .../UncaughtExceptionHandlerIntegration.java | 2 +- 6 files changed, 23 insertions(+), 32 deletions(-) diff --git a/sentry/src/main/java/io/sentry/DuplicateEventDetectionEventProcessor.java b/sentry/src/main/java/io/sentry/DuplicateEventDetectionEventProcessor.java index 5004e6514e..fc178d789f 100644 --- a/sentry/src/main/java/io/sentry/DuplicateEventDetectionEventProcessor.java +++ b/sentry/src/main/java/io/sentry/DuplicateEventDetectionEventProcessor.java @@ -1,6 +1,5 @@ package io.sentry; -import io.sentry.util.Objects; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -16,7 +15,7 @@ public final class DuplicateEventDetectionEventProcessor implements EventProcess private final @NotNull SentryOptions options; public DuplicateEventDetectionEventProcessor(final @NotNull SentryOptions options) { - this.options = Objects.requireNonNull(options, "options are required"); + this.options = options; } @Override diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index 5ad19c79e3..8c684bfb65 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -8,7 +8,6 @@ import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; import io.sentry.util.HintUtils; -import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; @@ -29,7 +28,7 @@ public final class MainEventProcessor implements EventProcessor, Closeable { private volatile @Nullable HostnameCache hostnameCache = null; public MainEventProcessor(final @NotNull SentryOptions options) { - this.options = Objects.requireNonNull(options, "The SentryOptions is required."); + this.options = options; final SentryStackTraceFactory sentryStackTraceFactory = new SentryStackTraceFactory(this.options); @@ -38,17 +37,6 @@ public MainEventProcessor(final @NotNull SentryOptions options) { sentryThreadFactory = new SentryThreadFactory(sentryStackTraceFactory); } - MainEventProcessor( - final @NotNull SentryOptions options, - final @NotNull SentryThreadFactory sentryThreadFactory, - final @NotNull SentryExceptionFactory sentryExceptionFactory) { - this.options = Objects.requireNonNull(options, "The SentryOptions is required."); - this.sentryThreadFactory = - Objects.requireNonNull(sentryThreadFactory, "The SentryThreadFactory is required."); - this.sentryExceptionFactory = - Objects.requireNonNull(sentryExceptionFactory, "The SentryExceptionFactory is required."); - } - @Override public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) { setCommons(event); diff --git a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java index 8b2b570946..d47776a162 100644 --- a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java +++ b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java @@ -6,7 +6,6 @@ import io.sentry.protocol.SentryStackFrame; import io.sentry.protocol.SentryStackTrace; import io.sentry.protocol.SentryThread; -import io.sentry.util.Objects; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; @@ -31,8 +30,7 @@ public final class SentryExceptionFactory { * @param sentryStackTraceFactory the sentryStackTraceFactory */ public SentryExceptionFactory(final @NotNull SentryStackTraceFactory sentryStackTraceFactory) { - this.sentryStackTraceFactory = - Objects.requireNonNull(sentryStackTraceFactory, "The SentryStackTraceFactory is required."); + this.sentryStackTraceFactory = sentryStackTraceFactory; } @NotNull diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 298e37795b..2fca898839 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -655,6 +655,24 @@ public void activate() { executorService = new SentryExecutorService(this); executorService.prewarm(); } + + // SpotlightIntegration is loaded via reflection to allow the sentry-spotlight module + // to be excluded from release builds, preventing insecure HTTP URLs from appearing in APKs + try { + final Class clazz = Class.forName("io.sentry.spotlight.SpotlightIntegration"); + boolean alreadyRegistered = false; + for (final Integration integration : integrations) { + if (clazz.isInstance(integration)) { + alreadyRegistered = true; + break; + } + } + if (!alreadyRegistered) { + integrations.add((Integration) clazz.getConstructor().newInstance()); + } + } catch (Throwable ignored) { + // SpotlightIntegration not available + } } /** @@ -3340,16 +3358,6 @@ private SentryOptions(final boolean empty) { integrations.add(new ShutdownHookIntegration()); - // SpotlightIntegration is loaded via reflection to allow the sentry-spotlight module - // to be excluded from release builds, preventing insecure HTTP URLs from appearing in APKs - try { - final Class clazz = Class.forName("io.sentry.spotlight.SpotlightIntegration"); - final Integration spotlight = (Integration) clazz.getConstructor().newInstance(); - integrations.add(spotlight); - } catch (Throwable ignored) { - // SpotlightIntegration not available - } - eventProcessors.add(new MainEventProcessor(this)); eventProcessors.add(new DuplicateEventDetectionEventProcessor(this)); diff --git a/sentry/src/main/java/io/sentry/SentryThreadFactory.java b/sentry/src/main/java/io/sentry/SentryThreadFactory.java index 0b8f258499..8bd1448326 100644 --- a/sentry/src/main/java/io/sentry/SentryThreadFactory.java +++ b/sentry/src/main/java/io/sentry/SentryThreadFactory.java @@ -3,7 +3,6 @@ import io.sentry.protocol.SentryStackFrame; import io.sentry.protocol.SentryStackTrace; import io.sentry.protocol.SentryThread; -import io.sentry.util.Objects; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -26,8 +25,7 @@ public final class SentryThreadFactory { * @param sentryStackTraceFactory the SentryStackTraceFactory */ public SentryThreadFactory(final @NotNull SentryStackTraceFactory sentryStackTraceFactory) { - this.sentryStackTraceFactory = - Objects.requireNonNull(sentryStackTraceFactory, "The SentryStackTraceFactory is required."); + this.sentryStackTraceFactory = sentryStackTraceFactory; } /** diff --git a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java index 8ca02cb855..6ea6189579 100644 --- a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java +++ b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java @@ -44,7 +44,7 @@ public UncaughtExceptionHandlerIntegration() { } UncaughtExceptionHandlerIntegration(final @NotNull UncaughtExceptionHandler threadAdapter) { - this.threadAdapter = Objects.requireNonNull(threadAdapter, "threadAdapter is required."); + this.threadAdapter = threadAdapter; } @Override From 45d3d3599d20a900b0531f03314e1d42d4021f4a Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 2 Mar 2026 14:18:26 +0100 Subject: [PATCH 2/5] Switch new thread to executorService in AssetsModulesLoader --- .../core/AndroidOptionsInitializer.java | 2 +- .../internal/modules/AssetsModulesLoader.java | 18 +++++++++++++----- .../modules/AssetsModulesLoaderTest.kt | 7 ++++--- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 5bfebaed92..74d7c9bec1 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -200,7 +200,7 @@ static void initializeIntegrationsAndProcessors( final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); if (options.getModulesLoader() instanceof NoOpModulesLoader) { - options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger())); + options.setModulesLoader(new AssetsModulesLoader(context, options)); } if (options.getDebugMetaLoader() instanceof NoOpDebugMetaLoader) { options.setDebugMetaLoader(new AssetsDebugMetaLoader(context, options.getLogger())); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/modules/AssetsModulesLoader.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/modules/AssetsModulesLoader.java index 05bb75a60f..1aa8a89f8f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/modules/AssetsModulesLoader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/modules/AssetsModulesLoader.java @@ -1,8 +1,8 @@ package io.sentry.android.core.internal.modules; import android.content.Context; -import io.sentry.ILogger; import io.sentry.SentryLevel; +import io.sentry.SentryOptions; import io.sentry.android.core.ContextUtils; import io.sentry.internal.modules.ModulesLoader; import java.io.FileNotFoundException; @@ -18,13 +18,21 @@ public final class AssetsModulesLoader extends ModulesLoader { private final @NotNull Context context; - public AssetsModulesLoader(final @NotNull Context context, final @NotNull ILogger logger) { - super(logger); + public AssetsModulesLoader(final @NotNull Context context, final @NotNull SentryOptions options) { + super(options.getLogger()); this.context = ContextUtils.getApplicationContext(context); // pre-load modules on a bg thread to avoid doing so on the main thread in case of a crash/error - //noinspection Convert2MethodRef - new Thread(() -> getOrLoadModules()).start(); + try { + options + .getExecutorService() + .submit( + () -> { + getOrLoadModules(); + }); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "AssetsModulesLoader submit failed", e); + } } @Override diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/modules/AssetsModulesLoaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/modules/AssetsModulesLoaderTest.kt index 9df02a067d..128087a315 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/modules/AssetsModulesLoaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/modules/AssetsModulesLoaderTest.kt @@ -2,7 +2,8 @@ package io.sentry.android.core.internal.modules import android.content.Context import android.content.res.AssetManager -import io.sentry.ILogger +import io.sentry.SentryOptions +import io.sentry.test.ImmediateExecutorService import java.io.FileNotFoundException import java.nio.charset.Charset import kotlin.test.Test @@ -16,7 +17,7 @@ class AssetsModulesLoaderTest { class Fixture { val context = mock() val assets = mock() - val logger = mock() + val options = SentryOptions().apply { executorService = ImmediateExecutorService() } fun getSut( fileName: String = "sentry-external-modules.txt", @@ -31,7 +32,7 @@ class AssetsModulesLoaderTest { whenever(assets.open(fileName)).thenThrow(FileNotFoundException()) } whenever(context.assets).thenReturn(assets) - return AssetsModulesLoader(context, logger) + return AssetsModulesLoader(context, options) } } From fd1ce4a10e764062ae53adb4f4b3c7c5954d97ef Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 2 Mar 2026 14:42:43 +0100 Subject: [PATCH 3/5] activate options earlier --- .../android/core/AndroidOptionsInitializer.java | 1 + .../io/sentry/android/core/SentryAndroidTest.kt | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 74d7c9bec1..589c4f8d2a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -142,6 +142,7 @@ static void loadDefaultAndMetadataOptions( readDefaultOptionValues(options, finalContext, buildInfoProvider); AppState.getInstance().registerLifecycleObserver(options); + options.activate(); } @TestOnly diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 716d03d8d7..b7fad8abee 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -16,6 +16,7 @@ import io.sentry.DateUtils import io.sentry.Hint import io.sentry.ILogger import io.sentry.ISentryClient +import io.sentry.NoOpSentryExecutorService import io.sentry.Sentry import io.sentry.Sentry.OptionsConfiguration import io.sentry.SentryEnvelope @@ -528,6 +529,19 @@ class SentryAndroidTest { assertEquals(99, AppStartMetrics.getInstance().appStartTimeSpan.startUptimeMs) } + @Test + fun `executor service is not NoOp when AndroidConnectionStatusProvider is initialized`() { + var executorServiceIsNoOp = true + fixture.initSut(context = context) { options -> + options.dsn = "https://key@sentry.io/123" + // the config callback runs before initializeIntegrationsAndProcessors, which creates + // AndroidConnectionStatusProvider - so if the executor is already real here, + // it's guaranteed to be real when the provider calls submitSafe() + executorServiceIsNoOp = options.executorService is NoOpSentryExecutorService + } + assertFalse(executorServiceIsNoOp) + } + @Test fun `if the config options block throws still intializes android event processors`() { lateinit var optionsRef: SentryOptions From adb5d488337ed8eaacd1e4c0cef4c3899bc54fbe Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 2 Mar 2026 14:45:38 +0100 Subject: [PATCH 4/5] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fec0ecc646..e50e09b72b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Log an actionable error message when Relay returns HTTP 413 (Content Too Large) ([#5115](https://github.com/getsentry/sentry-java/pull/5115)) - Also switch the client report discard reason for all HTTP 4xx/5xx errors (except 429) from `network_error` to `send_error` - Trim DSN string before parsing to avoid `URISyntaxException` caused by trailing whitespace ([#5113](https://github.com/getsentry/sentry-java/pull/5113)) +- Reduce allocations and bytecode instructions during `Sentry.init` ([#5135](https://github.com/getsentry/sentry-java/pull/5135)) ### Dependencies From 53c13488d4ac4acb435be582af2b2450e7fb719c Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 2 Mar 2026 23:44:01 +0100 Subject: [PATCH 5/5] fix: Prevent SpotlightIntegration from being re-added after user removal Use AtomicBoolean to ensure activate() only loads SpotlightIntegration once, so users can remove it in their configuration callback without it being re-added by the second activate() call from Sentry.init(). Co-Authored-By: Claude Opus 4.6 --- .../main/java/io/sentry/SentryOptions.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 2fca898839..7b21661c22 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -46,6 +46,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicBoolean; import javax.net.ssl.SSLSocketFactory; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -318,6 +319,12 @@ public class SentryOptions { /** Sentry Executor Service that sends cached events and envelopes on App. start. */ private @NotNull ISentryExecutorService executorService = NoOpSentryExecutorService.getInstance(); + /** + * Whether SpotlightIntegration has already been loaded via reflection. This prevents re-adding it + * if the user removed it in their configuration callback and activate() is called again. + */ + private final @NotNull AtomicBoolean spotlightIntegrationLoaded = new AtomicBoolean(false); + /** connection timeout in milliseconds. */ private int connectionTimeoutMillis = 30_000; @@ -657,21 +664,15 @@ public void activate() { } // SpotlightIntegration is loaded via reflection to allow the sentry-spotlight module - // to be excluded from release builds, preventing insecure HTTP URLs from appearing in APKs - try { - final Class clazz = Class.forName("io.sentry.spotlight.SpotlightIntegration"); - boolean alreadyRegistered = false; - for (final Integration integration : integrations) { - if (clazz.isInstance(integration)) { - alreadyRegistered = true; - break; - } - } - if (!alreadyRegistered) { + // to be excluded from release builds, preventing insecure HTTP URLs from appearing in APKs. + // Only attempt once to avoid re-adding after user removal in their configuration callback. + if (spotlightIntegrationLoaded.compareAndSet(false, true)) { + try { + final Class clazz = Class.forName("io.sentry.spotlight.SpotlightIntegration"); integrations.add((Integration) clazz.getConstructor().newInstance()); + } catch (Throwable ignored) { + // SpotlightIntegration not available } - } catch (Throwable ignored) { - // SpotlightIntegration not available } }