diff --git a/CHANGELOG.md b/CHANGELOG.md index ae4bcf64f1..66b336a32d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ - Change OkHttp sub-spans to span attributes ([#3556](https://github.com/getsentry/sentry-java/pull/3556)) - This will reduce the number of spans created by the SDK +### Features + +- Add init priority settings ([#3674](https://github.com/getsentry/sentry-java/pull/3674)) + - You may now set `forceInit=true` (`force-init` for `.properties` files) to ensure a call to Sentry.init / SentryAndroid.init takes effect + ### Fixes - Use OpenTelemetry span name as fallback for transaction name ([#3557](https://github.com/getsentry/sentry-java/pull/3557)) diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index 9805de6e21..d5331c8c52 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -172,6 +172,7 @@ class SentryAutoConfigurationTest { "sentry.send-modules=false", "sentry.ignored-checkins=slug1,slugB", "sentry.enable-backpressure-handling=false", + "sentry.force-init=true", "sentry.cron.default-checkin-margin=10", "sentry.cron.default-max-runtime=30", "sentry.cron.default-timezone=America/New_York", @@ -209,6 +210,7 @@ class SentryAutoConfigurationTest { assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") assertThat(options.isEnableBackpressureHandling).isEqualTo(false) + assertThat(options.isForceInit).isEqualTo(true) assertThat(options.cron).isNotNull assertThat(options.cron!!.defaultCheckinMargin).isEqualTo(10L) assertThat(options.cron!!.defaultMaxRuntime).isEqualTo(30L) diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index a65926c934..6a44d074ce 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -171,6 +171,7 @@ class SentryAutoConfigurationTest { "sentry.send-modules=false", "sentry.ignored-checkins=slug1,slugB", "sentry.enable-backpressure-handling=false", + "sentry.force-init=true", "sentry.cron.default-checkin-margin=10", "sentry.cron.default-max-runtime=30", "sentry.cron.default-timezone=America/New_York", @@ -208,6 +209,7 @@ class SentryAutoConfigurationTest { assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") assertThat(options.isEnableBackpressureHandling).isEqualTo(false) + assertThat(options.isForceInit).isEqualTo(true) assertThat(options.cron).isNotNull assertThat(options.cron!!.defaultCheckinMargin).isEqualTo(10L) assertThat(options.cron!!.defaultMaxRuntime).isEqualTo(30L) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 05213c6b91..f742eb4dbc 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -459,6 +459,7 @@ public final class io/sentry/ExternalOptions { public fun isEnableBackpressureHandling ()Ljava/lang/Boolean; public fun isEnablePrettySerializationOutput ()Ljava/lang/Boolean; public fun isEnabled ()Ljava/lang/Boolean; + public fun isForceInit ()Ljava/lang/Boolean; public fun isSendDefaultPii ()Ljava/lang/Boolean; public fun isSendModules ()Ljava/lang/Boolean; public fun setCron (Lio/sentry/SentryOptions$Cron;)V @@ -472,6 +473,7 @@ public final class io/sentry/ExternalOptions { public fun setEnableUncaughtExceptionHandler (Ljava/lang/Boolean;)V public fun setEnabled (Ljava/lang/Boolean;)V public fun setEnvironment (Ljava/lang/String;)V + public fun setForceInit (Ljava/lang/Boolean;)V public fun setIdleTimeout (Ljava/lang/Long;)V public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V @@ -1028,6 +1030,16 @@ public abstract interface class io/sentry/ITransportFactory { public abstract fun create (Lio/sentry/SentryOptions;Lio/sentry/RequestDetails;)Lio/sentry/transport/ITransport; } +public final class io/sentry/InitPriority : java/lang/Enum { + public static final field HIGH Lio/sentry/InitPriority; + public static final field HIGHEST Lio/sentry/InitPriority; + public static final field LOW Lio/sentry/InitPriority; + public static final field LOWEST Lio/sentry/InitPriority; + public static final field MEDIUM Lio/sentry/InitPriority; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/InitPriority; + public static fun values ()[Lio/sentry/InitPriority; +} + public final class io/sentry/Instrumenter : java/lang/Enum { public static final field OTEL Lio/sentry/Instrumenter; public static final field SENTRY Lio/sentry/Instrumenter; @@ -2706,6 +2718,7 @@ public class io/sentry/SentryOptions { public fun getIgnoredSpanOrigins ()Ljava/util/List; public fun getInAppExcludes ()Ljava/util/List; public fun getInAppIncludes ()Ljava/util/List; + public fun getInitPriority ()Lio/sentry/InitPriority; public fun getInstrumenter ()Lio/sentry/Instrumenter; public fun getIntegrations ()Ljava/util/List; public fun getInternalTracesSampler ()Lio/sentry/TracesSampler; @@ -2775,6 +2788,7 @@ public class io/sentry/SentryOptions { public fun isEnableUserInteractionBreadcrumbs ()Z public fun isEnableUserInteractionTracing ()Z public fun isEnabled ()Z + public fun isForceInit ()Z public fun isPrintUncaughtStackTrace ()Z public fun isProfilingEnabled ()Z public fun isSendClientReports ()Z @@ -2828,10 +2842,12 @@ public class io/sentry/SentryOptions { public fun setEnvironment (Ljava/lang/String;)V public fun setExecutorService (Lio/sentry/ISentryExecutorService;)V public fun setFlushTimeoutMillis (J)V + public fun setForceInit (Z)V public fun setGestureTargetLocators (Ljava/util/List;)V public fun setIdleTimeout (Ljava/lang/Long;)V public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setIgnoredSpanOrigins (Ljava/util/List;)V + public fun setInitPriority (Lio/sentry/InitPriority;)V public fun setInstrumenter (Lio/sentry/Instrumenter;)V public fun setLogger (Lio/sentry/ILogger;)V public fun setMainThreadChecker (Lio/sentry/util/thread/IMainThreadChecker;)V @@ -5548,6 +5564,11 @@ public final class io/sentry/util/HttpUtils { public static fun isSecurityCookie (Ljava/lang/String;Ljava/util/List;)Z } +public final class io/sentry/util/InitUtil { + public fun ()V + public static fun shouldInit (Lio/sentry/SentryOptions;Lio/sentry/SentryOptions;Z)Z +} + public final class io/sentry/util/IntegrationUtils { public fun ()V public static fun addIntegrationToSdkVersion (Ljava/lang/Class;)V diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index aa5aa43937..515ea6c08c 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -51,6 +51,7 @@ public final class ExternalOptions { private @Nullable Boolean sendModules; private @Nullable Boolean sendDefaultPii; private @Nullable Boolean enableBackpressureHandling; + private @Nullable Boolean forceInit; private @Nullable SentryOptions.Cron cron; @@ -73,6 +74,7 @@ public final class ExternalOptions { options.setDebug(propertiesProvider.getBooleanProperty("debug")); options.setEnableDeduplication(propertiesProvider.getBooleanProperty("enable-deduplication")); options.setSendClientReports(propertiesProvider.getBooleanProperty("send-client-reports")); + options.setForceInit(propertiesProvider.getBooleanProperty("force-init")); final String maxRequestBodySize = propertiesProvider.getProperty("max-request-body-size"); if (maxRequestBodySize != null) { options.setMaxRequestBodySize( @@ -451,6 +453,14 @@ public void setEnableBackpressureHandling(final @Nullable Boolean enableBackpres return enableBackpressureHandling; } + public void setForceInit(final @Nullable Boolean forceInit) { + this.forceInit = forceInit; + } + + public @Nullable Boolean isForceInit() { + return forceInit; + } + @ApiStatus.Experimental public @Nullable SentryOptions.Cron getCron() { return cron; diff --git a/sentry/src/main/java/io/sentry/InitPriority.java b/sentry/src/main/java/io/sentry/InitPriority.java new file mode 100644 index 0000000000..7548851c16 --- /dev/null +++ b/sentry/src/main/java/io/sentry/InitPriority.java @@ -0,0 +1,12 @@ +package io.sentry; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public enum InitPriority { + LOWEST, + LOW, + MEDIUM, + HIGH, + HIGHEST; +} diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index f8c431c191..a213e6ae3c 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -1055,17 +1055,15 @@ public void setSpanContext( @ApiStatus.Internal @Override public void replaceOptions(final @NotNull SentryOptions options) { - if (!getClient().isEnabled()) { - this.options = options; - final Queue oldBreadcrumbs = breadcrumbs; - breadcrumbs = createBreadcrumbsList(options.getMaxBreadcrumbs()); - for (Breadcrumb breadcrumb : oldBreadcrumbs) { - /* - this should trigger beforeBreadcrumb - and notify observers for breadcrumbs added before options where customized in Sentry.init - */ - addBreadcrumb(breadcrumb); - } + this.options = options; + final Queue oldBreadcrumbs = breadcrumbs; + breadcrumbs = createBreadcrumbsList(options.getMaxBreadcrumbs()); + for (Breadcrumb breadcrumb : oldBreadcrumbs) { + /* + this should trigger beforeBreadcrumb + and notify observers for breadcrumbs added before options where customized in Sentry.init + */ + addBreadcrumb(breadcrumb); } } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 676d6e83e8..7807bced45 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -17,6 +17,7 @@ import io.sentry.transport.NoOpEnvelopeCache; import io.sentry.util.DebugMetaPropertiesApplier; import io.sentry.util.FileUtils; +import io.sentry.util.InitUtil; import io.sentry.util.LoadClass; import io.sentry.util.Platform; import io.sentry.util.thread.IMainThreadChecker; @@ -279,43 +280,55 @@ private static synchronized void init( "Sentry has been already initialized. Previous configuration will be overwritten."); } - if (!initConfigurations(options)) { + if (!preInitConfigurations(options)) { return; } options.getLogger().log(SentryLevel.INFO, "GlobalHubMode: '%s'", String.valueOf(globalHubMode)); Sentry.globalHubMode = globalHubMode; - globalScope.replaceOptions(options); + final boolean shouldInit = InitUtil.shouldInit(globalScope.getOptions(), options, isEnabled()); + if (shouldInit) { + globalScope.replaceOptions(options); - final IScopes scopes = getCurrentScopes(); - final IScope rootScope = new Scope(options); - final IScope rootIsolationScope = new Scope(options); - rootScopes = new Scopes(rootScope, rootIsolationScope, globalScope, "Sentry.init"); + final IScopes scopes = getCurrentScopes(); + final IScope rootScope = new Scope(options); + final IScope rootIsolationScope = new Scope(options); + rootScopes = new Scopes(rootScope, rootIsolationScope, globalScope, "Sentry.init"); - getScopesStorage().set(rootScopes); + getScopesStorage().set(rootScopes); - scopes.close(true); - globalScope.bindClient(new SentryClient(rootScopes.getOptions())); + scopes.close(true); - // If the executorService passed in the init is the same that was previously closed, we have to - // set a new one - if (options.getExecutorService().isClosed()) { - options.setExecutorService(new SentryExecutorService()); - } + initConfigurations(options); - // when integrations are registered on Scopes ctor and async integrations are fired, - // it might and actually happened that integrations called captureSomething - // and Scopes was still NoOp. - // Registering integrations here make sure that Scopes is already created. - for (final Integration integration : options.getIntegrations()) { - integration.register(ScopesAdapter.getInstance(), options); - } + globalScope.bindClient(new SentryClient(options)); + + // If the executorService passed in the init is the same that was previously closed, we have + // to + // set a new one + if (options.getExecutorService().isClosed()) { + options.setExecutorService(new SentryExecutorService()); + } + // when integrations are registered on Scopes ctor and async integrations are fired, + // it might and actually happened that integrations called captureSomething + // and Scopes was still NoOp. + // Registering integrations here make sure that Scopes is already created. + for (final Integration integration : options.getIntegrations()) { + integration.register(ScopesAdapter.getInstance(), options); + } - notifyOptionsObservers(options); + notifyOptionsObservers(options); - finalizePreviousSession(options, ScopesAdapter.getInstance()); + finalizePreviousSession(options, ScopesAdapter.getInstance()); - handleAppStartProfilingConfig(options, options.getExecutorService()); + handleAppStartProfilingConfig(options, options.getExecutorService()); + } else { + options + .getLogger() + .log( + SentryLevel.WARNING, + "This init call has been ignored due to priority being too low."); + } } @SuppressWarnings("FutureReturnValueIgnored") @@ -419,8 +432,7 @@ private static void notifyOptionsObservers(final @NotNull SentryOptions options) } } - @SuppressWarnings("FutureReturnValueIgnored") - private static boolean initConfigurations(final @NotNull SentryOptions options) { + private static boolean preInitConfigurations(final @NotNull SentryOptions options) { if (options.isEnableExternalConfiguration()) { options.merge(ExternalOptions.from(PropertiesProviderFactory.create(), options.getLogger())); } @@ -438,6 +450,11 @@ private static boolean initConfigurations(final @NotNull SentryOptions options) @SuppressWarnings("unused") final Dsn parsedDsn = new Dsn(dsn); + return true; + } + + @SuppressWarnings("FutureReturnValueIgnored") + private static void initConfigurations(final @NotNull SentryOptions options) { ILogger logger = options.getLogger(); if (options.isDebug() && logger instanceof NoOpLogger) { @@ -534,8 +551,6 @@ private static boolean initConfigurations(final @NotNull SentryOptions options) options.setBackpressureMonitor(new BackpressureMonitor(options, ScopesAdapter.getInstance())); options.getBackpressureMonitor().start(); } - - return true; } /** Close the SDK */ diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 7e24e81dce..8dbe00ed7a 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -489,6 +489,10 @@ public class SentryOptions { private @NotNull ScopeType defaultScopeType = ScopeType.ISOLATION; + private @NotNull InitPriority initPriority = InitPriority.MEDIUM; + + private boolean forceInit = false; + /** * Adds an event processor * @@ -2440,6 +2444,33 @@ public void setDefaultScopeType(final @NotNull ScopeType scopeType) { return defaultScopeType; } + @ApiStatus.Internal + public void setInitPriority(final @NotNull InitPriority initPriority) { + this.initPriority = initPriority; + } + + @ApiStatus.Internal + public @NotNull InitPriority getInitPriority() { + return initPriority; + } + + /** + * If set to true a call to Sentry.init (or SentryAndroid.init) will go through and replace + * previous options if there are any. + * + *

By default the SDK will check whether a previous call to Sentry.init has higher priority + * than the current one and decide whether to actually perform the init and replace options. + * + * @param forceInit true = replace previous init and options + */ + public void setForceInit(final boolean forceInit) { + this.forceInit = forceInit; + } + + public boolean isForceInit() { + return forceInit; + } + /** The BeforeSend callback */ public interface BeforeSendCallback { @@ -2636,6 +2667,9 @@ public void merge(final @NotNull ExternalOptions options) { if (options.getSendClientReports() != null) { setSendClientReports(options.getSendClientReports()); } + if (options.isForceInit() != null) { + setForceInit(options.isForceInit()); + } final Map tags = new HashMap<>(options.getTags()); for (final Map.Entry tag : tags.entrySet()) { this.tags.put(tag.getKey(), tag.getValue()); diff --git a/sentry/src/main/java/io/sentry/util/InitUtil.java b/sentry/src/main/java/io/sentry/util/InitUtil.java new file mode 100644 index 0000000000..f651383a78 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/InitUtil.java @@ -0,0 +1,28 @@ +package io.sentry.util; + +import io.sentry.SentryOptions; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class InitUtil { + public static boolean shouldInit( + final @Nullable SentryOptions previousOptions, + final @NotNull SentryOptions newOptions, + final boolean isEnabled) { + if (!isEnabled) { + return true; + } + + if (previousOptions == null) { + return true; + } + + if (newOptions.isForceInit()) { + return true; + } + + return previousOptions.getInitPriority().ordinal() <= newOptions.getInitPriority().ordinal(); + } +} diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index fd5b363219..8c525da45a 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -293,6 +293,13 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with forceInit set to true`() { + withPropertiesFile("force-init=true") { options -> + assertTrue(options.isForceInit == true) + } + } + private fun withPropertiesFile(textLines: List = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) { // create a sentry.properties file in temporary folder val temporaryFolder = TemporaryFolder() diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index c11eafdc5a..e12b09e1d5 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -374,6 +374,7 @@ class SentryOptionsTest { externalOptions.isEnableBackpressureHandling = false externalOptions.maxRequestBodySize = SentryOptions.RequestSize.MEDIUM externalOptions.isSendDefaultPii = true + externalOptions.isForceInit = true externalOptions.cron = SentryOptions.Cron().apply { defaultCheckinMargin = 10L defaultMaxRuntime = 30L @@ -412,6 +413,7 @@ class SentryOptionsTest { assertFalse(options.isSendModules) assertEquals(listOf("slug1", "slug-B"), options.ignoredCheckIns) assertFalse(options.isEnableBackpressureHandling) + assertTrue(options.isForceInit) assertNotNull(options.cron) assertEquals(10L, options.cron?.defaultCheckinMargin) assertEquals(30L, options.cron?.defaultMaxRuntime) @@ -726,4 +728,9 @@ class SentryOptionsTest { assertEquals(30, options.cron?.defaultFailureIssueThreshold) assertEquals(40, options.cron?.defaultRecoveryThreshold) } + + @Test + fun `when options is initialized, InitPriority is set to MEDIUM by default`() { + assertEquals(SentryOptions().initPriority, InitPriority.MEDIUM) + } } diff --git a/sentry/src/test/java/io/sentry/util/InitUtilTest.kt b/sentry/src/test/java/io/sentry/util/InitUtilTest.kt new file mode 100644 index 0000000000..c3c73f6167 --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/InitUtilTest.kt @@ -0,0 +1,107 @@ +package io.sentry.util + +import io.sentry.InitPriority +import io.sentry.SentryOptions +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class InitUtilTest { + + private var previousOptions: SentryOptions? = null + private var newOptions: SentryOptions? = null + private var clientEnabled: Boolean = true + + @BeforeTest + fun setup() { + previousOptions = null + newOptions = null + clientEnabled = true + } + + @Test + fun `first init on empty options goes through`() { + givenPreviousOptions(SentryOptions.empty()) + givenNewOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenClientDisabled() + + thenInitIsPerformed() + } + + @Test + fun `init with same priority goes through`() { + givenPreviousOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenNewOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenClientEnabled() + + thenInitIsPerformed() + } + + @Test + fun `init without previous options goes through`() { + givenPreviousOptions(null) + givenNewOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenClientEnabled() + + thenInitIsPerformed() + } + + @Test + fun `init with lower priority is ignored if already initialized`() { + givenPreviousOptions(SentryOptions().also { it.initPriority = InitPriority.LOW }) + givenNewOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenClientEnabled() + + thenInitIsIgnored() + } + + @Test + fun `init with lower priority goes through if not yet initialized`() { + givenPreviousOptions(SentryOptions().also { it.initPriority = InitPriority.LOW }) + givenNewOptions(SentryOptions().also { it.initPriority = InitPriority.LOWEST }) + givenClientDisabled() + + thenInitIsPerformed() + } + + @Test + fun `init with lower priority goes through with forceInit if already initialized`() { + givenPreviousOptions(SentryOptions().also { it.initPriority = InitPriority.LOW }) + givenNewOptions( + SentryOptions().also { + it.initPriority = InitPriority.LOWEST + it.isForceInit = true + } + ) + givenClientEnabled() + + thenInitIsPerformed() + } + + private fun givenPreviousOptions(options: SentryOptions?) { + previousOptions = options + } + + private fun givenNewOptions(options: SentryOptions?) { + newOptions = options + } + + private fun givenClientDisabled() { + clientEnabled = false + } + + private fun givenClientEnabled() { + clientEnabled = true + } + + private fun thenInitIsPerformed() { + val shouldInit = InitUtil.shouldInit(previousOptions, newOptions!!, clientEnabled) + assertTrue(shouldInit) + } + + private fun thenInitIsIgnored() { + val shouldInit = InitUtil.shouldInit(previousOptions, newOptions!!, clientEnabled) + assertFalse(shouldInit) + } +}