diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a7262f49e..2e51ad73ba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add deadlineTimeout option ([#4555](https://github.com/getsentry/sentry-java/pull/4555)) + ### Fixes - Allow multiple UncaughtExceptionHandlerIntegrations to be active at the same time ([#4462](https://github.com/getsentry/sentry-java/pull/4462)) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 00f30fcacb9..9d748e5a27a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -189,8 +189,12 @@ private void startTracing(final @NotNull Activity activity) { } final TransactionOptions transactionOptions = new TransactionOptions(); + + // Set deadline timeout based on configured option + final long deadlineTimeoutMillis = options.getDeadlineTimeout(); + // No deadline when zero or negative value is set transactionOptions.setDeadlineTimeout( - TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION); + deadlineTimeoutMillis <= 0 ? null : deadlineTimeoutMillis); if (options.isEnableActivityLifecycleTracingAutoFinish()) { transactionOptions.setIdleTimeout(options.getIdleTimeout()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 17fbe2d39c3..37c48b2f087 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -127,6 +127,8 @@ final class ManifestMetadataReader { static final String ENABLE_AUTO_TRACE_ID_GENERATION = "io.sentry.traces.enable-auto-id-generation"; + static final String DEADLINE_TIMEOUT = "io.sentry.traces.deadline-timeout"; + static final String FEEDBACK_NAME_REQUIRED = "io.sentry.feedback.is-name-required"; static final String FEEDBACK_SHOW_NAME = "io.sentry.feedback.show-name"; @@ -446,6 +448,9 @@ static void applyMetadata( ENABLE_AUTO_TRACE_ID_GENERATION, options.isEnableAutoTraceIdGeneration())); + options.setDeadlineTimeout( + readLong(metadata, logger, DEADLINE_TIMEOUT, options.getDeadlineTimeout())); + if (options.getSessionReplay().getSessionSampleRate() == null) { final double sessionSampleRate = readDouble(metadata, logger, REPLAYS_SESSION_SAMPLE_RATE); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java index 7ffc5d2412f..cd89db72f53 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/gestures/SentryGestureListener.java @@ -250,8 +250,13 @@ private void startTracing(final @NotNull UiElement target, final @NotNull Gestur final TransactionOptions transactionOptions = new TransactionOptions(); transactionOptions.setWaitForChildren(true); + + // Set deadline timeout based on configured option + final long deadlineTimeoutMillis = options.getDeadlineTimeout(); + // No deadline when zero or negative value is set transactionOptions.setDeadlineTimeout( - TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION); + deadlineTimeoutMillis <= 0 ? null : deadlineTimeoutMillis); + transactionOptions.setIdleTimeout(options.getIdleTimeout()); transactionOptions.setTrimEnd(true); transactionOptions.setOrigin(TRACE_ORIGIN + "." + target.getOrigin()); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index cb9eb60b14c..9e94d7b9905 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -225,6 +225,80 @@ class ActivityLifecycleIntegrationTest { ) } + @Test + fun `Activity transaction uses custom deadline timeout when autoTransactionDeadlineTimeoutMillis is set to positive value`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + fixture.options.deadlineTimeout = 60000L // 60 seconds + + sut.register(fixture.scopes, fixture.options) + sut.onActivityCreated(mock(), fixture.bundle) + + verify(fixture.scopes) + .startTransaction( + any(), + check { transactionOptions -> + assertEquals(60000L, transactionOptions.deadlineTimeout) + }, + ) + } + + @Test + fun `Activity transaction uses no deadline timeout when autoTransactionDeadlineTimeoutMillis is set to zero`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + fixture.options.deadlineTimeout = 0L // No deadline + + sut.register(fixture.scopes, fixture.options) + sut.onActivityCreated(mock(), fixture.bundle) + + verify(fixture.scopes) + .startTransaction( + any(), + check { transactionOptions -> + assertNull(transactionOptions.deadlineTimeout) + }, + ) + } + + @Test + fun `Activity transaction uses no deadline timeout when autoTransactionDeadlineTimeoutMillis is set to negative value`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + fixture.options.deadlineTimeout = -1L // No deadline + + sut.register(fixture.scopes, fixture.options) + sut.onActivityCreated(mock(), fixture.bundle) + + verify(fixture.scopes) + .startTransaction( + any(), + check { transactionOptions -> + assertNull(transactionOptions.deadlineTimeout) + }, + ) + } + + @Test + fun `Activity transaction uses default deadline timeout when autoTransactionDeadlineTimeoutMillis is default`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + + sut.register(fixture.scopes, fixture.options) + sut.onActivityCreated(mock(), fixture.bundle) + + verify(fixture.scopes) + .startTransaction( + any(), + check { transactionOptions -> + assertEquals( + TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION, + transactionOptions.deadlineTimeout, + ) + }, + ) + } + @Test fun `Activity gets added to ActivityFramesTracker during transaction creation`() { val sut = fixture.getSut() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 616d72174dd..5ad197d829d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -9,6 +9,7 @@ import io.sentry.ILogger import io.sentry.ProfileLifecycle import io.sentry.SentryLevel import io.sentry.SentryReplayOptions +import io.sentry.TransactionOptions import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -1091,6 +1092,35 @@ class ManifestMetadataReaderTest { assertEquals(expectedIdleTimeout.toLong(), fixture.options.idleTimeout) } + @Test + fun `applyMetadata reads autoTransactionDeadlineTimeoutMillis from metadata`() { + // Arrange + val expectedTimeout = 60000 + val bundle = bundleOf(ManifestMetadataReader.DEADLINE_TIMEOUT to expectedTimeout) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(expectedTimeout.toLong(), fixture.options.deadlineTimeout) + } + + @Test + fun `applyMetadata reads autoTransactionDeadlineTimeoutMillis from metadata and keep default value if not found`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals( + TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION, + fixture.options.deadlineTimeout, + ) + } + @Test fun `applyMetadata without specifying idleTimeout, stays default`() { // Arrange diff --git a/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt b/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt index b4db4957ba1..b5e99b21865 100644 --- a/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt +++ b/sentry-android-navigation/src/main/java/io/sentry/android/navigation/SentryNavigationListener.kt @@ -131,7 +131,12 @@ constructor( TransactionOptions().also { it.isWaitForChildren = true it.idleTimeout = scopes.options.idleTimeout - it.deadlineTimeout = TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION + + // Set deadline timeout based on configured option + val deadlineTimeoutMillis = scopes.options.deadlineTimeout + // No deadline when zero or negative value is set + it.deadlineTimeout = if (deadlineTimeoutMillis <= 0) null else deadlineTimeoutMillis + it.isTrimEnd = true } diff --git a/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt b/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt index c67cd9d1f88..16cc56ad889 100644 --- a/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt +++ b/sentry-android-navigation/src/test/java/io/sentry/android/navigation/SentryNavigationListenerTest.kt @@ -399,6 +399,48 @@ class SentryNavigationListenerTest { ) } + @Test + fun `Navigation listener uses custom deadline timeout when set to positive value`() { + val sut = fixture.getSut() + fixture.options.deadlineTimeout = 60000L + + sut.onDestinationChanged(fixture.navController, fixture.destination, null) + + verify(fixture.scopes) + .startTransaction( + any(), + check { options -> assertEquals(60000L, options.deadlineTimeout) }, + ) + } + + @Test + fun `Navigation listener uses no deadline timeout when set to zero`() { + val sut = fixture.getSut() + fixture.options.deadlineTimeout = 0L + + sut.onDestinationChanged(fixture.navController, fixture.destination, null) + + verify(fixture.scopes) + .startTransaction( + any(), + check { options -> assertNull(options.deadlineTimeout) }, + ) + } + + @Test + fun `Navigation listener uses no deadline timeout when set to negative value`() { + val sut = fixture.getSut() + fixture.options.deadlineTimeout = -1L + + sut.onDestinationChanged(fixture.navController, fixture.destination, null) + + verify(fixture.scopes) + .startTransaction( + any(), + check { options -> assertNull(options.deadlineTimeout) }, + ) + } + @Test fun `onDestinationChanged sets scope screen`() { val sut = fixture.getSut() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 171cd7eaa88..9ce63363e95 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3282,6 +3282,7 @@ public class io/sentry/SentryOptions { public fun getContinuousProfiler ()Lio/sentry/IContinuousProfiler; public fun getCron ()Lio/sentry/SentryOptions$Cron; public fun getDateProvider ()Lio/sentry/SentryDateProvider; + public fun getDeadlineTimeout ()J public fun getDebugMetaLoader ()Lio/sentry/internal/debugmeta/IDebugMetaLoader; public fun getDefaultScopeType ()Lio/sentry/ScopeType; public fun getDiagnosticLevel ()Lio/sentry/SentryLevel; @@ -3412,6 +3413,7 @@ public class io/sentry/SentryOptions { public fun setContinuousProfiler (Lio/sentry/IContinuousProfiler;)V public fun setCron (Lio/sentry/SentryOptions$Cron;)V public fun setDateProvider (Lio/sentry/SentryDateProvider;)V + public fun setDeadlineTimeout (J)V public fun setDebug (Z)V public fun setDebugMetaLoader (Lio/sentry/internal/debugmeta/IDebugMetaLoader;)V public fun setDefaultScopeType (Lio/sentry/ScopeType;)V diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index cb306f0aaf3..4b7bd4d0e53 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -574,6 +574,16 @@ public class SentryOptions { */ private boolean startProfilerOnAppStart = false; + /** + * Controls the deadline timeout in milliseconds for automatic transactions. When set to a + * positive value, that value is used as the deadline timeout. When set to a value less than or + * equal to 0, no deadline is applied and transactions will only finish when explicitly finished + * or when the activity lifecycle ends. + * + *

Default is 30000 (30 seconds). + */ + private long deadlineTimeout = TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION; + private @NotNull SentryOptions.Logs logs = new SentryOptions.Logs(); private @NotNull ISocketTagger socketTagger = NoOpSocketTagger.getInstance(); @@ -2020,6 +2030,24 @@ public void setStartProfilerOnAppStart(final boolean startProfilerOnAppStart) { this.startProfilerOnAppStart = startProfilerOnAppStart; } + public long getDeadlineTimeout() { + return deadlineTimeout; + } + + /** + * Controls the deadline timeout in milliseconds for automatic transactions. When set to a + * positive value, that value is used as the deadline timeout. When set to a value less than or + * equal to 0, no deadline is applied and transactions will only finish when explicitly finished + * or when the activity lifecycle ends. + * + *

Default is 30000 (30 seconds). + * + * @param deadlineTimeout the timeout in milliseconds + */ + public void setDeadlineTimeout(long deadlineTimeout) { + this.deadlineTimeout = deadlineTimeout; + } + /** * Returns the profiling traces dir. path if set * diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index f132807e428..c43ad1513ed 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -815,4 +815,31 @@ class SentryOptionsTest { options.setTag(null, null) assertTrue(options.tags.isEmpty()) } + + @Test + fun `autoTransactionDeadlineTimeoutMillis option defaults to 30000`() { + val options = SentryOptions.empty() + assertEquals(30000L, options.deadlineTimeout) + } + + @Test + fun `autoTransactionDeadlineTimeoutMillis option can be changed`() { + val options = SentryOptions.empty() + options.deadlineTimeout = 60000L + assertEquals(60000L, options.deadlineTimeout) + } + + @Test + fun `autoTransactionDeadlineTimeoutMillis option can be set to zero value`() { + val options = SentryOptions.empty() + options.deadlineTimeout = 0L + assertEquals(0L, options.deadlineTimeout) + } + + @Test + fun `autoTransactionDeadlineTimeoutMillis option can be set to negative value`() { + val options = SentryOptions.empty() + options.deadlineTimeout = -1L + assertEquals(-1L, options.deadlineTimeout) + } }