From 0566579dce775c468d10c29056686da9d326ff5f Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 29 Jun 2023 14:14:54 +0200 Subject: [PATCH 01/23] Move enableNdk to SentryAndroidOptions (#2793) --- CHANGELOG.md | 5 ++ .../api/sentry-android-core.api | 4 ++ .../sentry/android/core/NdkIntegration.java | 2 +- .../android/core/SentryAndroidOptions.java | 48 +++++++++++++++++-- .../android/core/SentryAndroidOptionsTest.kt | 13 +++++ sentry/api/sentry.api | 4 -- .../main/java/io/sentry/SentryOptions.java | 45 ----------------- .../test/java/io/sentry/SentryOptionsTest.kt | 5 -- 8 files changed, 68 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bd46b6a4d8..96a2e75d80b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ - Add debouncing mechanism and before-capture callbacks for screenshots and view hierarchies ([#2773](https://github.com/getsentry/sentry-java/pull/2773)) +### Fixes + +Breaking changes: +- Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) + ## 6.23.0 ### Features diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 7fd64ed8c53..41e6a6fb956 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -225,8 +225,10 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun isEnableAppLifecycleBreadcrumbs ()Z public fun isEnableAutoActivityLifecycleTracing ()Z public fun isEnableFramesTracking ()Z + public fun isEnableNdk ()Z public fun isEnableNetworkEventBreadcrumbs ()Z public fun isEnableRootCheck ()Z + public fun isEnableScopeSync ()Z public fun isEnableSystemEventBreadcrumbs ()Z public fun setAnrEnabled (Z)V public fun setAnrReportInDebug (Z)V @@ -243,8 +245,10 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setEnableAppLifecycleBreadcrumbs (Z)V public fun setEnableAutoActivityLifecycleTracing (Z)V public fun setEnableFramesTracking (Z)V + public fun setEnableNdk (Z)V public fun setEnableNetworkEventBreadcrumbs (Z)V public fun setEnableRootCheck (Z)V + public fun setEnableScopeSync (Z)V public fun setEnableSystemEventBreadcrumbs (Z)V public fun setNativeSdkName (Ljava/lang/String;)V public fun setProfilingTracesHz (I)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java index 4757c96879f..840f109c56a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java @@ -68,7 +68,7 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions } } - private void disableNdkIntegration(final @NotNull SentryOptions options) { + private void disableNdkIntegration(final @NotNull SentryAndroidOptions options) { options.setEnableNdk(false); options.setEnableScopeSync(false); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index f28112ca118..b4f6d871fc6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -157,6 +157,15 @@ public final class SentryAndroidOptions extends SentryOptions { private @Nullable BeforeCaptureCallback beforeViewHierarchyCaptureCallback; + /** Turns NDK on or off. Default is enabled. */ + private boolean enableNdk = true; + + /** + * Enable the Java to NDK Scope sync. The default value for sentry-java is disabled and enabled + * for sentry-android. + */ + private boolean enableScopeSync = true; + public interface BeforeCaptureCallback { /** @@ -186,9 +195,6 @@ public SentryAndroidOptions() { setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME); setSdkVersion(createSdkVersion()); setAttachServerName(false); - - // enable scope sync for Android by default - setEnableScopeSync(true); } private @NotNull SdkVersion createSdkVersion() { @@ -510,4 +516,40 @@ public void setBeforeViewHierarchyCaptureCallback( final @NotNull BeforeCaptureCallback beforeViewHierarchyCaptureCallback) { this.beforeViewHierarchyCaptureCallback = beforeViewHierarchyCaptureCallback; } + + /** + * Check if NDK is ON or OFF Default is ON + * + * @return true if ON or false otherwise + */ + public boolean isEnableNdk() { + return enableNdk; + } + + /** + * Sets NDK to ON or OFF + * + * @param enableNdk true if ON or false otherwise + */ + public void setEnableNdk(boolean enableNdk) { + this.enableNdk = enableNdk; + } + + /** + * Returns if the Java to NDK Scope sync is enabled + * + * @return true if enabled or false otherwise + */ + public boolean isEnableScopeSync() { + return enableScopeSync; + } + + /** + * Enables or not the Java to NDK Scope sync + * + * @param enableScopeSync true if enabled or false otherwise + */ + public void setEnableScopeSync(boolean enableScopeSync) { + this.enableScopeSync = enableScopeSync; + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt index 07e16af252a..044380d6bea 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt @@ -133,6 +133,19 @@ class SentryAndroidOptionsTest { assertNull(sentryOptions.nativeSdkName) } + @Test + fun `when options is initialized, enableScopeSync is enabled by default`() { + assertTrue(SentryAndroidOptions().isEnableScopeSync) + } + + @Test + fun `enableScopeSync can be properly disabled`() { + val options = SentryAndroidOptions() + options.isEnableScopeSync = false + + assertFalse(options.isEnableScopeSync) + } + private class CustomDebugImagesLoader : IDebugImagesLoader { override fun loadDebugImages(): List? = null override fun clearDebugImages() {} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 187725650ec..3053ee5b222 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1718,8 +1718,6 @@ public class io/sentry/SentryOptions { public fun isEnableAutoSessionTracking ()Z public fun isEnableDeduplication ()Z public fun isEnableExternalConfiguration ()Z - public fun isEnableNdk ()Z - public fun isEnableScopeSync ()Z public fun isEnableShutdownHook ()Z public fun isEnableTimeToFullDisplayTracing ()Z public fun isEnableUncaughtExceptionHandler ()Z @@ -1751,8 +1749,6 @@ public class io/sentry/SentryOptions { public fun setEnableAutoSessionTracking (Z)V public fun setEnableDeduplication (Z)V public fun setEnableExternalConfiguration (Z)V - public fun setEnableNdk (Z)V - public fun setEnableScopeSync (Z)V public fun setEnableShutdownHook (Z)V public fun setEnableTimeToFullDisplayTracing (Z)V public fun setEnableTracing (Ljava/lang/Boolean;)V diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index c5bbb2aeebc..7599d07027c 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -103,9 +103,6 @@ public class SentryOptions { */ private boolean debug; - /** Turns NDK on or off. Default is enabled. */ - private boolean enableNdk = true; - /** Logger interface to log useful debugging information if debug is enabled */ private @NotNull ILogger logger = NoOpLogger.getInstance(); @@ -291,12 +288,6 @@ public class SentryOptions { private final @NotNull List optionsObservers = new CopyOnWriteArrayList<>(); - /** - * Enable the Java to NDK Scope sync. The default value for sentry-java is disabled and enabled - * for sentry-android. - */ - private boolean enableScopeSync; - /** * Enables loading additional options from external locations like {@code sentry.properties} file * or environment variables, system properties. @@ -589,24 +580,6 @@ public void setEnvelopeReader(final @Nullable IEnvelopeReader envelopeReader) { envelopeReader != null ? envelopeReader : NoOpEnvelopeReader.getInstance(); } - /** - * Check if NDK is ON or OFF Default is ON - * - * @return true if ON or false otherwise - */ - public boolean isEnableNdk() { - return enableNdk; - } - - /** - * Sets NDK to ON or OFF - * - * @param enableNdk true if ON or false otherwise - */ - public void setEnableNdk(boolean enableNdk) { - this.enableNdk = enableNdk; - } - /** * Returns the shutdown timeout in Millis * @@ -1386,24 +1359,6 @@ public List getOptionsObservers() { return optionsObservers; } - /** - * Returns if the Java to NDK Scope sync is enabled - * - * @return true if enabled or false otherwise - */ - public boolean isEnableScopeSync() { - return enableScopeSync; - } - - /** - * Enables or not the Java to NDK Scope sync - * - * @param enableScopeSync true if enabled or false otherwise - */ - public void setEnableScopeSync(boolean enableScopeSync) { - this.enableScopeSync = enableScopeSync; - } - /** * Returns if loading properties from external sources is enabled. * diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 3d3b654dc67..bf0703988c1 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -209,11 +209,6 @@ class SentryOptionsTest { assertTrue(SentryOptions().isAttachStacktrace) } - @Test - fun `when options is initialized, enableScopeSync is false`() { - assertFalse(SentryOptions().isEnableScopeSync) - } - @Test fun `when options is initialized, isProfilingEnabled is false`() { assertFalse(SentryOptions().isProfilingEnabled) From 4f55fd9e438142f7392baa94a752c84516e8d56e Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 7 Jul 2023 12:30:58 +0200 Subject: [PATCH 02/23] Capture failed HTTP requests by default (#2794) --- CHANGELOG.md | 1 + .../android/okhttp/SentryOkHttpInterceptor.kt | 2 +- .../okhttp/SentryOkHttpInterceptorTest.kt | 36 ++++++++++++++----- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a2e75d80b..3f4c5d0d1df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Add debouncing mechanism and before-capture callbacks for screenshots and view hierarchies ([#2773](https://github.com/getsentry/sentry-java/pull/2773)) +- Capture failed HTTP requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794)) ### Fixes diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt index 7c19c1c3b08..86f5629d3ca 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt @@ -44,7 +44,7 @@ import java.io.IOException class SentryOkHttpInterceptor( private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null, - private val captureFailedRequests: Boolean = false, + private val captureFailedRequests: Boolean = true, private val failedRequestStatusCodes: List = listOf( HttpStatusCodeRange(HttpStatusCodeRange.DEFAULT_MIN, HttpStatusCodeRange.DEFAULT_MAX) ), diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt index c686d4b9a22..6987e7b9a37 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt @@ -57,7 +57,7 @@ class SentryOkHttpInterceptorTest { beforeSpan: SentryOkHttpInterceptor.BeforeSpanCallback? = null, includeMockServerInTracePropagationTargets: Boolean = true, keepDefaultTracePropagationTargets: Boolean = false, - captureFailedRequests: Boolean = false, + captureFailedRequests: Boolean? = false, failedRequestTargets: List = listOf(".*"), failedRequestStatusCodes: List = listOf( HttpStatusCodeRange( @@ -91,13 +91,22 @@ class SentryOkHttpInterceptorTest { .setResponseCode(httpStatusCode) ) - val interceptor = SentryOkHttpInterceptor( - hub, - beforeSpan, - captureFailedRequests = captureFailedRequests, - failedRequestTargets = failedRequestTargets, - failedRequestStatusCodes = failedRequestStatusCodes - ) + val interceptor = when (captureFailedRequests) { + null -> SentryOkHttpInterceptor( + hub, + beforeSpan, + failedRequestTargets = failedRequestTargets, + failedRequestStatusCodes = failedRequestStatusCodes + ) + + else -> SentryOkHttpInterceptor( + hub, + beforeSpan, + captureFailedRequests = captureFailedRequests, + failedRequestTargets = failedRequestTargets, + failedRequestStatusCodes = failedRequestStatusCodes + ) + } return OkHttpClient.Builder().addInterceptor(interceptor).build() } } @@ -349,6 +358,17 @@ class SentryOkHttpInterceptorTest { } } + @Test + fun `captures failed requests by default`() { + val sut = fixture.getSut( + httpStatusCode = 500, + captureFailedRequests = null + ) + sut.newCall(getRequest()).execute() + + verify(fixture.hub).captureEvent(any(), any()) + } + @Test fun `captures an event if captureFailedRequests is enabled and within the range`() { val sut = fixture.getSut( From 36afdd4d8cf1ba03522edf01d52ed0048abbc9ba Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 12 Jul 2023 09:18:29 +0200 Subject: [PATCH 03/23] Fix Changelog --- CHANGELOG.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a41254289d..3ec184b6abb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 7.0.0 + +### Features + +Breaking changes: +- Capture failed HTTP requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794)) + +### Fixes + +Breaking changes: +- Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) + ## Unreleased ### Fixes @@ -56,11 +68,6 @@ - If [ApplicationExitInfo#getTraceInputStream](https://developer.android.com/reference/android/app/ApplicationExitInfo#getTraceInputStream()) returns null, the SDK no longer reports an ANR event, as these events are not very useful without it. - Enhance regex patterns for native stackframes -### Fixes - -Breaking changes: -- Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) - ## 6.23.0 ### Features @@ -2295,4 +2302,4 @@ Features from the current SDK like `ANR` are also available (by default triggere Packages were released on [`bintray`](https://dl.bintray.com/getsentry/sentry-android/io/sentry/), [`jcenter`](https://jcenter.bintray.com/io/sentry/sentry-android/) We'd love to get feedback and we'll work in getting the GA `2.0.0` out soon. -Until then, the [stable SDK offered by Sentry is at version 1.7.28](https://github.com/getsentry/sentry-java/releases/tag/v1.7.28) +Until then, the [stable SDK offered by Sentry is at version 1.7.28](https://github.com/getsentry/sentry-java/releases/tag/v1.7.28) \ No newline at end of file From 1faf1682bb20eb15c4187aa9905c7b7db358c93f Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 19 Jul 2023 22:17:03 +0200 Subject: [PATCH 04/23] 7.0.0 -> Unreleased in changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec184b6abb..04d05f1bfcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 7.0.0 +## Unreleased ### Features @@ -2302,4 +2302,4 @@ Features from the current SDK like `ANR` are also available (by default triggere Packages were released on [`bintray`](https://dl.bintray.com/getsentry/sentry-android/io/sentry/), [`jcenter`](https://jcenter.bintray.com/io/sentry/sentry-android/) We'd love to get feedback and we'll work in getting the GA `2.0.0` out soon. -Until then, the [stable SDK offered by Sentry is at version 1.7.28](https://github.com/getsentry/sentry-java/releases/tag/v1.7.28) \ No newline at end of file +Until then, the [stable SDK offered by Sentry is at version 1.7.28](https://github.com/getsentry/sentry-java/releases/tag/v1.7.28) From cae0eeae7e7c76433c334db57453192762bae640 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 20 Jul 2023 09:10:15 +0200 Subject: [PATCH 05/23] Do not overwrite UI transaction status if set by the user (#2852) --- CHANGELOG.md | 2 ++ .../internal/gestures/SentryGestureListener.java | 8 +++++++- .../gestures/SentryGestureListenerTracingTest.kt | 12 ++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d05f1bfcb..2376590a8f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Breaking changes: ### Fixes +- Do not overwrite UI transaction status if set by the user ([#2852](https://github.com/getsentry/sentry-java/pull/2852)) + Breaking changes: - Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) 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 3dda6183cde..17b62eb3194 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 @@ -255,7 +255,13 @@ private void startTracing(final @NotNull UiElement target, final @NotNull String void stopTracing(final @NotNull SpanStatus status) { if (activeTransaction != null) { - activeTransaction.finish(status); + final SpanStatus currentStatus = activeTransaction.getStatus(); + // status might be set by other integrations, let's not overwrite it + if (currentStatus == null) { + activeTransaction.finish(status); + } else { + activeTransaction.finish(); + } } hub.configureScope( scope -> { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt index 5f216a660a0..41776e495a2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt @@ -14,6 +14,7 @@ import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryTracer import io.sentry.SpanStatus +import io.sentry.SpanStatus.OUT_OF_RANGE import io.sentry.TransactionContext import io.sentry.TransactionOptions import io.sentry.android.core.SentryAndroidOptions @@ -323,6 +324,17 @@ class SentryGestureListenerTracingTest { verify(fixture.transaction).scheduleFinish() } + @Test + fun `preserves existing transaction status`() { + val sut = fixture.getSut() + + sut.onSingleTapUp(fixture.event) + + fixture.transaction.status = OUT_OF_RANGE + sut.stopTracing(SpanStatus.CANCELLED) + assertEquals(OUT_OF_RANGE, fixture.transaction.status) + } + internal open class ScrollableListView : AbsListView(mock()) { override fun getAdapter(): ListAdapter = mock() override fun setSelection(position: Int) = Unit From 3838091e72ee3712c6382fdcc91bf7c3b4cfaef4 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 21 Jul 2023 10:39:31 +0200 Subject: [PATCH 06/23] Reduce flush timeout to 4s on Android to avoid ANRs (#2858) --- CHANGELOG.md | 1 + .../android/core/AndroidOptionsInitializer.java | 5 +++++ .../android/core/AndroidOptionsInitializerTest.kt | 14 ++++++++++++++ .../main/java/io/sentry/cache/EnvelopeCache.java | 6 +++++- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2376590a8f0..1f463eee192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Breaking changes: - Capture failed HTTP requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794)) +- Reduce flush timeout to 4s on Android to avoid ANRs ([#2858](https://github.com/getsentry/sentry-java/pull/2858)) ### Fixes 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 f3cf00f96c5..a564a5ebe1a 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 @@ -41,6 +41,8 @@ @SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references final class AndroidOptionsInitializer { + static final long DEFAULT_FLUSH_TIMEOUT_MS = 4000; + static final String SENTRY_COMPOSE_GESTURE_INTEGRATION_CLASS_NAME = "io.sentry.compose.gestures.ComposeGestureTargetLocator"; @@ -93,6 +95,9 @@ static void loadDefaultAndMetadataOptions( options.setDateProvider(new SentryAndroidDateProvider()); + // set a lower flush timeout on Android to avoid ANRs + options.setFlushTimeoutMillis(DEFAULT_FLUSH_TIMEOUT_MS); + ManifestMetadataReader.applyMetadata(context, options, buildInfoProvider); initializeCacheDirs(context, options); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 7f48dcd7ef8..2632b2921fd 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -174,6 +174,20 @@ class AndroidOptionsInitializerTest { assertTrue(innerLogger.get(loggerField) is AndroidLogger) } + @Test + fun `flush timeout is set to Android specific default value`() { + fixture.initSut() + assertEquals(AndroidOptionsInitializer.DEFAULT_FLUSH_TIMEOUT_MS, fixture.sentryOptions.flushTimeoutMillis) + } + + @Test + fun `flush timeout can be overridden`() { + fixture.initSut(configureOptions = { + flushTimeoutMillis = 1234 + }) + assertEquals(1234, fixture.sentryOptions.flushTimeoutMillis) + } + @Test fun `AndroidEventProcessor added to processors list`() { fixture.initSut() diff --git a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java index 24d7c62aa3d..0b5f8ac2fb7 100644 --- a/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java @@ -67,6 +67,8 @@ public class EnvelopeCache extends CacheStrategy implements IEnvelopeCache { public static final String STARTUP_CRASH_MARKER_FILE = "startup_crash"; + private static final long SESSION_FLUSH_DISK_TIMEOUT_MS = 15000; + private final CountDownLatch previousSessionLatch; private final @NotNull Map fileNameMap = new WeakHashMap<>(); @@ -429,7 +431,9 @@ public void discard(final @NotNull SentryEnvelope envelope) { /** Awaits until the previous session (if any) is flushed to its own file. */ public boolean waitPreviousSessionFlush() { try { - return previousSessionLatch.await(options.getFlushTimeoutMillis(), TimeUnit.MILLISECONDS); + // use fixed timeout instead of configurable options.getFlushTimeoutMillis() to ensure there's + // enough time to flush the session to disk + return previousSessionLatch.await(SESSION_FLUSH_DISK_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); options.getLogger().log(DEBUG, "Timed out waiting for previous session to flush."); From 19fca0449107d59aa2a8de47b715141929be4db6 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 24 Jul 2023 10:45:41 +0200 Subject: [PATCH 07/23] Set ip_address = {{auto}} by default (#2860) --- CHANGELOG.md | 2 ++ .../android/core/AnrV2EventProcessor.java | 32 +++++-------------- .../core/DefaultAndroidEventProcessor.java | 27 +++++++--------- .../android/core/AnrV2EventProcessorTest.kt | 7 ++++ .../core/DefaultAndroidEventProcessorTest.kt | 25 +++++++++++++++ .../java/io/sentry/MainEventProcessor.java | 15 ++++----- .../java/io/sentry/MainEventProcessorTest.kt | 15 ++------- 7 files changed, 62 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f463eee192..3735251ff15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ Breaking changes: - Capture failed HTTP requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794)) - Reduce flush timeout to 4s on Android to avoid ANRs ([#2858](https://github.com/getsentry/sentry-java/pull/2858)) +- Set ip_address to {{auto}} by default, even if sendDefaultPII is disabled ([#2860](https://github.com/getsentry/sentry-java/pull/2860)) + - Instead use the "Prevent Storing of IP Addresses" option in the "Security & Privacy" project settings on sentry.io ### Fixes diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index 6a4b7edfa1c..81422ea514b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -475,35 +475,19 @@ private void setExceptions(final @NotNull SentryEvent event, final @NotNull Obje } private void mergeUser(final @NotNull SentryBaseEvent event) { - if (options.isSendDefaultPii()) { - if (event.getUser() == null) { - final User user = new User(); - user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); - event.setUser(user); - } else if (event.getUser().getIpAddress() == null) { - event.getUser().setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); - } + @Nullable User user = event.getUser(); + if (user == null) { + user = new User(); + event.setUser(user); } // userId should be set even if event is Cached as the userId is static and won't change anyway. - final User user = event.getUser(); - if (user == null) { - event.setUser(getDefaultUser()); - } else if (user.getId() == null) { + if (user.getId() == null) { user.setId(getDeviceId()); } - } - - /** - * Sets the default user which contains only the userId. - * - * @return the User object - */ - private @NotNull User getDefaultUser() { - User user = new User(); - user.setId(getDeviceId()); - - return user; + if (user.getIpAddress() == null) { + user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); + } } private @Nullable String getDeviceId() { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 8d88c6674ae..446443f5441 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -20,6 +20,7 @@ import io.sentry.DateUtils; import io.sentry.EventProcessor; import io.sentry.Hint; +import io.sentry.IpAddressUtils; import io.sentry.SentryBaseEvent; import io.sentry.SentryEvent; import io.sentry.SentryLevel; @@ -165,13 +166,19 @@ private boolean shouldApplyScopeData( } private void mergeUser(final @NotNull SentryBaseEvent event) { - // userId should be set even if event is Cached as the userId is static and won't change anyway. - final User user = event.getUser(); + @Nullable User user = event.getUser(); if (user == null) { - event.setUser(getDefaultUser()); - } else if (user.getId() == null) { + user = new User(); + event.setUser(user); + } + + // userId should be set even if event is Cached as the userId is static and won't change anyway. + if (user.getId() == null) { user.setId(getDeviceId()); } + if (user.getIpAddress() == null) { + user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); + } } private void setDevice( @@ -728,18 +735,6 @@ private void setAppPackageInfo(final @NotNull App app, final @NotNull PackageInf } } - /** - * Sets the default user which contains only the userId. - * - * @return the User object - */ - public @NotNull User getDefaultUser() { - User user = new User(); - user.setId(getDeviceId()); - - return user; - } - private @Nullable String getDeviceId() { try { return Installation.id(context); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt index 69598a0e1e6..d6a05ddd2c9 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt @@ -406,6 +406,10 @@ class AnrV2EventProcessorTest { debugMeta = DebugMeta().apply { images = listOf(DebugImage().apply { type = DebugImage.PROGUARD; uuid = "uuid1" }) } + user = User().apply { + id = "42" + ipAddress = "2.4.8.16" + } } assertEquals("NotAndroid", processed.platform) @@ -427,6 +431,9 @@ class AnrV2EventProcessorTest { assertEquals(2, processed.debugMeta!!.images!!.size) assertEquals("uuid1", processed.debugMeta!!.images!![0].uuid) assertEquals("uuid", processed.debugMeta!!.images!![1].uuid) + + assertEquals("42", processed.user!!.id) + assertEquals("2.4.8.16", processed.user!!.ipAddress) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt index 8747409ccea..65d86c8cfa8 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt @@ -295,6 +295,31 @@ class DefaultAndroidEventProcessorTest { } } + @Test + fun `when event user data does not have ip address set, sets {{auto}} as the ip address`() { + val sut = fixture.getSut(context) + val event = SentryEvent().apply { + user = User() + } + sut.process(event, Hint()) + assertNotNull(event.user) { + assertEquals("{{auto}}", it.ipAddress) + } + } + + @Test + fun `when event has ip address set, keeps original ip address`() { + val sut = fixture.getSut(context) + val event = SentryEvent() + event.user = User().apply { + ipAddress = "192.168.0.1" + } + sut.process(event, Hint()) + assertNotNull(event.user) { + assertEquals("192.168.0.1", it.ipAddress) + } + } + @Test fun `Executor service should be called on ctor`() { val sut = fixture.getSut(context) diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index d0468559583..abbf21c84e5 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -220,14 +220,13 @@ private void setTags(final @NotNull SentryBaseEvent event) { } private void mergeUser(final @NotNull SentryBaseEvent event) { - if (options.isSendDefaultPii()) { - if (event.getUser() == null) { - final User user = new User(); - user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); - event.setUser(user); - } else if (event.getUser().getIpAddress() == null) { - event.getUser().setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); - } + @Nullable User user = event.getUser(); + if (user == null) { + user = new User(); + event.setUser(user); + } + if (user.getIpAddress() == null) { + user.setIpAddress(IpAddressUtils.DEFAULT_IP_ADDRESS); } } diff --git a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt index 4e6b4f3d99a..bbdd252ce30 100644 --- a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt @@ -297,7 +297,7 @@ class MainEventProcessorTest { } @Test - fun `when event does not have ip address set and sendDefaultPii is set to true, sets {{auto}} as the ip address`() { + fun `when event does not have ip address set, sets {{auto}} as the ip address`() { val sut = fixture.getSut(sendDefaultPii = true) val event = SentryEvent() sut.process(event, Hint()) @@ -307,7 +307,7 @@ class MainEventProcessorTest { } @Test - fun `when event has ip address set and sendDefaultPii is set to true, keeps original ip address`() { + fun `when event has ip address set, keeps original ip address`() { val sut = fixture.getSut(sendDefaultPii = true) val event = SentryEvent() event.user = User().apply { @@ -319,17 +319,6 @@ class MainEventProcessorTest { } } - @Test - fun `when event does not have ip address set and sendDefaultPii is set to false, does not set ip address`() { - val sut = fixture.getSut(sendDefaultPii = false) - val event = SentryEvent() - event.user = User() - sut.process(event, Hint()) - assertNotNull(event.user) { - assertNull(it.ipAddress) - } - } - @Test fun `when event has environment set, does not overwrite environment`() { val sut = fixture.getSut(environment = null) From 77176836f238577aa7aa0671d8bec835e713a17f Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 25 Jul 2023 18:26:44 +0200 Subject: [PATCH 08/23] Fix test --- .../core/DefaultAndroidEventProcessorTest.kt | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt index 4eabf545824..bad710a4520 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt @@ -317,19 +317,6 @@ class DefaultAndroidEventProcessorTest { } } - @Test - fun `Executor service should be called on ctor`() { - val sut = fixture.getSut(context) - - val contextData = sut.contextData.get() - - assertNotNull(contextData) - assertNotNull(contextData[ROOTED]) - assertNotNull(contextData[KERNEL_VERSION]) - assertNotNull(contextData[EMULATOR]) - assertNotNull(contextData[SIDE_LOADED]) - } - @Test fun `Processor won't throw exception`() { val sut = fixture.getSut(context) From 1c0c691f3877756b7c2afc0193b7014ade9b8d6e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 25 Jul 2023 19:44:35 +0200 Subject: [PATCH 09/23] Measure AppStart time till First Draw (#2851) --- CHANGELOG.md | 1 + .../core/ActivityLifecycleIntegration.java | 22 +++++----- .../io/sentry/android/core/AppStartState.java | 4 ++ .../core/SentryPerformanceProvider.java | 31 +++++++++++++- .../core/ActivityLifecycleIntegrationTest.kt | 10 +++++ .../sentry/android/core/AppStartStateTest.kt | 13 ++++++ .../core/SentryPerformanceProviderTest.kt | 41 +++++++++++++++++-- 7 files changed, 107 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbe08f61985..7db0dd372b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Breaking changes: ### Fixes +- Measure AppStart time till First Draw instead of `onResume` ([#2851](https://github.com/getsentry/sentry-java/pull/2851)) - Do not overwrite UI transaction status if set by the user ([#2852](https://github.com/getsentry/sentry-java/pull/2852)) Breaking changes: 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 ee4ea10b729..070fed2e0fe 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 @@ -400,17 +400,6 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) { @Override public synchronized void onActivityResumed(final @NotNull Activity activity) { if (performanceEnabled) { - // app start span - @Nullable final SentryDate appStartStartTime = AppStartState.getInstance().getAppStartTime(); - @Nullable final SentryDate appStartEndTime = AppStartState.getInstance().getAppStartEndTime(); - // in case the SentryPerformanceProvider is disabled it does not set the app start times, - // and we need to set the end time manually here, - // the start time gets set manually in SentryAndroid.init() - if (appStartStartTime != null && appStartEndTime == null) { - AppStartState.getInstance().setAppStartEnd(); - } - finishAppStartSpan(); - final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity); final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity); final View rootView = activity.findViewById(android.R.id.content); @@ -540,6 +529,17 @@ private void cancelTtfdAutoClose() { } private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable ISpan ttidSpan) { + // app start span + @Nullable final SentryDate appStartStartTime = AppStartState.getInstance().getAppStartTime(); + @Nullable final SentryDate appStartEndTime = AppStartState.getInstance().getAppStartEndTime(); + // in case the SentryPerformanceProvider is disabled it does not set the app start times, + // and we need to set the end time manually here, + // the start time gets set manually in SentryAndroid.init() + if (appStartStartTime != null && appStartEndTime == null) { + AppStartState.getInstance().setAppStartEnd(); + } + finishAppStartSpan(); + if (options != null && ttidSpan != null) { final SentryDate endDate = options.getDateProvider().now(); final long durationNanos = endDate.diff(ttidSpan.getStartDate()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java index de690aa6683..0c38d04d48a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppStartState.java @@ -45,6 +45,10 @@ synchronized void setAppStartEnd() { @TestOnly void setAppStartEnd(final long appStartEndMillis) { + if (this.appStartEndMillis != null) { + // only set app start end once + return; + } this.appStartEndMillis = appStartEndMillis; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 7ba270351b3..b0f66446dec 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -1,13 +1,18 @@ package io.sentry.android.core; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.pm.ProviderInfo; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.SystemClock; +import android.view.View; +import io.sentry.NoOpLogger; import io.sentry.SentryDate; +import io.sentry.android.core.internal.util.FirstDrawDoneListener; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -33,8 +38,22 @@ public final class SentryPerformanceProvider extends EmptySecureContentProvider private @Nullable Application application; + private final @NotNull BuildInfoProvider buildInfoProvider; + + private final @NotNull MainLooperHandler mainHandler; + public SentryPerformanceProvider() { AppStartState.getInstance().setAppStartTime(appStartMillis, appStartTime); + buildInfoProvider = new BuildInfoProvider(NoOpLogger.getInstance()); + mainHandler = new MainLooperHandler(); + } + + SentryPerformanceProvider( + final @NotNull BuildInfoProvider buildInfoProvider, + final @NotNull MainLooperHandler mainHandler) { + AppStartState.getInstance().setAppStartTime(appStartMillis, appStartTime); + this.buildInfoProvider = buildInfoProvider; + this.mainHandler = mainHandler; } @Override @@ -100,12 +119,22 @@ public void onActivityCreated(@NotNull Activity activity, @Nullable Bundle saved @Override public void onActivityStarted(@NotNull Activity activity) {} + @SuppressLint("NewApi") @Override public void onActivityResumed(@NotNull Activity activity) { if (!firstActivityResumed) { // sets App start as finished when the very first activity calls onResume firstActivityResumed = true; - AppStartState.getInstance().setAppStartEnd(); + final View rootView = activity.findViewById(android.R.id.content); + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN + && rootView != null) { + FirstDrawDoneListener.registerForNextDraw( + rootView, () -> AppStartState.getInstance().setAppStartEnd(), buildInfoProvider); + } else { + // Posting a task to the main thread's handler will make it executed after it finished + // its current job. That is, right after the activity draws the layout. + mainHandler.post(() -> AppStartState.getInstance().setAppStartEnd()); + } } if (application != null) { application.unregisterActivityLifecycleCallbacks(this); 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 a07f6692d60..1004575976a 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 @@ -889,13 +889,17 @@ class ActivityLifecycleIntegrationTest { AppStartState.getInstance().setColdStart(false) // when activity is created + val view = fixture.createView() val activity = mock() + whenever(activity.findViewById(any())).thenReturn(view) sut.onActivityCreated(activity, fixture.bundle) // then app-start end time should still be null assertNull(AppStartState.getInstance().appStartEndTime) // when activity is resumed sut.onActivityResumed(activity) + Thread.sleep(1) + runFirstDraw(view) // end-time should be set assertNotNull(AppStartState.getInstance().appStartEndTime) } @@ -936,10 +940,14 @@ class ActivityLifecycleIntegrationTest { AppStartState.getInstance().setColdStart(false) // when activity is created, started and resumed multiple times + val view = fixture.createView() val activity = mock() + whenever(activity.findViewById(any())).thenReturn(view) sut.onActivityCreated(activity, fixture.bundle) sut.onActivityStarted(activity) sut.onActivityResumed(activity) + Thread.sleep(1) + runFirstDraw(view) val firstAppStartEndTime = AppStartState.getInstance().appStartEndTime @@ -948,6 +956,8 @@ class ActivityLifecycleIntegrationTest { sut.onActivityStopped(activity) sut.onActivityStarted(activity) sut.onActivityResumed(activity) + Thread.sleep(1) + runFirstDraw(view) // then the end time should not be overwritten assertEquals( diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt index 421274b42a6..29cb7c0d7e2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AppStartStateTest.kt @@ -1,6 +1,7 @@ package io.sentry.android.core import io.sentry.SentryInstantDate +import io.sentry.SentryLongDate import io.sentry.SentryNanotimeDate import java.util.Date import kotlin.test.BeforeTest @@ -58,6 +59,18 @@ class AppStartStateTest { assertSame(date, sut.appStartTime) } + @Test + fun `do not overwrite app start end time if already set`() { + val sut = AppStartState.getInstance() + + sut.setColdStart(true) + sut.setAppStartTime(1, SentryLongDate(1000000)) + sut.setAppStartEnd(2) + sut.setAppStartEnd(3) + + assertEquals(0, SentryLongDate(2000000).compareTo(sut.appStartEndTime!!)) + } + @Test fun `do not overwrite cold start value if already set`() { val sut = AppStartState.getInstance() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index cf3ca7c2eab..f913ca92684 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -1,14 +1,21 @@ package io.sentry.android.core +import android.app.Activity import android.app.Application import android.content.pm.ProviderInfo import android.os.Bundle +import android.os.Looper +import android.view.View +import android.view.ViewTreeObserver +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.SentryNanotimeDate import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.robolectric.Shadows import java.util.Date import kotlin.test.BeforeTest import kotlin.test.Test @@ -84,11 +91,21 @@ class SentryPerformanceProviderTest { val mockContext = ContextUtilsTest.createMockContext(true) providerInfo.authority = AUTHORITY - val provider = SentryPerformanceProvider() + val provider = SentryPerformanceProvider( + mock { + whenever(mock.sdkInfoVersion).thenReturn(29) + }, + MainLooperHandler() + ) provider.attachInfo(mockContext, providerInfo) - provider.onActivityCreated(mock(), Bundle()) - provider.onActivityResumed(mock()) + val view = createView() + val activity = mock() + whenever(activity.findViewById(any())).thenReturn(view) + provider.onActivityCreated(activity, Bundle()) + provider.onActivityResumed(activity) + Thread.sleep(1) + runFirstDraw(view) assertNotNull(AppStartState.getInstance().appStartInterval) assertNotNull(AppStartState.getInstance().appStartEndTime) @@ -97,6 +114,24 @@ class SentryPerformanceProviderTest { .unregisterActivityLifecycleCallbacks(any()) } + private fun createView(): View { + val view = View(ApplicationProvider.getApplicationContext()) + + // Adding a listener forces ViewTreeObserver.mOnDrawListeners to be initialized and non-null. + val dummyListener = ViewTreeObserver.OnDrawListener {} + view.viewTreeObserver.addOnDrawListener(dummyListener) + view.viewTreeObserver.removeOnDrawListener(dummyListener) + + return view + } + + private fun runFirstDraw(view: View) { + // Removes OnDrawListener in the next OnGlobalLayout after onDraw + view.viewTreeObserver.dispatchOnDraw() + view.viewTreeObserver.dispatchOnGlobalLayout() + Shadows.shadowOf(Looper.getMainLooper()).idle() + } + companion object { private const val AUTHORITY = "io.sentry.sample.SentryPerformanceProvider" } From 803f03ccf6eae06f6dc624cbcf3668a3a10a77e2 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 25 Jul 2023 20:02:51 +0200 Subject: [PATCH 10/23] update coroutines to 1.6.1, use CopyableThreadContextElement (#2838) Co-authored-by: Sentry Github Bot Co-authored-by: Roman Zavarnitsyn --- CHANGELOG.md | 1 + buildSrc/src/main/java/Config.kt | 2 +- .../api/sentry-kotlin-extensions.api | 6 +- .../java/io/sentry/kotlin/SentryContext.kt | 13 ++- .../io/sentry/kotlin/SentryContextTest.kt | 87 +++++++++++++++++++ 5 files changed, 104 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7db0dd372b8..ebb102f788d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Breaking changes: Breaking changes: - Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) +- Fix Coroutine Context Propagation using CopyableThreadContextElement, requires `kotlinx-coroutines-core` version `1.6.1` or higher ([#2838](https://github.com/getsentry/sentry-java/pull/2838)) ## Unreleased diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 6d54ef9296a..2674bc2350e 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -104,7 +104,7 @@ object Config { val retrofit2 = "$retrofit2Group:retrofit:$retrofit2Version" val retrofit2Gson = "$retrofit2Group:converter-gson:$retrofit2Version" - val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3" + val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1" val fragment = "androidx.fragment:fragment-ktx:1.3.5" diff --git a/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api b/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api index 5cc7b874555..d501240a3ab 100644 --- a/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api +++ b/sentry-kotlin-extensions/api/sentry-kotlin-extensions.api @@ -1,7 +1,11 @@ -public final class io/sentry/kotlin/SentryContext : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/ThreadContextElement { +public final class io/sentry/kotlin/SentryContext : kotlin/coroutines/AbstractCoroutineContextElement, kotlinx/coroutines/CopyableThreadContextElement { public fun ()V + public fun (Lio/sentry/IHub;)V + public synthetic fun (Lio/sentry/IHub;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun copyForChild ()Lkotlinx/coroutines/CopyableThreadContextElement; public fun fold (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; + public fun mergeForChild (Lkotlin/coroutines/CoroutineContext$Element;)Lkotlin/coroutines/CoroutineContext; public fun minusKey (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; public fun plus (Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; public fun restoreThreadContext (Lkotlin/coroutines/CoroutineContext;Lio/sentry/IHub;)V diff --git a/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt b/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt index cd6042e7481..3cf22a20da3 100644 --- a/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt +++ b/sentry-kotlin-extensions/src/main/java/io/sentry/kotlin/SentryContext.kt @@ -2,18 +2,25 @@ package io.sentry.kotlin import io.sentry.IHub import io.sentry.Sentry -import kotlinx.coroutines.ThreadContextElement +import kotlinx.coroutines.CopyableThreadContextElement import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.CoroutineContext /** * Sentry context element for [CoroutineContext]. */ -public class SentryContext : ThreadContextElement, AbstractCoroutineContextElement(Key) { +public class SentryContext(private val hub: IHub = Sentry.getCurrentHub().clone()) : + CopyableThreadContextElement, AbstractCoroutineContextElement(Key) { private companion object Key : CoroutineContext.Key - private val hub: IHub = Sentry.getCurrentHub().clone() + override fun copyForChild(): CopyableThreadContextElement { + return SentryContext(hub.clone()) + } + + override fun mergeForChild(overwritingElement: CoroutineContext.Element): CoroutineContext { + return overwritingElement[Key] ?: SentryContext(hub.clone()) + } override fun updateThreadContext(context: CoroutineContext): IHub { val oldState = Sentry.getCurrentHub() diff --git a/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt b/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt index 63f14bac55e..b54ceabc511 100644 --- a/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt +++ b/sentry-kotlin-extensions/src/test/java/io/sentry/kotlin/SentryContextTest.kt @@ -1,6 +1,7 @@ package io.sentry.kotlin import io.sentry.Sentry +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -8,6 +9,8 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull import kotlin.test.assertNull class SentryContextTest { @@ -80,6 +83,90 @@ class SentryContextTest { } } + @Test + fun testContextIsClonedWhenPassedToChild() = runBlocking { + Sentry.setTag("parent", "parentValue") + launch(SentryContext()) { + Sentry.setTag("c1", "c1value") + assertEquals("c1value", getTag("c1")) + assertEquals("parentValue", getTag("parent")) + assertNull(getTag("c2")) + + val c2 = launch() { + Sentry.setTag("c2", "c2value") + assertEquals("c2value", getTag("c2")) + assertEquals("parentValue", getTag("parent")) + assertNotNull(getTag("c1")) + } + + c2.join() + + assertNotNull(getTag("c1")) + assertNull(getTag("c2")) + } + assertNull(getTag("c1")) + assertNull(getTag("c2")) + } + + @Test + fun testExplicitlyPassedContextOverridesPropagatedContext() = runBlocking { + Sentry.setTag("parent", "parentValue") + launch(SentryContext()) { + Sentry.setTag("c1", "c1value") + assertEquals("c1value", getTag("c1")) + assertEquals("parentValue", getTag("parent")) + assertNull(getTag("c2")) + + val c2 = launch( + SentryContext( + Sentry.getCurrentHub().clone().also { + it.setTag("cloned", "clonedValue") + } + ) + ) { + Sentry.setTag("c2", "c2value") + assertEquals("c2value", getTag("c2")) + assertEquals("parentValue", getTag("parent")) + assertNotNull(getTag("c1")) + assertNotNull(getTag("cloned")) + } + + c2.join() + + assertNotNull(getTag("c1")) + assertNull(getTag("c2")) + assertNull(getTag("cloned")) + } + assertNull(getTag("c1")) + assertNull(getTag("c2")) + assertNull(getTag("cloned")) + } + + @Test + fun `mergeForChild returns copy of initial context if Key not present`() { + val initialContextElement = SentryContext( + Sentry.getCurrentHub().clone().also { + it.setTag("cloned", "clonedValue") + } + ) + val mergedContextElement = initialContextElement.mergeForChild(CoroutineName("test")) + + assertNotEquals(initialContextElement, mergedContextElement) + assertNotNull((mergedContextElement)[initialContextElement.key]) + } + + @Test + fun `mergeForChild returns passed context`() { + val initialContextElement = SentryContext( + Sentry.getCurrentHub().clone().also { + it.setTag("cloned", "clonedValue") + } + ) + val mergedContextElement = SentryContext().mergeForChild(initialContextElement) + + assertEquals(initialContextElement, mergedContextElement) + } + private fun getTag(tag: String): String? { var value: String? = null Sentry.configureScope { From 8e78ac36cafe010268772ff3cbfe303ece5ed95f Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 4 Aug 2023 13:15:55 +0200 Subject: [PATCH 11/23] Reduce timeout of AsyncHttpTransport to avoid ANR (#2879) * reduced timeout of AsyncHttpTransport close to flushTimeoutMillis (default 4s in Android and 15s in Java) to avoid ANRs in Android --- CHANGELOG.md | 1 + .../java/io/sentry/transport/AsyncHttpTransport.java | 2 +- .../java/io/sentry/transport/AsyncHttpTransportTest.kt | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebb102f788d..d4b03cd492f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Breaking changes: - Reduce flush timeout to 4s on Android to avoid ANRs ([#2858](https://github.com/getsentry/sentry-java/pull/2858)) - Set ip_address to {{auto}} by default, even if sendDefaultPII is disabled ([#2860](https://github.com/getsentry/sentry-java/pull/2860)) - Instead use the "Prevent Storing of IP Addresses" option in the "Security & Privacy" project settings on sentry.io +- Reduce timeout of AsyncHttpTransport to avoid ANR ([#2879](https://github.com/getsentry/sentry-java/pull/2879)) ### Fixes diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index cbd54947264..d750b96c7c7 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -140,7 +140,7 @@ public void close() throws IOException { executor.shutdown(); options.getLogger().log(SentryLevel.DEBUG, "Shutting down"); try { - if (!executor.awaitTermination(1, TimeUnit.MINUTES)) { + if (!executor.awaitTermination(options.getFlushTimeoutMillis(), TimeUnit.MILLISECONDS)) { options .getLogger() .log( diff --git a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt index ce59db50255..20f2ff69799 100644 --- a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt +++ b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt @@ -25,6 +25,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.io.IOException import java.util.Date +import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals @@ -321,6 +322,15 @@ class AsyncHttpTransportTest { ) } + @Test + fun `close uses flushTimeoutMillis option to schedule termination`() { + fixture.sentryOptions.flushTimeoutMillis = 123 + val sut = fixture.getSUT() + sut.close() + + verify(fixture.executor).awaitTermination(eq(123), eq(TimeUnit.MILLISECONDS)) + } + private fun createSession(): Session { return Session("123", User(), "env", "release") } From 0b3de21ff2f139a1821fe4e26e2491e8b3bc8740 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 8 Aug 2023 09:30:06 +0200 Subject: [PATCH 12/23] Add deadline timeout for automatic transactions (#2865) * Add deadline timeout for automatic transactions * Update Changelog * Address PR comments * Fix formatting * Add missing test, improve naming * Ensure deadline timeout is only set once --- CHANGELOG.md | 2 + .../core/ActivityLifecycleIntegration.java | 3 + .../gestures/SentryGestureListener.java | 2 + .../core/ActivityLifecycleIntegrationTest.kt | 10 +- .../SentryGestureListenerTracingTest.kt | 18 +++ .../navigation/SentryNavigationListener.kt | 5 +- .../SentryNavigationListenerTest.kt | 24 ++- sentry/api/sentry.api | 3 + .../src/main/java/io/sentry/SentryTracer.java | 139 +++++++++++++----- .../java/io/sentry/TransactionOptions.java | 35 +++++ .../test/java/io/sentry/SentryTracerTest.kt | 76 ++++++++-- 11 files changed, 267 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b03cd492f..d65166145bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Breaking changes: - Set ip_address to {{auto}} by default, even if sendDefaultPII is disabled ([#2860](https://github.com/getsentry/sentry-java/pull/2860)) - Instead use the "Prevent Storing of IP Addresses" option in the "Security & Privacy" project settings on sentry.io - Reduce timeout of AsyncHttpTransport to avoid ANR ([#2879](https://github.com/getsentry/sentry-java/pull/2879)) +- Add deadline timeout for automatic transactions ([#2865](https://github.com/getsentry/sentry-java/pull/2865)) + - This affects all automatically generated transactions on Android (UI, clicks), the default timeout is 30s ### Fixes 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 070fed2e0fe..4cf44e7f493 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 @@ -192,6 +192,9 @@ private void startTracing(final @NotNull Activity activity) { final Boolean coldStart = AppStartState.getInstance().isColdStart(); final TransactionOptions transactionOptions = new TransactionOptions(); + transactionOptions.setDeadlineTimeout( + TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION); + if (options.isEnableActivityLifecycleTracingAutoFinish()) { transactionOptions.setIdleTimeout(options.getIdleTimeout()); transactionOptions.setTrimEnd(true); 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 e502aea71dc..2493f5b5f04 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 @@ -237,6 +237,8 @@ private void startTracing(final @NotNull UiElement target, final @NotNull String final TransactionOptions transactionOptions = new TransactionOptions(); transactionOptions.setWaitForChildren(true); + transactionOptions.setDeadlineTimeout( + TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION); transactionOptions.setIdleTimeout(options.getIdleTimeout()); transactionOptions.setTrimEnd(true); 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 1004575976a..45f9780730e 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 @@ -43,7 +43,6 @@ import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never -import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.Shadows.shadowOf @@ -354,7 +353,7 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `Transaction op is ui_load`() { + fun `Transaction op is ui_load and idle+deadline timeouts are set`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -365,11 +364,14 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, fixture.bundle) verify(fixture.hub).startTransaction( - check { + check { assertEquals("ui.load", it.operation) assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource) }, - any() + check { transactionOptions -> + assertEquals(fixture.options.idleTimeout, transactionOptions.idleTimeout) + assertEquals(TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION, transactionOptions.deadlineTimeout) + } ) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt index b9a7eee52c3..f39f70c5521 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt @@ -206,6 +206,24 @@ class SentryGestureListenerTracingTest { ) } + @Test + fun `captures transaction and both idle+deadline timeouts are set`() { + val sut = fixture.getSut() + + sut.onSingleTapUp(fixture.event) + + verify(fixture.hub).startTransaction( + any(), + check { transactionOptions -> + assertEquals(fixture.options.idleTimeout, transactionOptions.idleTimeout) + assertEquals( + TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION, + transactionOptions.deadlineTimeout + ) + } + ) + } + @Test fun `captures transaction with interaction event type as op`() { val sut = fixture.getSut() 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 dae348bef52..8ca52e3c414 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 @@ -132,15 +132,16 @@ class SentryNavigationListener @JvmOverloads constructor( // we add '/' to the name to match dart and web pattern name = "/" + name.substringBefore('/') // strip out arguments from the tx name - val transactonOptions = TransactionOptions().also { + val transactionOptions = TransactionOptions().also { it.isWaitForChildren = true it.idleTimeout = hub.options.idleTimeout + it.deadlineTimeout = TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION it.isTrimEnd = true } val transaction = hub.startTransaction( TransactionContext(name, TransactionNameSource.ROUTE, NAVIGATION_OP), - transactonOptions + transactionOptions ) transaction.spanContext.origin = traceOriginAppendix?.let { 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 0d7441c549d..e1c899d136f 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 @@ -94,7 +94,12 @@ class SentryNavigationListenerTest { whenever(context.resources).thenReturn(resources) whenever(navController.context).thenReturn(context) whenever(destination.route).thenReturn(toRoute) - return SentryNavigationListener(hub, enableBreadcrumbs, enableTracing, traceOriginAppendix) + return SentryNavigationListener( + hub, + enableBreadcrumbs, + enableTracing, + traceOriginAppendix + ) } } @@ -355,7 +360,8 @@ class SentryNavigationListenerTest { fun `starts new trace if performance is disabled`() { val sut = fixture.getSut(enableTracing = false) - val argumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ScopeCallback::class.java) + val argumentCaptor: ArgumentCaptor = + ArgumentCaptor.forClass(ScopeCallback::class.java) val scope = Scope(fixture.options) val propagationContextAtStart = scope.propagationContext whenever(fixture.hub.configureScope(argumentCaptor.capture())).thenAnswer { @@ -385,4 +391,18 @@ class SentryNavigationListenerTest { assertEquals("auto.navigation.jetpack_compose", fixture.transaction.spanContext.origin) } + + @Test + fun `Navigation listener transactions set automatic deadline timeout`() { + val sut = fixture.getSut() + + sut.onDestinationChanged(fixture.navController, fixture.destination, null) + + verify(fixture.hub).startTransaction( + any(), + check { options -> + assertEquals(TransactionOptions.DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION, options.deadlineTimeout) + } + ) + } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 2b83f5fb0d4..c1e55e642ea 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2352,8 +2352,10 @@ public abstract interface class io/sentry/TransactionFinishedCallback { } public final class io/sentry/TransactionOptions : io/sentry/SpanOptions { + public static final field DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION J public fun ()V public fun getCustomSamplingContext ()Lio/sentry/CustomSamplingContext; + public fun getDeadlineTimeout ()Ljava/lang/Long; public fun getIdleTimeout ()Ljava/lang/Long; public fun getStartTimestamp ()Lio/sentry/SentryDate; public fun getTransactionFinishedCallback ()Lio/sentry/TransactionFinishedCallback; @@ -2361,6 +2363,7 @@ public final class io/sentry/TransactionOptions : io/sentry/SpanOptions { public fun isWaitForChildren ()Z public fun setBindToScope (Z)V public fun setCustomSamplingContext (Lio/sentry/CustomSamplingContext;)V + public fun setDeadlineTimeout (Ljava/lang/Long;)V public fun setIdleTimeout (Ljava/lang/Long;)V public fun setStartTimestamp (Lio/sentry/SentryDate;)V public fun setTransactionFinishedCallback (Lio/sentry/TransactionFinishedCallback;)V diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index e3cc1498e50..f3ffb8db450 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -37,10 +37,14 @@ public final class SentryTracer implements ITransaction { */ private @NotNull FinishStatus finishStatus = FinishStatus.NOT_FINISHED; - private volatile @Nullable TimerTask timerTask; + private volatile @Nullable TimerTask idleTimeoutTask; + private volatile @Nullable TimerTask deadlineTimeoutTask; + private volatile @Nullable Timer timer = null; private final @NotNull Object timerLock = new Object(); - private final @NotNull AtomicBoolean isFinishTimerRunning = new AtomicBoolean(false); + + private final @NotNull AtomicBoolean isIdleFinishTimerRunning = new AtomicBoolean(false); + private final @NotNull AtomicBoolean isDeadlineTimerRunning = new AtomicBoolean(false); private final @NotNull Baggage baggage; private @NotNull TransactionNameSource transactionNameSource; @@ -92,8 +96,11 @@ public SentryTracer( transactionPerformanceCollector.start(this); } - if (transactionOptions.getIdleTimeout() != null) { + if (transactionOptions.getIdleTimeout() != null + || transactionOptions.getDeadlineTimeout() != null) { timer = new Timer(true); + + scheduleDeadlineTimeout(); scheduleFinish(); } } @@ -101,34 +108,47 @@ public SentryTracer( @Override public void scheduleFinish() { synchronized (timerLock) { - cancelTimer(); if (timer != null) { - isFinishTimerRunning.set(true); - timerTask = - new TimerTask() { - @Override - public void run() { - finishFromTimer(); - } - }; - - try { - timer.schedule(timerTask, transactionOptions.getIdleTimeout()); - } catch (Throwable e) { - hub.getOptions() - .getLogger() - .log(SentryLevel.WARNING, "Failed to schedule finish timer", e); - // if we failed to schedule the finish timer for some reason, we finish it here right away - finishFromTimer(); + final @Nullable Long idleTimeout = transactionOptions.getIdleTimeout(); + + if (idleTimeout != null) { + cancelIdleTimer(); + isIdleFinishTimerRunning.set(true); + idleTimeoutTask = + new TimerTask() { + @Override + public void run() { + onIdleTimeoutReached(); + } + }; + + try { + timer.schedule(idleTimeoutTask, idleTimeout); + } catch (Throwable e) { + hub.getOptions() + .getLogger() + .log(SentryLevel.WARNING, "Failed to schedule finish timer", e); + // if we failed to schedule the finish timer for some reason, we finish it here right + // away + onIdleTimeoutReached(); + } } } } } - private void finishFromTimer() { - final SpanStatus status = getStatus(); + private void onIdleTimeoutReached() { + final @Nullable SpanStatus status = getStatus(); finish((status != null) ? status : SpanStatus.OK); - isFinishTimerRunning.set(false); + isIdleFinishTimerRunning.set(false); + } + + private void onDeadlineTimeoutReached() { + final @Nullable SpanStatus status = getStatus(); + forceFinish( + (status != null) ? status : SpanStatus.DEADLINE_EXCEEDED, + transactionOptions.getIdleTimeout() != null); + isDeadlineTimerRunning.set(false); } @Override @@ -222,6 +242,8 @@ public void finish( if (timer != null) { synchronized (timerLock) { if (timer != null) { + cancelIdleTimer(); + cancelDeadlineTimer(); timer.cancel(); timer = null; } @@ -244,12 +266,51 @@ public void finish( } } - private void cancelTimer() { + private void cancelIdleTimer() { synchronized (timerLock) { - if (timerTask != null) { - timerTask.cancel(); - isFinishTimerRunning.set(false); - timerTask = null; + if (idleTimeoutTask != null) { + idleTimeoutTask.cancel(); + isIdleFinishTimerRunning.set(false); + idleTimeoutTask = null; + } + } + } + + private void scheduleDeadlineTimeout() { + final @Nullable Long deadlineTimeOut = transactionOptions.getDeadlineTimeout(); + if (deadlineTimeOut != null) { + synchronized (timerLock) { + if (timer != null) { + cancelDeadlineTimer(); + isDeadlineTimerRunning.set(true); + deadlineTimeoutTask = + new TimerTask() { + @Override + public void run() { + onDeadlineTimeoutReached(); + } + }; + try { + timer.schedule(deadlineTimeoutTask, deadlineTimeOut); + } catch (Throwable e) { + hub.getOptions() + .getLogger() + .log(SentryLevel.WARNING, "Failed to schedule finish timer", e); + // if we failed to schedule the finish timer for some reason, we finish it here right + // away + onDeadlineTimeoutReached(); + } + } + } + } + } + + private void cancelDeadlineTimer() { + synchronized (timerLock) { + if (deadlineTimeoutTask != null) { + deadlineTimeoutTask.cancel(); + isDeadlineTimerRunning.set(false); + deadlineTimeoutTask = null; } } } @@ -360,7 +421,7 @@ private ISpan createChild( Objects.requireNonNull(parentSpanId, "parentSpanId is required"); Objects.requireNonNull(operation, "operation is required"); - cancelTimer(); + cancelIdleTimer(); final Span span = new Span( root.getTraceId(), @@ -720,8 +781,14 @@ Span getRoot() { @TestOnly @Nullable - TimerTask getTimerTask() { - return timerTask; + TimerTask getIdleTimeoutTask() { + return idleTimeoutTask; + } + + @TestOnly + @Nullable + TimerTask getDeadlineTimeoutTask() { + return deadlineTimeoutTask; } @TestOnly @@ -733,7 +800,13 @@ Timer getTimer() { @TestOnly @NotNull AtomicBoolean isFinishTimerRunning() { - return isFinishTimerRunning; + return isIdleFinishTimerRunning; + } + + @TestOnly + @NotNull + AtomicBoolean isDeadlineTimerRunning() { + return isDeadlineTimerRunning; } @TestOnly diff --git a/sentry/src/main/java/io/sentry/TransactionOptions.java b/sentry/src/main/java/io/sentry/TransactionOptions.java index 3362d979408..0ae4b94ace3 100644 --- a/sentry/src/main/java/io/sentry/TransactionOptions.java +++ b/sentry/src/main/java/io/sentry/TransactionOptions.java @@ -1,10 +1,13 @@ package io.sentry; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; /** Sentry Transaction options */ public final class TransactionOptions extends SpanOptions { + @ApiStatus.Internal public static final long DEFAULT_DEADLINE_TIMEOUT_AUTO_TRANSACTION = 300000; + /** * Arbitrary data used in {@link SamplingContext} to determine if transaction is going to be * sampled. @@ -34,6 +37,16 @@ public final class TransactionOptions extends SpanOptions { */ private @Nullable Long idleTimeout = null; + /** + * The deadline time, measured in ms, to wait until the transaction will be force-finished with + * deadline-exceeded status./ + * + *

When set to {@code null} the transaction won't be forcefully finished. + * + *

The default is 30 seconds. + */ + private @Nullable Long deadlineTimeout = null; + /** * When `waitForChildren` is set to `true` and this callback is set, it's called before the * transaction is captured. @@ -121,6 +134,28 @@ public void setWaitForChildren(boolean waitForChildren) { return idleTimeout; } + /** + * Sets the deadlineTimeout. If set, an transaction and it's child spans will be force-finished + * with status {@link SpanStatus#DEADLINE_EXCEEDED} in case the transaction isn't finished in + * time. + * + * @param deadlineTimeoutMs - the deadlineTimeout, in ms - or null if no deadline should be set + */ + @ApiStatus.Internal + public void setDeadlineTimeout(@Nullable Long deadlineTimeoutMs) { + this.deadlineTimeout = deadlineTimeoutMs; + } + + /** + * Gets the deadlineTimeout + * + * @return deadlineTimeout - the deadlineTimeout, in ms - or null if no deadline is set + */ + @ApiStatus.Internal + public @Nullable Long getDeadlineTimeout() { + return deadlineTimeout; + } + /** * Sets the idleTimeout * diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index ee698af6c08..d150148c08a 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -43,6 +43,7 @@ class SentryTracerTest { startTimestamp: SentryDate? = null, waitForChildren: Boolean = false, idleTimeout: Long? = null, + deadlineTimeout: Long? = null, trimEnd: Boolean = false, transactionFinishedCallback: TransactionFinishedCallback? = null, samplingDecision: TracesSamplingDecision? = null, @@ -54,6 +55,7 @@ class SentryTracerTest { transactionOptions.startTimestamp = startTimestamp transactionOptions.isWaitForChildren = waitForChildren transactionOptions.idleTimeout = idleTimeout + transactionOptions.deadlineTimeout = deadlineTimeout transactionOptions.isTrimEnd = trimEnd transactionOptions.transactionFinishedCallback = transactionFinishedCallback return SentryTracer(TransactionContext("name", "op", samplingDecision), hub, transactionOptions, performanceCollector) @@ -751,17 +753,73 @@ class SentryTracerTest { } @Test - fun `when initialized without idleTimeout, does not schedule finish timer`() { + fun `when initialized without deadlineTimeout, does not schedule finish timer`() { val transaction = fixture.getSut() + assertNull(transaction.deadlineTimeoutTask) + } + + @Test + fun `when initialized with deadlineTimeout, schedules finish timer`() { + val transaction = fixture.getSut(deadlineTimeout = 50) + + assertTrue(transaction.isDeadlineTimerRunning.get()) + assertNotNull(transaction.deadlineTimeoutTask) + } + + @Test + fun `when deadline is reached transaction is finished`() { + // when a transaction with a deadline timeout is created + // and the tx and child keep on running + val transaction = fixture.getSut(deadlineTimeout = 20) + val span = transaction.startChild("op") + + // and the deadline is exceed + await.untilFalse(transaction.isDeadlineTimerRunning) + + // then both tx + span should be force finished + assertEquals(transaction.isFinished, true) + assertEquals(SpanStatus.DEADLINE_EXCEEDED, transaction.status) + assertEquals(SpanStatus.DEADLINE_EXCEEDED, span.status) + } + + @Test + fun `when transaction is finished before deadline is reached, deadline should not be running anymore`() { + val transaction = fixture.getSut(deadlineTimeout = 1000) + val span = transaction.startChild("op") + + span.finish(SpanStatus.OK) + transaction.finish(SpanStatus.OK) + + assertEquals(transaction.isDeadlineTimerRunning.get(), false) + assertNull(transaction.deadlineTimeoutTask) + assertEquals(transaction.isFinished, true) + assertEquals(SpanStatus.OK, transaction.status) + assertEquals(SpanStatus.OK, span.status) + } + + @Test + fun `when initialized with idleTimeout it has no influence on deadline timeout`() { + val transaction = fixture.getSut(idleTimeout = 3000, deadlineTimeout = 20) + val deadlineTimeoutTask = transaction.deadlineTimeoutTask + + val span = transaction.startChild("op") + // when the span finishes, it re-schedules the idle task + span.finish() + + // but the deadline timeout task should not be re-scheduled + assertEquals(deadlineTimeoutTask, transaction.deadlineTimeoutTask) + } - assertNull(transaction.timerTask) + @Test + fun `when initialized without idleTimeout, does not schedule finish timer`() { + val transaction = fixture.getSut() + assertNull(transaction.idleTimeoutTask) } @Test fun `when initialized with idleTimeout, schedules finish timer`() { val transaction = fixture.getSut(idleTimeout = 50) - - assertNotNull(transaction.timerTask) + assertNotNull(transaction.idleTimeoutTask) } @Test @@ -801,20 +859,20 @@ class SentryTracerTest { transaction.startChild("op") - assertNull(transaction.timerTask) + assertNull(transaction.idleTimeoutTask) } @Test fun `when a child is finished and the transaction is idle, resets the timer`() { val transaction = fixture.getSut(waitForChildren = true, idleTimeout = 3000) - val initialTime = transaction.timerTask!!.scheduledExecutionTime() + val initialTime = transaction.idleTimeoutTask!!.scheduledExecutionTime() val span = transaction.startChild("op") Thread.sleep(1) span.finish() - val timerAfterFinishingChild = transaction.timerTask!!.scheduledExecutionTime() + val timerAfterFinishingChild = transaction.idleTimeoutTask!!.scheduledExecutionTime() assertTrue { timerAfterFinishingChild > initialTime } } @@ -828,7 +886,7 @@ class SentryTracerTest { Thread.sleep(1) span.finish() - assertNull(transaction.timerTask) + assertNull(transaction.idleTimeoutTask) } @Test @@ -1220,7 +1278,7 @@ class SentryTracerTest { @Test fun `when timer is cancelled, schedule finish does not crash`() { - val tracer = fixture.getSut(idleTimeout = 50) + val tracer = fixture.getSut(idleTimeout = 50, deadlineTimeout = 100) tracer.timer!!.cancel() tracer.scheduleFinish() } From ee21d538d271e16bdd8ff1bc2c8e87875154463f Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 8 Aug 2023 12:04:10 +0200 Subject: [PATCH 13/23] Apollo v2 BeforeSpanCallback: Allow returning null (#2890) --- CHANGELOG.md | 1 + .../sentry/apollo/SentryApolloInterceptor.kt | 17 +++++++++++------ .../apollo/SentryApolloInterceptorTest.kt | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d65166145bb..9644f61f7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Breaking changes: - Reduce timeout of AsyncHttpTransport to avoid ANR ([#2879](https://github.com/getsentry/sentry-java/pull/2879)) - Add deadline timeout for automatic transactions ([#2865](https://github.com/getsentry/sentry-java/pull/2865)) - This affects all automatically generated transactions on Android (UI, clicks), the default timeout is 30s +- Apollo v2 BeforeSpanCallback now allows returning null ([#2890](https://github.com/getsentry/sentry-java/pull/2890)) ### Fixes diff --git a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt index bc8e37d83ef..9cdd005a956 100644 --- a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt +++ b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt @@ -142,7 +142,7 @@ class SentryApolloInterceptor( } private fun finish(span: ISpan, request: InterceptorRequest, response: InterceptorResponse? = null) { - var newSpan: ISpan = span + var newSpan: ISpan? = span if (beforeSpan != null) { try { newSpan = beforeSpan.execute(span, request, response) @@ -150,7 +150,12 @@ class SentryApolloInterceptor( hub.options.logger.log(SentryLevel.ERROR, "An error occurred while executing beforeSpan on ApolloInterceptor", e) } } - newSpan.finish() + if (newSpan == null) { + // span is dropped + span.spanContext.sampled = false + } else { + span.finish() + } response?.let { if (it.httpResponse.isPresent) { @@ -166,9 +171,9 @@ class SentryApolloInterceptor( breadcrumb.setData("response_body_size", contentLength) } - val hint = Hint().also { - it.set(APOLLO_REQUEST, httpRequest) - it.set(APOLLO_RESPONSE, httpResponse) + val hint = Hint().apply { + set(APOLLO_REQUEST, httpRequest) + set(APOLLO_RESPONSE, httpResponse) } hub.addBreadcrumb(breadcrumb, hint) } @@ -192,6 +197,6 @@ class SentryApolloInterceptor( * @param request the HTTP request executed by okHttp * @param response the HTTP response received by okHttp */ - fun execute(span: ISpan, request: InterceptorRequest, response: InterceptorResponse?): ISpan + fun execute(span: ISpan, request: InterceptorRequest, response: InterceptorResponse?): ISpan? } } diff --git a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt index 67e19112ca3..a68edab92b7 100644 --- a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt +++ b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt @@ -35,6 +35,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue class SentryApolloInterceptorTest { @@ -177,6 +178,24 @@ class SentryApolloInterceptorTest { ) } + @Test + fun `when beforeSpan callback returns null, span is dropped`() { + executeQuery( + fixture.getSut { _, _, _ -> + null + } + ) + + verify(fixture.hub).captureTransaction( + check { + assertTrue(it.spans.isEmpty()) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + @Test fun `when customizer throws, exception is handled`() { executeQuery( From 55b103cfc55d2377027bcad6d3497ea936394268 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 11 Aug 2023 07:30:49 +0200 Subject: [PATCH 14/23] Start a new automatic transaction on every click (#2891) --- CHANGELOG.md | 2 + .../gestures/SentryGestureListener.java | 79 +++++++++++++------ .../SentryGestureListenerTracingTest.kt | 11 ++- 3 files changed, 63 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9644f61f7a3..cda183b5185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Breaking changes: - Add deadline timeout for automatic transactions ([#2865](https://github.com/getsentry/sentry-java/pull/2865)) - This affects all automatically generated transactions on Android (UI, clicks), the default timeout is 30s - Apollo v2 BeforeSpanCallback now allows returning null ([#2890](https://github.com/getsentry/sentry-java/pull/2890)) +- Automatic user interaction tracking: every click now starts a new automatic transaction ([#2891](https://github.com/getsentry/sentry-java/pull/2891)) + - Previously performing a click on the same UI widget twice would keep the existing transaction running, the new behavior now better aligns with other SDKs ### Fixes 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 2493f5b5f04..32a1ffb06a3 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 @@ -32,6 +32,13 @@ @ApiStatus.Internal public final class SentryGestureListener implements GestureDetector.OnGestureListener { + private enum GestureType { + Click, + Scroll, + Swipe, + Unknown + } + static final String UI_ACTION = "ui.action"; private static final String TRACE_ORIGIN = "auto.ui.gesture_listener"; @@ -41,7 +48,7 @@ public final class SentryGestureListener implements GestureDetector.OnGestureLis private @Nullable UiElement activeUiElement = null; private @Nullable ITransaction activeTransaction = null; - private @Nullable String activeEventType = null; + private @NotNull GestureType activeEventType = GestureType.Unknown; private final ScrollState scrollState = new ScrollState(); @@ -61,7 +68,7 @@ public void onUp(final @NotNull MotionEvent motionEvent) { return; } - if (scrollState.type == null) { + if (scrollState.type == GestureType.Unknown) { options .getLogger() .log(SentryLevel.DEBUG, "Unable to define scroll type. No breadcrumb captured."); @@ -107,8 +114,8 @@ public boolean onSingleTapUp(final @Nullable MotionEvent motionEvent) { return false; } - addBreadcrumb(target, "click", Collections.emptyMap(), motionEvent); - startTracing(target, "click"); + addBreadcrumb(target, GestureType.Click, Collections.emptyMap(), motionEvent); + startTracing(target, GestureType.Click); return false; } @@ -123,7 +130,7 @@ public boolean onScroll( return false; } - if (scrollState.type == null) { + if (scrollState.type == GestureType.Unknown) { final @Nullable UiElement target = ViewUtils.findTarget( options, decorView, firstEvent.getX(), firstEvent.getY(), UiElement.Type.SCROLLABLE); @@ -140,7 +147,7 @@ public boolean onScroll( } scrollState.setTarget(target); - scrollState.type = "scroll"; + scrollState.type = GestureType.Scroll; } return false; } @@ -151,7 +158,7 @@ public boolean onFling( final @Nullable MotionEvent motionEvent1, final float v, final float v1) { - scrollState.type = "swipe"; + scrollState.type = GestureType.Swipe; return false; } @@ -164,7 +171,7 @@ public void onLongPress(MotionEvent motionEvent) {} // region utils private void addBreadcrumb( final @NotNull UiElement target, - final @NotNull String eventType, + final @NotNull GestureType eventType, final @NotNull Map additionalData, final @NotNull MotionEvent motionEvent) { @@ -172,24 +179,29 @@ private void addBreadcrumb( return; } + final String type = getGestureType(eventType); + final Hint hint = new Hint(); hint.set(ANDROID_MOTION_EVENT, motionEvent); hint.set(ANDROID_VIEW, target.getView()); hub.addBreadcrumb( Breadcrumb.userInteraction( - eventType, - target.getResourceName(), - target.getClassName(), - target.getTag(), - additionalData), + type, target.getResourceName(), target.getClassName(), target.getTag(), additionalData), hint); } - private void startTracing(final @NotNull UiElement target, final @NotNull String eventType) { - final UiElement uiElement = activeUiElement; + private void startTracing(final @NotNull UiElement target, final @NotNull GestureType eventType) { + + final boolean isNewGestureSameAsActive = + (eventType == activeEventType && target.equals(activeUiElement)); + final boolean isClickGesture = eventType == GestureType.Click; + // we always want to start new transaction/traces for clicks, for swipe/scroll only if the + // target changed + final boolean isNewInteraction = isClickGesture || !isNewGestureSameAsActive; + if (!(options.isTracingEnabled() && options.isEnableUserInteractionTracing())) { - if (!(target.equals(uiElement) && eventType.equals(activeEventType))) { + if (isNewInteraction) { TracingUtils.startNewTrace(hub); activeUiElement = target; activeEventType = eventType; @@ -206,9 +218,7 @@ private void startTracing(final @NotNull UiElement target, final @NotNull String final @Nullable String viewIdentifier = target.getIdentifier(); if (activeTransaction != null) { - if (target.equals(uiElement) - && eventType.equals(activeEventType) - && !activeTransaction.isFinished()) { + if (!isNewInteraction && !activeTransaction.isFinished()) { options .getLogger() .log( @@ -233,7 +243,7 @@ private void startTracing(final @NotNull UiElement target, final @NotNull String // we can only bind to the scope if there's no running transaction final String name = getActivityName(activity) + "." + viewIdentifier; - final String op = UI_ACTION + "." + eventType; + final String op = UI_ACTION + "." + getGestureType(eventType); final TransactionOptions transactionOptions = new TransactionOptions(); transactionOptions.setWaitForChildren(true); @@ -270,13 +280,15 @@ void stopTracing(final @NotNull SpanStatus status) { } hub.configureScope( scope -> { + // avoid method refs on Android due to some issues with older AGP setups + // noinspection Convert2MethodRef clearScope(scope); }); activeTransaction = null; if (activeUiElement != null) { activeUiElement = null; } - activeEventType = null; + activeEventType = GestureType.Unknown; } @VisibleForTesting @@ -337,11 +349,32 @@ void applyScope(final @NotNull Scope scope, final @NotNull ITransaction transact } return decorView; } + + @NotNull + private static String getGestureType(final @NotNull GestureType eventType) { + final @NotNull String type; + switch (eventType) { + case Click: + type = "click"; + break; + case Scroll: + type = "scroll"; + break; + case Swipe: + type = "swipe"; + break; + default: + case Unknown: + type = "unknown"; + break; + } + return type; + } // endregion // region scroll logic private static final class ScrollState { - private @Nullable String type = null; + private @NotNull GestureType type = GestureType.Unknown; private @Nullable UiElement target; private float startX = 0f; private float startY = 0f; @@ -378,7 +411,7 @@ private void setTarget(final @NotNull UiElement target) { private void reset() { target = null; - type = null; + type = GestureType.Unknown; startX = 0f; startY = 0f; } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt index f39f70c5521..308632c6ed6 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerTracingTest.kt @@ -28,6 +28,7 @@ import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import kotlin.test.Test @@ -333,20 +334,18 @@ class SentryGestureListenerTracingTest { SpanContext(SentryId.EMPTY_ID, SpanId.EMPTY_ID, "op", null, null) ) + // when the same button is clicked twice + sut.onSingleTapUp(fixture.event) sut.onSingleTapUp(fixture.event) - verify(fixture.hub).startTransaction( + // then two transaction should be captured + verify(fixture.hub, times(2)).startTransaction( check { assertEquals("Activity.test_button", it.name) assertEquals(TransactionNameSource.COMPONENT, it.transactionNameSource) }, any() ) - - // second view interaction - sut.onSingleTapUp(fixture.event) - - verify(fixture.transaction).scheduleFinish() } @Test From b0e93e82b9263f902834df12af772c61f296f167 Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 16 Aug 2023 13:38:41 +0200 Subject: [PATCH 15/23] Bump min API to 19 (#2883) * updated minimum Android SDK version to 19 * removed few workarounds for version < 19 --- CHANGELOG.md | 1 + buildSrc/src/main/java/Config.kt | 4 +- .../core/ActivityLifecycleIntegration.java | 5 +- .../core/AndroidOptionsInitializer.java | 10 +-- .../android/core/AnrV2EventProcessor.java | 9 +- .../io/sentry/android/core/ContextUtils.java | 48 +++++------ .../sentry/android/core/DeviceInfoUtil.java | 85 +++---------------- .../core/SentryPerformanceProvider.java | 4 +- .../internal/util/FirstDrawDoneListener.java | 22 +---- .../core/internal/util/ScreenshotUtils.java | 16 ++-- .../core/ActivityLifecycleIntegrationTest.kt | 13 +-- .../core/AndroidOptionsInitializerTest.kt | 13 +-- .../android/core/AnrV2EventProcessorTest.kt | 3 +- .../android/core/ConnectivityCheckerTest.kt | 8 +- .../core/DefaultAndroidEventProcessorTest.kt | 12 --- .../core/SentryPerformanceProviderTest.kt | 3 +- .../util/FirstDrawDoneListenerTest.kt | 5 +- .../sqlite/SentrySupportSQLiteDatabase.kt | 3 - 18 files changed, 68 insertions(+), 196 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cda183b5185..0e98675a09b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Breaking changes: Breaking changes: - Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) - Fix Coroutine Context Propagation using CopyableThreadContextElement, requires `kotlinx-coroutines-core` version `1.6.1` or higher ([#2838](https://github.com/getsentry/sentry-java/pull/2838)) +- Bump min API to 19 ([#2883](https://github.com/getsentry/sentry-java/pull/2883)) ## Unreleased diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 2674bc2350e..d095120e24c 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -31,9 +31,9 @@ object Config { object Android { private val sdkVersion = 33 - val minSdkVersion = 14 + val minSdkVersion = 19 val minSdkVersionOkHttp = 21 - val minSdkVersionNdk = 16 + val minSdkVersionNdk = 19 val minSdkVersionCompose = 21 val targetSdkVersion = sdkVersion val compileSdkVersion = sdkVersion 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 4cf44e7f493..0d44e08567b 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 @@ -3,7 +3,6 @@ import static io.sentry.MeasurementUnit.Duration.MILLISECOND; import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY; -import android.annotation.SuppressLint; import android.app.Activity; import android.app.Application; import android.os.Build; @@ -399,15 +398,13 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) { addBreadcrumb(activity, "started"); } - @SuppressLint("NewApi") @Override public synchronized void onActivityResumed(final @NotNull Activity activity) { if (performanceEnabled) { final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity); final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity); final View rootView = activity.findViewById(android.R.id.content); - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN - && rootView != null) { + if (rootView != null) { FirstDrawDoneListener.registerForNextDraw( rootView, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider); } else { 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 6cc4c2a6d83..60c3160fe8b 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 @@ -5,7 +5,6 @@ import android.app.Application; import android.content.Context; import android.content.pm.PackageInfo; -import android.os.Build; import io.sentry.DeduplicateMultithreadedEventProcessor; import io.sentry.DefaultTransactionPerformanceCollector; import io.sentry.ILogger; @@ -213,10 +212,7 @@ static void installDefaultIntegrations( // Integrations are registered in the same order. NDK before adding Watch outbox, // because sentry-native move files around and we don't want to watch that. - final Class sentryNdkClass = - isNdkAvailable(buildInfoProvider) - ? loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options.getLogger()) - : null; + final Class sentryNdkClass = loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options.getLogger()); options.addIntegration(new NdkIntegration(sentryNdkClass)); // this integration uses android.os.FileObserver, we can't move to sentry @@ -325,8 +321,4 @@ private static void initializeCacheDirs( final File cacheDir = new File(context.getCacheDir(), "sentry"); options.setCacheDirPath(cacheDir.getAbsolutePath()); } - - private static boolean isNdkAvailable(final @NotNull BuildInfoProvider buildInfoProvider) { - return buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN; - } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index c060f475af5..54a500825c1 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -527,7 +527,7 @@ private void setDevice(final @NotNull SentryBaseEvent event) { private @NotNull Device getDevice() { Device device = new Device(); if (options.isSendDefaultPii()) { - device.setName(ContextUtils.getDeviceName(context, buildInfoProvider)); + device.setName(ContextUtils.getDeviceName(context)); } device.setManufacturer(Build.MANUFACTURER); device.setBrand(Build.BRAND); @@ -566,13 +566,8 @@ private void setDevice(final @NotNull SentryBaseEvent event) { return device; } - @SuppressLint("NewApi") private @NotNull Long getMemorySize(final @NotNull ActivityManager.MemoryInfo memInfo) { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN) { - return memInfo.totalMem; - } - // using Runtime as a fallback - return java.lang.Runtime.getRuntime().totalMemory(); // JVM in bytes too + return memInfo.totalMem; } private void mergeOS(final @NotNull SentryBaseEvent event) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java index 6a0457e50ef..2ce5ac4fb07 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java @@ -301,14 +301,8 @@ static boolean isForegroundImportance(final @NotNull Context context) { } } - @SuppressLint("NewApi") // we're wrapping into if-check with sdk version - static @Nullable String getDeviceName( - final @NotNull Context context, final @NotNull BuildInfoProvider buildInfoProvider) { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - return Settings.Global.getString(context.getContentResolver(), "device_name"); - } else { - return null; - } + static @Nullable String getDeviceName(final @NotNull Context context) { + return Settings.Global.getString(context.getContentResolver(), "device_name"); } @SuppressWarnings("deprecation") @@ -346,8 +340,6 @@ static boolean isForegroundImportance(final @NotNull Context context) { } } - // we perform an if-check for that, but lint fails to recognize - @SuppressLint("NewApi") static void setAppPackageInfo( final @NotNull PackageInfo packageInfo, final @NotNull BuildInfoProvider buildInfoProvider, @@ -356,26 +348,24 @@ static void setAppPackageInfo( app.setAppVersion(packageInfo.versionName); app.setAppBuild(ContextUtils.getVersionCode(packageInfo, buildInfoProvider)); - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN) { - final Map permissions = new HashMap<>(); - final String[] requestedPermissions = packageInfo.requestedPermissions; - final int[] requestedPermissionsFlags = packageInfo.requestedPermissionsFlags; - - if (requestedPermissions != null - && requestedPermissions.length > 0 - && requestedPermissionsFlags != null - && requestedPermissionsFlags.length > 0) { - for (int i = 0; i < requestedPermissions.length; i++) { - String permission = requestedPermissions[i]; - permission = permission.substring(permission.lastIndexOf('.') + 1); - - final boolean granted = - (requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) - == REQUESTED_PERMISSION_GRANTED; - permissions.put(permission, granted ? "granted" : "not_granted"); - } + final Map permissions = new HashMap<>(); + final String[] requestedPermissions = packageInfo.requestedPermissions; + final int[] requestedPermissionsFlags = packageInfo.requestedPermissionsFlags; + + if (requestedPermissions != null + && requestedPermissions.length > 0 + && requestedPermissionsFlags != null + && requestedPermissionsFlags.length > 0) { + for (int i = 0; i < requestedPermissions.length; i++) { + String permission = requestedPermissions[i]; + permission = permission.substring(permission.lastIndexOf('.') + 1); + + final boolean granted = + (requestedPermissionsFlags[i] & REQUESTED_PERMISSION_GRANTED) + == REQUESTED_PERMISSION_GRANTED; + permissions.put(permission, granted ? "granted" : "not_granted"); } - app.setPermissions(permissions); } + app.setPermissions(permissions); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java index d74a8b1e2d8..f9fcbf972d2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java @@ -88,7 +88,7 @@ public Device collectDeviceInformation( final @NotNull Device device = new Device(); if (options.isSendDefaultPii()) { - device.setName(ContextUtils.getDeviceName(context, buildInfoProvider)); + device.setName(ContextUtils.getDeviceName(context)); } device.setManufacturer(Build.MANUFACTURER); device.setBrand(Build.BRAND); @@ -196,7 +196,7 @@ private void setDeviceIO(final @NotNull Device device, final boolean includeDyna ContextUtils.getMemInfo(context, options.getLogger()); if (memInfo != null) { // in bytes - device.setMemorySize(getMemorySize(memInfo)); + device.setMemorySize(memInfo.totalMem); if (includeDynamicData) { device.setFreeMemory(memInfo.availMem); device.setLowMemory(memInfo.lowMemory); @@ -338,16 +338,6 @@ private Device.DeviceOrientation getOrientation() { return deviceOrientation; } - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) - @NotNull - private Long getMemorySize(final @NotNull ActivityManager.MemoryInfo memInfo) { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN) { - return memInfo.totalMem; - } - // using Runtime as a fallback - return java.lang.Runtime.getRuntime().totalMemory(); // JVM in bytes too - } - /** * Get the total amount of internal storage, in bytes. * @@ -356,8 +346,8 @@ private Long getMemorySize(final @NotNull ActivityManager.MemoryInfo memInfo) { @Nullable private Long getTotalInternalStorage(final @NotNull StatFs stat) { try { - long blockSize = getBlockSizeLong(stat); - long totalBlocks = getBlockCountLong(stat); + long blockSize = stat.getBlockSizeLong(); + long totalBlocks = stat.getBlockCountLong(); return totalBlocks * blockSize; } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Error getting total internal storage amount.", e); @@ -365,45 +355,6 @@ private Long getTotalInternalStorage(final @NotNull StatFs stat) { } } - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) - private long getBlockSizeLong(final @NotNull StatFs stat) { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - return stat.getBlockSizeLong(); - } - return getBlockSizeDep(stat); - } - - @SuppressWarnings({"deprecation"}) - private int getBlockSizeDep(final @NotNull StatFs stat) { - return stat.getBlockSize(); - } - - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) - private long getBlockCountLong(final @NotNull StatFs stat) { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - return stat.getBlockCountLong(); - } - return getBlockCountDep(stat); - } - - @SuppressWarnings({"deprecation"}) - private int getBlockCountDep(final @NotNull StatFs stat) { - return stat.getBlockCount(); - } - - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) - private long getAvailableBlocksLong(final @NotNull StatFs stat) { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - return stat.getAvailableBlocksLong(); - } - return getAvailableBlocksDep(stat); - } - - @SuppressWarnings({"deprecation"}) - private int getAvailableBlocksDep(final @NotNull StatFs stat) { - return stat.getAvailableBlocks(); - } - /** * Get the unused amount of internal storage, in bytes. * @@ -412,8 +363,8 @@ private int getAvailableBlocksDep(final @NotNull StatFs stat) { @Nullable private Long getUnusedInternalStorage(final @NotNull StatFs stat) { try { - long blockSize = getBlockSizeLong(stat); - long availableBlocks = getAvailableBlocksLong(stat); + long blockSize = stat.getBlockSizeLong(); + long availableBlocks = stat.getAvailableBlocksLong(); return availableBlocks * blockSize; } catch (Throwable e) { options @@ -437,23 +388,9 @@ private StatFs getExternalStorageStat(final @Nullable File internalStorage) { return null; } - @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) - @Nullable - private File[] getExternalFilesDirs() { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.KITKAT) { - return context.getExternalFilesDirs(null); - } else { - File single = context.getExternalFilesDir(null); - if (single != null) { - return new File[] {single}; - } - } - return null; - } - @Nullable private File getExternalStorageDep(final @Nullable File internalStorage) { - final @Nullable File[] externalFilesDirs = getExternalFilesDirs(); + final @Nullable File[] externalFilesDirs = context.getExternalFilesDirs(null); if (externalFilesDirs != null) { // return the 1st file which is not the emulated internal storage @@ -490,8 +427,8 @@ private File getExternalStorageDep(final @Nullable File internalStorage) { @Nullable private Long getTotalExternalStorage(final @NotNull StatFs stat) { try { - final long blockSize = getBlockSizeLong(stat); - final long totalBlocks = getBlockCountLong(stat); + final long blockSize = stat.getBlockSizeLong(); + final long totalBlocks = stat.getBlockCountLong(); return totalBlocks * blockSize; } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Error getting total external storage amount.", e); @@ -515,8 +452,8 @@ private boolean isExternalStorageMounted() { @Nullable private Long getUnusedExternalStorage(final @NotNull StatFs stat) { try { - final long blockSize = getBlockSizeLong(stat); - final long availableBlocks = getAvailableBlocksLong(stat); + final long blockSize = stat.getBlockSizeLong(); + final long availableBlocks = stat.getAvailableBlocksLong(); return availableBlocks * blockSize; } catch (Throwable e) { options diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index b0f66446dec..91992b3c5e6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -6,7 +6,6 @@ import android.content.Context; import android.content.pm.ProviderInfo; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.SystemClock; import android.view.View; @@ -126,8 +125,7 @@ public void onActivityResumed(@NotNull Activity activity) { // sets App start as finished when the very first activity calls onResume firstActivityResumed = true; final View rootView = activity.findViewById(android.R.id.content); - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN - && rootView != null) { + if (rootView != null) { FirstDrawDoneListener.registerForNextDraw( rootView, () -> AppStartState.getInstance().setAppStartEnd(), buildInfoProvider); } else { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java index 10c160377b2..11978c7bec5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java @@ -1,12 +1,10 @@ package io.sentry.android.core.internal.util; -import android.annotation.SuppressLint; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.View; import android.view.ViewTreeObserver; -import androidx.annotation.RequiresApi; import io.sentry.android.core.BuildInfoProvider; import java.util.concurrent.atomic.AtomicReference; import org.jetbrains.annotations.NotNull; @@ -19,8 +17,6 @@ * href="https://github.com/firebase/firebase-android-sdk/blob/master/firebase-perf/src/main/java/com/google/firebase/perf/util/FirstDrawDoneListener.java">Firebase * under the Apache License, Version 2.0. */ -@SuppressLint("ObsoleteSdkInt") -@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) public class FirstDrawDoneListener implements ViewTreeObserver.OnDrawListener { private final @NotNull Handler mainThreadHandler = new Handler(Looper.getMainLooper()); private final @NotNull AtomicReference viewReference; @@ -35,8 +31,8 @@ public static void registerForNextDraw( // Handle bug prior to API 26 where OnDrawListener from the floating ViewTreeObserver is not // merged into the real ViewTreeObserver. // https://android.googlesource.com/platform/frameworks/base/+/9f8ec54244a5e0343b9748db3329733f259604f3 - if (buildInfoProvider.getSdkInfoVersion() < 26 - && !isAliveAndAttached(view, buildInfoProvider)) { + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.O + && !isAliveAndAttached(view)) { view.addOnAttachStateChangeListener( new View.OnAttachStateChangeListener() { @Override @@ -83,17 +79,7 @@ public void onDraw() { * @return true if the View is already attached and the ViewTreeObserver is not a floating * placeholder. */ - private static boolean isAliveAndAttached( - final @NotNull View view, final @NotNull BuildInfoProvider buildInfoProvider) { - return view.getViewTreeObserver().isAlive() && isAttachedToWindow(view, buildInfoProvider); - } - - @SuppressLint("NewApi") - private static boolean isAttachedToWindow( - final @NotNull View view, final @NotNull BuildInfoProvider buildInfoProvider) { - if (buildInfoProvider.getSdkInfoVersion() >= 19) { - return view.isAttachedToWindow(); - } - return view.getWindowToken() != null; + private static boolean isAliveAndAttached(final @NotNull View view) { + return view.getViewTreeObserver().isAlive() && view.isAttachedToWindow(); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java index 88f3e126dee..35cfaa002f2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java @@ -1,10 +1,8 @@ package io.sentry.android.core.internal.util; -import android.annotation.SuppressLint; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.os.Build; import android.view.View; import androidx.annotation.Nullable; import io.sentry.ILogger; @@ -35,8 +33,10 @@ public class ScreenshotUtils { final @NotNull IMainThreadChecker mainThreadChecker, final @NotNull ILogger logger, final @NotNull BuildInfoProvider buildInfoProvider) { + // We are keeping BuildInfoProvider param for compatibility, as it's being used by + // cross-platform SDKs - if (!isActivityValid(activity, buildInfoProvider) + if (!isActivityValid(activity) || activity.getWindow() == null || activity.getWindow().getDecorView() == null || activity.getWindow().getDecorView().getRootView() == null) { @@ -91,13 +91,7 @@ public class ScreenshotUtils { return null; } - @SuppressLint("NewApi") - private static boolean isActivityValid( - final @NotNull Activity activity, final @NotNull BuildInfoProvider buildInfoProvider) { - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - return !activity.isFinishing() && !activity.isDestroyed(); - } else { - return !activity.isFinishing(); - } + private static boolean isActivityValid(final @NotNull Activity activity) { + return !activity.isFinishing() && !activity.isDestroyed(); } } 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 45f9780730e..3ab3633f93e 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 @@ -4,6 +4,7 @@ import android.app.Activity import android.app.ActivityManager import android.app.ActivityManager.RunningAppProcessInfo import android.app.Application +import android.os.Build import android.os.Bundle import android.os.Looper import android.view.View @@ -83,7 +84,7 @@ class ActivityLifecycleIntegrationTest { val buildInfo = mock() fun getSut( - apiVersion: Int = 29, + apiVersion: Int = Build.VERSION_CODES.Q, importance: Int = RunningAppProcessInfo.IMPORTANCE_FOREGROUND, initializer: Sentry.OptionsConfiguration? = null ): ActivityLifecycleIntegration { @@ -718,7 +719,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `do not stop transaction on resumed if API less than 29 and ttid and ttfd are finished`() { - val sut = fixture.getSut(14) + val sut = fixture.getSut(Build.VERSION_CODES.P) fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true sut.register(fixture.hub, fixture.options) @@ -734,7 +735,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `start transaction on created if API less than 29`() { - val sut = fixture.getSut(14) + val sut = fixture.getSut(Build.VERSION_CODES.P) fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -782,7 +783,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `App start is Cold when savedInstanceState is null`() { - val sut = fixture.getSut(14) + val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -794,7 +795,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `App start is Warm when savedInstanceState is not null`() { - val sut = fixture.getSut(14) + val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -807,7 +808,7 @@ class ActivityLifecycleIntegrationTest { @Test fun `Do not overwrite App start type after set`() { - val sut = fixture.getSut(14) + val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 2632b2921fd..1b147b106ae 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -2,6 +2,7 @@ package io.sentry.android.core import android.content.Context import android.content.res.AssetManager +import android.os.Build import android.os.Bundle import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -95,7 +96,7 @@ class AndroidOptionsInitializerTest { } fun initSutWithClassLoader( - minApi: Int = 16, + minApi: Int = Build.VERSION_CODES.KITKAT, classesToLoad: List = emptyList(), isFragmentAvailable: Boolean = false, isTimberAvailable: Boolean = false @@ -137,7 +138,7 @@ class AndroidOptionsInitializerTest { ) } - private fun createBuildInfo(minApi: Int = 16): BuildInfoProvider { + private fun createBuildInfo(minApi: Int): BuildInfoProvider { val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(minApi) return buildInfo @@ -349,14 +350,6 @@ class AndroidOptionsInitializerTest { assertNotNull((actual as NdkIntegration).sentryNdkClass) } - @Test - fun `NdkIntegration won't be enabled because API is lower than 16`() { - fixture.initSutWithClassLoader(minApi = 14, classesToLoad = listOfNotNull(NdkIntegration.SENTRY_NDK_CLASS_NAME)) - - val actual = fixture.sentryOptions.integrations.firstOrNull { it is NdkIntegration } - assertNull((actual as NdkIntegration).sentryNdkClass) - } - @Test fun `NdkIntegration won't be enabled, if class not found`() { fixture.initSutWithClassLoader(classesToLoad = emptyList()) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt index d6a05ddd2c9..2d331a75633 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt @@ -3,6 +3,7 @@ package io.sentry.android.core import android.app.ActivityManager import android.app.ActivityManager.MemoryInfo import android.content.Context +import android.os.Build import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb @@ -84,7 +85,7 @@ class AnrV2EventProcessorTest { fun getSut( dir: TemporaryFolder, - currentSdk: Int = 21, + currentSdk: Int = Build.VERSION_CODES.LOLLIPOP, populateScopeCache: Boolean = false, populateOptionsCache: Boolean = false ): AnrV2EventProcessor { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt index ca2698ccd55..9542adce1c2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt @@ -109,7 +109,7 @@ class ConnectivityCheckerTest { @Test fun `When sdkInfoVersion is not min Marshmallow, return null for getConnectionType`() { val buildInfo = mock() - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) assertNull(ConnectivityChecker.getConnectionType(mock(), mock(), buildInfo)) } @@ -142,7 +142,7 @@ class ConnectivityCheckerTest { @Test fun `When network is TYPE_WIFI, return wifi`() { - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) whenever(networkInfo.type).thenReturn(TYPE_WIFI) assertEquals("wifi", ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) @@ -160,7 +160,7 @@ class ConnectivityCheckerTest { @Test fun `When network is TYPE_ETHERNET, return ethernet`() { - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) whenever(networkInfo.type).thenReturn(TYPE_ETHERNET) assertEquals( @@ -181,7 +181,7 @@ class ConnectivityCheckerTest { @Test fun `When network is TYPE_MOBILE, return cellular`() { - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) whenever(networkInfo.type).thenReturn(TYPE_MOBILE) assertEquals( diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt index bad710a4520..70b20e2ab2a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt @@ -133,18 +133,6 @@ class DefaultAndroidEventProcessorTest { } } - @Test - fun `when Android version is below JELLY_BEAN, does not add permissions`() { - whenever(fixture.buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - val sut = fixture.getSut(context) - - assertNotNull(sut.process(SentryEvent(), Hint())) { - // assert adds permissions - val unknown = it.contexts.app!!.permissions - assertNull(unknown) - } - } - @Test fun `When Transaction and hint is not Cached, data should be applied`() { val sut = fixture.getSut(context) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index f913ca92684..aa9d9cc26bf 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -3,6 +3,7 @@ package io.sentry.android.core import android.app.Activity import android.app.Application import android.content.pm.ProviderInfo +import android.os.Build import android.os.Bundle import android.os.Looper import android.view.View @@ -93,7 +94,7 @@ class SentryPerformanceProviderTest { val provider = SentryPerformanceProvider( mock { - whenever(mock.sdkInfoVersion).thenReturn(29) + whenever(mock.sdkInfoVersion).thenReturn(Build.VERSION_CODES.Q) }, MainLooperHandler() ) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt index 07fc383e1de..a7b4cc3f8c8 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt @@ -1,6 +1,7 @@ package io.sentry.android.core.internal.util import android.content.Context +import android.os.Build import android.os.Handler import android.os.Looper import android.view.View @@ -31,7 +32,7 @@ class FirstDrawDoneListenerTest { val buildInfo = mock() lateinit var onDrawListeners: ArrayList - fun getSut(apiVersion: Int = 26): View { + fun getSut(apiVersion: Int = Build.VERSION_CODES.O): View { whenever(buildInfo.sdkInfoVersion).thenReturn(apiVersion) val view = View(application) @@ -52,7 +53,7 @@ class FirstDrawDoneListenerTest { @Test fun `registerForNextDraw adds listener on attach state changed on sdk 25-`() { - val view = fixture.getSut(25) + val view = fixture.getSut(Build.VERSION_CODES.N_MR1) // OnDrawListener is not registered, it is delayed for later FirstDrawDoneListener.registerForNextDraw(view, {}, fixture.buildInfo) diff --git a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabase.kt b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabase.kt index 4c944fb07da..ac48c1a504e 100644 --- a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabase.kt +++ b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentrySupportSQLiteDatabase.kt @@ -3,9 +3,7 @@ package io.sentry.android.sqlite import android.annotation.SuppressLint import android.database.Cursor import android.database.SQLException -import android.os.Build import android.os.CancellationSignal -import androidx.annotation.RequiresApi import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteStatement @@ -62,7 +60,6 @@ internal class SentrySupportSQLiteDatabase( } } - @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) override fun query( query: SupportSQLiteQuery, cancellationSignal: CancellationSignal? From bdf13798925e78d4ed8abd6c16669641a3d21361 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 28 Aug 2023 13:22:47 +0200 Subject: [PATCH 16/23] Fix don't overwrite the span status of unfinished spans (#2859) --- CHANGELOG.md | 2 ++ .../src/main/java/io/sentry/SentryTracer.java | 13 +++---- .../java/io/sentry/protocol/SentrySpan.java | 6 ++-- .../test/java/io/sentry/SentryTracerTest.kt | 19 +++++++--- .../java/io/sentry/protocol/SentrySpanTest.kt | 35 +++++++++++++++++++ 5 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 sentry/src/test/java/io/sentry/protocol/SentrySpanTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e98675a09b..1cbd0b89ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Breaking changes: - Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) - Fix Coroutine Context Propagation using CopyableThreadContextElement, requires `kotlinx-coroutines-core` version `1.6.1` or higher ([#2838](https://github.com/getsentry/sentry-java/pull/2838)) - Bump min API to 19 ([#2883](https://github.com/getsentry/sentry-java/pull/2883)) +- Fix don't overwrite the span status of unfinished spans ([#2859](https://github.com/getsentry/sentry-java/pull/2859)) + - If you're using a self hosted version of sentry, sentry self hosted >= 22.12.0 is required ## Unreleased diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index f3ffb8db450..2593ea64a4b 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -213,14 +213,11 @@ public void finish( performanceCollectionData.clear(); } - // finish unfinished children - for (final Span child : children) { - if (!child.isFinished()) { - child.setSpanFinishedCallback( - null); // reset the callback, as we're already in the finish method - child.finish(SpanStatus.DEADLINE_EXCEEDED, finishTimestamp); - } - } + // any un-finished childs will remain unfinished + // as relay takes care of setting the end-timestamp + deadline_exceeded + // see + // https://github.com/getsentry/relay/blob/40697d0a1c54e5e7ad8d183fc7f9543b94fe3839/relay-general/src/store/transactions/processor.rs#L374-L378 + root.finish(finishStatus.spanStatus, finishTimestamp); hub.configureScope( diff --git a/sentry/src/main/java/io/sentry/protocol/SentrySpan.java b/sentry/src/main/java/io/sentry/protocol/SentrySpan.java index 70b1b635cc3..b627aa4c9f5 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentrySpan.java +++ b/sentry/src/main/java/io/sentry/protocol/SentrySpan.java @@ -61,8 +61,10 @@ public SentrySpan(final @NotNull Span span, final @Nullable Map this.tags = tagsCopy != null ? tagsCopy : new ConcurrentHashMap<>(); // we lose precision here, from potential nanosecond precision down to 10 microsecond precision this.timestamp = - DateUtils.nanosToSeconds( - span.getStartDate().laterDateNanosTimestampByDiff(span.getFinishDate())); + span.getFinishDate() == null + ? null + : DateUtils.nanosToSeconds( + span.getStartDate().laterDateNanosTimestampByDiff(span.getFinishDate())); // we lose precision here, from potential nanosecond precision down to 10 microsecond precision this.startTimestamp = DateUtils.nanosToSeconds(span.getStartDate().nanoTimestamp()); this.data = data; diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index d150148c08a..ad276ad1ff0 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -538,18 +538,27 @@ class SentryTracerTest { } @Test - fun `finishing unfinished spans with the transaction timestamp`() { + fun `when finishing, unfinished spans won't have any automatic end-date or status`() { val transaction = fixture.getSut(samplingDecision = TracesSamplingDecision(true)) + // span with no status set val span = transaction.startChild("op") as Span - transaction.startChild("op2") + span.finish(SpanStatus.INVALID_ARGUMENT) + + // span with a status + val span1 = transaction.startChild("op2") + transaction.finish(SpanStatus.INVALID_ARGUMENT) verify(fixture.hub, times(1)).captureTransaction( check { assertEquals(2, it.spans.size) - assertEquals(transaction.root.finishDate, span.finishDate) - assertEquals(SpanStatus.DEADLINE_EXCEEDED, it.spans[0].status) - assertEquals(SpanStatus.DEADLINE_EXCEEDED, it.spans[1].status) + // span status/timestamp is retained + assertNotNull(it.spans[0].status) + assertNotNull(it.spans[0].timestamp) + + // span status/timestamp remains untouched + assertNull(it.spans[1].status) + assertNull(it.spans[1].timestamp) }, anyOrNull(), anyOrNull(), diff --git a/sentry/src/test/java/io/sentry/protocol/SentrySpanTest.kt b/sentry/src/test/java/io/sentry/protocol/SentrySpanTest.kt new file mode 100644 index 00000000000..27499be0a0c --- /dev/null +++ b/sentry/src/test/java/io/sentry/protocol/SentrySpanTest.kt @@ -0,0 +1,35 @@ +package io.sentry.protocol + +import io.sentry.IHub +import io.sentry.SentryLongDate +import io.sentry.SentryTracer +import io.sentry.Span +import io.sentry.SpanOptions +import io.sentry.TransactionContext +import org.junit.Test +import org.mockito.kotlin.mock +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class SentrySpanTest { + + @Test + fun `end timestamps is kept null if not provided`() { + // when a span with a start timestamp is generated + val span = Span( + TransactionContext("name", "op"), + mock(), + mock(), + SentryLongDate(1000000), + SpanOptions() + ) + + val sentrySpan = SentrySpan(span) + + // then the start timestamp should be correctly set + assertEquals(0.001, sentrySpan.startTimestamp) + + // but the end time should remain untouched + assertNull(sentrySpan.timestamp) + } +} From cd268a300fe308a8fe4390920cc751f5737f058e Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 31 Aug 2023 14:15:00 +0200 Subject: [PATCH 17/23] (Android) Use root transaction instead of last active span (#2855) --- CHANGELOG.md | 1 + .../android/okhttp/SentryOkHttpEvent.kt | 4 +- .../android/okhttp/SentryOkHttpInterceptor.kt | 4 +- .../apollo3/SentryApollo3HttpInterceptor.kt | 3 +- .../apollo3/SentryApollo3InterceptorTest.kt | 22 +++++++ .../util/Apollo3PlatformTestManipulator.kt | 8 +++ .../sentry/apollo/SentryApolloInterceptor.kt | 2 +- .../apollo/SentryApolloInterceptorTest.kt | 22 +++++++ .../util/ApolloPlatformTestManipulator.kt | 8 +++ sentry/api/sentry.api | 4 ++ sentry/src/main/java/io/sentry/Hub.java | 16 +++++ .../src/main/java/io/sentry/HubAdapter.java | 6 ++ sentry/src/main/java/io/sentry/IHub.java | 9 +++ sentry/src/main/java/io/sentry/NoOpHub.java | 5 ++ sentry/src/main/java/io/sentry/Sentry.java | 11 +++- .../file/FileIOSpanManager.java | 2 +- .../main/java/io/sentry/util/Platform.java | 2 +- .../src/test/java/io/sentry/HubAdapterTest.kt | 5 ++ sentry/src/test/java/io/sentry/HubTest.kt | 27 +++++++-- sentry/src/test/java/io/sentry/SentryTest.kt | 60 +++++++++++++++++++ .../file/FileIOSpanManagerTest.kt | 44 ++++++++++++++ .../io/sentry/util/PlatformTestManipulator.kt | 4 ++ 22 files changed, 257 insertions(+), 12 deletions(-) create mode 100644 sentry-apollo-3/src/test/java/io/sentry/util/Apollo3PlatformTestManipulator.kt create mode 100644 sentry-apollo/src/test/java/io/sentry/util/ApolloPlatformTestManipulator.kt create mode 100644 sentry/src/test/java/io/sentry/instrumentation/file/FileIOSpanManagerTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cbd0b89ca4..638653a5792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Breaking changes: - Apollo v2 BeforeSpanCallback now allows returning null ([#2890](https://github.com/getsentry/sentry-java/pull/2890)) - Automatic user interaction tracking: every click now starts a new automatic transaction ([#2891](https://github.com/getsentry/sentry-java/pull/2891)) - Previously performing a click on the same UI widget twice would keep the existing transaction running, the new behavior now better aligns with other SDKs +- Android only: If global hub mode is enabled, Sentry.getSpan() returns the root span instead of the latest span ([#2855](https://github.com/getsentry/sentry-java/pull/2855)) ### Fixes diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt index 5727eb7a838..eaad6484a67 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt @@ -14,6 +14,7 @@ import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEAD import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT +import io.sentry.util.Platform import io.sentry.util.UrlUtils import okhttp3.Request import okhttp3.Response @@ -37,7 +38,8 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req val method: String = request.method // We start the call span that will contain all the others - callRootSpan = hub.span?.startChild("http.client", "$method $url") + val parentSpan = if (Platform.isAndroid()) hub.transaction else hub.span + callRootSpan = parentSpan?.startChild("http.client", "$method $url") callRootSpan?.spanContext?.origin = TRACE_ORIGIN urlDetails.applyToSpan(callRootSpan) diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt index a8e65352129..7b2099739d4 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt @@ -19,6 +19,7 @@ import io.sentry.exception.ExceptionMechanismException import io.sentry.exception.SentryHttpClientException import io.sentry.protocol.Mechanism import io.sentry.util.HttpUtils +import io.sentry.util.Platform import io.sentry.util.PropagationTargetsUtils import io.sentry.util.TracingUtils import io.sentry.util.UrlUtils @@ -78,7 +79,8 @@ class SentryOkHttpInterceptor( isFromEventListener = true } else { // read the span from the bound scope - span = hub.span?.startChild("http.client", "$method $url") + val parentSpan = if (Platform.isAndroid()) hub.transaction else hub.span + span = parentSpan?.startChild("http.client", "$method $url") isFromEventListener = false } diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt index 906af7c8c67..1a8eb4213b5 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -32,6 +32,7 @@ import io.sentry.util.PropagationTargetsUtils import io.sentry.util.TracingUtils import io.sentry.util.UrlUtils import io.sentry.vendor.Base64 +import okhttp3.internal.platform.Platform import okio.Buffer import org.jetbrains.annotations.ApiStatus @@ -62,7 +63,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( request: HttpRequest, chain: HttpInterceptorChain ): HttpResponse { - val activeSpan = hub.span + val activeSpan = if (io.sentry.util.Platform.isAndroid()) hub.transaction else hub.span val operationName = getHeader(HEADER_APOLLO_OPERATION_NAME, request.headers) val operationType = decodeHeaderValue(request, SENTRY_APOLLO_3_OPERATION_TYPE) diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt index d197eeae4a0..1c65d07ee95 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -25,11 +25,13 @@ import io.sentry.TransactionContext import io.sentry.apollo3.SentryApollo3HttpInterceptor.BeforeSpanCallback import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryTransaction +import io.sentry.util.Apollo3PlatformTestManipulator import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.SocketPolicy +import org.junit.Before import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.check @@ -112,6 +114,11 @@ class SentryApollo3InterceptorTest { private val fixture = Fixture() + @Before + fun setup() { + Apollo3PlatformTestManipulator.pretendIsAndroid(false) + } + @Test fun `creates a span around the successful request`() { executeQuery() @@ -307,6 +314,20 @@ class SentryApollo3InterceptorTest { assert(packageInfo.version == BuildConfig.VERSION_NAME) } + @Test + fun `attaches to root transaction on Android`() { + Apollo3PlatformTestManipulator.pretendIsAndroid(true) + executeQuery(fixture.getSut()) + verify(fixture.hub).transaction + } + + @Test + fun `attaches to child span on non-Android`() { + Apollo3PlatformTestManipulator.pretendIsAndroid(false) + executeQuery(fixture.getSut()) + verify(fixture.hub).span + } + private fun assertTransactionDetails(it: SentryTransaction, httpStatusCode: Int? = 200, contentLength: Long? = 0L) { assertEquals(1, it.spans.size) val httpClientSpan = it.spans.first() @@ -328,6 +349,7 @@ class SentryApollo3InterceptorTest { var tx: ITransaction? = null if (isSpanActive) { tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub) + whenever(fixture.hub.transaction).thenReturn(tx) whenever(fixture.hub.span).thenReturn(tx) } diff --git a/sentry-apollo-3/src/test/java/io/sentry/util/Apollo3PlatformTestManipulator.kt b/sentry-apollo-3/src/test/java/io/sentry/util/Apollo3PlatformTestManipulator.kt new file mode 100644 index 00000000000..b639cab40e4 --- /dev/null +++ b/sentry-apollo-3/src/test/java/io/sentry/util/Apollo3PlatformTestManipulator.kt @@ -0,0 +1,8 @@ +package io.sentry.util + +object Apollo3PlatformTestManipulator { + + fun pretendIsAndroid(isAndroid: Boolean) { + Platform.isAndroid = isAndroid + } +} diff --git a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt index 9cdd005a956..b2d9b4943e8 100644 --- a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt +++ b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt @@ -44,7 +44,7 @@ class SentryApolloInterceptor( } override fun interceptAsync(request: InterceptorRequest, chain: ApolloInterceptorChain, dispatcher: Executor, callBack: CallBack) { - val activeSpan = hub.span + val activeSpan = if (io.sentry.util.Platform.isAndroid()) hub.transaction else hub.span if (activeSpan == null) { val headers = addTracingHeaders(request, null) val modifiedRequest = request.toBuilder().requestHeaders(headers).build() diff --git a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt index a68edab92b7..7ca5d1b82f4 100644 --- a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt +++ b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt @@ -19,11 +19,13 @@ import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryTransaction +import io.sentry.util.ApolloPlatformTestManipulator import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.SocketPolicy +import org.junit.Before import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.check @@ -93,6 +95,11 @@ class SentryApolloInterceptorTest { private val fixture = Fixture() + @Before + fun setup() { + ApolloPlatformTestManipulator.pretendIsAndroid(false) + } + @Test fun `creates a span around the successful request`() { executeQuery() @@ -234,6 +241,20 @@ class SentryApolloInterceptorTest { assert(packageInfo.version == BuildConfig.VERSION_NAME) } + @Test + fun `attaches to root transaction on Android`() { + ApolloPlatformTestManipulator.pretendIsAndroid(true) + executeQuery(fixture.getSut()) + verify(fixture.hub).transaction + } + + @Test + fun `attaches to child span on non-Android`() { + ApolloPlatformTestManipulator.pretendIsAndroid(false) + executeQuery(fixture.getSut()) + verify(fixture.hub).span + } + private fun assertTransactionDetails(it: SentryTransaction) { assertEquals(1, it.spans.size) val httpClientSpan = it.spans.first() @@ -250,6 +271,7 @@ class SentryApolloInterceptorTest { var tx: ITransaction? = null if (isSpanActive) { tx = SentryTracer(TransactionContext("op", "desc", TracesSamplingDecision(true)), fixture.hub) + whenever(fixture.hub.transaction).thenReturn(tx) whenever(fixture.hub.span).thenReturn(tx) } diff --git a/sentry-apollo/src/test/java/io/sentry/util/ApolloPlatformTestManipulator.kt b/sentry-apollo/src/test/java/io/sentry/util/ApolloPlatformTestManipulator.kt new file mode 100644 index 00000000000..219b95d08a0 --- /dev/null +++ b/sentry-apollo/src/test/java/io/sentry/util/ApolloPlatformTestManipulator.kt @@ -0,0 +1,8 @@ +package io.sentry.util + +object ApolloPlatformTestManipulator { + + fun pretendIsAndroid(isAndroid: Boolean) { + Platform.isAndroid = isAndroid + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index c1e55e642ea..cdb0800c35f 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -364,6 +364,7 @@ public final class io/sentry/Hub : io/sentry/IHub { public fun getOptions ()Lio/sentry/SentryOptions; public fun getSpan ()Lio/sentry/ISpan; public fun getTraceparent ()Lio/sentry/SentryTraceHeader; + public fun getTransaction ()Lio/sentry/ITransaction; public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z public fun popScope ()V @@ -411,6 +412,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun getOptions ()Lio/sentry/SentryOptions; public fun getSpan ()Lio/sentry/ISpan; public fun getTraceparent ()Lio/sentry/SentryTraceHeader; + public fun getTransaction ()Lio/sentry/ITransaction; public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z public fun popScope ()V @@ -483,6 +485,7 @@ public abstract interface class io/sentry/IHub { public abstract fun getOptions ()Lio/sentry/SentryOptions; public abstract fun getSpan ()Lio/sentry/ISpan; public abstract fun getTraceparent ()Lio/sentry/SentryTraceHeader; + public abstract fun getTransaction ()Lio/sentry/ITransaction; public abstract fun isCrashedLastRun ()Ljava/lang/Boolean; public abstract fun isEnabled ()Z public abstract fun popScope ()V @@ -869,6 +872,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun getOptions ()Lio/sentry/SentryOptions; public fun getSpan ()Lio/sentry/ISpan; public fun getTraceparent ()Lio/sentry/SentryTraceHeader; + public fun getTransaction ()Lio/sentry/ITransaction; public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z public fun popScope ()V diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 3171c67a95b..90baf3ed87a 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -762,6 +762,22 @@ public void flush(long timeoutMillis) { return span; } + @Override + @ApiStatus.Internal + public @Nullable ITransaction getTransaction() { + ITransaction span = null; + if (!isEnabled()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Instance is disabled and this 'getTransaction' call is a no-op."); + } else { + span = stack.peek().getScope().getTransaction(); + } + return span; + } + @Override @ApiStatus.Internal public void setSpanContext( diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index e6ec2208746..a33fe4e8885 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -221,6 +221,12 @@ public void setSpanContext( return Sentry.getCurrentHub().getSpan(); } + @Override + @ApiStatus.Internal + public @Nullable ITransaction getTransaction() { + return Sentry.getCurrentHub().getTransaction(); + } + @Override public @NotNull SentryOptions getOptions() { return Sentry.getCurrentHub().getOptions(); diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 97583f66e8c..bf4223576e8 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -554,6 +554,15 @@ void setSpanContext( @Nullable ISpan getSpan(); + /** + * Returns the transaction. + * + * @return the transaction or null when no active transaction is running. + */ + @ApiStatus.Internal + @Nullable + ITransaction getTransaction(); + /** * Gets the {@link SentryOptions} attached to current scope. * diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index aa5d8469751..6ad44552bb0 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -179,6 +179,11 @@ public void setSpanContext( return null; } + @Override + public @Nullable ITransaction getTransaction() { + return null; + } + @Override public @NotNull SentryOptions getOptions() { return emptyOptions; diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 254c1fffd50..a7d7330d3f7 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -15,6 +15,7 @@ import io.sentry.transport.NoOpEnvelopeCache; import io.sentry.util.DebugMetaPropertiesApplier; import io.sentry.util.FileUtils; +import io.sentry.util.Platform; import io.sentry.util.thread.IMainThreadChecker; import io.sentry.util.thread.MainThreadChecker; import io.sentry.util.thread.NoOpMainThreadChecker; @@ -918,10 +919,16 @@ public static void endSession() { /** * Gets the current active transaction or span. * - * @return the active span or null when no active transaction is running + * @return the active span or null when no active transaction is running. In case of + * globalHubMode=true, always the active transaction is returned, rather than the last active + * span. */ public static @Nullable ISpan getSpan() { - return getCurrentHub().getSpan(); + if (globalHubMode && Platform.isAndroid()) { + return getCurrentHub().getTransaction(); + } else { + return getCurrentHub().getSpan(); + } } /** diff --git a/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java b/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java index accee3b0510..956996ce04b 100644 --- a/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java +++ b/sentry/src/main/java/io/sentry/instrumentation/file/FileIOSpanManager.java @@ -29,7 +29,7 @@ final class FileIOSpanManager { private final @NotNull SentryStackTraceFactory stackTraceFactory; static @Nullable ISpan startSpan(final @NotNull IHub hub, final @NotNull String op) { - final ISpan parent = hub.getSpan(); + final ISpan parent = Platform.isAndroid() ? hub.getTransaction() : hub.getSpan(); return parent != null ? parent.startChild(op) : null; } diff --git a/sentry/src/main/java/io/sentry/util/Platform.java b/sentry/src/main/java/io/sentry/util/Platform.java index 0a5d06cec51..b08b6e584fb 100644 --- a/sentry/src/main/java/io/sentry/util/Platform.java +++ b/sentry/src/main/java/io/sentry/util/Platform.java @@ -6,7 +6,7 @@ @ApiStatus.Internal public final class Platform { - private static boolean isAndroid; + static boolean isAndroid; static boolean isJavaNinePlus; static { diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index 6fec20ca7da..f906c5e3c8f 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -226,6 +226,11 @@ class HubAdapterTest { verify(hub).span } + @Test fun `getTransaction calls Hub`() { + HubAdapter.getInstance().transaction + verify(hub).transaction + } + @Test fun `getOptions calls Hub`() { HubAdapter.getInstance().options verify(hub).options diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index 96a44462d49..da18dd3d82d 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -1607,15 +1607,32 @@ class HubTest { @Test fun `when there is no active transaction, getSpan returns null`() { val hub = generateHub() - assertNull(hub.getSpan()) + assertNull(hub.span) } @Test - fun `when there is active transaction bound to the scope, getSpan returns active transaction`() { + fun `when there is no active transaction, getTransaction returns null`() { + val hub = generateHub() + assertNull(hub.transaction) + } + + @Test + fun `when there is active transaction bound to the scope, getTransaction and getSpan return active transaction`() { val hub = generateHub() val tx = hub.startTransaction("aTransaction", "op") - hub.configureScope { it.setTransaction(tx) } - assertEquals(tx, hub.getSpan()) + hub.configureScope { it.transaction = tx } + + assertEquals(tx, hub.transaction) + assertEquals(tx, hub.span) + } + + @Test + fun `when there is a transaction but the hub is closed, getTransaction returns null`() { + val hub = generateHub() + hub.startTransaction("name", "op") + hub.close() + + assertNull(hub.transaction) } @Test @@ -1625,6 +1642,8 @@ class HubTest { hub.configureScope { it.setTransaction(tx) } hub.configureScope { it.setTransaction(tx) } val span = tx.startChild("op") + + assertEquals(tx, hub.transaction) assertEquals(span, hub.span) } // endregion diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index c19063e8c46..dd9e6fc9756 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -9,6 +9,7 @@ import io.sentry.internal.modules.IModulesLoader import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryId import io.sentry.test.ImmediateExecutorService +import io.sentry.util.PlatformTestManipulator import io.sentry.util.thread.IMainThreadChecker import io.sentry.util.thread.MainThreadChecker import org.awaitility.kotlin.await @@ -723,6 +724,65 @@ class SentryTest { assertFalse(previousSessionFile.exists()) } + @Test + fun `getSpan calls hub getSpan`() { + val hub = mock() + Sentry.init({ + it.dsn = dsn + }, false) + Sentry.setCurrentHub(hub) + Sentry.getSpan() + verify(hub).span + } + + @Test + fun `getSpan calls returns root span if globalhub mode is enabled on Android`() { + PlatformTestManipulator.pretendIsAndroid(true) + Sentry.init({ + it.dsn = dsn + it.enableTracing = true + it.sampleRate = 1.0 + }, true) + + val transaction = Sentry.startTransaction("name", "op-root", true) + transaction.startChild("op-child") + + val span = Sentry.getSpan()!! + assertEquals("op-root", span.operation) + PlatformTestManipulator.pretendIsAndroid(false) + } + + @Test + fun `getSpan calls returns child span if globalhub mode is enabled, but the platform is not Android`() { + PlatformTestManipulator.pretendIsAndroid(false) + Sentry.init({ + it.dsn = dsn + it.enableTracing = true + it.sampleRate = 1.0 + }, false) + + val transaction = Sentry.startTransaction("name", "op-root", true) + transaction.startChild("op-child") + + val span = Sentry.getSpan()!! + assertEquals("op-child", span.operation) + } + + @Test + fun `getSpan calls returns child span if globalhub mode is disabled`() { + Sentry.init({ + it.dsn = dsn + it.enableTracing = true + it.sampleRate = 1.0 + }, false) + + val transaction = Sentry.startTransaction("name", "op-root", true) + transaction.startChild("op-child") + + val span = Sentry.getSpan()!! + assertEquals("op-child", span.operation) + } + private class InMemoryOptionsObserver : IOptionsObserver { var release: String? = null private set diff --git a/sentry/src/test/java/io/sentry/instrumentation/file/FileIOSpanManagerTest.kt b/sentry/src/test/java/io/sentry/instrumentation/file/FileIOSpanManagerTest.kt new file mode 100644 index 00000000000..00c89f27bea --- /dev/null +++ b/sentry/src/test/java/io/sentry/instrumentation/file/FileIOSpanManagerTest.kt @@ -0,0 +1,44 @@ +package io.sentry.instrumentation.file + +import io.sentry.IHub +import io.sentry.ISpan +import io.sentry.ITransaction +import io.sentry.util.PlatformTestManipulator +import org.junit.After +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.Test + +class FileIOSpanManagerTest { + + @After + fun cleanup() { + PlatformTestManipulator.pretendIsAndroid(false) + } + + @Test + fun `startSpan uses transaction on Android platform`() { + val hub = mock() + val transaction = mock() + whenever(hub.transaction).thenReturn(transaction) + + PlatformTestManipulator.pretendIsAndroid(true) + + FileIOSpanManager.startSpan(hub, "op.read") + verify(transaction).startChild(any()) + } + + @Test + fun `startSpan uses last span on non-Android platforms`() { + val hub = mock() + val span = mock() + whenever(hub.span).thenReturn(span) + + PlatformTestManipulator.pretendIsAndroid(false) + + FileIOSpanManager.startSpan(hub, "op.read") + verify(span).startChild(any()) + } +} diff --git a/sentry/src/test/java/io/sentry/util/PlatformTestManipulator.kt b/sentry/src/test/java/io/sentry/util/PlatformTestManipulator.kt index a849cf3d6dc..3eb4662f7ce 100644 --- a/sentry/src/test/java/io/sentry/util/PlatformTestManipulator.kt +++ b/sentry/src/test/java/io/sentry/util/PlatformTestManipulator.kt @@ -6,5 +6,9 @@ class PlatformTestManipulator { fun pretendJavaNinePlus(isJavaNinePlus: Boolean) { Platform.isJavaNinePlus = isJavaNinePlus } + + fun pretendIsAndroid(isAndroid: Boolean) { + Platform.isAndroid = isAndroid + } } } From 421d724d4fe008592fd2ed7c08ae43b8c5a20fdd Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 8 Sep 2023 12:16:08 +0200 Subject: [PATCH 18/23] Fix changelog --- CHANGELOG.md | 58 ++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea19766c2ac..4baa6a4f32b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## Unreleased + +### Features + +Breaking changes: +- Capture failed HTTP requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794)) +- Reduce flush timeout to 4s on Android to avoid ANRs ([#2858](https://github.com/getsentry/sentry-java/pull/2858)) +- Set ip_address to {{auto}} by default, even if sendDefaultPII is disabled ([#2860](https://github.com/getsentry/sentry-java/pull/2860)) + - Instead use the "Prevent Storing of IP Addresses" option in the "Security & Privacy" project settings on sentry.io +- Reduce timeout of AsyncHttpTransport to avoid ANR ([#2879](https://github.com/getsentry/sentry-java/pull/2879)) +- Add deadline timeout for automatic transactions ([#2865](https://github.com/getsentry/sentry-java/pull/2865)) + - This affects all automatically generated transactions on Android (UI, clicks), the default timeout is 30s +- Apollo v2 BeforeSpanCallback now allows returning null ([#2890](https://github.com/getsentry/sentry-java/pull/2890)) +- Automatic user interaction tracking: every click now starts a new automatic transaction ([#2891](https://github.com/getsentry/sentry-java/pull/2891)) + - Previously performing a click on the same UI widget twice would keep the existing transaction running, the new behavior now better aligns with other SDKs +- Android only: If global hub mode is enabled, Sentry.getSpan() returns the root span instead of the latest span ([#2855](https://github.com/getsentry/sentry-java/pull/2855)) + +### Fixes + +- Measure AppStart time till First Draw instead of `onResume` ([#2851](https://github.com/getsentry/sentry-java/pull/2851)) +- Do not overwrite UI transaction status if set by the user ([#2852](https://github.com/getsentry/sentry-java/pull/2852)) + +Breaking changes: +- Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) +- Fix Coroutine Context Propagation using CopyableThreadContextElement, requires `kotlinx-coroutines-core` version `1.6.1` or higher ([#2838](https://github.com/getsentry/sentry-java/pull/2838)) +- Bump min API to 19 ([#2883](https://github.com/getsentry/sentry-java/pull/2883)) +- Fix don't overwrite the span status of unfinished spans ([#2859](https://github.com/getsentry/sentry-java/pull/2859)) + - If you're using a self hosted version of sentry, sentry self hosted >= 22.12.0 is required + ## 6.29.0 ### Features @@ -34,35 +63,6 @@ ### Features -Breaking changes: -- Capture failed HTTP requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794)) -- Reduce flush timeout to 4s on Android to avoid ANRs ([#2858](https://github.com/getsentry/sentry-java/pull/2858)) -- Set ip_address to {{auto}} by default, even if sendDefaultPII is disabled ([#2860](https://github.com/getsentry/sentry-java/pull/2860)) - - Instead use the "Prevent Storing of IP Addresses" option in the "Security & Privacy" project settings on sentry.io -- Reduce timeout of AsyncHttpTransport to avoid ANR ([#2879](https://github.com/getsentry/sentry-java/pull/2879)) -- Add deadline timeout for automatic transactions ([#2865](https://github.com/getsentry/sentry-java/pull/2865)) - - This affects all automatically generated transactions on Android (UI, clicks), the default timeout is 30s -- Apollo v2 BeforeSpanCallback now allows returning null ([#2890](https://github.com/getsentry/sentry-java/pull/2890)) -- Automatic user interaction tracking: every click now starts a new automatic transaction ([#2891](https://github.com/getsentry/sentry-java/pull/2891)) - - Previously performing a click on the same UI widget twice would keep the existing transaction running, the new behavior now better aligns with other SDKs -- Android only: If global hub mode is enabled, Sentry.getSpan() returns the root span instead of the latest span ([#2855](https://github.com/getsentry/sentry-java/pull/2855)) - -### Fixes - -- Measure AppStart time till First Draw instead of `onResume` ([#2851](https://github.com/getsentry/sentry-java/pull/2851)) -- Do not overwrite UI transaction status if set by the user ([#2852](https://github.com/getsentry/sentry-java/pull/2852)) - -Breaking changes: -- Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) -- Fix Coroutine Context Propagation using CopyableThreadContextElement, requires `kotlinx-coroutines-core` version `1.6.1` or higher ([#2838](https://github.com/getsentry/sentry-java/pull/2838)) -- Bump min API to 19 ([#2883](https://github.com/getsentry/sentry-java/pull/2883)) -- Fix don't overwrite the span status of unfinished spans ([#2859](https://github.com/getsentry/sentry-java/pull/2859)) - - If you're using a self hosted version of sentry, sentry self hosted >= 22.12.0 is required - -## Unreleased - -### Features - - Add TraceOrigin to Transactions and Spans ([#2803](https://github.com/getsentry/sentry-java/pull/2803)) ### Fixes From c383ed17e51e31dc25d983f16add54c0cb32584d Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 8 Sep 2023 20:02:11 +0200 Subject: [PATCH 19/23] Make SDK unity-compatible (#2847) --- CHANGELOG.md | 4 + .../api/sentry-android-core.api | 8 +- sentry-android-core/proguard-rules.pro | 17 ++- .../core/ActivityLifecycleIntegration.java | 3 +- .../sentry/android/core/AnrIntegration.java | 9 +- .../android/core/AnrV2EventProcessor.java | 10 ++ .../sentry/android/core/AnrV2Integration.java | 9 +- .../AppComponentsBreadcrumbsIntegration.java | 3 +- .../android/core/AppLifecycleIntegration.java | 4 +- .../sentry/android/core/NdkIntegration.java | 4 +- .../core/NetworkBreadcrumbsIntegration.java | 4 +- .../PhoneStateBreadcrumbsIntegration.java | 3 +- .../core/ScreenshotEventProcessor.java | 16 ++- .../SystemEventsBreadcrumbsIntegration.java | 3 +- .../TempSensorBreadcrumbsIntegration.java | 3 +- .../core/UserInteractionIntegration.java | 4 +- .../core/ViewHierarchyEventProcessor.java | 17 ++- .../util/AndroidMainThreadChecker.java | 18 +++ .../android/core/AnrV2EventProcessorTest.kt | 2 + .../fragment/FragmentLifecycleIntegration.kt | 3 +- .../api/sentry-android-navigation.api | 2 +- .../navigation/SentryNavigationListener.kt | 6 +- sentry-android-ndk/api/sentry-android-ndk.api | 2 +- .../sentry/android/ndk/NdkScopeObserver.java | 4 +- .../api/sentry-android-okhttp.api | 2 +- .../android/okhttp/SentryOkHttpInterceptor.kt | 6 +- .../android/timber/SentryTimberIntegration.kt | 3 +- sentry-apollo-3/api/sentry-apollo-3.api | 3 +- .../apollo3/SentryApollo3HttpInterceptor.kt | 10 +- sentry-apollo/api/sentry-apollo.api | 2 +- .../sentry/apollo/SentryApolloInterceptor.kt | 6 +- .../compose/SentryNavigationIntegration.kt | 10 +- sentry/api/sentry.api | 104 ++++++++++++------ sentry/src/main/java/io/sentry/Hub.java | 5 + .../src/main/java/io/sentry/HubAdapter.java | 5 + sentry/src/main/java/io/sentry/IHub.java | 4 +- .../main/java/io/sentry/IOptionsObserver.java | 12 +- .../main/java/io/sentry/IScopeObserver.java | 30 ++--- .../src/main/java/io/sentry/Integration.java | 2 +- .../main/java/io/sentry/IntegrationName.java | 16 --- .../main/java/io/sentry/MeasurementUnit.java | 25 ++++- sentry/src/main/java/io/sentry/NoOpHub.java | 3 + .../java/io/sentry/ScopeObserverAdapter.java | 56 ++++++++++ ...achedEnvelopeFireAndForgetIntegration.java | 4 +- .../io/sentry/ShutdownHookIntegration.java | 4 +- .../UncaughtExceptionHandlerIntegration.java | 4 +- .../sentry/cache/PersistingScopeObserver.java | 4 +- .../java/io/sentry/hints/AbnormalExit.java | 8 +- .../java/io/sentry/util/IntegrationUtils.java | 23 ++++ .../util/thread/IMainThreadChecker.java | 13 +-- .../sentry/util/thread/MainThreadChecker.java | 18 +++ .../util/thread/NoOpMainThreadChecker.java | 17 +++ .../java/io/sentry/MainEventProcessorTest.kt | 2 + .../test/java/io/sentry/SentryClientTest.kt | 2 + sentry/src/test/java/io/sentry/SentryTest.kt | 4 + .../java/io/sentry/cache/EnvelopeCacheTest.kt | 10 +- 56 files changed, 418 insertions(+), 157 deletions(-) delete mode 100644 sentry/src/main/java/io/sentry/IntegrationName.java create mode 100644 sentry/src/main/java/io/sentry/ScopeObserverAdapter.java create mode 100644 sentry/src/main/java/io/sentry/util/IntegrationUtils.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4baa6a4f32b..58230171001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ Breaking changes: - Bump min API to 19 ([#2883](https://github.com/getsentry/sentry-java/pull/2883)) - Fix don't overwrite the span status of unfinished spans ([#2859](https://github.com/getsentry/sentry-java/pull/2859)) - If you're using a self hosted version of sentry, sentry self hosted >= 22.12.0 is required +- Migrate from `default` interface methods to proper implementations in each interface implementor ([#2847](https://github.com/getsentry/sentry-java/pull/2847)) + - This prevents issues when using the SDK on older AGP versions (< 4.x.x) + - Make sure to align Sentry dependencies to the same version when bumping the SDK to 7.+, otherwise it will crash at runtime due to binary incompatibility. + (E.g. if you're using `-timber`, `-okhttp` or other packages) ## 6.29.0 diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index c6937760a23..bf9fcdfd334 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -63,6 +63,7 @@ public final class io/sentry/android/core/AnrIntegrationFactory { public final class io/sentry/android/core/AnrV2EventProcessor : io/sentry/BackfillingEventProcessor { public fun (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; + public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } public class io/sentry/android/core/AnrV2Integration : io/sentry/Integration, java/io/Closeable { @@ -73,6 +74,7 @@ public class io/sentry/android/core/AnrV2Integration : io/sentry/Integration, ja public final class io/sentry/android/core/AnrV2Integration$AnrV2Hint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/AbnormalExit, io/sentry/hints/Backfillable { public fun (JLio/sentry/ILogger;JZZ)V + public fun ignoreCurrentThread ()Z public fun mechanism ()Ljava/lang/String; public fun shouldEnrich ()Z public fun timestamp ()Ljava/lang/Long; @@ -205,9 +207,10 @@ public final class io/sentry/android/core/PhoneStateBreadcrumbsIntegration : io/ public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } -public final class io/sentry/android/core/ScreenshotEventProcessor : io/sentry/EventProcessor, io/sentry/IntegrationName { +public final class io/sentry/android/core/ScreenshotEventProcessor : io/sentry/EventProcessor { public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; + public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } public final class io/sentry/android/core/SentryAndroid { @@ -350,9 +353,10 @@ public final class io/sentry/android/core/UserInteractionIntegration : android/a public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } -public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentry/EventProcessor, io/sentry/IntegrationName { +public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentry/EventProcessor { public fun (Lio/sentry/android/core/SentryAndroidOptions;)V public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; + public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; public static fun snapshotViewHierarchy (Landroid/app/Activity;Lio/sentry/ILogger;)Lio/sentry/protocol/ViewHierarchy; public static fun snapshotViewHierarchy (Landroid/app/Activity;Ljava/util/List;Lio/sentry/util/thread/IMainThreadChecker;Lio/sentry/ILogger;)Lio/sentry/protocol/ViewHierarchy; public static fun snapshotViewHierarchy (Landroid/view/View;)Lio/sentry/protocol/ViewHierarchy; diff --git a/sentry-android-core/proguard-rules.pro b/sentry-android-core/proguard-rules.pro index 79daaace033..9b0e74c6d0f 100644 --- a/sentry-android-core/proguard-rules.pro +++ b/sentry-android-core/proguard-rules.pro @@ -31,7 +31,22 @@ -keepattributes LineNumberTable,SourceFile # Keep Classnames for integrations --keepnames class * implements io.sentry.IntegrationName +-keepnames class * implements io.sentry.Integration + +-dontwarn io.sentry.apollo.SentryApolloInterceptor +-keepnames class io.sentry.apollo.SentryApolloInterceptor + +-dontwarn io.sentry.apollo3.SentryApollo3HttpInterceptor +-keepnames class io.sentry.apollo3.SentryApollo3HttpInterceptor + +-dontwarn io.sentry.android.okhttp.SentryOkHttpInterceptor +-keepnames class io.sentry.android.okhttp.SentryOkHttpInterceptor + +-dontwarn io.sentry.android.navigation.SentryNavigationListener +-keepnames class io.sentry.android.navigation.SentryNavigationListener + +-keepnames class io.sentry.android.core.ScreenshotEventProcessor +-keepnames class io.sentry.android.core.ViewHierarchyEventProcessor # Keep any custom option classes like SentryAndroidOptions, as they're loaded via reflection # Also keep method names, as they're e.g. used by native via JNI calls 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 0d44e08567b..2826366b4b6 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 @@ -2,6 +2,7 @@ import static io.sentry.MeasurementUnit.Duration.MILLISECOND; import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import android.app.Activity; import android.app.Application; @@ -126,7 +127,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio application.registerActivityLifecycleCallbacks(this); this.options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } private boolean isPerformanceEnabled(final @NotNull SentryAndroidOptions options) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java index 4c6ac6373af..0b0d56b093e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import android.annotation.SuppressLint; import android.content.Context; import io.sentry.Hint; @@ -74,7 +76,7 @@ private void register(final @NotNull IHub hub, final @NotNull SentryAndroidOptio anrWatchDog.start(); options.getLogger().log(SentryLevel.DEBUG, "AnrIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } } @@ -161,5 +163,10 @@ public String mechanism() { public boolean ignoreCurrentThread() { return true; } + + @Override + public @Nullable Long timestamp() { + return null; + } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index 54a500825c1..f8fbc498b08 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -48,6 +48,7 @@ import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryStackTrace; import io.sentry.protocol.SentryThread; +import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; import io.sentry.util.HintUtils; import java.util.ArrayList; @@ -90,6 +91,15 @@ public AnrV2EventProcessor( sentryExceptionFactory = new SentryExceptionFactory(sentryStackTraceFactory); } + @Override + public @NotNull SentryTransaction process( + @NotNull SentryTransaction transaction, @NotNull Hint hint) { + // that's only necessary because on newer versions of Unity, if not overriding this method, it's + // throwing 'java.lang.AbstractMethodError: abstract method' and the reason is probably + // compilation mismatch + return transaction; + } + @Override public @Nullable SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { final Object unwrappedHint = HintUtils.getSentrySdkHint(hint); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index f3d257d225b..66278d35b88 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ApplicationExitInfo; @@ -93,7 +95,7 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { options.getLogger().log(SentryLevel.DEBUG, "Failed to start AnrProcessor.", e); } options.getLogger().log(SentryLevel.DEBUG, "AnrV2Integration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } @@ -365,6 +367,11 @@ public AnrV2Hint( this.isBackgroundAnr = isBackgroundAnr; } + @Override + public boolean ignoreCurrentThread() { + return false; + } + @Override public Long timestamp() { return timestamp; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java index c9a552a5b76..eef47076837 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java @@ -1,6 +1,7 @@ package io.sentry.android.core; import static io.sentry.TypeCheckHint.ANDROID_CONFIGURATION; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import android.content.ComponentCallbacks2; import android.content.Context; @@ -53,7 +54,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio options .getLogger() .log(SentryLevel.DEBUG, "AppComponentsBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } catch (Throwable e) { this.options.setEnableAppComponentBreadcrumbs(false); options.getLogger().log(SentryLevel.INFO, e, "ComponentCallbacks2 is not available."); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java index 7f4724967f4..3e8fe6383f8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import androidx.lifecycle.ProcessLifecycleOwner; import io.sentry.IHub; import io.sentry.Integration; @@ -94,7 +96,7 @@ private void addObserver(final @NotNull IHub hub) { try { ProcessLifecycleOwner.get().getLifecycle().addObserver(watcher); options.getLogger().log(SentryLevel.DEBUG, "AppLifecycleIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } catch (Throwable e) { // This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in // connection with conflicting dependencies of the androidx.lifecycle. diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java index 840f109c56a..3a4a91498e7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NdkIntegration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import io.sentry.IHub; import io.sentry.Integration; import io.sentry.SentryLevel; @@ -53,7 +55,7 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions method.invoke(null, args); this.options.getLogger().log(SentryLevel.DEBUG, "NdkIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } catch (NoSuchMethodException e) { disableNdkIntegration(this.options); this.options diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java index 317cb604677..6aa7caf4c90 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import android.annotation.SuppressLint; import android.content.Context; import android.net.ConnectivityManager; @@ -79,7 +81,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio return; } logger.log(SentryLevel.DEBUG, "NetworkBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java index be23c668c7a..ea0426609c6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PhoneStateBreadcrumbsIntegration.java @@ -1,6 +1,7 @@ package io.sentry.android.core; import static android.Manifest.permission.READ_PHONE_STATE; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import android.content.Context; import android.telephony.TelephonyManager; @@ -53,7 +54,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio telephonyManager.listen(listener, android.telephony.PhoneStateListener.LISTEN_CALL_STATE); options.getLogger().log(SentryLevel.DEBUG, "PhoneStateBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } catch (Throwable e) { this.options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java index 0812e550157..33e37a4d2e4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ScreenshotEventProcessor.java @@ -2,16 +2,17 @@ import static io.sentry.TypeCheckHint.ANDROID_ACTIVITY; import static io.sentry.android.core.internal.util.ScreenshotUtils.takeScreenshot; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import android.app.Activity; import io.sentry.Attachment; import io.sentry.EventProcessor; import io.sentry.Hint; -import io.sentry.IntegrationName; import io.sentry.SentryEvent; import io.sentry.SentryLevel; import io.sentry.android.core.internal.util.AndroidCurrentDateProvider; import io.sentry.android.core.internal.util.Debouncer; +import io.sentry.protocol.SentryTransaction; import io.sentry.util.HintUtils; import io.sentry.util.Objects; import org.jetbrains.annotations.ApiStatus; @@ -23,7 +24,7 @@ * captured. */ @ApiStatus.Internal -public final class ScreenshotEventProcessor implements EventProcessor, IntegrationName { +public final class ScreenshotEventProcessor implements EventProcessor { private final @NotNull SentryAndroidOptions options; private final @NotNull BuildInfoProvider buildInfoProvider; @@ -40,10 +41,19 @@ public ScreenshotEventProcessor( this.debouncer = new Debouncer(AndroidCurrentDateProvider.getInstance(), DEBOUNCE_WAIT_TIME_MS); if (options.isAttachScreenshot()) { - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } + @Override + public @NotNull SentryTransaction process( + @NotNull SentryTransaction transaction, @NotNull Hint hint) { + // that's only necessary because on newer versions of Unity, if not overriding this method, it's + // throwing 'java.lang.AbstractMethodError: abstract method' and the reason is probably + // compilation mismatch + return transaction; + } + @Override public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) { if (!event.isErrored()) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java index 26f8e2bfb5c..2d800847385 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SystemEventsBreadcrumbsIntegration.java @@ -31,6 +31,7 @@ import static android.content.Intent.ACTION_TIMEZONE_CHANGED; import static android.content.Intent.ACTION_TIME_CHANGED; import static io.sentry.TypeCheckHint.ANDROID_INTENT; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import android.content.BroadcastReceiver; import android.content.Context; @@ -103,7 +104,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio this.options .getLogger() .log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } catch (Throwable e) { this.options.setEnableSystemEventBreadcrumbs(false); this.options diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java index e4577b43ecc..66c29ddefff 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/TempSensorBreadcrumbsIntegration.java @@ -2,6 +2,7 @@ import static android.content.Context.SENSOR_SERVICE; import static io.sentry.TypeCheckHint.ANDROID_SENSOR_EVENT; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; import android.content.Context; import android.hardware.Sensor; @@ -62,7 +63,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio options .getLogger() .log(SentryLevel.DEBUG, "TempSensorBreadcrumbsIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } else { this.options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java index dda56272729..c361529671f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import android.app.Activity; import android.app.Application; import android.os.Bundle; @@ -119,7 +121,7 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { if (isAndroidXAvailable) { application.registerActivityLifecycleCallbacks(this); this.options.getLogger().log(SentryLevel.DEBUG, "UserInteractionIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } else { options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java index b667e048884..569e227dc76 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ViewHierarchyEventProcessor.java @@ -1,5 +1,7 @@ package io.sentry.android.core; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import android.app.Activity; import android.view.View; import android.view.ViewGroup; @@ -9,7 +11,6 @@ import io.sentry.Hint; import io.sentry.ILogger; import io.sentry.ISerializer; -import io.sentry.IntegrationName; import io.sentry.SentryEvent; import io.sentry.SentryLevel; import io.sentry.android.core.internal.gestures.ViewUtils; @@ -17,6 +18,7 @@ import io.sentry.android.core.internal.util.AndroidMainThreadChecker; import io.sentry.android.core.internal.util.Debouncer; import io.sentry.internal.viewhierarchy.ViewHierarchyExporter; +import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.ViewHierarchy; import io.sentry.protocol.ViewHierarchyNode; import io.sentry.util.HintUtils; @@ -34,7 +36,7 @@ /** ViewHierarchyEventProcessor responsible for taking a snapshot of the current view hierarchy. */ @ApiStatus.Internal -public final class ViewHierarchyEventProcessor implements EventProcessor, IntegrationName { +public final class ViewHierarchyEventProcessor implements EventProcessor { private final @NotNull SentryAndroidOptions options; private final @NotNull Debouncer debouncer; @@ -47,10 +49,19 @@ public ViewHierarchyEventProcessor(final @NotNull SentryAndroidOptions options) this.debouncer = new Debouncer(AndroidCurrentDateProvider.getInstance(), DEBOUNCE_WAIT_TIME_MS); if (options.isAttachViewHierarchy()) { - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } + @Override + public @NotNull SentryTransaction process( + @NotNull SentryTransaction transaction, @NotNull Hint hint) { + // that's only necessary because on newer versions of Unity, if not overriding this method, it's + // throwing 'java.lang.AbstractMethodError: abstract method' and the reason is probably + // compilation mismatch + return transaction; + } + @Override public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) { if (!event.isErrored()) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidMainThreadChecker.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidMainThreadChecker.java index a893fa87d16..aa54790c472 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidMainThreadChecker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidMainThreadChecker.java @@ -1,8 +1,10 @@ package io.sentry.android.core.internal.util; import android.os.Looper; +import io.sentry.protocol.SentryThread; import io.sentry.util.thread.IMainThreadChecker; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; /** Class that checks if a given thread is the Android Main/UI thread */ @ApiStatus.Internal @@ -20,4 +22,20 @@ private AndroidMainThreadChecker() {} public boolean isMainThread(final long threadId) { return Looper.getMainLooper().getThread().getId() == threadId; } + + @Override + public boolean isMainThread(final @NotNull Thread thread) { + return isMainThread(thread.getId()); + } + + @Override + public boolean isMainThread() { + return isMainThread(Thread.currentThread()); + } + + @Override + public boolean isMainThread(final @NotNull SentryThread sentryThread) { + final Long threadId = sentryThread.getId(); + return threadId != null && isMainThread(threadId); + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt index 2d331a75633..a792a5b3c58 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2EventProcessorTest.kt @@ -551,6 +551,8 @@ class AnrV2EventProcessorTest { internal class AbnormalExitHint(val mechanism: String? = null) : AbnormalExit, Backfillable { override fun mechanism(): String? = mechanism + override fun ignoreCurrentThread(): Boolean = false + override fun timestamp(): Long? = null override fun shouldEnrich(): Boolean = true } diff --git a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt index 81f0e7e5052..4129ea43566 100644 --- a/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt +++ b/sentry-android-fragment/src/main/java/io/sentry/android/fragment/FragmentLifecycleIntegration.kt @@ -10,6 +10,7 @@ import io.sentry.Integration import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel.DEBUG import io.sentry.SentryOptions +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import java.io.Closeable class FragmentLifecycleIntegration( @@ -48,7 +49,7 @@ class FragmentLifecycleIntegration( application.registerActivityLifecycleCallbacks(this) options.logger.log(DEBUG, "FragmentLifecycleIntegration installed.") - addIntegrationToSdkVersion() + addIntegrationToSdkVersion(javaClass) SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-android-fragment", BuildConfig.VERSION_NAME) } diff --git a/sentry-android-navigation/api/sentry-android-navigation.api b/sentry-android-navigation/api/sentry-android-navigation.api index 61a98025bff..79151bb3fb4 100644 --- a/sentry-android-navigation/api/sentry-android-navigation.api +++ b/sentry-android-navigation/api/sentry-android-navigation.api @@ -6,7 +6,7 @@ public final class io/sentry/android/navigation/BuildConfig { public fun ()V } -public final class io/sentry/android/navigation/SentryNavigationListener : androidx/navigation/NavController$OnDestinationChangedListener, io/sentry/IntegrationName { +public final class io/sentry/android/navigation/SentryNavigationListener : androidx/navigation/NavController$OnDestinationChangedListener { public static final field Companion Lio/sentry/android/navigation/SentryNavigationListener$Companion; public static final field NAVIGATION_OP Ljava/lang/String; public fun ()V 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 8ca52e3c414..8fdf8b0df88 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 @@ -9,7 +9,6 @@ import io.sentry.Hint import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ITransaction -import io.sentry.IntegrationName import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel.DEBUG import io.sentry.SentryLevel.INFO @@ -19,6 +18,7 @@ import io.sentry.TransactionContext import io.sentry.TransactionOptions import io.sentry.TypeCheckHint import io.sentry.protocol.TransactionNameSource +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import io.sentry.util.TracingUtils import java.lang.ref.WeakReference @@ -38,7 +38,7 @@ class SentryNavigationListener @JvmOverloads constructor( private val enableNavigationBreadcrumbs: Boolean = true, private val enableNavigationTracing: Boolean = true, private val traceOriginAppendix: String? = null -) : NavController.OnDestinationChangedListener, IntegrationName { +) : NavController.OnDestinationChangedListener { private var previousDestinationRef: WeakReference? = null private var previousArgs: Bundle? = null @@ -48,7 +48,7 @@ class SentryNavigationListener @JvmOverloads constructor( private var activeTransaction: ITransaction? = null init { - addIntegrationToSdkVersion() + addIntegrationToSdkVersion(javaClass) SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-android-navigation", BuildConfig.VERSION_NAME) } diff --git a/sentry-android-ndk/api/sentry-android-ndk.api b/sentry-android-ndk/api/sentry-android-ndk.api index afd7baf890e..30e9cbb7b68 100644 --- a/sentry-android-ndk/api/sentry-android-ndk.api +++ b/sentry-android-ndk/api/sentry-android-ndk.api @@ -6,7 +6,7 @@ public final class io/sentry/android/ndk/BuildConfig { public fun ()V } -public final class io/sentry/android/ndk/NdkScopeObserver : io/sentry/IScopeObserver { +public final class io/sentry/android/ndk/NdkScopeObserver : io/sentry/ScopeObserverAdapter { public fun (Lio/sentry/SentryOptions;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun removeExtra (Ljava/lang/String;)V diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java index ebc4c9bc47d..009bba9b811 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/NdkScopeObserver.java @@ -2,7 +2,7 @@ import io.sentry.Breadcrumb; import io.sentry.DateUtils; -import io.sentry.IScopeObserver; +import io.sentry.ScopeObserverAdapter; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.protocol.User; @@ -14,7 +14,7 @@ import org.jetbrains.annotations.Nullable; @ApiStatus.Internal -public final class NdkScopeObserver implements IScopeObserver { +public final class NdkScopeObserver extends ScopeObserverAdapter { private final @NotNull SentryOptions options; private final @NotNull INativeScope nativeScope; diff --git a/sentry-android-okhttp/api/sentry-android-okhttp.api b/sentry-android-okhttp/api/sentry-android-okhttp.api index 11c140061e9..a96e3787a61 100644 --- a/sentry-android-okhttp/api/sentry-android-okhttp.api +++ b/sentry-android-okhttp/api/sentry-android-okhttp.api @@ -46,7 +46,7 @@ public final class io/sentry/android/okhttp/SentryOkHttpEventListener : okhttp3/ public final class io/sentry/android/okhttp/SentryOkHttpEventListener$Companion { } -public final class io/sentry/android/okhttp/SentryOkHttpInterceptor : io/sentry/IntegrationName, okhttp3/Interceptor { +public final class io/sentry/android/okhttp/SentryOkHttpInterceptor : okhttp3/Interceptor { public fun ()V public fun (Lio/sentry/IHub;)V public fun (Lio/sentry/IHub;Lio/sentry/android/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;)V diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt index 7b2099739d4..4bda5a9c817 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt @@ -7,7 +7,6 @@ import io.sentry.HttpStatusCodeRange import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan -import io.sentry.IntegrationName import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS @@ -19,6 +18,7 @@ import io.sentry.exception.ExceptionMechanismException import io.sentry.exception.SentryHttpClientException import io.sentry.protocol.Mechanism import io.sentry.util.HttpUtils +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import io.sentry.util.Platform import io.sentry.util.PropagationTargetsUtils import io.sentry.util.TracingUtils @@ -51,14 +51,14 @@ class SentryOkHttpInterceptor( HttpStatusCodeRange(HttpStatusCodeRange.DEFAULT_MIN, HttpStatusCodeRange.DEFAULT_MAX) ), private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) -) : Interceptor, IntegrationName { +) : Interceptor { constructor() : this(HubAdapter.getInstance()) constructor(hub: IHub) : this(hub, null) constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) init { - addIntegrationToSdkVersion() + addIntegrationToSdkVersion(javaClass) SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-android-okhttp", BuildConfig.VERSION_NAME) } diff --git a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt index 6477a2531af..d043faa5f6d 100644 --- a/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt +++ b/sentry-android-timber/src/main/java/io/sentry/android/timber/SentryTimberIntegration.kt @@ -7,6 +7,7 @@ import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.android.timber.BuildConfig.VERSION_NAME +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import timber.log.Timber import java.io.Closeable @@ -28,7 +29,7 @@ class SentryTimberIntegration( logger.log(SentryLevel.DEBUG, "SentryTimberIntegration installed.") SentryIntegrationPackageStorage.getInstance().addPackage("maven:io.sentry:sentry-android-timber", VERSION_NAME) - addIntegrationToSdkVersion() + addIntegrationToSdkVersion(javaClass) } override fun close() { diff --git a/sentry-apollo-3/api/sentry-apollo-3.api b/sentry-apollo-3/api/sentry-apollo-3.api index e98f72e45c5..0fa4e717a0d 100644 --- a/sentry-apollo-3/api/sentry-apollo-3.api +++ b/sentry-apollo-3/api/sentry-apollo-3.api @@ -8,7 +8,7 @@ public final class io/sentry/apollo3/SentryApollo3ClientException : java/lang/Ex public final fun getSerialVersionUID ()J } -public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor, io/sentry/IntegrationName { +public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor { public static final field Companion Lio/sentry/apollo3/SentryApollo3HttpInterceptor$Companion; public static final field DEFAULT_CAPTURE_FAILED_REQUESTS Z public static final field SENTRY_APOLLO_3_OPERATION_TYPE Ljava/lang/String; @@ -20,7 +20,6 @@ public final class io/sentry/apollo3/SentryApollo3HttpInterceptor : com/apollogr public fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;)V public synthetic fun (Lio/sentry/IHub;Lio/sentry/apollo3/SentryApollo3HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun dispose ()V - public fun getIntegrationName ()Ljava/lang/String; public fun intercept (Lcom/apollographql/apollo3/api/http/HttpRequest;Lcom/apollographql/apollo3/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt index 7ca3262dc68..c6aeb8755ae 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -14,7 +14,6 @@ import io.sentry.Hint import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan -import io.sentry.IntegrationName import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel @@ -29,6 +28,7 @@ import io.sentry.protocol.Mechanism import io.sentry.protocol.Request import io.sentry.protocol.Response import io.sentry.util.HttpUtils +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import io.sentry.util.PropagationTargetsUtils import io.sentry.util.TracingUtils import io.sentry.util.UrlUtils @@ -45,10 +45,10 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( private val beforeSpan: BeforeSpanCallback? = null, private val captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) -) : HttpInterceptor, IntegrationName { +) : HttpInterceptor { init { - addIntegrationToSdkVersion() + addIntegrationToSdkVersion("Apollo3") if (captureFailedRequests) { SentryIntegrationPackageStorage.getInstance() .addIntegration("Apollo3ClientError") @@ -136,10 +136,6 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( return requestBuilder.build() } - override fun getIntegrationName(): String { - return super.getIntegrationName().replace("Http", "") - } - private fun removeSentryInternalHeaders(headers: List): List { return headers.filterNot { it.name.equals(SENTRY_APOLLO_3_VARIABLES, true) || diff --git a/sentry-apollo/api/sentry-apollo.api b/sentry-apollo/api/sentry-apollo.api index bf1ab6abed7..8c18bce06eb 100644 --- a/sentry-apollo/api/sentry-apollo.api +++ b/sentry-apollo/api/sentry-apollo.api @@ -3,7 +3,7 @@ public final class io/sentry/apollo/BuildConfig { public static final field VERSION_NAME Ljava/lang/String; } -public final class io/sentry/apollo/SentryApolloInterceptor : com/apollographql/apollo/interceptor/ApolloInterceptor, io/sentry/IntegrationName { +public final class io/sentry/apollo/SentryApolloInterceptor : com/apollographql/apollo/interceptor/ApolloInterceptor { public fun ()V public fun (Lio/sentry/IHub;)V public fun (Lio/sentry/IHub;Lio/sentry/apollo/SentryApolloInterceptor$BeforeSpanCallback;)V diff --git a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt index f37277de7c1..faa8a549a92 100644 --- a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt +++ b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt @@ -18,13 +18,13 @@ import io.sentry.Hint import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan -import io.sentry.IntegrationName import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TypeCheckHint.APOLLO_REQUEST import io.sentry.TypeCheckHint.APOLLO_RESPONSE +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion import io.sentry.util.TracingUtils import java.util.Locale import java.util.concurrent.Executor @@ -34,13 +34,13 @@ private const val TRACE_ORIGIN = "auto.graphql.apollo" class SentryApolloInterceptor( private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null -) : ApolloInterceptor, IntegrationName { +) : ApolloInterceptor { constructor(hub: IHub) : this(hub, null) constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) init { - addIntegrationToSdkVersion() + addIntegrationToSdkVersion(javaClass) SentryIntegrationPackageStorage.getInstance().addPackage("maven:io.sentry:sentry-apollo", BuildConfig.VERSION_NAME) } diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt index ef84b7579d2..4af368f0652 100644 --- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt +++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryNavigationIntegration.kt @@ -13,10 +13,10 @@ import androidx.navigation.NavController import androidx.navigation.NavHostController import io.sentry.Breadcrumb import io.sentry.ITransaction -import io.sentry.IntegrationName import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryOptions import io.sentry.android.navigation.SentryNavigationListener +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion private const val TRACE_ORIGIN_APPENDIX = "jetpack_compose" @@ -24,10 +24,10 @@ internal class SentryLifecycleObserver( private val navController: NavController, private val navListener: NavController.OnDestinationChangedListener = SentryNavigationListener(traceOriginAppendix = TRACE_ORIGIN_APPENDIX) -) : LifecycleEventObserver, IntegrationName { +) : LifecycleEventObserver { init { - addIntegrationToSdkVersion() + addIntegrationToSdkVersion("ComposeNavigation") SentryIntegrationPackageStorage.getInstance().addPackage("maven:io.sentry:sentry-compose", BuildConfig.VERSION_NAME) } @@ -39,10 +39,6 @@ internal class SentryLifecycleObserver( } } - override fun getIntegrationName(): String { - return "ComposeNavigation" - } - fun dispose() { navController.removeOnDestinationChangedListener(navListener) } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 6fe9fb85cbf..f29b86f9995 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -350,6 +350,7 @@ public final class io/sentry/HttpStatusCodeRange { public final class io/sentry/Hub : io/sentry/IHub { public fun (Lio/sentry/SentryOptions;)V + public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -397,6 +398,7 @@ public final class io/sentry/Hub : io/sentry/IHub { } public final class io/sentry/HubAdapter : io/sentry/IHub { + public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -459,7 +461,7 @@ public abstract interface class io/sentry/IEnvelopeSender { } public abstract interface class io/sentry/IHub { - public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V + public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun addBreadcrumb (Ljava/lang/String;)V public fun addBreadcrumb (Ljava/lang/String;Ljava/lang/String;)V @@ -537,30 +539,30 @@ public abstract interface class io/sentry/IMemoryCollector { } public abstract interface class io/sentry/IOptionsObserver { - public fun setDist (Ljava/lang/String;)V - public fun setEnvironment (Ljava/lang/String;)V - public fun setProguardUuid (Ljava/lang/String;)V - public fun setRelease (Ljava/lang/String;)V - public fun setSdkVersion (Lio/sentry/protocol/SdkVersion;)V - public fun setTags (Ljava/util/Map;)V + public abstract fun setDist (Ljava/lang/String;)V + public abstract fun setEnvironment (Ljava/lang/String;)V + public abstract fun setProguardUuid (Ljava/lang/String;)V + public abstract fun setRelease (Ljava/lang/String;)V + public abstract fun setSdkVersion (Lio/sentry/protocol/SdkVersion;)V + public abstract fun setTags (Ljava/util/Map;)V } public abstract interface class io/sentry/IScopeObserver { - public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V - public fun removeExtra (Ljava/lang/String;)V - public fun removeTag (Ljava/lang/String;)V - public fun setBreadcrumbs (Ljava/util/Collection;)V - public fun setContexts (Lio/sentry/protocol/Contexts;)V - public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V - public fun setExtras (Ljava/util/Map;)V - public fun setFingerprint (Ljava/util/Collection;)V - public fun setLevel (Lio/sentry/SentryLevel;)V - public fun setRequest (Lio/sentry/protocol/Request;)V - public fun setTag (Ljava/lang/String;Ljava/lang/String;)V - public fun setTags (Ljava/util/Map;)V - public fun setTrace (Lio/sentry/SpanContext;)V - public fun setTransaction (Ljava/lang/String;)V - public fun setUser (Lio/sentry/protocol/User;)V + public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;)V + public abstract fun removeExtra (Ljava/lang/String;)V + public abstract fun removeTag (Ljava/lang/String;)V + public abstract fun setBreadcrumbs (Ljava/util/Collection;)V + public abstract fun setContexts (Lio/sentry/protocol/Contexts;)V + public abstract fun setExtra (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun setExtras (Ljava/util/Map;)V + public abstract fun setFingerprint (Ljava/util/Collection;)V + public abstract fun setLevel (Lio/sentry/SentryLevel;)V + public abstract fun setRequest (Lio/sentry/protocol/Request;)V + public abstract fun setTag (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun setTags (Ljava/util/Map;)V + public abstract fun setTrace (Lio/sentry/SpanContext;)V + public abstract fun setTransaction (Ljava/lang/String;)V + public abstract fun setUser (Lio/sentry/protocol/User;)V } public abstract interface class io/sentry/ISentryClient { @@ -676,15 +678,10 @@ public final class io/sentry/Instrumenter : java/lang/Enum { public static fun values ()[Lio/sentry/Instrumenter; } -public abstract interface class io/sentry/Integration : io/sentry/IntegrationName { +public abstract interface class io/sentry/Integration { public abstract fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } -public abstract interface class io/sentry/IntegrationName { - public fun addIntegrationToSdkVersion ()V - public fun getIntegrationName ()Ljava/lang/String; -} - public final class io/sentry/IpAddressUtils { public static final field DEFAULT_IP_ADDRESS Ljava/lang/String; public static fun isDefault (Ljava/lang/String;)Z @@ -795,12 +792,13 @@ public final class io/sentry/MainEventProcessor : io/sentry/EventProcessor, java public abstract interface class io/sentry/MeasurementUnit { public static final field NONE Ljava/lang/String; - public fun apiName ()Ljava/lang/String; + public abstract fun apiName ()Ljava/lang/String; public abstract fun name ()Ljava/lang/String; } public final class io/sentry/MeasurementUnit$Custom : io/sentry/MeasurementUnit { public fun (Ljava/lang/String;)V + public fun apiName ()Ljava/lang/String; public fun name ()Ljava/lang/String; } @@ -813,6 +811,7 @@ public final class io/sentry/MeasurementUnit$Duration : java/lang/Enum, io/sentr public static final field NANOSECOND Lio/sentry/MeasurementUnit$Duration; public static final field SECOND Lio/sentry/MeasurementUnit$Duration; public static final field WEEK Lio/sentry/MeasurementUnit$Duration; + public fun apiName ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Lio/sentry/MeasurementUnit$Duration; public static fun values ()[Lio/sentry/MeasurementUnit$Duration; } @@ -820,6 +819,7 @@ public final class io/sentry/MeasurementUnit$Duration : java/lang/Enum, io/sentr public final class io/sentry/MeasurementUnit$Fraction : java/lang/Enum, io/sentry/MeasurementUnit { public static final field PERCENT Lio/sentry/MeasurementUnit$Fraction; public static final field RATIO Lio/sentry/MeasurementUnit$Fraction; + public fun apiName ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Lio/sentry/MeasurementUnit$Fraction; public static fun values ()[Lio/sentry/MeasurementUnit$Fraction; } @@ -839,6 +839,7 @@ public final class io/sentry/MeasurementUnit$Information : java/lang/Enum, io/se public static final field PETABYTE Lio/sentry/MeasurementUnit$Information; public static final field TEBIBYTE Lio/sentry/MeasurementUnit$Information; public static final field TERABYTE Lio/sentry/MeasurementUnit$Information; + public fun apiName ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Lio/sentry/MeasurementUnit$Information; public static fun values ()[Lio/sentry/MeasurementUnit$Information; } @@ -857,6 +858,7 @@ public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { } public final class io/sentry/NoOpHub : io/sentry/IHub { + public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun bindClient (Lio/sentry/ISentryClient;)V public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -1277,6 +1279,25 @@ public abstract interface class io/sentry/ScopeCallback { public abstract fun run (Lio/sentry/Scope;)V } +public abstract class io/sentry/ScopeObserverAdapter : io/sentry/IScopeObserver { + public fun ()V + public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V + public fun removeExtra (Ljava/lang/String;)V + public fun removeTag (Ljava/lang/String;)V + public fun setBreadcrumbs (Ljava/util/Collection;)V + public fun setContexts (Lio/sentry/protocol/Contexts;)V + public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V + public fun setExtras (Ljava/util/Map;)V + public fun setFingerprint (Ljava/util/Collection;)V + public fun setLevel (Lio/sentry/SentryLevel;)V + public fun setRequest (Lio/sentry/protocol/Request;)V + public fun setTag (Ljava/lang/String;Ljava/lang/String;)V + public fun setTags (Ljava/util/Map;)V + public fun setTrace (Lio/sentry/SpanContext;)V + public fun setTransaction (Ljava/lang/String;)V + public fun setUser (Lio/sentry/protocol/User;)V +} + public final class io/sentry/SendCachedEnvelopeFireAndForgetIntegration : io/sentry/Integration { public fun (Lio/sentry/SendCachedEnvelopeFireAndForgetIntegration$SendFireAndForgetFactory;)V public final fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V @@ -1830,7 +1851,6 @@ public class io/sentry/SentryOptions { public fun isEnableAutoSessionTracking ()Z public fun isEnableDeduplication ()Z public fun isEnableExternalConfiguration ()Z - public fun isEnableNdk ()Z public fun isEnablePrettySerializationOutput ()Z public fun isEnableShutdownHook ()Z public fun isEnableTimeToFullDisplayTracing ()Z @@ -2529,7 +2549,7 @@ public final class io/sentry/cache/PersistingOptionsObserver : io/sentry/IOption public fun setTags (Ljava/util/Map;)V } -public final class io/sentry/cache/PersistingScopeObserver : io/sentry/IScopeObserver { +public final class io/sentry/cache/PersistingScopeObserver : io/sentry/ScopeObserverAdapter { public static final field BREADCRUMBS_FILENAME Ljava/lang/String; public static final field CONTEXTS_FILENAME Ljava/lang/String; public static final field EXTRAS_FILENAME Ljava/lang/String; @@ -2681,9 +2701,9 @@ public final class io/sentry/exception/SentryHttpClientException : java/lang/Exc } public abstract interface class io/sentry/hints/AbnormalExit { - public fun ignoreCurrentThread ()Z + public abstract fun ignoreCurrentThread ()Z public abstract fun mechanism ()Ljava/lang/String; - public fun timestamp ()Ljava/lang/Long; + public abstract fun timestamp ()Ljava/lang/Long; } public abstract interface class io/sentry/hints/ApplyScopeData { @@ -4235,6 +4255,12 @@ public final class io/sentry/util/HttpUtils { public static fun isSecurityCookie (Ljava/lang/String;Ljava/util/List;)Z } +public final class io/sentry/util/IntegrationUtils { + public fun ()V + public static fun addIntegrationToSdkVersion (Ljava/lang/Class;)V + public static fun addIntegrationToSdkVersion (Ljava/lang/String;)V +} + public final class io/sentry/util/JsonSerializationUtils { public fun ()V public static fun atomicIntegerArrayToList (Ljava/util/concurrent/atomic/AtomicIntegerArray;)Ljava/util/List; @@ -4365,21 +4391,27 @@ public final class io/sentry/util/UrlUtils$UrlDetails { } public abstract interface class io/sentry/util/thread/IMainThreadChecker { - public fun isMainThread ()Z + public abstract fun isMainThread ()Z public abstract fun isMainThread (J)Z - public fun isMainThread (Lio/sentry/protocol/SentryThread;)Z - public fun isMainThread (Ljava/lang/Thread;)Z + public abstract fun isMainThread (Lio/sentry/protocol/SentryThread;)Z + public abstract fun isMainThread (Ljava/lang/Thread;)Z } public final class io/sentry/util/thread/MainThreadChecker : io/sentry/util/thread/IMainThreadChecker { public static fun getInstance ()Lio/sentry/util/thread/MainThreadChecker; + public fun isMainThread ()Z public fun isMainThread (J)Z + public fun isMainThread (Lio/sentry/protocol/SentryThread;)Z + public fun isMainThread (Ljava/lang/Thread;)Z } public final class io/sentry/util/thread/NoOpMainThreadChecker : io/sentry/util/thread/IMainThreadChecker { public fun ()V public static fun getInstance ()Lio/sentry/util/thread/NoOpMainThreadChecker; + public fun isMainThread ()Z public fun isMainThread (J)Z + public fun isMainThread (Lio/sentry/protocol/SentryThread;)Z + public fun isMainThread (Ljava/lang/Thread;)Z } public class io/sentry/vendor/Base64 { diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 90baf3ed87a..cc6dc0e6ef8 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -372,6 +372,11 @@ public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb, final @Nullable } } + @Override + public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb) { + addBreadcrumb(breadcrumb, new Hint()); + } + @Override public void setLevel(final @Nullable SentryLevel level) { if (!isEnabled()) { diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index a33fe4e8885..1a64484a6b7 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -87,6 +87,11 @@ public void addBreadcrumb(@NotNull Breadcrumb breadcrumb, @Nullable Hint hint) { Sentry.addBreadcrumb(breadcrumb, hint); } + @Override + public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb) { + addBreadcrumb(breadcrumb, new Hint()); + } + @Override public void setLevel(@Nullable SentryLevel level) { Sentry.setLevel(level); diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 42689a3ad33..36b695e11b3 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -203,9 +203,7 @@ SentryId captureException( * * @param breadcrumb the breadcrumb */ - default void addBreadcrumb(@NotNull Breadcrumb breadcrumb) { - addBreadcrumb(breadcrumb, new Hint()); - } + void addBreadcrumb(@NotNull Breadcrumb breadcrumb); /** * Adds a breadcrumb to the current Scope diff --git a/sentry/src/main/java/io/sentry/IOptionsObserver.java b/sentry/src/main/java/io/sentry/IOptionsObserver.java index 519e9222b56..54cacc666ae 100644 --- a/sentry/src/main/java/io/sentry/IOptionsObserver.java +++ b/sentry/src/main/java/io/sentry/IOptionsObserver.java @@ -11,15 +11,15 @@ */ public interface IOptionsObserver { - default void setRelease(@Nullable String release) {} + void setRelease(@Nullable String release); - default void setProguardUuid(@Nullable String proguardUuid) {} + void setProguardUuid(@Nullable String proguardUuid); - default void setSdkVersion(@Nullable SdkVersion sdkVersion) {} + void setSdkVersion(@Nullable SdkVersion sdkVersion); - default void setEnvironment(@Nullable String environment) {} + void setEnvironment(@Nullable String environment); - default void setDist(@Nullable String dist) {} + void setDist(@Nullable String dist); - default void setTags(@NotNull Map tags) {} + void setTags(@NotNull Map tags); } diff --git a/sentry/src/main/java/io/sentry/IScopeObserver.java b/sentry/src/main/java/io/sentry/IScopeObserver.java index d8d8bc68e60..4a103668d2a 100644 --- a/sentry/src/main/java/io/sentry/IScopeObserver.java +++ b/sentry/src/main/java/io/sentry/IScopeObserver.java @@ -13,33 +13,33 @@ * subscribe to only those properties, that they are interested in. */ public interface IScopeObserver { - default void setUser(@Nullable User user) {} + void setUser(@Nullable User user); - default void addBreadcrumb(@NotNull Breadcrumb crumb) {} + void addBreadcrumb(@NotNull Breadcrumb crumb); - default void setBreadcrumbs(@NotNull Collection breadcrumbs) {} + void setBreadcrumbs(@NotNull Collection breadcrumbs); - default void setTag(@NotNull String key, @NotNull String value) {} + void setTag(@NotNull String key, @NotNull String value); - default void removeTag(@NotNull String key) {} + void removeTag(@NotNull String key); - default void setTags(@NotNull Map tags) {} + void setTags(@NotNull Map tags); - default void setExtra(@NotNull String key, @NotNull String value) {} + void setExtra(@NotNull String key, @NotNull String value); - default void removeExtra(@NotNull String key) {} + void removeExtra(@NotNull String key); - default void setExtras(@NotNull Map extras) {} + void setExtras(@NotNull Map extras); - default void setRequest(@Nullable Request request) {} + void setRequest(@Nullable Request request); - default void setFingerprint(@NotNull Collection fingerprint) {} + void setFingerprint(@NotNull Collection fingerprint); - default void setLevel(@Nullable SentryLevel level) {} + void setLevel(@Nullable SentryLevel level); - default void setContexts(@NotNull Contexts contexts) {} + void setContexts(@NotNull Contexts contexts); - default void setTransaction(@Nullable String transaction) {} + void setTransaction(@Nullable String transaction); - default void setTrace(@Nullable SpanContext spanContext) {} + void setTrace(@Nullable SpanContext spanContext); } diff --git a/sentry/src/main/java/io/sentry/Integration.java b/sentry/src/main/java/io/sentry/Integration.java index 110a8ea516d..54b17e4d515 100644 --- a/sentry/src/main/java/io/sentry/Integration.java +++ b/sentry/src/main/java/io/sentry/Integration.java @@ -6,7 +6,7 @@ * Code that provides middlewares, bindings or hooks into certain frameworks or environments, along * with code that inserts those bindings and activates them. */ -public interface Integration extends IntegrationName { +public interface Integration { /** * Registers an integration * diff --git a/sentry/src/main/java/io/sentry/IntegrationName.java b/sentry/src/main/java/io/sentry/IntegrationName.java deleted file mode 100644 index ca3e98cd2cb..00000000000 --- a/sentry/src/main/java/io/sentry/IntegrationName.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.sentry; - -public interface IntegrationName { - default String getIntegrationName() { - return this.getClass() - .getSimpleName() - .replace("Sentry", "") - .replace("Integration", "") - .replace("Interceptor", "") - .replace("EventProcessor", ""); - } - - default void addIntegrationToSdkVersion() { - SentryIntegrationPackageStorage.getInstance().addIntegration(getIntegrationName()); - } -} diff --git a/sentry/src/main/java/io/sentry/MeasurementUnit.java b/sentry/src/main/java/io/sentry/MeasurementUnit.java index 5dc00261d1e..0e8ebe9bc29 100644 --- a/sentry/src/main/java/io/sentry/MeasurementUnit.java +++ b/sentry/src/main/java/io/sentry/MeasurementUnit.java @@ -47,6 +47,11 @@ enum Duration implements MeasurementUnit { /** Week (`"week"`), 604,800 seconds. */ WEEK; + + @Override + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } } /** Size of information derived from bytes. */ @@ -92,6 +97,11 @@ enum Information implements MeasurementUnit { /** Exbibyte (`"exbibyte"`), 2^60 bytes. */ EXBIBYTE; + + @Override + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } } /** Fractions such as percentages. */ @@ -101,6 +111,11 @@ enum Fraction implements MeasurementUnit { /** Ratio expressed as a fraction of `100`. `100%` equals a ratio of `1.0`. */ PERCENT; + + @Override + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } } /** @@ -119,6 +134,11 @@ public Custom(@NotNull String name) { public @NotNull String name() { return name; } + + @Override + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } } @NotNull @@ -126,7 +146,6 @@ public Custom(@NotNull String name) { /** Unit adhering to the API spec. */ @ApiStatus.Internal - default @NotNull String apiName() { - return name().toLowerCase(Locale.ROOT); - } + @NotNull + String apiName(); } diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 6ad44552bb0..79a0fb1960d 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -77,6 +77,9 @@ public void close() {} @Override public void addBreadcrumb(@NotNull Breadcrumb breadcrumb, @Nullable Hint hint) {} + @Override + public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb) {} + @Override public void setLevel(@Nullable SentryLevel level) {} diff --git a/sentry/src/main/java/io/sentry/ScopeObserverAdapter.java b/sentry/src/main/java/io/sentry/ScopeObserverAdapter.java new file mode 100644 index 00000000000..38d0cdf7a10 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ScopeObserverAdapter.java @@ -0,0 +1,56 @@ +package io.sentry; + +import io.sentry.protocol.Contexts; +import io.sentry.protocol.Request; +import io.sentry.protocol.User; +import java.util.Collection; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class ScopeObserverAdapter implements IScopeObserver { + @Override + public void setUser(@Nullable User user) {} + + @Override + public void addBreadcrumb(@NotNull Breadcrumb crumb) {} + + @Override + public void setBreadcrumbs(@NotNull Collection breadcrumbs) {} + + @Override + public void setTag(@NotNull String key, @NotNull String value) {} + + @Override + public void removeTag(@NotNull String key) {} + + @Override + public void setTags(@NotNull Map tags) {} + + @Override + public void setExtra(@NotNull String key, @NotNull String value) {} + + @Override + public void removeExtra(@NotNull String key) {} + + @Override + public void setExtras(@NotNull Map extras) {} + + @Override + public void setRequest(@Nullable Request request) {} + + @Override + public void setFingerprint(@NotNull Collection fingerprint) {} + + @Override + public void setLevel(@Nullable SentryLevel level) {} + + @Override + public void setContexts(@NotNull Contexts contexts) {} + + @Override + public void setTransaction(@Nullable String transaction) {} + + @Override + public void setTrace(@Nullable SpanContext spanContext) {} +} diff --git a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java index 01f9b960183..170b06c528b 100644 --- a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java +++ b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java @@ -1,5 +1,7 @@ package io.sentry; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import io.sentry.util.Objects; import java.io.File; import java.util.concurrent.RejectedExecutionException; @@ -88,7 +90,7 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions options .getLogger() .log(SentryLevel.DEBUG, "SendCachedEventFireAndForgetIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } catch (RejectedExecutionException e) { options .getLogger() diff --git a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java index c6a8785e46a..b144f2d88a3 100644 --- a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java +++ b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java @@ -1,5 +1,7 @@ package io.sentry; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; @@ -33,7 +35,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio thread = new Thread(() -> hub.flush(options.getFlushTimeoutMillis())); runtime.addShutdownHook(thread); options.getLogger().log(SentryLevel.DEBUG, "ShutdownHookIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } else { options.getLogger().log(SentryLevel.INFO, "enableShutdownHook is disabled."); } diff --git a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java index 1264e3780ec..775fe1a232e 100644 --- a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java +++ b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java @@ -1,5 +1,7 @@ package io.sentry; +import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; + import com.jakewharton.nopen.annotation.Open; import io.sentry.exception.ExceptionMechanismException; import io.sentry.hints.BlockingFlushHint; @@ -79,7 +81,7 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions this.options .getLogger() .log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration installed."); - addIntegrationToSdkVersion(); + addIntegrationToSdkVersion(getClass()); } } diff --git a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java index bfccc8d30f9..0c4a110733e 100644 --- a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java +++ b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java @@ -3,8 +3,8 @@ import static io.sentry.SentryLevel.ERROR; import io.sentry.Breadcrumb; -import io.sentry.IScopeObserver; import io.sentry.JsonDeserializer; +import io.sentry.ScopeObserverAdapter; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.SpanContext; @@ -16,7 +16,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class PersistingScopeObserver implements IScopeObserver { +public final class PersistingScopeObserver extends ScopeObserverAdapter { public static final String SCOPE_CACHE = ".scope-cache"; public static final String USER_FILENAME = "user.json"; diff --git a/sentry/src/main/java/io/sentry/hints/AbnormalExit.java b/sentry/src/main/java/io/sentry/hints/AbnormalExit.java index d8d5d304541..f9e67bc7607 100644 --- a/sentry/src/main/java/io/sentry/hints/AbnormalExit.java +++ b/sentry/src/main/java/io/sentry/hints/AbnormalExit.java @@ -10,13 +10,9 @@ public interface AbnormalExit { String mechanism(); /** Whether the current thread should be ignored from being marked as crashed, e.g. a watchdog */ - default boolean ignoreCurrentThread() { - return false; - } + boolean ignoreCurrentThread(); /** When exactly the abnormal exit happened */ @Nullable - default Long timestamp() { - return null; - } + Long timestamp(); } diff --git a/sentry/src/main/java/io/sentry/util/IntegrationUtils.java b/sentry/src/main/java/io/sentry/util/IntegrationUtils.java new file mode 100644 index 00000000000..6d504c14516 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/IntegrationUtils.java @@ -0,0 +1,23 @@ +package io.sentry.util; + +import io.sentry.SentryIntegrationPackageStorage; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class IntegrationUtils { + public static void addIntegrationToSdkVersion(final @NotNull Class clazz) { + final String name = + clazz + .getSimpleName() + .replace("Sentry", "") + .replace("Integration", "") + .replace("Interceptor", "") + .replace("EventProcessor", ""); + addIntegrationToSdkVersion(name); + } + + public static void addIntegrationToSdkVersion(final @NotNull String name) { + SentryIntegrationPackageStorage.getInstance().addIntegration(name); + } +} diff --git a/sentry/src/main/java/io/sentry/util/thread/IMainThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/IMainThreadChecker.java index 19cd1c76a0a..cf763b49592 100644 --- a/sentry/src/main/java/io/sentry/util/thread/IMainThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/IMainThreadChecker.java @@ -15,18 +15,14 @@ public interface IMainThreadChecker { * @param thread the Thread * @return true if it is the main thread or false otherwise */ - default boolean isMainThread(Thread thread) { - return isMainThread(thread.getId()); - } + boolean isMainThread(final @NotNull Thread thread); /** * Checks if the calling/current thread is the Main/UI thread * * @return true if it is the main thread or false otherwise */ - default boolean isMainThread() { - return isMainThread(Thread.currentThread()); - } + boolean isMainThread(); /** * Checks if a given thread is the Main/UI thread @@ -34,8 +30,5 @@ default boolean isMainThread() { * @param sentryThread the SentryThread * @return true if it is the main thread or false otherwise */ - default boolean isMainThread(final @NotNull SentryThread sentryThread) { - final Long threadId = sentryThread.getId(); - return threadId != null && isMainThread(threadId); - } + boolean isMainThread(final @NotNull SentryThread sentryThread); } diff --git a/sentry/src/main/java/io/sentry/util/thread/MainThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/MainThreadChecker.java index 0f61c29ce6f..c81ccbd6683 100644 --- a/sentry/src/main/java/io/sentry/util/thread/MainThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/MainThreadChecker.java @@ -1,6 +1,8 @@ package io.sentry.util.thread; +import io.sentry.protocol.SentryThread; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; /** * Class that checks if a given thread is the Main/UI thread. The Main thread is denoted by the @@ -25,4 +27,20 @@ private MainThreadChecker() {} public boolean isMainThread(long threadId) { return mainThreadId == threadId; } + + @Override + public boolean isMainThread(final @NotNull Thread thread) { + return isMainThread(thread.getId()); + } + + @Override + public boolean isMainThread() { + return isMainThread(Thread.currentThread()); + } + + @Override + public boolean isMainThread(final @NotNull SentryThread sentryThread) { + final Long threadId = sentryThread.getId(); + return threadId != null && isMainThread(threadId); + } } diff --git a/sentry/src/main/java/io/sentry/util/thread/NoOpMainThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/NoOpMainThreadChecker.java index bc1b6e58963..2248e363a4a 100644 --- a/sentry/src/main/java/io/sentry/util/thread/NoOpMainThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/NoOpMainThreadChecker.java @@ -1,6 +1,8 @@ package io.sentry.util.thread; +import io.sentry.protocol.SentryThread; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; @ApiStatus.Internal public final class NoOpMainThreadChecker implements IMainThreadChecker { @@ -15,4 +17,19 @@ public static NoOpMainThreadChecker getInstance() { public boolean isMainThread(long threadId) { return false; } + + @Override + public boolean isMainThread(@NotNull Thread thread) { + return false; + } + + @Override + public boolean isMainThread() { + return false; + } + + @Override + public boolean isMainThread(@NotNull SentryThread sentryThread) { + return false; + } } diff --git a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt index bbdd252ce30..ec932ebc861 100644 --- a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt @@ -617,5 +617,7 @@ class MainEventProcessorTest { override fun mechanism(): String? = null override fun ignoreCurrentThread(): Boolean = ignoreCurrentThread + + override fun timestamp(): Long? = null } } diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 41d3947a5e1..da9a82791a6 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -2557,6 +2557,8 @@ class SentryClientTest { private class AbnormalHint(private val mechanism: String? = null) : AbnormalExit { override fun mechanism(): String? = mechanism + override fun ignoreCurrentThread(): Boolean = false + override fun timestamp(): Long? = null } private fun eventProcessorThrows(): EventProcessor { diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index c2e685e96ce..d0f675d270c 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -8,6 +8,7 @@ import io.sentry.internal.modules.CompositeModulesLoader import io.sentry.internal.modules.IModulesLoader import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryId +import io.sentry.protocol.SentryThread import io.sentry.test.ImmediateExecutorService import io.sentry.util.PlatformTestManipulator import io.sentry.util.thread.IMainThreadChecker @@ -868,6 +869,9 @@ class SentryTest { private class CustomMainThreadChecker : IMainThreadChecker { override fun isMainThread(threadId: Long): Boolean = false + override fun isMainThread(thread: Thread): Boolean = false + override fun isMainThread(): Boolean = false + override fun isMainThread(sentryThread: SentryThread): Boolean = false } private class CustomMemoryCollector : ICollector { diff --git a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt index 33970f40939..260c6ae9810 100644 --- a/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt +++ b/sentry/src/test/java/io/sentry/cache/EnvelopeCacheTest.kt @@ -238,7 +238,11 @@ class EnvelopeCacheTest { fixture.options.serializer.serialize(session, previousSessionFile.bufferedWriter()) val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) - val abnormalHint = AbnormalExit { "abnormal_mechanism" } + val abnormalHint = object : AbnormalExit { + override fun mechanism(): String? = "abnormal_mechanism" + override fun ignoreCurrentThread(): Boolean = false + override fun timestamp(): Long? = null + } val hints = HintUtils.createWithTypeCheckHint(abnormalHint) cache.store(envelope, hints) @@ -261,7 +265,7 @@ class EnvelopeCacheTest { val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) val abnormalHint = object : AbnormalExit { override fun mechanism(): String = "abnormal_mechanism" - + override fun ignoreCurrentThread(): Boolean = false override fun timestamp(): Long = sessionExitedWithAbnormal } val hints = HintUtils.createWithTypeCheckHint(abnormalHint) @@ -284,7 +288,7 @@ class EnvelopeCacheTest { val envelope = SentryEnvelope.from(fixture.options.serializer, SentryEvent(), null) val abnormalHint = object : AbnormalExit { override fun mechanism(): String = "abnormal_mechanism" - + override fun ignoreCurrentThread(): Boolean = false override fun timestamp(): Long = sessionExitedWithAbnormal } val hints = HintUtils.createWithTypeCheckHint(abnormalHint) From 72e23fc1674693c90a7d9cfc36cf0f22181e03a4 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 27 Sep 2023 09:08:24 +0200 Subject: [PATCH 20/23] Capture unfinished transactions on crash (#2938) --- CHANGELOG.md | 2 + .../api/sentry-android-core.api | 2 + .../sentry/android/core/AnrV2Integration.java | 8 +++ .../core/ActivityLifecycleIntegrationTest.kt | 2 +- sentry/api/sentry.api | 18 ++++--- .../src/main/java/io/sentry/ITransaction.java | 8 ++- .../main/java/io/sentry/NoOpTransaction.java | 8 ++- .../src/main/java/io/sentry/SentryClient.java | 14 +++-- .../src/main/java/io/sentry/SentryTracer.java | 17 +++--- .../UncaughtExceptionHandlerIntegration.java | 23 +++++++- .../sentry/hints/DiskFlushNotification.java | 8 +++ .../sentry/transport/AsyncHttpTransport.java | 12 ++++- .../test/java/io/sentry/SentryClientTest.kt | 46 +++++++++++++++- .../test/java/io/sentry/SentryTracerTest.kt | 8 +-- ...UncaughtExceptionHandlerIntegrationTest.kt | 42 +++++++++++++++ .../transport/AsyncHttpTransportTest.kt | 52 +++++++++++++++++++ 16 files changed, 240 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58230171001..f71d7e9497a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ Breaking changes: - Measure AppStart time till First Draw instead of `onResume` ([#2851](https://github.com/getsentry/sentry-java/pull/2851)) - Do not overwrite UI transaction status if set by the user ([#2852](https://github.com/getsentry/sentry-java/pull/2852)) +- Capture unfinished transaction on Scope with status `aborted` in case a crash happens ([#2938](https://github.com/getsentry/sentry-java/pull/2938)) + - This will fix the link between transactions and corresponding crashes, you'll be able to see them in a single trace Breaking changes: - Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index bf9fcdfd334..42ff145ec1a 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -75,7 +75,9 @@ public class io/sentry/android/core/AnrV2Integration : io/sentry/Integration, ja public final class io/sentry/android/core/AnrV2Integration$AnrV2Hint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/AbnormalExit, io/sentry/hints/Backfillable { public fun (JLio/sentry/ILogger;JZZ)V public fun ignoreCurrentThread ()Z + public fun isFlushable (Lio/sentry/protocol/SentryId;)Z public fun mechanism ()Ljava/lang/String; + public fun setFlushable (Lio/sentry/protocol/SentryId;)V public fun shouldEnrich ()Z public fun timestamp ()Ljava/lang/Long; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 66278d35b88..6c7181641c2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -386,6 +386,14 @@ public boolean shouldEnrich() { public String mechanism() { return isBackgroundAnr ? "anr_background" : "anr_foreground"; } + + @Override + public boolean isFlushable(@Nullable SentryId eventId) { + return true; + } + + @Override + public void setFlushable(@NotNull SentryId eventId) {} } static final class ParseResult { 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 3ab3633f93e..d74cb6d090f 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 @@ -1465,7 +1465,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - fixture.transaction.forceFinish(OK, false) + fixture.transaction.forceFinish(OK, false, null) verify(fixture.activityFramesTracker).setMetrics(activity, fixture.transaction.eventId) } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index f29b86f9995..ecc49d177ac 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -643,8 +643,8 @@ public abstract interface class io/sentry/ISpan { } public abstract interface class io/sentry/ITransaction : io/sentry/ISpan { - public abstract fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;Z)V - public abstract fun forceFinish (Lio/sentry/SpanStatus;Z)V + public abstract fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;ZLio/sentry/Hint;)V + public abstract fun forceFinish (Lio/sentry/SpanStatus;ZLio/sentry/Hint;)V public abstract fun getContexts ()Lio/sentry/protocol/Contexts; public abstract fun getEventId ()Lio/sentry/protocol/SentryId; public abstract fun getLatestActiveSpan ()Lio/sentry/Span; @@ -954,8 +954,8 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V - public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;Z)V - public fun forceFinish (Lio/sentry/SpanStatus;Z)V + public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;ZLio/sentry/Hint;)V + public fun forceFinish (Lio/sentry/SpanStatus;ZLio/sentry/Hint;)V public fun getContexts ()Lio/sentry/protocol/Contexts; public fun getData (Ljava/lang/String;)Ljava/lang/Object; public fun getDescription ()Ljava/lang/String; @@ -2023,8 +2023,8 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V - public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;Z)V - public fun forceFinish (Lio/sentry/SpanStatus;Z)V + public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;ZLio/sentry/Hint;)V + public fun forceFinish (Lio/sentry/SpanStatus;ZLio/sentry/Hint;)V public fun getChildren ()Ljava/util/List; public fun getContexts ()Lio/sentry/protocol/Contexts; public fun getData ()Ljava/util/Map; @@ -2471,8 +2471,10 @@ public final class io/sentry/UncaughtExceptionHandlerIntegration : io/sentry/Int public fun uncaughtException (Ljava/lang/Thread;Ljava/lang/Throwable;)V } -public class io/sentry/UncaughtExceptionHandlerIntegration$UncaughtExceptionHint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/SessionEnd { +public class io/sentry/UncaughtExceptionHandlerIntegration$UncaughtExceptionHint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/SessionEnd, io/sentry/hints/TransactionEnd { public fun (JLio/sentry/ILogger;)V + public fun isFlushable (Lio/sentry/protocol/SentryId;)Z + public fun setFlushable (Lio/sentry/protocol/SentryId;)V } public final class io/sentry/UserFeedback : io/sentry/JsonSerializable, io/sentry/JsonUnknown { @@ -2723,7 +2725,9 @@ public abstract interface class io/sentry/hints/Cached { } public abstract interface class io/sentry/hints/DiskFlushNotification { + public abstract fun isFlushable (Lio/sentry/protocol/SentryId;)Z public abstract fun markFlushed ()V + public abstract fun setFlushable (Lio/sentry/protocol/SentryId;)V } public final class io/sentry/hints/EventDropReason : java/lang/Enum { diff --git a/sentry/src/main/java/io/sentry/ITransaction.java b/sentry/src/main/java/io/sentry/ITransaction.java index 127dc448ee4..a34e4918022 100644 --- a/sentry/src/main/java/io/sentry/ITransaction.java +++ b/sentry/src/main/java/io/sentry/ITransaction.java @@ -99,13 +99,17 @@ ISpan startChild( * @param dropIfNoChildren true, if the transaction should be dropped when it e.g. contains no * child spans. Usually true, but can be set to falseS for situations were the transaction and * profile provide crucial context (e.g. ANRs) + * @param hint An optional hint to pass down to the client/transport layer */ @ApiStatus.Internal - void forceFinish(@NotNull final SpanStatus status, boolean dropIfNoChildren); + void forceFinish(@NotNull final SpanStatus status, boolean dropIfNoChildren, @Nullable Hint hint); @ApiStatus.Internal void finish( - @Nullable SpanStatus status, @Nullable SentryDate timestamp, boolean dropIfNoChildren); + @Nullable SpanStatus status, + @Nullable SentryDate timestamp, + boolean dropIfNoChildren, + @Nullable Hint hint); @ApiStatus.Internal void setContext(@NotNull String key, @NotNull Object context); diff --git a/sentry/src/main/java/io/sentry/NoOpTransaction.java b/sentry/src/main/java/io/sentry/NoOpTransaction.java index 902c8a7da6c..5bf9f3a19f6 100644 --- a/sentry/src/main/java/io/sentry/NoOpTransaction.java +++ b/sentry/src/main/java/io/sentry/NoOpTransaction.java @@ -102,11 +102,15 @@ public void setName(@NotNull String name, @NotNull TransactionNameSource transac public void scheduleFinish() {} @Override - public void forceFinish(@NotNull SpanStatus status, boolean dropIfNoChildren) {} + public void forceFinish( + @NotNull SpanStatus status, boolean dropIfNoChildren, @Nullable Hint hint) {} @Override public void finish( - @Nullable SpanStatus status, @Nullable SentryDate timestamp, boolean dropIfNoChildren) {} + @Nullable SpanStatus status, + @Nullable SentryDate timestamp, + boolean dropIfNoChildren, + @Nullable Hint hint) {} @Override public boolean isFinished() { diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index bd981842e38..97090ae730f 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -4,6 +4,7 @@ import io.sentry.exception.SentryEnvelopeException; import io.sentry.hints.AbnormalExit; import io.sentry.hints.Backfillable; +import io.sentry.hints.DiskFlushNotification; import io.sentry.hints.TransactionEnd; import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; @@ -209,14 +210,19 @@ private boolean shouldApplyScopeData( sentryId = SentryId.EMPTY_ID; } - // if we encountered an abnormal exit finish tracing in order to persist and send + // if we encountered a crash/abnormal exit finish tracing in order to persist and send // any running transaction / profiling data if (scope != null) { - @Nullable ITransaction transaction = scope.getTransaction(); + final @Nullable ITransaction transaction = scope.getTransaction(); if (transaction != null) { - // TODO if we want to do the same for crashes, e.g. check for event.isCrashed() if (HintUtils.hasType(hint, TransactionEnd.class)) { - transaction.forceFinish(SpanStatus.ABORTED, false); + final Object sentrySdkHint = HintUtils.getSentrySdkHint(hint); + if (sentrySdkHint instanceof DiskFlushNotification) { + ((DiskFlushNotification) sentrySdkHint).setFlushable(transaction.getEventId()); + transaction.forceFinish(SpanStatus.ABORTED, false, hint); + } else { + transaction.forceFinish(SpanStatus.ABORTED, false, null); + } } } } diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 2593ea64a4b..bdea0885959 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -147,12 +147,14 @@ private void onDeadlineTimeoutReached() { final @Nullable SpanStatus status = getStatus(); forceFinish( (status != null) ? status : SpanStatus.DEADLINE_EXCEEDED, - transactionOptions.getIdleTimeout() != null); + transactionOptions.getIdleTimeout() != null, + null); isDeadlineTimerRunning.set(false); } @Override - public @NotNull void forceFinish(@NotNull SpanStatus status, boolean dropIfNoChildren) { + public @NotNull void forceFinish( + final @NotNull SpanStatus status, final boolean dropIfNoChildren, final @Nullable Hint hint) { if (isFinished()) { return; } @@ -168,12 +170,15 @@ private void onDeadlineTimeoutReached() { span.setSpanFinishedCallback(null); span.finish(status, finishTimestamp); } - finish(status, finishTimestamp, dropIfNoChildren); + finish(status, finishTimestamp, dropIfNoChildren, hint); } @Override public void finish( - @Nullable SpanStatus status, @Nullable SentryDate finishDate, boolean dropIfNoChildren) { + @Nullable SpanStatus status, + @Nullable SentryDate finishDate, + boolean dropIfNoChildren, + @Nullable Hint hint) { // try to get the high precision timestamp from the root span SentryDate finishTimestamp = root.getFinishDate(); @@ -259,7 +264,7 @@ public void finish( } transaction.getMeasurements().putAll(measurements); - hub.captureTransaction(transaction, traceContext(), null, profilingTraceData); + hub.captureTransaction(transaction, traceContext(), hint, profilingTraceData); } } @@ -537,7 +542,7 @@ public void finish(@Nullable SpanStatus status) { @Override @ApiStatus.Internal public void finish(@Nullable SpanStatus status, @Nullable SentryDate finishDate) { - finish(status, finishDate, true); + finish(status, finishDate, true, null); } @Override diff --git a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java index 775fe1a232e..33e1a4a815b 100644 --- a/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java +++ b/sentry/src/main/java/io/sentry/UncaughtExceptionHandlerIntegration.java @@ -7,11 +7,13 @@ import io.sentry.hints.BlockingFlushHint; import io.sentry.hints.EventDropReason; import io.sentry.hints.SessionEnd; +import io.sentry.hints.TransactionEnd; import io.sentry.protocol.Mechanism; import io.sentry.protocol.SentryId; import io.sentry.util.HintUtils; import io.sentry.util.Objects; import java.io.Closeable; +import java.util.concurrent.atomic.AtomicReference; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -97,6 +99,11 @@ public void uncaughtException(Thread thread, Throwable thrown) { final SentryEvent event = new SentryEvent(throwable); event.setLevel(SentryLevel.FATAL); + final ITransaction transaction = hub.getTransaction(); + if (transaction == null && event.getEventId() != null) { + // if there's no active transaction on scope, this event can trigger flush notification + exceptionHint.setFlushable(event.getEventId()); + } final Hint hint = HintUtils.createWithTypeCheckHint(exceptionHint); final @NotNull SentryId sentryId = hub.captureEvent(event, hint); @@ -156,10 +163,24 @@ public void close() { @Open // open for tests @ApiStatus.Internal - public static class UncaughtExceptionHint extends BlockingFlushHint implements SessionEnd { + public static class UncaughtExceptionHint extends BlockingFlushHint + implements SessionEnd, TransactionEnd { + + private final AtomicReference flushableEventId = new AtomicReference<>(); public UncaughtExceptionHint(final long flushTimeoutMillis, final @NotNull ILogger logger) { super(flushTimeoutMillis, logger); } + + @Override + public boolean isFlushable(final @Nullable SentryId eventId) { + final SentryId unwrapped = flushableEventId.get(); + return unwrapped != null && unwrapped.equals(eventId); + } + + @Override + public void setFlushable(final @NotNull SentryId eventId) { + flushableEventId.set(eventId); + } } } diff --git a/sentry/src/main/java/io/sentry/hints/DiskFlushNotification.java b/sentry/src/main/java/io/sentry/hints/DiskFlushNotification.java index 52d32e8506b..cfe54d83119 100644 --- a/sentry/src/main/java/io/sentry/hints/DiskFlushNotification.java +++ b/sentry/src/main/java/io/sentry/hints/DiskFlushNotification.java @@ -1,5 +1,13 @@ package io.sentry.hints; +import io.sentry.protocol.SentryId; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + public interface DiskFlushNotification { void markFlushed(); + + boolean isFlushable(@Nullable SentryId eventId); + + void setFlushable(@NotNull SentryId eventId); } diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index d750b96c7c7..c866336a431 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -230,8 +230,16 @@ public void run() { hint, DiskFlushNotification.class, (diskFlushNotification) -> { - diskFlushNotification.markFlushed(); - options.getLogger().log(SentryLevel.DEBUG, "Disk flush envelope fired"); + if (diskFlushNotification.isFlushable(envelope.getHeader().getEventId())) { + diskFlushNotification.markFlushed(); + options.getLogger().log(SentryLevel.DEBUG, "Disk flush envelope fired"); + } else { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Not firing envelope flush as there's an ongoing transaction"); + } }); if (transportGate.isConnected()) { diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index da9a82791a6..d8d775bd8c2 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -11,6 +11,7 @@ import io.sentry.hints.AbnormalExit import io.sentry.hints.ApplyScopeData import io.sentry.hints.Backfillable import io.sentry.hints.Cached +import io.sentry.hints.DiskFlushNotification import io.sentry.hints.TransactionEnd import io.sentry.protocol.Contexts import io.sentry.protocol.Mechanism @@ -2170,7 +2171,50 @@ class SentryClientTest { sut.captureEvent(SentryEvent(), scope, transactionEndHint) - verify(transaction).forceFinish(SpanStatus.ABORTED, false) + verify(transaction).forceFinish(SpanStatus.ABORTED, false, null) + verify(fixture.transport).send( + check { + assertEquals(1, it.items.count()) + }, + any() + ) + } + + @Test + fun `when event has DiskFlushNotification, TransactionEnds set transaction id as flushable`() { + val sut = fixture.getSut() + + // build up a running transaction + val spanContext = SpanContext("op.load") + val transaction = mock() + whenever(transaction.name).thenReturn("transaction") + whenever(transaction.eventId).thenReturn(SentryId()) + whenever(transaction.spanContext).thenReturn(spanContext) + + // scope + val scope = mock() + whenever(scope.transaction).thenReturn(transaction) + whenever(scope.breadcrumbs).thenReturn(LinkedList()) + whenever(scope.extras).thenReturn(emptyMap()) + whenever(scope.contexts).thenReturn(Contexts()) + val scopePropagationContext = PropagationContext() + whenever(scope.propagationContext).thenReturn(scopePropagationContext) + doAnswer { (it.arguments[0] as IWithPropagationContext).accept(scopePropagationContext); scopePropagationContext }.whenever(scope).withPropagationContext(any()) + + var capturedEventId: SentryId? = null + val transactionEnd = object : TransactionEnd, DiskFlushNotification { + override fun markFlushed() {} + override fun isFlushable(eventId: SentryId?): Boolean = true + override fun setFlushable(eventId: SentryId) { + capturedEventId = eventId + } + } + val transactionEndHint = HintUtils.createWithTypeCheckHint(transactionEnd) + + sut.captureEvent(SentryEvent(), scope, transactionEndHint) + + assertEquals(transaction.eventId, capturedEventId) + verify(transaction).forceFinish(SpanStatus.ABORTED, false, transactionEndHint) verify(fixture.transport).send( check { assertEquals(1, it.items.count()) diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index ad276ad1ff0..be5f05c3d85 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -1195,7 +1195,7 @@ class SentryTracerTest { // and it's finished transaction.finish(SpanStatus.OK) // but forceFinish is called as well - transaction.forceFinish(SpanStatus.ABORTED, false) + transaction.forceFinish(SpanStatus.ABORTED, false, null) // then it should keep it's original status assertEquals(SpanStatus.OK, transaction.status) @@ -1218,7 +1218,7 @@ class SentryTracerTest { span0.finish(SpanStatus.OK) val span0FinishDate = span0.finishDate - transaction.forceFinish(SpanStatus.ABORTED, false) + transaction.forceFinish(SpanStatus.ABORTED, false, null) // then the first span should keep it's status assertTrue(span0.isFinished) @@ -1251,7 +1251,7 @@ class SentryTracerTest { ) // and force-finished but dropping is disabled - transaction.forceFinish(SpanStatus.ABORTED, false) + transaction.forceFinish(SpanStatus.ABORTED, false, null) // then a transaction should be captured with 0 spans verify(fixture.hub).captureTransaction( @@ -1274,7 +1274,7 @@ class SentryTracerTest { ) // and force-finish with dropping enabled - transaction.forceFinish(SpanStatus.ABORTED, true) + transaction.forceFinish(SpanStatus.ABORTED, true, null) // then the transaction should be captured with 0 spans verify(fixture.hub, never()).captureTransaction( diff --git a/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt b/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt index 023194190bf..01353d5ac03 100644 --- a/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt +++ b/sentry/src/test/java/io/sentry/UncaughtExceptionHandlerIntegrationTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.UncaughtExceptionHandlerIntegration.UncaughtExceptionHint import io.sentry.exception.ExceptionMechanismException import io.sentry.hints.DiskFlushNotification import io.sentry.hints.EventDropReason.MULTITHREADED_DEDUPLICATION @@ -249,4 +250,45 @@ class UncaughtExceptionHandlerIntegrationTest { any() ) } + + @Test + fun `when there is no active transaction on scope, sets current event id as flushable`() { + val eventCaptor = argumentCaptor() + whenever(fixture.hub.captureEvent(eventCaptor.capture(), any())) + .thenReturn(SentryId.EMPTY_ID) + + val sut = fixture.getSut() + + sut.register(fixture.hub, fixture.options) + sut.uncaughtException(fixture.thread, fixture.throwable) + + verify(fixture.hub).captureEvent( + any(), + argThat { + (HintUtils.getSentrySdkHint(this) as UncaughtExceptionHint) + .isFlushable(eventCaptor.firstValue.eventId) + } + ) + } + + @Test + fun `when there is active transaction on scope, does not set current event id as flushable`() { + val eventCaptor = argumentCaptor() + whenever(fixture.hub.transaction).thenReturn(mock()) + whenever(fixture.hub.captureEvent(eventCaptor.capture(), any())) + .thenReturn(SentryId.EMPTY_ID) + + val sut = fixture.getSut() + + sut.register(fixture.hub, fixture.options) + sut.uncaughtException(fixture.thread, fixture.throwable) + + verify(fixture.hub).captureEvent( + any(), + argThat { + !(HintUtils.getSentrySdkHint(this) as UncaughtExceptionHint) + .isFlushable(eventCaptor.firstValue.eventId) + } + ) + } } diff --git a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt index 20f2ff69799..8acb718f3a3 100644 --- a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt +++ b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt @@ -12,6 +12,8 @@ import io.sentry.SentryOptionsManipulator import io.sentry.Session import io.sentry.clientreport.NoOpClientReportRecorder import io.sentry.dsnString +import io.sentry.hints.DiskFlushNotification +import io.sentry.protocol.SentryId import io.sentry.protocol.User import io.sentry.util.HintUtils import org.mockito.kotlin.any @@ -28,6 +30,8 @@ import java.util.Date import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class AsyncHttpTransportTest { @@ -331,6 +335,54 @@ class AsyncHttpTransportTest { verify(fixture.executor).awaitTermination(eq(123), eq(TimeUnit.MILLISECONDS)) } + @Test + fun `when DiskFlushNotification is not flushable, does not flush`() { + // given + val ev = SentryEvent() + val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, ev, null) + whenever(fixture.rateLimiter.filter(any(), anyOrNull())).thenAnswer { it.arguments[0] } + + var calledFlush = false + val sentryHint = object : DiskFlushNotification { + override fun markFlushed() { + calledFlush = true + } + override fun isFlushable(eventId: SentryId?): Boolean = false + override fun setFlushable(eventId: SentryId) = Unit + } + val hint = HintUtils.createWithTypeCheckHint(sentryHint) + + // when + fixture.getSUT().send(envelope, hint) + + // then + assertFalse(calledFlush) + } + + @Test + fun `when DiskFlushNotification is flushable, marks it as flushed`() { + // given + val ev = SentryEvent() + val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, ev, null) + whenever(fixture.rateLimiter.filter(any(), anyOrNull())).thenAnswer { it.arguments[0] } + + var calledFlush = false + val sentryHint = object : DiskFlushNotification { + override fun markFlushed() { + calledFlush = true + } + override fun isFlushable(eventId: SentryId?): Boolean = envelope.header.eventId == eventId + override fun setFlushable(eventId: SentryId) = Unit + } + val hint = HintUtils.createWithTypeCheckHint(sentryHint) + + // when + fixture.getSUT().send(envelope, hint) + + // then + assertTrue(calledFlush) + } + private fun createSession(): Session { return Session("123", User(), "env", "release") } From d6cd78c586a0bc4451daae7317fed03f8464705d Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 28 Sep 2023 17:29:43 +0200 Subject: [PATCH 21/23] Observe network state to upload any unsent envelopes (#2910) Co-authored-by: Roman Zavarnitsyn Co-authored-by: Sentry Github Bot --- CHANGELOG.md | 4 + .../core/AndroidOptionsInitializer.java | 9 +- .../android/core/AndroidTransportGate.java | 17 +-- .../sentry/android/core/DeviceInfoUtil.java | 8 +- .../core/EnvelopeFileObserverIntegration.java | 3 +- .../core/NetworkBreadcrumbsIntegration.java | 8 +- .../core/SendCachedEnvelopeIntegration.java | 90 ++++++++++--- ...a => AndroidConnectionStatusProvider.java} | 107 ++++++++++++--- ...=> AndroidConnectionStatusProviderTest.kt} | 124 +++++++++++++----- .../android/core/AndroidTransportGateTest.kt | 13 +- .../android/core/InternalSentrySdkTest.kt | 5 + .../core/SendCachedEnvelopeIntegrationTest.kt | 97 +++++++++++++- .../core/SessionTrackingIntegrationTest.kt | 5 + .../api/sentry-apache-http-client-5.api | 1 + .../apache/ApacheHttpClientTransport.java | 5 + sentry/api/sentry.api | 55 +++++++- .../java/io/sentry/DirectoryProcessor.java | 65 +++++++-- .../main/java/io/sentry/EnvelopeSender.java | 5 +- sentry/src/main/java/io/sentry/Hub.java | 8 ++ .../src/main/java/io/sentry/HubAdapter.java | 7 + .../io/sentry/IConnectionStatusProvider.java | 57 ++++++++ sentry/src/main/java/io/sentry/IHub.java | 5 + .../main/java/io/sentry/ISentryClient.java | 5 + .../sentry/NoOpConnectionStatusProvider.java | 28 ++++ sentry/src/main/java/io/sentry/NoOpHub.java | 6 + .../main/java/io/sentry/NoOpSentryClient.java | 6 + .../src/main/java/io/sentry/OutboxSender.java | 5 +- ...achedEnvelopeFireAndForgetIntegration.java | 77 +++++++++-- .../SendFireAndForgetEnvelopeSender.java | 6 +- .../sentry/SendFireAndForgetOutboxSender.java | 3 +- .../src/main/java/io/sentry/SentryClient.java | 6 + .../main/java/io/sentry/SentryOptions.java | 13 ++ .../main/java/io/sentry/hints/Enqueable.java | 6 + .../sentry/transport/AsyncHttpTransport.java | 14 ++ .../java/io/sentry/transport/ITransport.java | 4 + .../sentry/transport/NoOpEnvelopeCache.java | 4 +- .../io/sentry/transport/NoOpTransport.java | 6 + .../java/io/sentry/transport/RateLimiter.java | 45 ++++--- .../io/sentry/transport/StdoutTransport.java | 6 + .../java/io/sentry/DirectoryProcessorTest.kt | 79 +++++++++-- .../test/java/io/sentry/EnvelopeSenderTest.kt | 12 +- .../NoOpConnectionStatusProviderTest.kt | 33 +++++ .../test/java/io/sentry/OutboxSenderTest.kt | 35 +---- ...hedEnvelopeFireAndForgetIntegrationTest.kt | 107 ++++++++++++++- .../test/java/io/sentry/SentryOptionsTest.kt | 25 ++++ .../transport/AsyncHttpTransportTest.kt | 19 +++ 46 files changed, 1042 insertions(+), 206 deletions(-) rename sentry-android-core/src/main/java/io/sentry/android/core/internal/util/{ConnectivityChecker.java => AndroidConnectionStatusProvider.java} (73%) rename sentry-android-core/src/test/java/io/sentry/android/core/{ConnectivityCheckerTest.kt => AndroidConnectionStatusProviderTest.kt} (61%) create mode 100644 sentry/src/main/java/io/sentry/IConnectionStatusProvider.java create mode 100644 sentry/src/main/java/io/sentry/NoOpConnectionStatusProvider.java create mode 100644 sentry/src/main/java/io/sentry/hints/Enqueable.java create mode 100644 sentry/src/test/java/io/sentry/NoOpConnectionStatusProviderTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index f71d7e9497a..60b52ea9c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ Breaking changes: - Automatic user interaction tracking: every click now starts a new automatic transaction ([#2891](https://github.com/getsentry/sentry-java/pull/2891)) - Previously performing a click on the same UI widget twice would keep the existing transaction running, the new behavior now better aligns with other SDKs - Android only: If global hub mode is enabled, Sentry.getSpan() returns the root span instead of the latest span ([#2855](https://github.com/getsentry/sentry-java/pull/2855)) +- Observe network state to upload any unsent envelopes ([#2910](https://github.com/getsentry/sentry-java/pull/2910)) + - Android: it works out-of-the-box as part of the default `SendCachedEnvelopeIntegration` + - JVM: you'd have to install `SendCachedEnvelopeFireAndForgetIntegration` as mentioned in https://docs.sentry.io/platforms/java/configuration/#configuring-offline-caching and provide your own implementation of `IConnectionStatusProvider` via `SentryOptions` +- Do not try to send and drop cached envelopes when rate-limiting is active ([#2937](https://github.com/getsentry/sentry-java/pull/2937)) ### Fixes 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 60c3160fe8b..e014d1ac402 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 @@ -8,6 +8,7 @@ import io.sentry.DeduplicateMultithreadedEventProcessor; import io.sentry.DefaultTransactionPerformanceCollector; import io.sentry.ILogger; +import io.sentry.NoOpConnectionStatusProvider; import io.sentry.SendFireAndForgetEnvelopeSender; import io.sentry.SendFireAndForgetOutboxSender; import io.sentry.SentryLevel; @@ -15,6 +16,7 @@ import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader; import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator; import io.sentry.android.core.internal.modules.AssetsModulesLoader; +import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider; import io.sentry.android.core.internal.util.AndroidMainThreadChecker; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.fragment.FragmentLifecycleIntegration; @@ -130,6 +132,11 @@ static void initializeIntegrationsAndProcessors( options.setEnvelopeDiskCache(new AndroidEnvelopeCache(options)); } + if (options.getConnectionStatusProvider() instanceof NoOpConnectionStatusProvider) { + options.setConnectionStatusProvider( + new AndroidConnectionStatusProvider(context, options.getLogger(), buildInfoProvider)); + } + options.addEventProcessor(new DeduplicateMultithreadedEventProcessor(options)); options.addEventProcessor( new DefaultAndroidEventProcessor(context, buildInfoProvider, options)); @@ -137,7 +144,7 @@ static void initializeIntegrationsAndProcessors( options.addEventProcessor(new ScreenshotEventProcessor(options, buildInfoProvider)); options.addEventProcessor(new ViewHierarchyEventProcessor(options)); options.addEventProcessor(new AnrV2EventProcessor(context, options, buildInfoProvider)); - options.setTransportGate(new AndroidTransportGate(context, options.getLogger())); + options.setTransportGate(new AndroidTransportGate(options)); final SentryFrameMetricsCollector frameMetricsCollector = new SentryFrameMetricsCollector(context, options, buildInfoProvider); options.setTransactionProfiler( diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransportGate.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransportGate.java index fd9215970a3..fa138a802f2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransportGate.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransportGate.java @@ -1,29 +1,26 @@ package io.sentry.android.core; -import android.content.Context; -import io.sentry.ILogger; -import io.sentry.android.core.internal.util.ConnectivityChecker; +import io.sentry.IConnectionStatusProvider; +import io.sentry.SentryOptions; import io.sentry.transport.ITransportGate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.TestOnly; final class AndroidTransportGate implements ITransportGate { - private final Context context; - private final @NotNull ILogger logger; + private final @NotNull SentryOptions options; - AndroidTransportGate(final @NotNull Context context, final @NotNull ILogger logger) { - this.context = context; - this.logger = logger; + AndroidTransportGate(final @NotNull SentryOptions options) { + this.options = options; } @Override public boolean isConnected() { - return isConnected(ConnectivityChecker.getConnectionStatus(context, logger)); + return isConnected(options.getConnectionStatusProvider().getConnectionStatus()); } @TestOnly - boolean isConnected(final @NotNull ConnectivityChecker.Status status) { + boolean isConnected(final @NotNull IConnectionStatusProvider.ConnectionStatus status) { // let's assume its connected if there's no permission or something as we can't really know // whether is online or not. switch (status) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java index f9fcbf972d2..9ad18e26d8a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DeviceInfoUtil.java @@ -16,7 +16,6 @@ import android.util.DisplayMetrics; import io.sentry.DateUtils; import io.sentry.SentryLevel; -import io.sentry.android.core.internal.util.ConnectivityChecker; import io.sentry.android.core.internal.util.CpuInfoUtils; import io.sentry.android.core.internal.util.DeviceOrientations; import io.sentry.android.core.internal.util.RootChecker; @@ -180,8 +179,8 @@ private void setDeviceIO(final @NotNull Device device, final boolean includeDyna } Boolean connected; - switch (ConnectivityChecker.getConnectionStatus(context, options.getLogger())) { - case NOT_CONNECTED: + switch (options.getConnectionStatusProvider().getConnectionStatus()) { + case DISCONNECTED: connected = false; break; case CONNECTED: @@ -223,8 +222,7 @@ private void setDeviceIO(final @NotNull Device device, final boolean includeDyna if (device.getConnectionType() == null) { // wifi, ethernet or cellular, null if none - device.setConnectionType( - ConnectivityChecker.getConnectionType(context, options.getLogger(), buildInfoProvider)); + device.setConnectionType(options.getConnectionStatusProvider().getConnectionType()); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java index 140b9449822..7dfa784555c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/EnvelopeFileObserverIntegration.java @@ -43,7 +43,8 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions options.getEnvelopeReader(), options.getSerializer(), logger, - options.getFlushTimeoutMillis()); + options.getFlushTimeoutMillis(), + options.getMaxQueueSize()); observer = new EnvelopeFileObserver(path, outboxSender, logger, options.getFlushTimeoutMillis()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java index 6aa7caf4c90..6d6a0daa407 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java @@ -18,7 +18,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.TypeCheckHint; -import io.sentry.android.core.internal.util.ConnectivityChecker; +import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider; import io.sentry.util.Objects; import java.io.Closeable; import java.io.IOException; @@ -71,7 +71,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio networkCallback = new NetworkBreadcrumbsNetworkCallback(hub, buildInfoProvider); final boolean registered = - ConnectivityChecker.registerNetworkCallback( + AndroidConnectionStatusProvider.registerNetworkCallback( context, logger, buildInfoProvider, networkCallback); // The specific error is logged in the ConnectivityChecker method @@ -88,7 +88,7 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio @Override public void close() throws IOException { if (networkCallback != null) { - ConnectivityChecker.unregisterNetworkCallback( + AndroidConnectionStatusProvider.unregisterNetworkCallback( context, logger, buildInfoProvider, networkCallback); logger.log(SentryLevel.DEBUG, "NetworkBreadcrumbsIntegration remove."); } @@ -210,7 +210,7 @@ static class NetworkBreadcrumbConnectionDetail { this.signalStrength = strength > -100 ? strength : 0; this.isVpn = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN); String connectionType = - ConnectivityChecker.getConnectionType(networkCapabilities, buildInfoProvider); + AndroidConnectionStatusProvider.getConnectionType(networkCapabilities, buildInfoProvider); this.type = connectionType != null ? connectionType : ""; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java index e0d08325b45..061d2d232ca 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SendCachedEnvelopeIntegration.java @@ -1,23 +1,36 @@ package io.sentry.android.core; +import io.sentry.DataCategory; +import io.sentry.IConnectionStatusProvider; import io.sentry.IHub; import io.sentry.Integration; import io.sentry.SendCachedEnvelopeFireAndForgetIntegration; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.transport.RateLimiter; import io.sentry.util.LazyEvaluator; import io.sentry.util.Objects; +import java.io.Closeable; +import java.io.IOException; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -final class SendCachedEnvelopeIntegration implements Integration { +final class SendCachedEnvelopeIntegration + implements Integration, IConnectionStatusProvider.IConnectionStatusObserver, Closeable { private final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory factory; private final @NotNull LazyEvaluator startupCrashMarkerEvaluator; + private final AtomicBoolean startupCrashHandled = new AtomicBoolean(false); + private @Nullable IConnectionStatusProvider connectionStatusProvider; + private @Nullable IHub hub; + private @Nullable SentryAndroidOptions options; + private @Nullable SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget sender; public SendCachedEnvelopeIntegration( final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory factory, @@ -28,8 +41,8 @@ public SendCachedEnvelopeIntegration( @Override public void register(@NotNull IHub hub, @NotNull SentryOptions options) { - Objects.requireNonNull(hub, "Hub is required"); - final SentryAndroidOptions androidOptions = + this.hub = Objects.requireNonNull(hub, "Hub is required"); + this.options = Objects.requireNonNull( (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, "SentryAndroidOptions is required"); @@ -40,51 +53,92 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { return; } - final SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget sender = - factory.create(hub, androidOptions); + connectionStatusProvider = options.getConnectionStatusProvider(); + connectionStatusProvider.addConnectionStatusObserver(this); + + sender = factory.create(hub, options); + + sendCachedEnvelopes(hub, this.options); + } + + @Override + public void close() throws IOException { + if (connectionStatusProvider != null) { + connectionStatusProvider.removeConnectionStatusObserver(this); + } + } + + @Override + public void onConnectionStatusChanged( + final @NotNull IConnectionStatusProvider.ConnectionStatus status) { + if (hub != null && options != null) { + sendCachedEnvelopes(hub, options); + } + } + + @SuppressWarnings({"NullAway"}) + private synchronized void sendCachedEnvelopes( + final @NotNull IHub hub, final @NotNull SentryAndroidOptions options) { + + if (connectionStatusProvider != null + && connectionStatusProvider.getConnectionStatus() + == IConnectionStatusProvider.ConnectionStatus.DISCONNECTED) { + options.getLogger().log(SentryLevel.INFO, "SendCachedEnvelopeIntegration, no connection."); + return; + } + + // in case there's rate limiting active, skip processing + final @Nullable RateLimiter rateLimiter = hub.getRateLimiter(); + if (rateLimiter != null && rateLimiter.isActiveForCategory(DataCategory.All)) { + options + .getLogger() + .log(SentryLevel.INFO, "SendCachedEnvelopeIntegration, rate limiting active."); + return; + } if (sender == null) { - androidOptions.getLogger().log(SentryLevel.ERROR, "SendFireAndForget factory is null."); + options.getLogger().log(SentryLevel.ERROR, "SendCachedEnvelopeIntegration factory is null."); return; } try { - Future future = - androidOptions + final Future future = + options .getExecutorService() .submit( () -> { try { sender.send(); } catch (Throwable e) { - androidOptions + options .getLogger() .log(SentryLevel.ERROR, "Failed trying to send cached events.", e); } }); - if (startupCrashMarkerEvaluator.getValue()) { - androidOptions - .getLogger() - .log(SentryLevel.DEBUG, "Startup Crash marker exists, blocking flush."); + // startupCrashMarkerEvaluator remains true on subsequent runs, let's ensure we only block on + // the very first execution (=app start) + if (startupCrashMarkerEvaluator.getValue() + && startupCrashHandled.compareAndSet(false, true)) { + options.getLogger().log(SentryLevel.DEBUG, "Startup Crash marker exists, blocking flush."); try { - future.get(androidOptions.getStartupCrashFlushTimeoutMillis(), TimeUnit.MILLISECONDS); + future.get(options.getStartupCrashFlushTimeoutMillis(), TimeUnit.MILLISECONDS); } catch (TimeoutException e) { - androidOptions + options .getLogger() .log(SentryLevel.DEBUG, "Synchronous send timed out, continuing in the background."); } } - androidOptions.getLogger().log(SentryLevel.DEBUG, "SendCachedEnvelopeIntegration installed."); + options.getLogger().log(SentryLevel.DEBUG, "SendCachedEnvelopeIntegration installed."); } catch (RejectedExecutionException e) { - androidOptions + options .getLogger() .log( SentryLevel.ERROR, "Failed to call the executor. Cached events will not be sent. Did you call Sentry.close()?", e); } catch (Throwable e) { - androidOptions + options .getLogger() .log(SentryLevel.ERROR, "Failed to call the executor. Cached events will not be sent", e); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ConnectivityChecker.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java similarity index 73% rename from sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ConnectivityChecker.java rename to sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index 113ad55120d..30aeea685f2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ConnectivityChecker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -7,12 +7,17 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.os.Build; +import androidx.annotation.NonNull; +import io.sentry.IConnectionStatusProvider; import io.sentry.ILogger; import io.sentry.SentryLevel; import io.sentry.android.core.BuildInfoProvider; +import java.util.HashMap; +import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; /** * Note: ConnectivityManager sometimes throws SecurityExceptions on Android 11. Hence all relevant @@ -20,27 +25,29 @@ * details */ @ApiStatus.Internal -public final class ConnectivityChecker { +public final class AndroidConnectionStatusProvider implements IConnectionStatusProvider { - public enum Status { - CONNECTED, - NOT_CONNECTED, - NO_PERMISSION, - UNKNOWN - } + private final @NotNull Context context; + private final @NotNull ILogger logger; + private final @NotNull BuildInfoProvider buildInfoProvider; + private final @NotNull Map + registeredCallbacks; - private ConnectivityChecker() {} + public AndroidConnectionStatusProvider( + @NotNull Context context, + @NotNull ILogger logger, + @NotNull BuildInfoProvider buildInfoProvider) { + this.context = context; + this.logger = logger; + this.buildInfoProvider = buildInfoProvider; + this.registeredCallbacks = new HashMap<>(); + } - /** - * Return the Connection status - * - * @return the ConnectionStatus - */ - public static @NotNull ConnectivityChecker.Status getConnectionStatus( - final @NotNull Context context, final @NotNull ILogger logger) { + @Override + public @NotNull ConnectionStatus getConnectionStatus() { final ConnectivityManager connectivityManager = getConnectivityManager(context, logger); if (connectivityManager == null) { - return Status.UNKNOWN; + return ConnectionStatus.UNKNOWN; } return getConnectionStatus(context, connectivityManager, logger); // getActiveNetworkInfo might return null if VPN doesn't specify its @@ -50,6 +57,55 @@ private ConnectivityChecker() {} // connectivityManager.registerDefaultNetworkCallback(...) } + @Override + public @Nullable String getConnectionType() { + return getConnectionType(context, logger, buildInfoProvider); + } + + @SuppressLint("NewApi") // we have an if-check for that down below + @Override + public boolean addConnectionStatusObserver(final @NotNull IConnectionStatusObserver observer) { + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) { + logger.log(SentryLevel.DEBUG, "addConnectionStatusObserver requires Android 5+."); + return false; + } + + final ConnectivityManager.NetworkCallback callback = + new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(@NonNull Network network) { + observer.onConnectionStatusChanged(getConnectionStatus()); + } + + @Override + public void onLosing(@NonNull Network network, int maxMsToLive) { + observer.onConnectionStatusChanged(getConnectionStatus()); + } + + @Override + public void onLost(@NonNull Network network) { + observer.onConnectionStatusChanged(getConnectionStatus()); + } + + @Override + public void onUnavailable() { + observer.onConnectionStatusChanged(getConnectionStatus()); + } + }; + + registeredCallbacks.put(observer, callback); + return registerNetworkCallback(context, logger, buildInfoProvider, callback); + } + + @Override + public void removeConnectionStatusObserver(final @NotNull IConnectionStatusObserver observer) { + final @Nullable ConnectivityManager.NetworkCallback callback = + registeredCallbacks.remove(observer); + if (callback != null) { + unregisterNetworkCallback(context, logger, buildInfoProvider, callback); + } + } + /** * Return the Connection status * @@ -59,25 +115,27 @@ private ConnectivityChecker() {} * @return true if connected or no permission to check, false otherwise */ @SuppressWarnings({"deprecation", "MissingPermission"}) - private static @NotNull ConnectivityChecker.Status getConnectionStatus( + private static @NotNull IConnectionStatusProvider.ConnectionStatus getConnectionStatus( final @NotNull Context context, final @NotNull ConnectivityManager connectivityManager, final @NotNull ILogger logger) { if (!Permissions.hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE)) { logger.log(SentryLevel.INFO, "No permission (ACCESS_NETWORK_STATE) to check network status."); - return Status.NO_PERMISSION; + return ConnectionStatus.NO_PERMISSION; } try { final android.net.NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); if (activeNetworkInfo == null) { logger.log(SentryLevel.INFO, "NetworkInfo is null, there's no active network."); - return Status.NOT_CONNECTED; + return ConnectionStatus.DISCONNECTED; } - return activeNetworkInfo.isConnected() ? Status.CONNECTED : Status.NOT_CONNECTED; + return activeNetworkInfo.isConnected() + ? ConnectionStatus.CONNECTED + : ConnectionStatus.DISCONNECTED; } catch (Throwable t) { logger.log(SentryLevel.ERROR, "Could not retrieve Connection Status", t); - return Status.UNKNOWN; + return ConnectionStatus.UNKNOWN; } } @@ -273,4 +331,11 @@ public static void unregisterNetworkCallback( logger.log(SentryLevel.ERROR, "unregisterNetworkCallback failed", t); } } + + @TestOnly + @NotNull + public Map + getRegisteredCallbacks() { + return registeredCallbacks; + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidConnectionStatusProviderTest.kt similarity index 61% rename from sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt rename to sentry-android-core/src/test/java/io/sentry/android/core/AndroidConnectionStatusProviderTest.kt index 9542adce1c2..359fee49cc0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ConnectivityCheckerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidConnectionStatusProviderTest.kt @@ -14,11 +14,13 @@ import android.net.NetworkCapabilities.TRANSPORT_ETHERNET import android.net.NetworkCapabilities.TRANSPORT_WIFI import android.net.NetworkInfo import android.os.Build -import io.sentry.android.core.internal.util.ConnectivityChecker +import io.sentry.IConnectionStatusProvider +import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import kotlin.test.BeforeTest @@ -28,8 +30,9 @@ import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue -class ConnectivityCheckerTest { +class AndroidConnectionStatusProviderTest { + private lateinit var connectionStatusProvider: AndroidConnectionStatusProvider private lateinit var contextMock: Context private lateinit var connectivityManager: ConnectivityManager private lateinit var networkInfo: NetworkInfo @@ -54,24 +57,26 @@ class ConnectivityCheckerTest { networkCapabilities = mock() whenever(connectivityManager.getNetworkCapabilities(any())).thenReturn(networkCapabilities) + + connectionStatusProvider = AndroidConnectionStatusProvider(contextMock, mock(), buildInfo) } @Test fun `When network is active and connected with permission, return CONNECTED for isConnected`() { whenever(networkInfo.isConnected).thenReturn(true) assertEquals( - ConnectivityChecker.Status.CONNECTED, - ConnectivityChecker.getConnectionStatus(contextMock, mock()) + IConnectionStatusProvider.ConnectionStatus.CONNECTED, + connectionStatusProvider.connectionStatus ) } @Test - fun `When network is active but not connected with permission, return NOT_CONNECTED for isConnected`() { + fun `When network is active but not connected with permission, return DISCONNECTED for isConnected`() { whenever(networkInfo.isConnected).thenReturn(false) assertEquals( - ConnectivityChecker.Status.NOT_CONNECTED, - ConnectivityChecker.getConnectionStatus(contextMock, mock()) + IConnectionStatusProvider.ConnectionStatus.DISCONNECTED, + connectionStatusProvider.connectionStatus ) } @@ -80,30 +85,31 @@ class ConnectivityCheckerTest { whenever(contextMock.checkPermission(any(), any(), any())).thenReturn(PERMISSION_DENIED) assertEquals( - ConnectivityChecker.Status.NO_PERMISSION, - ConnectivityChecker.getConnectionStatus(contextMock, mock()) + IConnectionStatusProvider.ConnectionStatus.NO_PERMISSION, + connectionStatusProvider.connectionStatus ) } @Test - fun `When network is not active, return NOT_CONNECTED for isConnected`() { + fun `When network is not active, return DISCONNECTED for isConnected`() { assertEquals( - ConnectivityChecker.Status.NOT_CONNECTED, - ConnectivityChecker.getConnectionStatus(contextMock, mock()) + IConnectionStatusProvider.ConnectionStatus.DISCONNECTED, + connectionStatusProvider.connectionStatus ) } @Test fun `When ConnectivityManager is not available, return UNKNOWN for isConnected`() { + whenever(contextMock.getSystemService(any())).thenReturn(null) assertEquals( - ConnectivityChecker.Status.UNKNOWN, - ConnectivityChecker.getConnectionStatus(mock(), mock()) + IConnectionStatusProvider.ConnectionStatus.UNKNOWN, + connectionStatusProvider.connectionStatus ) } @Test fun `When ConnectivityManager is not available, return null for getConnectionType`() { - assertNull(ConnectivityChecker.getConnectionType(mock(), mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(mock(), mock(), buildInfo)) } @Test @@ -111,33 +117,33 @@ class ConnectivityCheckerTest { val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) - assertNull(ConnectivityChecker.getConnectionType(mock(), mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(mock(), mock(), buildInfo)) } @Test fun `When there's no permission, return null for getConnectionType`() { whenever(contextMock.checkPermission(any(), any(), any())).thenReturn(PERMISSION_DENIED) - assertNull(ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test fun `When network is not active, return null for getConnectionType`() { whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) - assertNull(ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test fun `When network capabilities are not available, return null for getConnectionType`() { - assertNull(ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test fun `When network capabilities has TRANSPORT_WIFI, return wifi`() { whenever(networkCapabilities.hasTransport(eq(TRANSPORT_WIFI))).thenReturn(true) - assertEquals("wifi", ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertEquals("wifi", AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test @@ -145,7 +151,7 @@ class ConnectivityCheckerTest { whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) whenever(networkInfo.type).thenReturn(TYPE_WIFI) - assertEquals("wifi", ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertEquals("wifi", AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test @@ -154,7 +160,7 @@ class ConnectivityCheckerTest { assertEquals( "ethernet", - ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo) + AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo) ) } @@ -165,7 +171,7 @@ class ConnectivityCheckerTest { assertEquals( "ethernet", - ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo) + AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo) ) } @@ -175,7 +181,7 @@ class ConnectivityCheckerTest { assertEquals( "cellular", - ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo) + AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo) ) } @@ -186,7 +192,7 @@ class ConnectivityCheckerTest { assertEquals( "cellular", - ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo) + AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo) ) } @@ -197,7 +203,7 @@ class ConnectivityCheckerTest { whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) whenever(contextMock.checkPermission(any(), any(), any())).thenReturn(PERMISSION_DENIED) val registered = - ConnectivityChecker.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) assertFalse(registered) verify(connectivityManager, never()).registerDefaultNetworkCallback(any()) @@ -207,7 +213,7 @@ class ConnectivityCheckerTest { fun `When sdkInfoVersion is not min N, do not register any NetworkCallback`() { whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) val registered = - ConnectivityChecker.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) assertFalse(registered) verify(connectivityManager, never()).registerDefaultNetworkCallback(any()) @@ -219,7 +225,7 @@ class ConnectivityCheckerTest { whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.N) whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) val registered = - ConnectivityChecker.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.registerNetworkCallback(contextMock, mock(), buildInfo, mock()) assertTrue(registered) verify(connectivityManager).registerDefaultNetworkCallback(any()) @@ -230,7 +236,7 @@ class ConnectivityCheckerTest { val buildInfo = mock() whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) - ConnectivityChecker.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) verify(connectivityManager, never()).unregisterNetworkCallback(any()) } @@ -238,7 +244,7 @@ class ConnectivityCheckerTest { @Test fun `unregisterNetworkCallback calls connectivityManager unregisterDefaultNetworkCallback`() { whenever(contextMock.getSystemService(any())).thenReturn(connectivityManager) - ConnectivityChecker.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) verify(connectivityManager).unregisterNetworkCallback(any()) } @@ -248,7 +254,7 @@ class ConnectivityCheckerTest { whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.S) whenever(connectivityManager.activeNetwork).thenThrow(SecurityException("Android OS Bug")) - assertNull(ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) + assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) } @Test @@ -256,8 +262,8 @@ class ConnectivityCheckerTest { whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.KITKAT) whenever(connectivityManager.activeNetworkInfo).thenThrow(SecurityException("Android OS Bug")) - assertNull(ConnectivityChecker.getConnectionType(contextMock, mock(), buildInfo)) - assertEquals(ConnectivityChecker.Status.UNKNOWN, ConnectivityChecker.getConnectionStatus(contextMock, mock())) + assertNull(AndroidConnectionStatusProvider.getConnectionType(contextMock, mock(), buildInfo)) + assertEquals(IConnectionStatusProvider.ConnectionStatus.UNKNOWN, connectionStatusProvider.connectionStatus) } @Test @@ -266,7 +272,7 @@ class ConnectivityCheckerTest { SecurityException("Android OS Bug") ) assertFalse( - ConnectivityChecker.registerNetworkCallback( + AndroidConnectionStatusProvider.registerNetworkCallback( contextMock, mock(), buildInfo, @@ -283,10 +289,58 @@ class ConnectivityCheckerTest { var failed = false try { - ConnectivityChecker.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) + AndroidConnectionStatusProvider.unregisterNetworkCallback(contextMock, mock(), buildInfo, mock()) } catch (t: Throwable) { failed = true } assertFalse(failed) } + + @Test + fun `connectionStatus returns NO_PERMISSIONS when context does not hold the permission`() { + whenever(contextMock.checkPermission(any(), any(), any())).thenReturn(PERMISSION_DENIED) + assertEquals(IConnectionStatusProvider.ConnectionStatus.NO_PERMISSION, connectionStatusProvider.connectionStatus) + } + + @Test + fun `connectionStatus returns ethernet when underlying mechanism provides ethernet`() { + whenever(networkCapabilities.hasTransport(eq(TRANSPORT_ETHERNET))).thenReturn(true) + assertEquals( + "ethernet", + connectionStatusProvider.connectionType + ) + } + + @Test + fun `adding and removing an observer works correctly`() { + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.N) + + val observer = IConnectionStatusProvider.IConnectionStatusObserver { } + val addResult = connectionStatusProvider.addConnectionStatusObserver(observer) + assertTrue(addResult) + + connectionStatusProvider.removeConnectionStatusObserver(observer) + assertTrue(connectionStatusProvider.registeredCallbacks.isEmpty()) + } + + @Test + fun `underlying callbacks correctly trigger update`() { + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.N) + + var callback: NetworkCallback? = null + whenever(connectivityManager.registerDefaultNetworkCallback(any())).then { invocation -> + callback = invocation.getArgument(0, NetworkCallback::class.java) + Unit + } + val observer = mock() + connectionStatusProvider.addConnectionStatusObserver(observer) + callback!!.onAvailable(mock()) + callback!!.onUnavailable() + callback!!.onLosing(mock(), 0) + callback!!.onLost(mock()) + callback!!.onUnavailable() + connectionStatusProvider.removeConnectionStatusObserver(observer) + + verify(observer, times(5)).onConnectionStatusChanged(any()) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransportGateTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransportGateTest.kt index 1b1ee683184..d31118728f2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransportGateTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransportGateTest.kt @@ -1,7 +1,6 @@ package io.sentry.android.core -import io.sentry.android.core.internal.util.ConnectivityChecker -import org.mockito.kotlin.mock +import io.sentry.IConnectionStatusProvider import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertNotNull @@ -11,7 +10,7 @@ class AndroidTransportGateTest { private class Fixture { fun getSut(): AndroidTransportGate { - return AndroidTransportGate(mock(), mock()) + return AndroidTransportGate(SentryAndroidOptions()) } } private val fixture = Fixture() @@ -23,21 +22,21 @@ class AndroidTransportGateTest { @Test fun `isConnected returns true if connection was not found`() { - assertTrue(fixture.getSut().isConnected(ConnectivityChecker.Status.UNKNOWN)) + assertTrue(fixture.getSut().isConnected(IConnectionStatusProvider.ConnectionStatus.UNKNOWN)) } @Test fun `isConnected returns true if connection is connected`() { - assertTrue(fixture.getSut().isConnected(ConnectivityChecker.Status.CONNECTED)) + assertTrue(fixture.getSut().isConnected(IConnectionStatusProvider.ConnectionStatus.CONNECTED)) } @Test fun `isConnected returns false if connection is not connected`() { - assertFalse(fixture.getSut().isConnected(ConnectivityChecker.Status.NOT_CONNECTED)) + assertFalse(fixture.getSut().isConnected(IConnectionStatusProvider.ConnectionStatus.DISCONNECTED)) } @Test fun `isConnected returns false if no permission`() { - assertTrue(fixture.getSut().isConnected(ConnectivityChecker.Status.NO_PERMISSION)) + assertTrue(fixture.getSut().isConnected(IConnectionStatusProvider.ConnectionStatus.NO_PERMISSION)) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt index f10594cf0b7..c2ffb9489ee 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt @@ -23,6 +23,7 @@ import io.sentry.protocol.Mechanism import io.sentry.protocol.SentryId import io.sentry.protocol.User import io.sentry.transport.ITransport +import io.sentry.transport.RateLimiter import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -63,6 +64,10 @@ class InternalSentrySdkTest { override fun flush(timeoutMillis: Long) { // no-op } + + override fun getRateLimiter(): RateLimiter? { + return null + } } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt index 36094f78ebb..df7e8632553 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SendCachedEnvelopeIntegrationTest.kt @@ -1,16 +1,21 @@ package io.sentry.android.core +import io.sentry.IConnectionStatusProvider +import io.sentry.IConnectionStatusProvider.ConnectionStatus import io.sentry.IHub import io.sentry.ILogger import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory import io.sentry.SentryLevel.DEBUG +import io.sentry.test.ImmediateExecutorService +import io.sentry.transport.RateLimiter import io.sentry.util.LazyEvaluator import org.awaitility.kotlin.await import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.util.concurrent.ExecutionException @@ -32,8 +37,12 @@ class SendCachedEnvelopeIntegrationTest { hasStartupCrashMarker: Boolean = false, hasSender: Boolean = true, delaySend: Long = 0L, - taskFails: Boolean = false + taskFails: Boolean = false, + useImmediateExecutor: Boolean = false ): SendCachedEnvelopeIntegration { + if (useImmediateExecutor) { + options.executorService = ImmediateExecutorService() + } options.cacheDirPath = cacheDirPath options.setLogger(logger) options.isDebug = true @@ -117,4 +126,90 @@ class SendCachedEnvelopeIntegrationTest { await.untilFalse(fixture.flag) verify(fixture.sender).send() } + + @Test + fun `registers for network connection changes`() { + val sut = fixture.getSut(hasStartupCrashMarker = false) + + val connectionStatusProvider = mock() + fixture.options.connectionStatusProvider = connectionStatusProvider + + sut.register(fixture.hub, fixture.options) + verify(connectionStatusProvider).addConnectionStatusObserver(any()) + } + + @Test + fun `when theres no network connection does nothing`() { + val sut = fixture.getSut(hasStartupCrashMarker = false) + + val connectionStatusProvider = mock() + fixture.options.connectionStatusProvider = connectionStatusProvider + + whenever(connectionStatusProvider.connectionStatus).thenReturn( + ConnectionStatus.DISCONNECTED + ) + + sut.register(fixture.hub, fixture.options) + verify(fixture.sender, never()).send() + } + + @Test + fun `when the network is not disconnected the factory is initialized`() { + val sut = fixture.getSut(hasStartupCrashMarker = false) + + val connectionStatusProvider = mock() + fixture.options.connectionStatusProvider = connectionStatusProvider + + whenever(connectionStatusProvider.connectionStatus).thenReturn( + ConnectionStatus.UNKNOWN + ) + + sut.register(fixture.hub, fixture.options) + verify(fixture.factory).create(any(), any()) + } + + @Test + fun `whenever network connection status changes, retries sending for relevant statuses`() { + val sut = fixture.getSut(hasStartupCrashMarker = false, useImmediateExecutor = true) + + val connectionStatusProvider = mock() + fixture.options.connectionStatusProvider = connectionStatusProvider + whenever(connectionStatusProvider.connectionStatus).thenReturn( + ConnectionStatus.DISCONNECTED + ) + sut.register(fixture.hub, fixture.options) + + // when there's no connection no factory create call should be done + verify(fixture.sender, never()).send() + + // but for any other status processing should be triggered + // CONNECTED + whenever(connectionStatusProvider.connectionStatus).thenReturn(ConnectionStatus.CONNECTED) + sut.onConnectionStatusChanged(ConnectionStatus.CONNECTED) + verify(fixture.sender).send() + + // UNKNOWN + whenever(connectionStatusProvider.connectionStatus).thenReturn(ConnectionStatus.UNKNOWN) + sut.onConnectionStatusChanged(ConnectionStatus.UNKNOWN) + verify(fixture.sender, times(2)).send() + + // NO_PERMISSION + whenever(connectionStatusProvider.connectionStatus).thenReturn(ConnectionStatus.NO_PERMISSION) + sut.onConnectionStatusChanged(ConnectionStatus.NO_PERMISSION) + verify(fixture.sender, times(3)).send() + } + + @Test + fun `when rate limiter is active, does not send envelopes`() { + val sut = fixture.getSut(hasStartupCrashMarker = false) + val rateLimiter = mock { + whenever(mock.isActiveForCategory(any())).thenReturn(true) + } + whenever(fixture.hub.rateLimiter).thenReturn(rateLimiter) + + sut.register(fixture.hub, fixture.options) + + // no factory call should be done if there's rate limiting active + verify(fixture.sender, never()).send() + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index b9bd902b50a..e28c1c39eda 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -21,6 +21,7 @@ import io.sentry.TraceContext import io.sentry.UserFeedback import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction +import io.sentry.transport.RateLimiter import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.robolectric.annotation.Config @@ -162,5 +163,9 @@ class SessionTrackingIntegrationTest { ): SentryId { TODO("Not yet implemented") } + + override fun getRateLimiter(): RateLimiter? { + TODO("Not yet implemented") + } } } diff --git a/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api b/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api index 7d7479c9fd3..9f33a4f115e 100644 --- a/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api +++ b/sentry-apache-http-client-5/api/sentry-apache-http-client-5.api @@ -2,6 +2,7 @@ public final class io/sentry/transport/apache/ApacheHttpClientTransport : io/sen public fun (Lio/sentry/SentryOptions;Lio/sentry/RequestDetails;Lorg/apache/hc/client5/http/impl/async/CloseableHttpAsyncClient;Lio/sentry/transport/RateLimiter;)V public fun close ()V public fun flush (J)V + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } diff --git a/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java b/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java index 8ff2aa01bf8..b5a0dc5c9f6 100644 --- a/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java +++ b/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java @@ -189,6 +189,11 @@ public void flush(long timeoutMillis) { } } + @Override + public @NotNull RateLimiter getRateLimiter() { + return rateLimiter; + } + @Override public void close() throws IOException { options.getLogger().log(DEBUG, "Shutting down"); diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index ecc49d177ac..538912164bb 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -239,7 +239,7 @@ public final class io/sentry/EnvelopeReader : io/sentry/IEnvelopeReader { } public final class io/sentry/EnvelopeSender : io/sentry/IEnvelopeSender { - public fun (Lio/sentry/IHub;Lio/sentry/ISerializer;Lio/sentry/ILogger;J)V + public fun (Lio/sentry/IHub;Lio/sentry/ISerializer;Lio/sentry/ILogger;JI)V public synthetic fun processDirectory (Ljava/io/File;)V public fun processEnvelopeFile (Ljava/lang/String;Lio/sentry/Hint;)V } @@ -373,6 +373,7 @@ public final class io/sentry/Hub : io/sentry/IHub { public fun getBaggage ()Lio/sentry/BaggageHeader; public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getOptions ()Lio/sentry/SentryOptions; + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; public fun getTraceparent ()Lio/sentry/SentryTraceHeader; public fun getTransaction ()Lio/sentry/ITransaction; @@ -422,6 +423,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public static fun getInstance ()Lio/sentry/HubAdapter; public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getOptions ()Lio/sentry/SentryOptions; + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; public fun getTraceparent ()Lio/sentry/SentryTraceHeader; public fun getTransaction ()Lio/sentry/ITransaction; @@ -452,6 +454,26 @@ public abstract interface class io/sentry/ICollector { public abstract fun setup ()V } +public abstract interface class io/sentry/IConnectionStatusProvider { + public abstract fun addConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)Z + public abstract fun getConnectionStatus ()Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public abstract fun getConnectionType ()Ljava/lang/String; + public abstract fun removeConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)V +} + +public final class io/sentry/IConnectionStatusProvider$ConnectionStatus : java/lang/Enum { + public static final field CONNECTED Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public static final field DISCONNECTED Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public static final field NO_PERMISSION Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public static final field UNKNOWN Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public static fun values ()[Lio/sentry/IConnectionStatusProvider$ConnectionStatus; +} + +public abstract interface class io/sentry/IConnectionStatusProvider$IConnectionStatusObserver { + public abstract fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V +} + public abstract interface class io/sentry/IEnvelopeReader { public abstract fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; } @@ -495,6 +517,7 @@ public abstract interface class io/sentry/IHub { public abstract fun getBaggage ()Lio/sentry/BaggageHeader; public abstract fun getLastEventId ()Lio/sentry/protocol/SentryId; public abstract fun getOptions ()Lio/sentry/SentryOptions; + public abstract fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public abstract fun getSpan ()Lio/sentry/ISpan; public abstract fun getTraceparent ()Lio/sentry/SentryTraceHeader; public abstract fun getTransaction ()Lio/sentry/ITransaction; @@ -588,6 +611,7 @@ public abstract interface class io/sentry/ISentryClient { public abstract fun captureUserFeedback (Lio/sentry/UserFeedback;)V public abstract fun close ()V public abstract fun flush (J)V + public abstract fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public abstract fun isEnabled ()Z } @@ -852,6 +876,14 @@ public final class io/sentry/MemoryCollectionData { public fun getUsedNativeMemory ()J } +public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectionStatusProvider { + public fun ()V + public fun addConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)Z + public fun getConnectionStatus ()Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public fun getConnectionType ()Ljava/lang/String; + public fun removeConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)V +} + public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public static fun getInstance ()Lio/sentry/NoOpEnvelopeReader; public fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; @@ -882,6 +914,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public static fun getInstance ()Lio/sentry/NoOpHub; public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getOptions ()Lio/sentry/SentryOptions; + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; public fun getTraceparent ()Lio/sentry/SentryTraceHeader; public fun getTransaction ()Lio/sentry/ITransaction; @@ -1042,7 +1075,7 @@ public final class io/sentry/OptionsContainer { } public final class io/sentry/OutboxSender : io/sentry/IEnvelopeSender { - public fun (Lio/sentry/IHub;Lio/sentry/IEnvelopeReader;Lio/sentry/ISerializer;Lio/sentry/ILogger;J)V + public fun (Lio/sentry/IHub;Lio/sentry/IEnvelopeReader;Lio/sentry/ISerializer;Lio/sentry/ILogger;JI)V public synthetic fun processDirectory (Ljava/io/File;)V public fun processEnvelopeFile (Ljava/lang/String;Lio/sentry/Hint;)V } @@ -1298,9 +1331,11 @@ public abstract class io/sentry/ScopeObserverAdapter : io/sentry/IScopeObserver public fun setUser (Lio/sentry/protocol/User;)V } -public final class io/sentry/SendCachedEnvelopeFireAndForgetIntegration : io/sentry/Integration { +public final class io/sentry/SendCachedEnvelopeFireAndForgetIntegration : io/sentry/IConnectionStatusProvider$IConnectionStatusObserver, io/sentry/Integration, java/io/Closeable { public fun (Lio/sentry/SendCachedEnvelopeFireAndForgetIntegration$SendFireAndForgetFactory;)V - public final fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V + public fun close ()V + public fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V + public fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } public abstract interface class io/sentry/SendCachedEnvelopeFireAndForgetIntegration$SendFireAndForget { @@ -1486,6 +1521,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient { public fun captureUserFeedback (Lio/sentry/UserFeedback;)V public fun close ()V public fun flush (J)V + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun isEnabled ()Z } @@ -1780,6 +1816,7 @@ public class io/sentry/SentryOptions { public fun getCacheDirPath ()Ljava/lang/String; public fun getClientReportRecorder ()Lio/sentry/clientreport/IClientReportRecorder; public fun getCollectors ()Ljava/util/List; + public fun getConnectionStatusProvider ()Lio/sentry/IConnectionStatusProvider; public fun getConnectionTimeoutMillis ()I public fun getContextTags ()Ljava/util/List; public fun getDateProvider ()Lio/sentry/SentryDateProvider; @@ -1873,6 +1910,7 @@ public class io/sentry/SentryOptions { public fun setBeforeSend (Lio/sentry/SentryOptions$BeforeSendCallback;)V public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V public fun setCacheDirPath (Ljava/lang/String;)V + public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V public fun setConnectionTimeoutMillis (I)V public fun setDateProvider (Lio/sentry/SentryDateProvider;)V public fun setDebug (Z)V @@ -2730,6 +2768,10 @@ public abstract interface class io/sentry/hints/DiskFlushNotification { public abstract fun setFlushable (Lio/sentry/protocol/SentryId;)V } +public abstract interface class io/sentry/hints/Enqueable { + public abstract fun markEnqueued ()V +} + public final class io/sentry/hints/EventDropReason : java/lang/Enum { public static final field MULTITHREADED_DEDUPLICATION Lio/sentry/hints/EventDropReason; public static fun valueOf (Ljava/lang/String;)Lio/sentry/hints/EventDropReason; @@ -4107,6 +4149,7 @@ public final class io/sentry/transport/AsyncHttpTransport : io/sentry/transport/ public fun (Lio/sentry/transport/QueuedThreadPoolExecutor;Lio/sentry/SentryOptions;Lio/sentry/transport/RateLimiter;Lio/sentry/transport/ITransportGate;Lio/sentry/transport/HttpConnection;)V public fun close ()V public fun flush (J)V + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } @@ -4121,6 +4164,7 @@ public abstract interface class io/sentry/transport/ICurrentDateProvider { public abstract interface class io/sentry/transport/ITransport : java/io/Closeable { public abstract fun flush (J)V + public abstract fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun send (Lio/sentry/SentryEnvelope;)V public abstract fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } @@ -4141,6 +4185,7 @@ public final class io/sentry/transport/NoOpTransport : io/sentry/transport/ITran public fun close ()V public fun flush (J)V public static fun getInstance ()Lio/sentry/transport/NoOpTransport; + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } @@ -4153,6 +4198,7 @@ public final class io/sentry/transport/RateLimiter { public fun (Lio/sentry/SentryOptions;)V public fun (Lio/sentry/transport/ICurrentDateProvider;Lio/sentry/SentryOptions;)V public fun filter (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/SentryEnvelope; + public fun isActiveForCategory (Lio/sentry/DataCategory;)Z public fun updateRetryAfterLimits (Ljava/lang/String;Ljava/lang/String;I)V } @@ -4170,6 +4216,7 @@ public final class io/sentry/transport/StdoutTransport : io/sentry/transport/ITr public fun (Lio/sentry/ISerializer;)V public fun close ()V public fun flush (J)V + public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun send (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } diff --git a/sentry/src/main/java/io/sentry/DirectoryProcessor.java b/sentry/src/main/java/io/sentry/DirectoryProcessor.java index d8924a38adb..5d60feba605 100644 --- a/sentry/src/main/java/io/sentry/DirectoryProcessor.java +++ b/sentry/src/main/java/io/sentry/DirectoryProcessor.java @@ -3,23 +3,37 @@ import static io.sentry.SentryLevel.ERROR; import io.sentry.hints.Cached; +import io.sentry.hints.Enqueable; import io.sentry.hints.Flushable; import io.sentry.hints.Retryable; import io.sentry.hints.SubmissionResult; +import io.sentry.transport.RateLimiter; import io.sentry.util.HintUtils; import java.io.File; +import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; abstract class DirectoryProcessor { + private static final long ENVELOPE_PROCESSING_DELAY = 100L; + private final @NotNull IHub hub; private final @NotNull ILogger logger; private final long flushTimeoutMillis; - - DirectoryProcessor(final @NotNull ILogger logger, final long flushTimeoutMillis) { + private final Queue processedEnvelopes; + + DirectoryProcessor( + final @NotNull IHub hub, + final @NotNull ILogger logger, + final long flushTimeoutMillis, + final int maxQueueSize) { + this.hub = hub; this.logger = logger; this.flushTimeoutMillis = flushTimeoutMillis; + this.processedEnvelopes = + SynchronizedQueue.synchronizedQueue(new CircularFifoQueue<>(maxQueueSize)); } public void processDirectory(final @NotNull File directory) { @@ -60,14 +74,36 @@ public void processDirectory(final @NotNull File directory) { continue; } - logger.log(SentryLevel.DEBUG, "Processing file: %s", file.getAbsolutePath()); + final String filePath = file.getAbsolutePath(); + // if envelope has already been submitted into the transport queue, we don't process it + // again + if (processedEnvelopes.contains(filePath)) { + logger.log( + SentryLevel.DEBUG, + "File '%s' has already been processed so it will not be processed again.", + filePath); + continue; + } + + // in case there's rate limiting active, skip processing + final @Nullable RateLimiter rateLimiter = hub.getRateLimiter(); + if (rateLimiter != null && rateLimiter.isActiveForCategory(DataCategory.All)) { + logger.log(SentryLevel.INFO, "DirectoryProcessor, rate limiting active."); + return; + } + + logger.log(SentryLevel.DEBUG, "Processing file: %s", filePath); final SendCachedEnvelopeHint cachedHint = - new SendCachedEnvelopeHint(flushTimeoutMillis, logger); + new SendCachedEnvelopeHint(flushTimeoutMillis, logger, filePath, processedEnvelopes); final Hint hint = HintUtils.createWithTypeCheckHint(cachedHint); - processFile(file, hint); + + // a short delay between processing envelopes to avoid bursting our server and hitting + // another rate limit https://develop.sentry.dev/sdk/features/#additional-capabilities + // InterruptedException will be handled by the outer try-catch + Thread.sleep(ENVELOPE_PROCESSING_DELAY); } } catch (Throwable e) { logger.log(SentryLevel.ERROR, e, "Failed processing '%s'", directory.getAbsolutePath()); @@ -79,16 +115,24 @@ public void processDirectory(final @NotNull File directory) { protected abstract boolean isRelevantFileName(String fileName); private static final class SendCachedEnvelopeHint - implements Cached, Retryable, SubmissionResult, Flushable { + implements Cached, Retryable, SubmissionResult, Flushable, Enqueable { boolean retry = false; boolean succeeded = false; private final CountDownLatch latch; private final long flushTimeoutMillis; private final @NotNull ILogger logger; - - public SendCachedEnvelopeHint(final long flushTimeoutMillis, final @NotNull ILogger logger) { + private final @NotNull String filePath; + private final @NotNull Queue processedEnvelopes; + + public SendCachedEnvelopeHint( + final long flushTimeoutMillis, + final @NotNull ILogger logger, + final @NotNull String filePath, + final @NotNull Queue processedEnvelopes) { this.flushTimeoutMillis = flushTimeoutMillis; + this.filePath = filePath; + this.processedEnvelopes = processedEnvelopes; this.latch = new CountDownLatch(1); this.logger = logger; } @@ -124,5 +168,10 @@ public void setResult(boolean succeeded) { public boolean isSuccess() { return succeeded; } + + @Override + public void markEnqueued() { + processedEnvelopes.add(filePath); + } } } diff --git a/sentry/src/main/java/io/sentry/EnvelopeSender.java b/sentry/src/main/java/io/sentry/EnvelopeSender.java index b74606fa1a5..598caad2804 100644 --- a/sentry/src/main/java/io/sentry/EnvelopeSender.java +++ b/sentry/src/main/java/io/sentry/EnvelopeSender.java @@ -25,8 +25,9 @@ public EnvelopeSender( final @NotNull IHub hub, final @NotNull ISerializer serializer, final @NotNull ILogger logger, - final long flushTimeoutMillis) { - super(logger, flushTimeoutMillis); + final long flushTimeoutMillis, + final int maxQueueSize) { + super(hub, logger, flushTimeoutMillis, maxQueueSize); this.hub = Objects.requireNonNull(hub, "Hub is required."); this.serializer = Objects.requireNonNull(serializer, "Serializer is required."); this.logger = Objects.requireNonNull(logger, "Logger is required."); diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index cc6dc0e6ef8..de0c782f1c4 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -7,6 +7,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; +import io.sentry.transport.RateLimiter; import io.sentry.util.ExceptionUtils; import io.sentry.util.HintUtils; import io.sentry.util.Objects; @@ -883,4 +884,11 @@ private Scope buildLocalScope( return null; } + + @ApiStatus.Internal + @Override + public @Nullable RateLimiter getRateLimiter() { + final StackItem item = stack.peek(); + return item.getClient().getRateLimiter(); + } } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 1a64484a6b7..cc01db177c5 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -3,6 +3,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; +import io.sentry.transport.RateLimiter; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -262,4 +263,10 @@ public void reportFullyDisplayed() { public @Nullable BaggageHeader getBaggage() { return Sentry.getBaggage(); } + + @ApiStatus.Internal + @Override + public @Nullable RateLimiter getRateLimiter() { + return Sentry.getCurrentHub().getRateLimiter(); + } } diff --git a/sentry/src/main/java/io/sentry/IConnectionStatusProvider.java b/sentry/src/main/java/io/sentry/IConnectionStatusProvider.java new file mode 100644 index 00000000000..1d75098e564 --- /dev/null +++ b/sentry/src/main/java/io/sentry/IConnectionStatusProvider.java @@ -0,0 +1,57 @@ +package io.sentry; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public interface IConnectionStatusProvider { + + enum ConnectionStatus { + UNKNOWN, + CONNECTED, + DISCONNECTED, + NO_PERMISSION + } + + interface IConnectionStatusObserver { + /** + * Invoked whenever the connection status changed. + * + * @param status the new connection status + */ + void onConnectionStatusChanged(@NotNull ConnectionStatus status); + } + + /** + * Gets the connection status. + * + * @return the current connection status + */ + @NotNull + ConnectionStatus getConnectionStatus(); + + /** + * Gets the connection type. + * + * @return the current connection type. E.g. "ethernet", "wifi" or "cellular" + */ + @Nullable + String getConnectionType(); + + /** + * Adds an observer for listening to connection status changes. + * + * @param observer the observer to register + * @return true if the observer was sucessfully registered + */ + boolean addConnectionStatusObserver(@NotNull final IConnectionStatusObserver observer); + + /** + * Removes an observer. + * + * @param observer a previously added observer via {@link + * #addConnectionStatusObserver(IConnectionStatusObserver)} + */ + void removeConnectionStatusObserver(@NotNull final IConnectionStatusObserver observer); +} diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 36b695e11b3..353129b2d7e 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -3,6 +3,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; +import io.sentry.transport.RateLimiter; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -633,4 +634,8 @@ TransactionContext continueTrace( */ @Nullable BaggageHeader getBaggage(); + + @ApiStatus.Internal + @Nullable + RateLimiter getRateLimiter(); } diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 4789eb3deca..0978d1d97b9 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -3,6 +3,7 @@ import io.sentry.protocol.Message; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; +import io.sentry.transport.RateLimiter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -264,4 +265,8 @@ SentryId captureTransaction( default @NotNull SentryId captureTransaction(@NotNull SentryTransaction transaction) { return captureTransaction(transaction, null, null, null); } + + @ApiStatus.Internal + @Nullable + RateLimiter getRateLimiter(); } diff --git a/sentry/src/main/java/io/sentry/NoOpConnectionStatusProvider.java b/sentry/src/main/java/io/sentry/NoOpConnectionStatusProvider.java new file mode 100644 index 00000000000..a1d66c9115b --- /dev/null +++ b/sentry/src/main/java/io/sentry/NoOpConnectionStatusProvider.java @@ -0,0 +1,28 @@ +package io.sentry; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class NoOpConnectionStatusProvider implements IConnectionStatusProvider { + @Override + public @NotNull ConnectionStatus getConnectionStatus() { + return ConnectionStatus.UNKNOWN; + } + + @Override + public @Nullable String getConnectionType() { + return null; + } + + @Override + public boolean addConnectionStatusObserver(@NotNull IConnectionStatusObserver observer) { + return false; + } + + @Override + public void removeConnectionStatusObserver(@NotNull IConnectionStatusObserver observer) { + // no-op + } +} diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 79a0fb1960d..719d9e2ffae 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -3,6 +3,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; +import io.sentry.transport.RateLimiter; import java.util.List; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -215,4 +216,9 @@ public void reportFullyDisplayed() {} public @Nullable BaggageHeader getBaggage() { return null; } + + @Override + public @Nullable RateLimiter getRateLimiter() { + return null; + } } diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 4afbdbb8c89..f2f8a16ba0d 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -2,6 +2,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; +import io.sentry.transport.RateLimiter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -52,4 +53,9 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint @Nullable ProfilingTraceData profilingTraceData) { return SentryId.EMPTY_ID; } + + @Override + public @Nullable RateLimiter getRateLimiter() { + return null; + } } diff --git a/sentry/src/main/java/io/sentry/OutboxSender.java b/sentry/src/main/java/io/sentry/OutboxSender.java index 9e704f0a0e9..709cbb8580b 100644 --- a/sentry/src/main/java/io/sentry/OutboxSender.java +++ b/sentry/src/main/java/io/sentry/OutboxSender.java @@ -46,8 +46,9 @@ public OutboxSender( final @NotNull IEnvelopeReader envelopeReader, final @NotNull ISerializer serializer, final @NotNull ILogger logger, - final long flushTimeoutMillis) { - super(logger, flushTimeoutMillis); + final long flushTimeoutMillis, + final int maxQueueSize) { + super(hub, logger, flushTimeoutMillis, maxQueueSize); this.hub = Objects.requireNonNull(hub, "Hub is required."); this.envelopeReader = Objects.requireNonNull(envelopeReader, "Envelope reader is required."); this.serializer = Objects.requireNonNull(serializer, "Serializer is required."); diff --git a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java index 170b06c528b..d13fbf7fcbf 100644 --- a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java +++ b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java @@ -2,16 +2,24 @@ import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; +import io.sentry.transport.RateLimiter; import io.sentry.util.Objects; +import java.io.Closeable; import java.io.File; +import java.io.IOException; import java.util.concurrent.RejectedExecutionException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** Sends cached events over when your App. is starting. */ -public final class SendCachedEnvelopeFireAndForgetIntegration implements Integration { +/** Sends cached events over when your App is starting or a network connection is present. */ +public final class SendCachedEnvelopeFireAndForgetIntegration + implements Integration, IConnectionStatusProvider.IConnectionStatusObserver, Closeable { private final @NotNull SendFireAndForgetFactory factory; + private @Nullable IConnectionStatusProvider connectionStatusProvider; + private @Nullable IHub hub; + private @Nullable SentryOptions options; + private @Nullable SendFireAndForget sender; public interface SendFireAndForget { void send(); @@ -54,11 +62,10 @@ public SendCachedEnvelopeFireAndForgetIntegration( this.factory = Objects.requireNonNull(factory, "SendFireAndForgetFactory is required"); } - @SuppressWarnings("FutureReturnValueIgnored") @Override - public final void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { - Objects.requireNonNull(hub, "Hub is required"); - Objects.requireNonNull(options, "SentryOptions is required"); + public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { + this.hub = Objects.requireNonNull(hub, "Hub is required"); + this.options = Objects.requireNonNull(options, "SentryOptions is required"); final String cachedDir = options.getCacheDirPath(); if (!factory.hasValidPath(cachedDir, options.getLogger())) { @@ -66,7 +73,58 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions return; } - final SendFireAndForget sender = factory.create(hub, options); + options + .getLogger() + .log(SentryLevel.DEBUG, "SendCachedEventFireAndForgetIntegration installed."); + addIntegrationToSdkVersion(getClass()); + + connectionStatusProvider = options.getConnectionStatusProvider(); + connectionStatusProvider.addConnectionStatusObserver(this); + + sender = factory.create(hub, options); + + sendCachedEnvelopes(hub, options); + } + + @Override + public void close() throws IOException { + if (connectionStatusProvider != null) { + connectionStatusProvider.removeConnectionStatusObserver(this); + } + } + + @Override + public void onConnectionStatusChanged( + final @NotNull IConnectionStatusProvider.ConnectionStatus status) { + if (hub != null && options != null) { + sendCachedEnvelopes(hub, options); + } + } + + @SuppressWarnings({"FutureReturnValueIgnored", "NullAway"}) + private synchronized void sendCachedEnvelopes( + final @NotNull IHub hub, final @NotNull SentryOptions options) { + + // skip run only if we're certainly disconnected + if (connectionStatusProvider != null + && connectionStatusProvider.getConnectionStatus() + == IConnectionStatusProvider.ConnectionStatus.DISCONNECTED) { + options + .getLogger() + .log(SentryLevel.INFO, "SendCachedEnvelopeFireAndForgetIntegration, no connection."); + return; + } + + // in case there's rate limiting active, skip processing + final @Nullable RateLimiter rateLimiter = hub.getRateLimiter(); + if (rateLimiter != null && rateLimiter.isActiveForCategory(DataCategory.All)) { + options + .getLogger() + .log( + SentryLevel.INFO, + "SendCachedEnvelopeFireAndForgetIntegration, rate limiting active."); + return; + } if (sender == null) { options.getLogger().log(SentryLevel.ERROR, "SendFireAndForget factory is null."); @@ -86,11 +144,6 @@ public final void register(final @NotNull IHub hub, final @NotNull SentryOptions .log(SentryLevel.ERROR, "Failed trying to send cached events.", e); } }); - - options - .getLogger() - .log(SentryLevel.DEBUG, "SendCachedEventFireAndForgetIntegration installed."); - addIntegrationToSdkVersion(getClass()); } catch (RejectedExecutionException e) { options .getLogger() diff --git a/sentry/src/main/java/io/sentry/SendFireAndForgetEnvelopeSender.java b/sentry/src/main/java/io/sentry/SendFireAndForgetEnvelopeSender.java index ecf4cb79136..e44d18a8d6b 100644 --- a/sentry/src/main/java/io/sentry/SendFireAndForgetEnvelopeSender.java +++ b/sentry/src/main/java/io/sentry/SendFireAndForgetEnvelopeSender.java @@ -33,7 +33,11 @@ public SendFireAndForgetEnvelopeSender( final EnvelopeSender envelopeSender = new EnvelopeSender( - hub, options.getSerializer(), options.getLogger(), options.getFlushTimeoutMillis()); + hub, + options.getSerializer(), + options.getLogger(), + options.getFlushTimeoutMillis(), + options.getMaxQueueSize()); return processDir(envelopeSender, dirPath, options.getLogger()); } diff --git a/sentry/src/main/java/io/sentry/SendFireAndForgetOutboxSender.java b/sentry/src/main/java/io/sentry/SendFireAndForgetOutboxSender.java index 0666b37cda3..fda41610fdf 100644 --- a/sentry/src/main/java/io/sentry/SendFireAndForgetOutboxSender.java +++ b/sentry/src/main/java/io/sentry/SendFireAndForgetOutboxSender.java @@ -37,7 +37,8 @@ public SendFireAndForgetOutboxSender( options.getEnvelopeReader(), options.getSerializer(), options.getLogger(), - options.getFlushTimeoutMillis()); + options.getFlushTimeoutMillis(), + options.getMaxQueueSize()); return processDir(outboxSender, dirPath, options.getLogger()); } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 97090ae730f..64a69c0e633 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -10,6 +10,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; +import io.sentry.transport.RateLimiter; import io.sentry.util.HintUtils; import io.sentry.util.Objects; import io.sentry.util.TracingUtils; @@ -825,6 +826,11 @@ public void flush(final long timeoutMillis) { transport.flush(timeoutMillis); } + @Override + public @Nullable RateLimiter getRateLimiter() { + return transport.getRateLimiter(); + } + private boolean sample() { // https://docs.sentry.io/development/sdk-dev/features/#event-sampling if (options.getSampleRate() != null && random != null) { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 9470080c803..90b3ff68305 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -425,6 +425,9 @@ public class SentryOptions { private final @NotNull FullyDisplayedReporter fullyDisplayedReporter = FullyDisplayedReporter.getInstance(); + private @NotNull IConnectionStatusProvider connectionStatusProvider = + new NoOpConnectionStatusProvider(); + /** Whether Sentry should be enabled */ private boolean enabled = true; @@ -2130,6 +2133,16 @@ public void addCollector(final @NotNull ICollector collector) { return collectors; } + @NotNull + public IConnectionStatusProvider getConnectionStatusProvider() { + return connectionStatusProvider; + } + + public void setConnectionStatusProvider( + final @NotNull IConnectionStatusProvider connectionStatusProvider) { + this.connectionStatusProvider = connectionStatusProvider; + } + /** The BeforeSend callback */ public interface BeforeSendCallback { diff --git a/sentry/src/main/java/io/sentry/hints/Enqueable.java b/sentry/src/main/java/io/sentry/hints/Enqueable.java new file mode 100644 index 00000000000..96d16c714cb --- /dev/null +++ b/sentry/src/main/java/io/sentry/hints/Enqueable.java @@ -0,0 +1,6 @@ +package io.sentry.hints; + +/** Marker interface for envelopes to notify when they are submitted to the http transport queue */ +public interface Enqueable { + void markEnqueued(); +} diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index c866336a431..7efbcbcaf3e 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -13,6 +13,7 @@ import io.sentry.clientreport.DiscardReason; import io.sentry.hints.Cached; import io.sentry.hints.DiskFlushNotification; +import io.sentry.hints.Enqueable; import io.sentry.hints.Retryable; import io.sentry.hints.SubmissionResult; import io.sentry.util.HintUtils; @@ -103,6 +104,14 @@ public void send(final @NotNull SentryEnvelope envelope, final @NotNull Hint hin options .getClientReportRecorder() .recordLostEnvelope(DiscardReason.QUEUE_OVERFLOW, envelopeThatMayIncludeClientReport); + } else { + HintUtils.runIfHasType( + hint, + Enqueable.class, + enqueable -> { + enqueable.markEnqueued(); + options.getLogger().log(SentryLevel.DEBUG, "Envelope enqueued"); + }); } } } @@ -135,6 +144,11 @@ private static QueuedThreadPoolExecutor initExecutor( 1, maxQueueSize, new AsyncConnectionThreadFactory(), storeEvents, logger); } + @Override + public @NotNull RateLimiter getRateLimiter() { + return rateLimiter; + } + @Override public void close() throws IOException { executor.shutdown(); diff --git a/sentry/src/main/java/io/sentry/transport/ITransport.java b/sentry/src/main/java/io/sentry/transport/ITransport.java index 131a5f04070..09fc034246c 100644 --- a/sentry/src/main/java/io/sentry/transport/ITransport.java +++ b/sentry/src/main/java/io/sentry/transport/ITransport.java @@ -5,6 +5,7 @@ import java.io.Closeable; import java.io.IOException; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** A transport is in charge of sending the event to the Sentry server. */ public interface ITransport extends Closeable { @@ -20,4 +21,7 @@ default void send(@NotNull SentryEnvelope envelope) throws IOException { * @param timeoutMillis time in milliseconds */ void flush(long timeoutMillis); + + @Nullable + RateLimiter getRateLimiter(); } diff --git a/sentry/src/main/java/io/sentry/transport/NoOpEnvelopeCache.java b/sentry/src/main/java/io/sentry/transport/NoOpEnvelopeCache.java index 27ce1dc3c39..d4902cf8b26 100644 --- a/sentry/src/main/java/io/sentry/transport/NoOpEnvelopeCache.java +++ b/sentry/src/main/java/io/sentry/transport/NoOpEnvelopeCache.java @@ -3,7 +3,7 @@ import io.sentry.Hint; import io.sentry.SentryEnvelope; import io.sentry.cache.IEnvelopeCache; -import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import org.jetbrains.annotations.NotNull; @@ -23,6 +23,6 @@ public void discard(@NotNull SentryEnvelope envelope) {} @NotNull @Override public Iterator iterator() { - return new ArrayList(0).iterator(); + return Collections.emptyIterator(); } } diff --git a/sentry/src/main/java/io/sentry/transport/NoOpTransport.java b/sentry/src/main/java/io/sentry/transport/NoOpTransport.java index 79d639ee0ba..f73605b049d 100644 --- a/sentry/src/main/java/io/sentry/transport/NoOpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/NoOpTransport.java @@ -5,6 +5,7 @@ import java.io.IOException; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; @ApiStatus.Internal public final class NoOpTransport implements ITransport { @@ -24,6 +25,11 @@ public void send(final @NotNull SentryEnvelope envelope, final @NotNull Hint hin @Override public void flush(long timeoutMillis) {} + @Override + public @Nullable RateLimiter getRateLimiter() { + return null; + } + @Override public void close() throws IOException {} } diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index 0ad8dbc55a3..ed4c04c6309 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -48,7 +48,7 @@ public RateLimiter(final @NotNull SentryOptions options) { // Optimize for/No allocations if no items are under 429 List dropItems = null; for (SentryEnvelopeItem item : envelope.getItems()) { - // using the raw value of the enum to not expose SentryEnvelopeItemType + // using the raw value of the enum to not expose SentryEnvelopeItemType if (isRetryAfter(item.getHeader().getType().getItemType())) { if (dropItems == null) { dropItems = new ArrayList<>(); @@ -87,26 +87,8 @@ public RateLimiter(final @NotNull SentryOptions options) { return envelope; } - /** - * It marks the hint when sending has failed, so it's not necessary to wait the timeout - * - * @param hint the Hints - * @param retry if event should be retried or not - */ - private static void markHintWhenSendingFailed(final @NotNull Hint hint, final boolean retry) { - HintUtils.runIfHasType(hint, SubmissionResult.class, result -> result.setResult(false)); - HintUtils.runIfHasType(hint, Retryable.class, retryable -> retryable.setRetry(retry)); - } - - /** - * Check if an itemType is retry after or not - * - * @param itemType the itemType (eg event, session, etc...) - * @return true if retry after or false otherwise - */ @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) - private boolean isRetryAfter(final @NotNull String itemType) { - final DataCategory dataCategory = getCategoryFromItemType(itemType); + public boolean isActiveForCategory(final @NotNull DataCategory dataCategory) { final Date currentDate = new Date(currentDateProvider.getCurrentTimeMillis()); // check all categories @@ -131,6 +113,29 @@ private boolean isRetryAfter(final @NotNull String itemType) { return false; } + /** + * It marks the hint when sending has failed, so it's not necessary to wait the timeout + * + * @param hint the Hints + * @param retry if event should be retried or not + */ + private static void markHintWhenSendingFailed(final @NotNull Hint hint, final boolean retry) { + HintUtils.runIfHasType(hint, SubmissionResult.class, result -> result.setResult(false)); + HintUtils.runIfHasType(hint, Retryable.class, retryable -> retryable.setRetry(retry)); + } + + /** + * Check if an itemType is retry after or not + * + * @param itemType the itemType (eg event, session, etc...) + * @return true if retry after or false otherwise + */ + @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) + private boolean isRetryAfter(final @NotNull String itemType) { + final DataCategory dataCategory = getCategoryFromItemType(itemType); + return isActiveForCategory(dataCategory); + } + /** * Returns a rate limiting category from item itemType * diff --git a/sentry/src/main/java/io/sentry/transport/StdoutTransport.java b/sentry/src/main/java/io/sentry/transport/StdoutTransport.java index c503df9f94b..99aed10eac7 100644 --- a/sentry/src/main/java/io/sentry/transport/StdoutTransport.java +++ b/sentry/src/main/java/io/sentry/transport/StdoutTransport.java @@ -6,6 +6,7 @@ import io.sentry.util.Objects; import java.io.IOException; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public final class StdoutTransport implements ITransport { @@ -33,6 +34,11 @@ public void flush(long timeoutMillis) { System.out.println("Flushing"); } + @Override + public @Nullable RateLimiter getRateLimiter() { + return null; + } + @Override public void close() {} } diff --git a/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt b/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt index 718aff315c8..e87f4256d58 100644 --- a/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/DirectoryProcessorTest.kt @@ -1,11 +1,15 @@ package io.sentry import io.sentry.hints.ApplyScopeData -import io.sentry.protocol.User +import io.sentry.hints.Enqueable +import io.sentry.hints.Retryable +import io.sentry.transport.RateLimiter import io.sentry.util.HintUtils import io.sentry.util.noFlushTimeout import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argWhere +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -34,8 +38,31 @@ class DirectoryProcessorTest { options.setLogger(logger) } - fun getSut(): OutboxSender { - return OutboxSender(hub, envelopeReader, serializer, logger, 15000) + fun getSut(isRetryable: Boolean = false, isRateLimitingActive: Boolean = false): OutboxSender { + val hintCaptor = argumentCaptor() + whenever(hub.captureEvent(any(), hintCaptor.capture())).then { + HintUtils.runIfHasType( + hintCaptor.firstValue, + Enqueable::class.java + ) { enqueable: Enqueable -> + enqueable.markEnqueued() + + // activate rate limiting when a first envelope was processed + if (isRateLimitingActive) { + val rateLimiter = mock { + whenever(mock.isActiveForCategory(any())).thenReturn(true) + } + whenever(hub.rateLimiter).thenReturn(rateLimiter) + } + } + HintUtils.runIfHasType( + hintCaptor.firstValue, + Retryable::class.java + ) { retryable -> + retryable.isRetry = isRetryable + } + } + return OutboxSender(hub, envelopeReader, serializer, logger, 500, 30) } } @@ -57,9 +84,6 @@ class DirectoryProcessorTest { fun `process directory folder has a non ApplyScopeData hint`() { val path = getTempEnvelope("envelope-event-attachment.txt") assertTrue(File(path).exists()) // sanity check -// val session = createSession() -// whenever(fixture.envelopeReader.read(any())).thenReturn(SentryEnvelope.from(fixture.serializer, session, null)) -// whenever(fixture.serializer.deserializeSession(any())).thenReturn(session) val event = SentryEvent() val envelope = SentryEnvelope.from(fixture.serializer, event, null) @@ -79,6 +103,45 @@ class DirectoryProcessorTest { verify(fixture.hub, never()).captureEnvelope(any(), any()) } + @Test + fun `when envelope has already been submitted to the queue, does not process it again`() { + getTempEnvelope("envelope-event-attachment.txt") + + val event = SentryEvent() + val envelope = SentryEnvelope.from(fixture.serializer, event, null) + + whenever(fixture.envelopeReader.read(any())).thenReturn(envelope) + whenever(fixture.serializer.deserialize(any(), eq(SentryEvent::class.java))).thenReturn(event) + + // make it retryable so it doesn't get deleted + val sut = fixture.getSut(isRetryable = true) + sut.processDirectory(file) + + // process it once again + sut.processDirectory(file) + + // should only capture once + verify(fixture.hub).captureEvent(any(), anyOrNull()) + } + + @Test + fun `when rate limiting gets active in the middle of processing, stops processing`() { + getTempEnvelope("envelope-event-attachment.txt") + getTempEnvelope("envelope-event-attachment.txt") + + val event = SentryEvent() + val envelope = SentryEnvelope.from(fixture.serializer, event, null) + + whenever(fixture.envelopeReader.read(any())).thenReturn(envelope) + whenever(fixture.serializer.deserialize(any(), eq(SentryEvent::class.java))).thenReturn(event) + + val sut = fixture.getSut(isRateLimitingActive = true) + sut.processDirectory(file) + + // should only capture once + verify(fixture.hub).captureEvent(any(), anyOrNull()) + } + private fun getTempEnvelope(fileName: String): String { val testFile = this::class.java.classLoader.getResource(fileName) val testFileBytes = testFile!!.readBytes() @@ -86,8 +149,4 @@ class DirectoryProcessorTest { Files.write(Paths.get(targetFile.toURI()), testFileBytes) return targetFile.absolutePath } - - private fun createSession(): Session { - return Session("123", User(), "env", "release") - } } diff --git a/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt b/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt index ded72ebf382..6f0ea9cb8a6 100644 --- a/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt +++ b/sentry/src/test/java/io/sentry/EnvelopeSenderTest.kt @@ -5,9 +5,11 @@ import io.sentry.hints.Retryable import io.sentry.util.HintUtils import io.sentry.util.noFlushTimeout import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever @@ -32,7 +34,13 @@ class EnvelopeSenderTest { } fun getSut(): EnvelopeSender { - return EnvelopeSender(hub!!, serializer!!, logger!!, options.flushTimeoutMillis) + return EnvelopeSender( + hub!!, + serializer!!, + logger!!, + options.flushTimeoutMillis, + options.maxQueueSize + ) } } @@ -74,7 +82,7 @@ class EnvelopeSenderTest { sut.processDirectory(File(tempDirectory.toUri())) testFile.deleteOnExit() verify(fixture.logger)!!.log(eq(SentryLevel.DEBUG), eq("File '%s' doesn't match extension expected."), any()) - verifyNoMoreInteractions(fixture.hub) + verify(fixture.hub, never())!!.captureEnvelope(any(), anyOrNull()) } @Test diff --git a/sentry/src/test/java/io/sentry/NoOpConnectionStatusProviderTest.kt b/sentry/src/test/java/io/sentry/NoOpConnectionStatusProviderTest.kt new file mode 100644 index 00000000000..0ccc911dcf4 --- /dev/null +++ b/sentry/src/test/java/io/sentry/NoOpConnectionStatusProviderTest.kt @@ -0,0 +1,33 @@ +package io.sentry + +import org.mockito.kotlin.mock +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull + +class NoOpConnectionStatusProviderTest { + + private val provider = NoOpConnectionStatusProvider() + + @Test + fun `provider returns unknown status`() { + assertEquals(IConnectionStatusProvider.ConnectionStatus.UNKNOWN, provider.connectionStatus) + } + + @Test + fun `connection type returns null`() { + assertNull(provider.connectionType) + } + + @Test + fun `adding a listener is a no-op and returns false`() { + val result = provider.addConnectionStatusObserver(mock()) + assertFalse(result) + } + + @Test + fun `removing a listener is a no-op`() { + provider.addConnectionStatusObserver(mock()) + } +} diff --git a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt index 4f8f1d9860e..933fab0a21d 100644 --- a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt +++ b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt @@ -22,7 +22,6 @@ import java.util.Date import java.util.UUID import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -42,7 +41,7 @@ class OutboxSenderTest { } fun getSut(): OutboxSender { - return OutboxSender(hub, envelopeReader, serializer, logger, 15000) + return OutboxSender(hub, envelopeReader, serializer, logger, 15000, 30) } } @@ -275,38 +274,6 @@ class OutboxSenderTest { verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), argWhere { it is FileNotFoundException }) } - @Test - fun `when hub is null, ctor throws`() { - val clazz = Class.forName("io.sentry.OutboxSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(null, mock(), mock(), mock(), null) - assertFailsWith { ctor.newInstance(params) } - } - - @Test - fun `when envelopeReader is null, ctor throws`() { - val clazz = Class.forName("io.sentry.OutboxSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(mock(), null, mock(), mock(), 15000) - assertFailsWith { ctor.newInstance(params) } - } - - @Test - fun `when serializer is null, ctor throws`() { - val clazz = Class.forName("io.sentry.OutboxSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(mock(), mock(), null, mock(), 15000) - assertFailsWith { ctor.newInstance(params) } - } - - @Test - fun `when logger is null, ctor throws`() { - val clazz = Class.forName("io.sentry.OutboxSender") - val ctor = clazz.getConstructor(IHub::class.java, IEnvelopeReader::class.java, ISerializer::class.java, ILogger::class.java, Long::class.java) - val params = arrayOf(mock(), mock(), mock(), null, 15000) - assertFailsWith { ctor.newInstance(params) } - } - @Test fun `when file name is null, should not be relevant`() { assertFalse(fixture.getSut().isRelevantFileName(null)) diff --git a/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt b/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt index a1c06d31dba..21a22861eb1 100644 --- a/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt +++ b/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt @@ -1,11 +1,15 @@ package io.sentry +import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget import io.sentry.protocol.SdkVersion +import io.sentry.test.ImmediateExecutorService +import io.sentry.transport.RateLimiter import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import kotlin.test.Test import kotlin.test.assertFalse @@ -17,8 +21,10 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { var hub: IHub = mock() var logger: ILogger = mock() var options = SentryOptions() + val sender = mock() var callback = mock().apply { whenever(hasValidPath(any(), any())).thenCallRealMethod() + whenever(create(any(), any())).thenReturn(sender) } init { @@ -27,7 +33,10 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { options.sdkVersion = SdkVersion("test", "1.2.3") } - fun getSut(): SendCachedEnvelopeFireAndForgetIntegration { + fun getSut(useImmediateExecutor: Boolean = true): SendCachedEnvelopeFireAndForgetIntegration { + if (useImmediateExecutor) { + options.executorService = ImmediateExecutorService() + } return SendCachedEnvelopeFireAndForgetIntegration(callback) } } @@ -40,7 +49,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { val sut = fixture.getSut() sut.register(fixture.hub, fixture.options) verify(fixture.logger).log(eq(SentryLevel.ERROR), eq("No cache dir path is defined in options.")) - verifyNoMoreInteractions(fixture.hub) + verify(fixture.sender, never()).send() } @Test @@ -67,7 +76,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.cacheDirPath = "abc" sut.register(fixture.hub, fixture.options) verify(fixture.logger).log(eq(SentryLevel.ERROR), eq("SendFireAndForget factory is null.")) - verifyNoMoreInteractions(fixture.hub) + verify(fixture.sender, never()).send() } @Test @@ -87,11 +96,99 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.cacheDirPath = "cache" fixture.options.executorService.close(0) whenever(fixture.callback.create(any(), any())).thenReturn(mock()) - val sut = fixture.getSut() + val sut = fixture.getSut(useImmediateExecutor = false) sut.register(fixture.hub, fixture.options) verify(fixture.logger).log(eq(SentryLevel.ERROR), eq("Failed to call the executor. Cached events will not be sent. Did you call Sentry.close()?"), any()) } + @Test + fun `registers for network connection changes`() { + val connectionStatusProvider = mock() + fixture.options.connectionStatusProvider = connectionStatusProvider + fixture.options.cacheDirPath = "cache" + + val sut = fixture.getSut() + sut.register(fixture.hub, fixture.options) + verify(connectionStatusProvider).addConnectionStatusObserver(any()) + } + + @Test + fun `when theres no network connection does nothing`() { + val connectionStatusProvider = mock() + whenever(connectionStatusProvider.connectionStatus).thenReturn( + IConnectionStatusProvider.ConnectionStatus.DISCONNECTED + ) + fixture.options.connectionStatusProvider = connectionStatusProvider + fixture.options.cacheDirPath = "cache" + + val sut = fixture.getSut() + sut.register(fixture.hub, fixture.options) + + sut.register(fixture.hub, fixture.options) + verify(fixture.sender, never()).send() + } + + @Test + fun `when the network is not disconnected the factory is initialized`() { + val connectionStatusProvider = mock() + whenever(connectionStatusProvider.connectionStatus).thenReturn( + IConnectionStatusProvider.ConnectionStatus.UNKNOWN + ) + fixture.options.connectionStatusProvider = connectionStatusProvider + fixture.options.cacheDirPath = "cache" + + val sut = fixture.getSut() + sut.register(fixture.hub, fixture.options) + + verify(fixture.sender).send() + } + + @Test + fun `whenever network connection status changes, retries sending for relevant statuses`() { + val connectionStatusProvider = mock() + whenever(connectionStatusProvider.connectionStatus).thenReturn( + IConnectionStatusProvider.ConnectionStatus.DISCONNECTED + ) + fixture.options.connectionStatusProvider = connectionStatusProvider + fixture.options.cacheDirPath = "cache" + + val sut = fixture.getSut() + sut.register(fixture.hub, fixture.options) + + // when there's no connection no factory create call should be done + verify(fixture.sender, never()).send() + + // but for any other status processing should be triggered + // CONNECTED + whenever(connectionStatusProvider.connectionStatus).thenReturn(IConnectionStatusProvider.ConnectionStatus.CONNECTED) + sut.onConnectionStatusChanged(IConnectionStatusProvider.ConnectionStatus.CONNECTED) + verify(fixture.sender).send() + + // UNKNOWN + whenever(connectionStatusProvider.connectionStatus).thenReturn(IConnectionStatusProvider.ConnectionStatus.UNKNOWN) + sut.onConnectionStatusChanged(IConnectionStatusProvider.ConnectionStatus.UNKNOWN) + verify(fixture.sender, times(2)).send() + + // NO_PERMISSION + whenever(connectionStatusProvider.connectionStatus).thenReturn(IConnectionStatusProvider.ConnectionStatus.NO_PERMISSION) + sut.onConnectionStatusChanged(IConnectionStatusProvider.ConnectionStatus.NO_PERMISSION) + verify(fixture.sender, times(3)).send() + } + + @Test + fun `when rate limiter is active, does not send envelopes`() { + val sut = fixture.getSut() + val rateLimiter = mock { + whenever(mock.isActiveForCategory(any())).thenReturn(true) + } + whenever(fixture.hub.rateLimiter).thenReturn(rateLimiter) + + sut.register(fixture.hub, fixture.options) + + // no factory call should be done if there's rate limiting active + verify(fixture.sender, never()).send() + } + private class CustomFactory : SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory { override fun create(hub: IHub, options: SentryOptions): SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget? { return null diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 4b7bcbecbf3..5267823df88 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -484,6 +484,31 @@ class SentryOptionsTest { } @Test + fun `when options are initialized, connectionStatusProvider is not null and default to noop`() { + assertNotNull(SentryOptions().connectionStatusProvider) + assertTrue(SentryOptions().connectionStatusProvider is NoOpConnectionStatusProvider) + } + + @Test + fun `when connectionStatusProvider is set, its returned as well`() { + val options = SentryOptions() + val customProvider = object : IConnectionStatusProvider { + override fun getConnectionStatus(): IConnectionStatusProvider.ConnectionStatus { + return IConnectionStatusProvider.ConnectionStatus.UNKNOWN + } + + override fun getConnectionType(): String? = null + + override fun addConnectionStatusObserver(observer: IConnectionStatusProvider.IConnectionStatusObserver) = false + + override fun removeConnectionStatusObserver(observer: IConnectionStatusProvider.IConnectionStatusObserver) { + // no-op + } + } + options.connectionStatusProvider = customProvider + assertEquals(customProvider, options.connectionStatusProvider) + } + fun `when options are initialized, enabled is set to true by default`() { assertTrue(SentryOptions().isEnabled) } diff --git a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt index 8acb718f3a3..2982e9567b1 100644 --- a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt +++ b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt @@ -13,6 +13,7 @@ import io.sentry.Session import io.sentry.clientreport.NoOpClientReportRecorder import io.sentry.dsnString import io.sentry.hints.DiskFlushNotification +import io.sentry.hints.Enqueable import io.sentry.protocol.SentryId import io.sentry.protocol.User import io.sentry.util.HintUtils @@ -383,6 +384,24 @@ class AsyncHttpTransportTest { assertTrue(calledFlush) } + @Test + fun `when event is Enqueable, marks it after sending to the queue`() { + val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, createSession(), null) + whenever(fixture.transportGate.isConnected).thenReturn(true) + whenever(fixture.rateLimiter.filter(any(), anyOrNull())).thenAnswer { it.arguments[0] } + whenever(fixture.connection.send(any())).thenReturn(TransportResult.success()) + + var called = false + val hint = HintUtils.createWithTypeCheckHint(object : Enqueable { + override fun markEnqueued() { + called = true + } + }) + fixture.getSUT().send(envelope, hint) + + assertTrue(called) + } + private fun createSession(): Session { return Session("123", User(), "env", "release") } From ca595ea8bb131916b38944e0caab9311c1dd6c72 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 28 Sep 2023 17:46:28 +0200 Subject: [PATCH 22/23] Highlight breaking chnages in changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d52b8db5c3b..f28311b42d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -Breaking changes: +**Breaking changes:** - Capture failed HTTP requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794)) - Reduce flush timeout to 4s on Android to avoid ANRs ([#2858](https://github.com/getsentry/sentry-java/pull/2858)) - Set ip_address to {{auto}} by default, even if sendDefaultPII is disabled ([#2860](https://github.com/getsentry/sentry-java/pull/2860)) @@ -28,7 +28,7 @@ Breaking changes: - Capture unfinished transaction on Scope with status `aborted` in case a crash happens ([#2938](https://github.com/getsentry/sentry-java/pull/2938)) - This will fix the link between transactions and corresponding crashes, you'll be able to see them in a single trace -Breaking changes: +**Breaking changes:** - Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) - Fix Coroutine Context Propagation using CopyableThreadContextElement, requires `kotlinx-coroutines-core` version `1.6.1` or higher ([#2838](https://github.com/getsentry/sentry-java/pull/2838)) - Bump min API to 19 ([#2883](https://github.com/getsentry/sentry-java/pull/2883)) From ece39a6eed4292c78d9c3245bbc4bb9ca8165667 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 28 Sep 2023 17:58:50 +0200 Subject: [PATCH 23/23] Api dump --- sentry/api/sentry.api | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index c258e5f595c..daf5d789ea4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -935,14 +935,6 @@ public final class io/sentry/MemoryCollectionData { public fun getUsedNativeMemory ()J } -public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectionStatusProvider { - public fun ()V - public fun addConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)Z - public fun getConnectionStatus ()Lio/sentry/IConnectionStatusProvider$ConnectionStatus; - public fun getConnectionType ()Ljava/lang/String; - public fun removeConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)V -} - public final class io/sentry/MonitorConfig : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun (Lio/sentry/MonitorSchedule;)V public fun getCheckinMargin ()Ljava/lang/Long; @@ -1036,6 +1028,14 @@ public final class io/sentry/MonitorScheduleUnit : java/lang/Enum { public static fun values ()[Lio/sentry/MonitorScheduleUnit; } +public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectionStatusProvider { + public fun ()V + public fun addConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)Z + public fun getConnectionStatus ()Lio/sentry/IConnectionStatusProvider$ConnectionStatus; + public fun getConnectionType ()Ljava/lang/String; + public fun removeConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)V +} + public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public static fun getInstance ()Lio/sentry/NoOpEnvelopeReader; public fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope;