From 0566579dce775c468d10c29056686da9d326ff5f Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 29 Jun 2023 14:14:54 +0200 Subject: [PATCH 01/55] 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 2bd46b6a4d..96a2e75d80 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 7fd64ed8c5..41e6a6fb95 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 4757c96879..840f109c56 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 f28112ca11..b4f6d871fc 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 07e16af252..044380d6be 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 187725650e..3053ee5b22 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 c5bbb2aeeb..7599d07027 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 3d3b654dc6..bf0703988c 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/55] 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 96a2e75d80..3f4c5d0d1d 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 7c19c1c3b0..86f5629d3c 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 c686d4b9a2..6987e7b9a3 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/55] Fix Changelog --- CHANGELOG.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a41254289..3ec184b6ab 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/55] 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 3ec184b6ab..04d05f1bfc 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/55] 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 04d05f1bfc..2376590a8f 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 3dda6183cd..17b62eb319 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 5f216a660a..41776e495a 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/55] 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 2376590a8f..1f463eee19 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 f3cf00f96c..a564a5ebe1 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 7f48dcd7ef..2632b2921f 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 24d7c62aa3..0b5f8ac2fb 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/55] 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 1f463eee19..3735251ff1 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 6a4b7edfa1..81422ea514 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 8d88c6674a..446443f544 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 69598a0e1e..d6a05ddd2c 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 8747409cce..65d86c8cfa 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 d046855958..abbf21c84e 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 4e6b4f3d99..bbdd252ce3 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/55] 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 4eabf54582..bad710a452 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/55] 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 cbe08f6198..7db0dd372b 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 ee4ea10b72..070fed2e0f 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 de690aa668..0c38d04d48 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 7ba270351b..b0f66446de 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 a07f6692d6..1004575976 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 421274b42a..29cb7c0d7e 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 cf3ca7c2ea..f913ca9268 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/55] 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 7db0dd372b..ebb102f788 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 6d54ef9296..2674bc2350 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 5cc7b87455..d501240a3a 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 cd6042e748..3cf22a20da 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 63f14bac55..b54ceabc51 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/55] 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 ebb102f788..d4b03cd492 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 cbd5494726..d750b96c7c 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 ce59db5025..20f2ff6979 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/55] 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 d4b03cd492..d65166145b 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 070fed2e0f..4cf44e7f49 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 e502aea71d..2493f5b5f0 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 1004575976..45f9780730 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 b9a7eee52c..f39f70c552 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 dae348bef5..8ca52e3c41 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 0d7441c549..e1c899d136 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 2b83f5fb0d..c1e55e642e 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 e3cc1498e5..f3ffb8db45 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 3362d97940..0ae4b94ace 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 ee698af6c0..d150148c08 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/55] 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 d65166145b..9644f61f7a 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 bc8e37d83e..9cdd005a95 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 67e19112ca..a68edab92b 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/55] 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 9644f61f7a..cda183b518 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 2493f5b5f0..32a1ffb06a 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 f39f70c552..308632c6ed 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/55] 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 cda183b518..0e98675a09 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 2674bc2350..d095120e24 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 4cf44e7f49..0d44e08567 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 6cc4c2a6d8..60c3160fe8 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 c060f475af..54a500825c 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 6a0457e50e..2ce5ac4fb0 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 d74a8b1e2d..f9fcbf972d 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 b0f66446de..91992b3c5e 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 10c160377b..11978c7bec 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 88f3e126de..35cfaa002f 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 45f9780730..3ab3633f93 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 2632b2921f..1b147b106a 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 d6a05ddd2c..2d331a7563 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 ca2698ccd5..9542adce1c 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 bad710a452..70b20e2ab2 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 f913ca9268..aa9d9cc26b 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 07fc383e1d..a7b4cc3f8c 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 4c944fb07d..ac48c1a504 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/55] 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 0e98675a09..1cbd0b89ca 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 f3ffb8db45..2593ea64a4 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 70b1b635cc..b627aa4c9f 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 d150148c08..ad276ad1ff 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 0000000000..27499be0a0 --- /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/55] (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 1cbd0b89ca..638653a579 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 5727eb7a83..eaad6484a6 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 a8e6535212..7b2099739d 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 906af7c8c6..1a8eb4213b 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 d197eeae4a..1c65d07ee9 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 0000000000..b639cab40e --- /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 9cdd005a95..b2d9b4943e 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 a68edab92b..7ca5d1b82f 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 0000000000..219b95d08a --- /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 c1e55e642e..cdb0800c35 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 3171c67a95..90baf3ed87 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 e6ec220874..a33fe4e888 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 97583f66e8..bf4223576e 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 aa5d846975..6ad44552bb 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 254c1fffd5..a7d7330d3f 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 accee3b051..956996ce04 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 0a5d06cec5..b08b6e584f 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 6fec20ca7d..f906c5e3c8 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 96a44462d4..da18dd3d82 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 c19063e8c4..dd9e6fc975 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 0000000000..00c89f27be --- /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 a849cf3d6d..3eb4662f7c 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/55] Fix changelog --- CHANGELOG.md | 58 ++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea19766c2a..4baa6a4f32 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/55] 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 4baa6a4f32..5823017100 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 c6937760a2..bf9fcdfd33 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 79daaace03..9b0e74c6d0 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 0d44e08567..2826366b4b 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 4c6ac6373a..0b0d56b093 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 54a500825c..f8fbc498b0 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 f3d257d225..66278d35b8 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 c9a552a5b7..eef4707683 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 7f4724967f..3e8fe6383f 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 840f109c56..3a4a91498e 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 317cb60467..6aa7caf4c9 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 be23c668c7..ea0426609c 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 0812e55015..33e37a4d2e 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 26f8e2bfb5..2d80084738 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 e4577b43ec..66c29ddeff 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 dda5627272..c361529671 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 b667e04888..569e227dc7 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 a893fa87d1..aa54790c47 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 2d331a7563..a792a5b3c5 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 81f0e7e505..4129ea4356 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 61a98025bf..79151bb3fb 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 8ca52e3c41..8fdf8b0df8 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 afd7baf890..30e9cbb7b6 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 ebc4c9bc47..009bba9b81 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 11c140061e..a96e3787a6 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 7b2099739d..4bda5a9c81 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 6477a2531a..d043faa5f6 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 e98f72e45c..0fa4e717a0 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 7ca3262dc6..c6aeb8755a 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 bf1ab6abed..8c18bce06e 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 f37277de7c..faa8a549a9 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 ef84b7579d..4af368f065 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 6fe9fb85cb..f29b86f999 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 90baf3ed87..cc6dc0e6ef 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 a33fe4e888..1a64484a6b 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 42689a3ad3..36b695e11b 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 519e9222b5..54cacc666a 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 d8d8bc68e6..4a103668d2 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 110a8ea516..54b17e4d51 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 ca3e98cd2c..0000000000 --- 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 5dc00261d1..0e8ebe9bc2 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 6ad44552bb..79a0fb1960 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 0000000000..38d0cdf7a1 --- /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 01f9b96018..170b06c528 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 c6a8785e46..b144f2d88a 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 1264e3780e..775fe1a232 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 bfccc8d30f..0c4a110733 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 d8d5d30454..f9e67bc760 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 0000000000..6d504c1451 --- /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 19cd1c76a0..cf763b4959 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 0f61c29ce6..c81ccbd668 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 bc1b6e5896..2248e363a4 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 bbdd252ce3..ec932ebc86 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 41d3947a5e..da9a82791a 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 c2e685e96c..d0f675d270 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 33970f4093..260c6ae981 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/55] 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 5823017100..f71d7e9497 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 bf9fcdfd33..42ff145ec1 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 66278d35b8..6c7181641c 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 3ab3633f93..d74cb6d090 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 f29b86f999..ecc49d177a 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 127dc448ee..a34e491802 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 902c8a7da6..5bf9f3a19f 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 bd981842e3..97090ae730 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 2593ea64a4..bdea088595 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 775fe1a232..33e1a4a815 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 52d32e8506..cfe54d8311 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 d750b96c7c..c866336a43 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 da9a82791a..d8d775bd8c 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 ad276ad1ff..be5f05c3d8 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 023194190b..01353d5ac0 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 20f2ff6979..8acb718f3a 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/55] 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 f71d7e9497..60b52ea9c3 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 60c3160fe8..e014d1ac40 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 fd9215970a..fa138a802f 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 f9fcbf972d..9ad18e26d8 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 140b944982..7dfa784555 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 6aa7caf4c9..6d6a0daa40 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 e0d08325b4..061d2d232c 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 113ad55120..30aeea685f 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 9542adce1c..359fee49cc 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 1b1ee68318..d31118728f 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 f10594cf0b..c2ffb9489e 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 36094f78eb..df7e863255 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 b9bd902b50..e28c1c39ed 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 7d7479c9fd..9f33a4f115 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 8ff2aa01bf..b5a0dc5c9f 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 ecc49d177a..538912164b 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 d8924a38ad..5d60feba60 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 b74606fa1a..598caad280 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 cc6dc0e6ef..de0c782f1c 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 1a64484a6b..cc01db177c 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 0000000000..1d75098e56 --- /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 36b695e11b..353129b2d7 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 4789eb3dec..0978d1d97b 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 0000000000..a1d66c9115 --- /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 79a0fb1960..719d9e2ffa 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 4afbdbb8c8..f2f8a16ba0 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 9e704f0a0e..709cbb8580 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 170b06c528..d13fbf7fcb 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 ecf4cb7913..e44d18a8d6 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 0666b37cda..fda41610fd 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 97090ae730..64a69c0e63 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 9470080c80..90b3ff6830 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 0000000000..96d16c714c --- /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 c866336a43..7efbcbcaf3 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 131a5f0407..09fc034246 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 27ce1dc3c3..d4902cf8b2 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 79d639ee0b..f73605b049 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 0ad8dbc55a..ed4c04c630 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 c503df9f94..99aed10eac 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 718aff315c..e87f4256d5 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 ded72ebf38..6f0ea9cb8a 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 0000000000..0ccc911dcf --- /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 4f8f1d9860..933fab0a21 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 a1c06d31db..21a22861eb 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 4b7bcbecbf..5267823df8 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 8acb718f3a..2982e9567b 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/55] Highlight breaking chnages in changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d52b8db5c3..f28311b42d 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/55] 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 c258e5f595..daf5d789ea 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; From 7554695a0876814c79e196389757e5001ffdaf45 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 28 Sep 2023 16:01:06 +0000 Subject: [PATCH 24/55] release: 7.0.0-beta.1 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f28311b42d..aef8f605ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 7.0.0-beta.1 ### Features diff --git a/gradle.properties b/gradle.properties index 1e9d0ec1b0..a3566cfdb7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ android.useAndroidX=true android.defaults.buildfeatures.buildconfig=true # Release information -versionName=6.30.0 +versionName=7.0.0-beta.1 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android From 1513e7e8d1b12630b9118b0ca1e80aead92588d5 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 6 Oct 2023 17:13:39 +0200 Subject: [PATCH 25/55] Improve start transaction overloads (#2964) --- CHANGELOG.md | 7 ++ .../io/sentry/samples/openfeign/Main.java | 6 +- sentry/api/sentry.api | 22 +--- sentry/src/main/java/io/sentry/Hub.java | 14 --- .../src/main/java/io/sentry/HubAdapter.java | 13 --- sentry/src/main/java/io/sentry/IHub.java | 98 ++--------------- sentry/src/main/java/io/sentry/NoOpHub.java | 13 --- sentry/src/main/java/io/sentry/Sentry.java | 103 ++---------------- .../src/test/java/io/sentry/HubAdapterTest.kt | 7 +- sentry/src/test/java/io/sentry/HubTest.kt | 6 +- sentry/src/test/java/io/sentry/SentryTest.kt | 8 +- 11 files changed, 45 insertions(+), 252 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aef8f605ff..cfb43f57c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +**Breaking changes:** +- Cleanup `startTransaction` overloads ([#2964](https://github.com/getsentry/sentry-java/pull/2964)) + - We have reduce the number of overloads by allowing to pass in `TransactionOptions` instead of having separate parameters for certain options. + - `TransactionOptions` has defaults set and can be customized + ## 7.0.0-beta.1 ### Features diff --git a/sentry-samples/sentry-samples-openfeign/src/main/java/io/sentry/samples/openfeign/Main.java b/sentry-samples/sentry-samples-openfeign/src/main/java/io/sentry/samples/openfeign/Main.java index a23dbe3ff9..2ce13a5263 100644 --- a/sentry-samples/sentry-samples-openfeign/src/main/java/io/sentry/samples/openfeign/Main.java +++ b/sentry-samples/sentry-samples-openfeign/src/main/java/io/sentry/samples/openfeign/Main.java @@ -7,6 +7,7 @@ import feign.gson.GsonEncoder; import io.sentry.ITransaction; import io.sentry.Sentry; +import io.sentry.TransactionOptions; import io.sentry.openfeign.SentryCapability; import java.util.List; @@ -40,7 +41,10 @@ public static void main(String[] args) { .decoder(new GsonDecoder()) .target(TodoApi.class, "https://jsonplaceholder.typicode.com/"); - final ITransaction transaction = Sentry.startTransaction("load-todos2", "console", true); + final TransactionOptions transactionOptions = new TransactionOptions(); + transactionOptions.setBindToScope(true); + final ITransaction transaction = + Sentry.startTransaction("load-todos2", "console", transactionOptions); final List all = todoApi.findAll(); System.out.println("Loaded " + all.size() + " todos"); System.out.println(todoApi.findById(1L)); diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index daf5d789ea..83e8facb75 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -448,7 +448,6 @@ public final class io/sentry/Hub : io/sentry/IHub { public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V public fun startSession ()V - public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/CustomSamplingContext;Z)Lio/sentry/ITransaction; public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public fun traceHeaders ()Lio/sentry/SentryTraceHeader; public fun withScope (Lio/sentry/ScopeCallback;)V @@ -499,8 +498,6 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V public fun startSession ()V - public fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; - public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/CustomSamplingContext;Z)Lio/sentry/ITransaction; public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public fun traceHeaders ()Lio/sentry/SentryTraceHeader; public fun withScope (Lio/sentry/ScopeCallback;)V @@ -596,14 +593,9 @@ public abstract interface class io/sentry/IHub { public abstract fun setUser (Lio/sentry/protocol/User;)V public abstract fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; - public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/CustomSamplingContext;)Lio/sentry/ITransaction; - public abstract fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/CustomSamplingContext;Z)Lio/sentry/ITransaction; public abstract fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; - public fun startTransaction (Lio/sentry/TransactionContext;Z)Lio/sentry/ITransaction; public fun startTransaction (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ITransaction; - public fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/CustomSamplingContext;)Lio/sentry/ITransaction; - public fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/CustomSamplingContext;Z)Lio/sentry/ITransaction; - public fun startTransaction (Ljava/lang/String;Ljava/lang/String;Z)Lio/sentry/ITransaction; + public fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public abstract fun traceHeaders ()Lio/sentry/SentryTraceHeader; public abstract fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1086,8 +1078,6 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V public fun startSession ()V - public fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; - public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/CustomSamplingContext;Z)Lio/sentry/ITransaction; public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public fun traceHeaders ()Lio/sentry/SentryTraceHeader; public fun withScope (Lio/sentry/ScopeCallback;)V @@ -1571,16 +1561,10 @@ public final class io/sentry/Sentry { public static fun setUser (Lio/sentry/protocol/User;)V public static fun startSession ()V public static fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; - public static fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/CustomSamplingContext;)Lio/sentry/ITransaction; - public static fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/CustomSamplingContext;Z)Lio/sentry/ITransaction; public static fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; - public static fun startTransaction (Lio/sentry/TransactionContext;Z)Lio/sentry/ITransaction; public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ITransaction; - public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/CustomSamplingContext;)Lio/sentry/ITransaction; - public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/CustomSamplingContext;Z)Lio/sentry/ITransaction; - public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ITransaction; - public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)Lio/sentry/ITransaction; - public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Z)Lio/sentry/ITransaction; + public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public static fun traceHeaders ()Lio/sentry/SentryTraceHeader; public static fun withScope (Lio/sentry/ScopeCallback;)V } diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 02f058ed03..64594f53a9 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -676,7 +676,6 @@ public void flush(long timeoutMillis) { return sentryId; } - @ApiStatus.Internal @Override public @NotNull ITransaction startTransaction( final @NotNull TransactionContext transactionContext, @@ -684,19 +683,6 @@ public void flush(long timeoutMillis) { return createTransaction(transactionContext, transactionOptions); } - @Override - public @NotNull ITransaction startTransaction( - final @NotNull TransactionContext transactionContext, - final @Nullable CustomSamplingContext customSamplingContext, - final boolean bindToScope) { - - final TransactionOptions transactionOptions = new TransactionOptions(); - transactionOptions.setCustomSamplingContext(customSamplingContext); - transactionOptions.setBindToScope(bindToScope); - - return createTransaction(transactionContext, transactionOptions); - } - private @NotNull ITransaction createTransaction( final @NotNull TransactionContext transactionContext, final @NotNull TransactionOptions transactionOptions) { diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 7baf146cd8..b655336314 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -188,19 +188,6 @@ public void flush(long timeoutMillis) { .captureTransaction(transaction, traceContext, hint, profilingTraceData); } - @Override - public @NotNull ITransaction startTransaction(@NotNull TransactionContext transactionContexts) { - return Sentry.startTransaction(transactionContexts); - } - - @Override - public @NotNull ITransaction startTransaction( - @NotNull TransactionContext transactionContexts, - @Nullable CustomSamplingContext customSamplingContext, - boolean bindToScope) { - return Sentry.startTransaction(transactionContexts, customSamplingContext, bindToScope); - } - @Override public @NotNull ITransaction startTransaction( @NotNull TransactionContext transactionContext, diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 9882ee309f..a6700df70e 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -404,85 +404,23 @@ default SentryId captureTransaction(@NotNull SentryTransaction transaction, @Nul * @return created transaction */ default @NotNull ITransaction startTransaction(@NotNull TransactionContext transactionContexts) { - return startTransaction(transactionContexts, false); + return startTransaction(transactionContexts, new TransactionOptions()); } /** - * Creates a Transaction and returns the instance. - * - * @param transactionContexts the transaction contexts - * @param bindToScope if transaction should be bound to scope - * @return created transaction - */ - default @NotNull ITransaction startTransaction( - @NotNull TransactionContext transactionContexts, boolean bindToScope) { - return startTransaction(transactionContexts, null, bindToScope); - } - - /** - * Creates a Transaction and returns the instance. Based on the passed sampling context the - * decision if transaction is sampled will be taken by {@link TracesSampler}. - * - * @param name the transaction name - * @param operation the operation - * @param customSamplingContext the sampling context - * @return created transaction. - */ - default @NotNull ITransaction startTransaction( - @NotNull String name, - @NotNull String operation, - @Nullable CustomSamplingContext customSamplingContext) { - return startTransaction(name, operation, customSamplingContext, false); - } - - /** - * Creates a Transaction and returns the instance. Based on the passed sampling context the - * decision if transaction is sampled will be taken by {@link TracesSampler}. + * Creates a Transaction and returns the instance. Based on the {@link + * SentryOptions#getTracesSampleRate()} the decision if transaction is sampled will be taken by + * {@link TracesSampler}. * * @param name the transaction name * @param operation the operation - * @param customSamplingContext the sampling context - * @param bindToScope if transaction should be bound to scope - * @return created transaction. - */ - default @NotNull ITransaction startTransaction( - @NotNull String name, - @NotNull String operation, - @Nullable CustomSamplingContext customSamplingContext, - boolean bindToScope) { - return startTransaction( - new TransactionContext(name, operation), customSamplingContext, bindToScope); - } - - /** - * Creates a Transaction and returns the instance. Based on the passed transaction and sampling - * contexts the decision if transaction is sampled will be taken by {@link TracesSampler}. - * - * @param transactionContexts the transaction context - * @param customSamplingContext the sampling context - * @return created transaction. + * @return created transaction */ default @NotNull ITransaction startTransaction( - @NotNull TransactionContext transactionContexts, - @Nullable CustomSamplingContext customSamplingContext) { - return startTransaction(transactionContexts, customSamplingContext, false); + final @NotNull String name, final @NotNull String operation) { + return startTransaction(name, operation, new TransactionOptions()); } - /** - * Creates a Transaction and returns the instance. Based on the passed transaction and sampling - * contexts the decision if transaction is sampled will be taken by {@link TracesSampler}. - * - * @param transactionContexts the transaction context - * @param customSamplingContext the sampling context - * @param bindToScope if transaction should be bound to scope - * @return created transaction. - */ - @NotNull - ITransaction startTransaction( - @NotNull TransactionContext transactionContexts, - @Nullable CustomSamplingContext customSamplingContext, - boolean bindToScope); - /** * Creates a Transaction and returns the instance. Based on the {@link * SentryOptions#getTracesSampleRate()} the decision if transaction is sampled will be taken by @@ -490,11 +428,14 @@ ITransaction startTransaction( * * @param name the transaction name * @param operation the operation + * @param transactionOptions the transaction options * @return created transaction */ default @NotNull ITransaction startTransaction( - final @NotNull String name, final @NotNull String operation) { - return startTransaction(name, operation, null); + final @NotNull String name, + final @NotNull String operation, + final @NotNull TransactionOptions transactionOptions) { + return startTransaction(new TransactionContext(name, operation), transactionOptions); } /** @@ -511,21 +452,6 @@ ITransaction startTransaction( final @NotNull TransactionContext transactionContext, final @NotNull TransactionOptions transactionOptions); - /** - * Creates a Transaction and returns the instance. Based on the {@link - * SentryOptions#getTracesSampleRate()} the decision if transaction is sampled will be taken by - * {@link TracesSampler}. - * - * @param name the transaction name - * @param operation the operation - * @param bindToScope if transaction should be bound to scope - * @return created transaction - */ - default @NotNull ITransaction startTransaction( - final @NotNull String name, final @NotNull String operation, final boolean bindToScope) { - return startTransaction(name, operation, (CustomSamplingContext) null, bindToScope); - } - /** * Returns the "sentry-trace" header that allows tracing across services. Can also be used in * <meta> HTML tags. Also see {@link IHub#getBaggage()}. diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index bf88e3beea..d5ecf6c4f0 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -146,19 +146,6 @@ public void flush(long timeoutMillis) {} return SentryId.EMPTY_ID; } - @Override - public @NotNull ITransaction startTransaction(@NotNull TransactionContext transactionContexts) { - return NoOpTransaction.getInstance(); - } - - @Override - public @NotNull ITransaction startTransaction( - @NotNull TransactionContext transactionContexts, - @Nullable CustomSamplingContext customSamplingContext, - boolean bindToScope) { - return NoOpTransaction.getInstance(); - } - @Override public @NotNull ITransaction startTransaction( @NotNull TransactionContext transactionContext, diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 9215388ff3..aeefb6bc22 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -764,27 +764,14 @@ public static void endSession() { * * @param name the transaction name * @param operation the operation - * @param bindToScope if transaction should be bound to scope - * @return created transaction - */ - public static @NotNull ITransaction startTransaction( - final @NotNull String name, final @NotNull String operation, final boolean bindToScope) { - return getCurrentHub().startTransaction(name, operation, bindToScope); - } - - /** - * Creates a Transaction and returns the instance. - * - * @param name the transaction name - * @param operation the operation - * @param description the description + * @param transactionOptions options for the transaction * @return created transaction */ public static @NotNull ITransaction startTransaction( final @NotNull String name, final @NotNull String operation, - final @Nullable String description) { - return startTransaction(name, operation, description, false); + final @NotNull TransactionOptions transactionOptions) { + return getCurrentHub().startTransaction(name, operation, transactionOptions); } /** @@ -793,15 +780,16 @@ public static void endSession() { * @param name the transaction name * @param operation the operation * @param description the description - * @param bindToScope if transaction should be bound to scope + * @param transactionOptions options for the transaction * @return created transaction */ public static @NotNull ITransaction startTransaction( final @NotNull String name, final @NotNull String operation, final @Nullable String description, - final boolean bindToScope) { - final ITransaction transaction = getCurrentHub().startTransaction(name, operation, bindToScope); + final @NotNull TransactionOptions transactionOptions) { + final ITransaction transaction = + getCurrentHub().startTransaction(name, operation, transactionOptions); transaction.setDescription(description); return transaction; } @@ -817,83 +805,6 @@ public static void endSession() { return getCurrentHub().startTransaction(transactionContexts); } - /** - * Creates a Transaction and returns the instance. - * - * @param transactionContexts the transaction contexts - * @param bindToScope if transaction should be bound to scope - * @return created transaction - */ - public static @NotNull ITransaction startTransaction( - final @NotNull TransactionContext transactionContexts, boolean bindToScope) { - return getCurrentHub().startTransaction(transactionContexts, bindToScope); - } - - /** - * Creates a Transaction and returns the instance. Based on the passed sampling context the - * decision if transaction is sampled will be taken by {@link TracesSampler}. - * - * @param name the transaction name - * @param operation the operation - * @param customSamplingContext the sampling context - * @return created transaction. - */ - public static @NotNull ITransaction startTransaction( - final @NotNull String name, - final @NotNull String operation, - final @NotNull CustomSamplingContext customSamplingContext) { - return getCurrentHub().startTransaction(name, operation, customSamplingContext); - } - - /** - * Creates a Transaction and returns the instance. Based on the passed sampling context the - * decision if transaction is sampled will be taken by {@link TracesSampler}. - * - * @param name the transaction name - * @param operation the operation - * @param customSamplingContext the sampling context - * @param bindToScope if transaction should be bound to scope - * @return created transaction. - */ - public static @NotNull ITransaction startTransaction( - final @NotNull String name, - final @NotNull String operation, - final @NotNull CustomSamplingContext customSamplingContext, - final boolean bindToScope) { - return getCurrentHub().startTransaction(name, operation, customSamplingContext, bindToScope); - } - - /** - * Creates a Transaction and returns the instance. Based on the passed transaction and sampling - * contexts the decision if transaction is sampled will be taken by {@link TracesSampler}. - * - * @param transactionContexts the transaction context - * @param customSamplingContext the sampling context - * @return created transaction. - */ - public static @NotNull ITransaction startTransaction( - final @NotNull TransactionContext transactionContexts, - final @NotNull CustomSamplingContext customSamplingContext) { - return getCurrentHub().startTransaction(transactionContexts, customSamplingContext); - } - - /** - * Creates a Transaction and returns the instance. Based on the passed transaction and sampling - * contexts the decision if transaction is sampled will be taken by {@link TracesSampler}. - * - * @param transactionContexts the transaction context - * @param customSamplingContext the sampling context - * @param bindToScope if transaction should be bound to scope - * @return created transaction. - */ - public static @NotNull ITransaction startTransaction( - final @NotNull TransactionContext transactionContexts, - final @Nullable CustomSamplingContext customSamplingContext, - final boolean bindToScope) { - return getCurrentHub() - .startTransaction(transactionContexts, customSamplingContext, bindToScope); - } - /** * Creates a Transaction and returns the instance. * diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index 0d1713e41d..df4c0bdbcc 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -2,8 +2,10 @@ package io.sentry import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User +import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.reset import org.mockito.kotlin.verify import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -206,10 +208,9 @@ class HubAdapterTest { val samplingContext = mock() val transactionOptions = mock() HubAdapter.getInstance().startTransaction(transactionContext) - verify(hub).startTransaction(eq(transactionContext)) + verify(hub).startTransaction(eq(transactionContext), any()) - HubAdapter.getInstance().startTransaction(transactionContext, samplingContext, false) - verify(hub).startTransaction(eq(transactionContext), eq(samplingContext), eq(false)) + reset(hub) HubAdapter.getInstance().startTransaction(transactionContext, transactionOptions) verify(hub).startTransaction(eq(transactionContext), eq(transactionOptions)) diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index 03fcbcaf33..44cffe2977 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -1497,7 +1497,7 @@ class HubTest { fun `when startTransaction with bindToScope set to false, transaction is not attached to the scope`() { val hub = generateHub() - hub.startTransaction("name", "op", false) + hub.startTransaction("name", "op", TransactionOptions()) hub.configureScope { assertNull(it.span) @@ -1519,7 +1519,7 @@ class HubTest { fun `when startTransaction with bindToScope set to true, transaction is attached to the scope`() { val hub = generateHub() - val transaction = hub.startTransaction("name", "op", true) + val transaction = hub.startTransaction("name", "op", TransactionOptions().also { it.isBindToScope = true }) hub.configureScope { assertEquals(transaction, it.span) @@ -1595,7 +1595,7 @@ class HubTest { val sut = Hub(options) sut.addBreadcrumb("Test") - sut.startTransaction("test", "test.op", true) + sut.startTransaction("test", "test.op", TransactionOptions().also { it.isBindToScope = true }) sut.close() // we have to clone the scope, so its isEnabled returns true, but it's still built up from diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 26379ddd21..9a19d9f837 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -240,7 +240,7 @@ class SentryTest { it.tracesSampleRate = 1.0 } - val transaction = Sentry.startTransaction("name", "op", "desc") + val transaction = Sentry.startTransaction("name", "op", "desc", TransactionOptions()) assertEquals("name", transaction.name) assertEquals("op", transaction.operation) assertEquals("desc", transaction.description) @@ -823,7 +823,7 @@ class SentryTest { it.sampleRate = 1.0 }, true) - val transaction = Sentry.startTransaction("name", "op-root", true) + val transaction = Sentry.startTransaction("name", "op-root", TransactionOptions().also { it.isBindToScope = true }) transaction.startChild("op-child") val span = Sentry.getSpan()!! @@ -840,7 +840,7 @@ class SentryTest { it.sampleRate = 1.0 }, false) - val transaction = Sentry.startTransaction("name", "op-root", true) + val transaction = Sentry.startTransaction("name", "op-root", TransactionOptions().also { it.isBindToScope = true }) transaction.startChild("op-child") val span = Sentry.getSpan()!! @@ -855,7 +855,7 @@ class SentryTest { it.sampleRate = 1.0 }, false) - val transaction = Sentry.startTransaction("name", "op-root", true) + val transaction = Sentry.startTransaction("name", "op-root", TransactionOptions().also { it.isBindToScope = true }) transaction.startChild("op-child") val span = Sentry.getSpan()!! From e0b84aa03590eb9ac3dfa6115eedb682902f1d52 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 11 Oct 2023 08:14:28 +0200 Subject: [PATCH 26/55] Guard raw logback message and parameters with `sendDefaultPii` if an `encoder` has been configured (#2976) --- CHANGELOG.md | 1 + .../io/sentry/logback/SentryAppender.java | 9 +++-- .../io/sentry/logback/SentryAppenderTest.kt | 34 +++++++++++++++---- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfb43f57c2..7a1ae4baac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Cleanup `startTransaction` overloads ([#2964](https://github.com/getsentry/sentry-java/pull/2964)) - We have reduce the number of overloads by allowing to pass in `TransactionOptions` instead of having separate parameters for certain options. - `TransactionOptions` has defaults set and can be customized +- Raw logback message and parameters are now guarded by `sendDefaultPii` if an `encoder` has been configured ([#2976](https://github.com/getsentry/sentry-java/pull/2976)) ## 7.0.0-beta.1 diff --git a/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java b/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java index 7ff9f5d9ba..d0be108149 100644 --- a/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java +++ b/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java @@ -99,9 +99,14 @@ protected void append(@NotNull ILoggingEvent eventObject) { protected @NotNull SentryEvent createEvent(@NotNull ILoggingEvent loggingEvent) { final SentryEvent event = new SentryEvent(DateUtils.getDateTime(loggingEvent.getTimeStamp())); final Message message = new Message(); - message.setMessage(loggingEvent.getMessage()); + + // if encoder is set we treat message+params as PII as encoders may be used to mask/strip PII + if (encoder == null || options.isSendDefaultPii()) { + message.setMessage(loggingEvent.getMessage()); + message.setParams(toParams(loggingEvent.getArgumentArray())); + } + message.setFormatted(formatted(loggingEvent)); - message.setParams(toParams(loggingEvent.getArgumentArray())); event.setMessage(message); event.setLogger(loggingEvent.getLoggerName()); event.setLevel(formatLevel(loggingEvent.getLevel())); diff --git a/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt b/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt index 3a4f8acaf2..4217954be1 100644 --- a/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt +++ b/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt @@ -35,7 +35,7 @@ import kotlin.test.assertNull import kotlin.test.assertTrue class SentryAppenderTest { - private class Fixture(dsn: String? = "http://key@localhost/proj", minimumBreadcrumbLevel: Level? = null, minimumEventLevel: Level? = null, contextTags: List? = null, encoder: Encoder? = null) { + private class Fixture(dsn: String? = "http://key@localhost/proj", minimumBreadcrumbLevel: Level? = null, minimumEventLevel: Level? = null, contextTags: List? = null, encoder: Encoder? = null, sendDefaultPii: Boolean = false) { val logger: Logger = LoggerFactory.getLogger(SentryAppenderTest::class.java) val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext val transportFactory = mock() @@ -47,6 +47,7 @@ class SentryAppenderTest { val appender = SentryAppender() val options = SentryOptions() options.dsn = dsn + options.isSendDefaultPii = sendDefaultPii contextTags?.forEach { options.addContextTag(it) } appender.setOptions(options) appender.setMinimumBreadcrumbLevel(minimumBreadcrumbLevel) @@ -118,14 +119,35 @@ class SentryAppenderTest { fun `encodes message`() { var encoder = PatternLayoutEncoder() encoder.pattern = "encoderadded %msg" - fixture = Fixture(minimumEventLevel = Level.DEBUG, encoder = encoder) - fixture.logger.info("testing encoding") + fixture = Fixture(minimumEventLevel = Level.DEBUG, encoder = encoder, sendDefaultPii = true) + fixture.logger.info("testing encoding {}", "param1") verify(fixture.transport).send( checkEvent { event -> assertNotNull(event.message) { message -> - assertEquals("encoderadded testing encoding", message.formatted) - assertEquals("testing encoding", message.message) + assertEquals("encoderadded testing encoding param1", message.formatted) + assertEquals("testing encoding {}", message.message) + assertEquals(listOf("param1"), message.params) + } + assertEquals("io.sentry.logback.SentryAppenderTest", event.logger) + }, + anyOrNull() + ) + } + + @Test + fun `if encoder is set treats raw message and params as PII`() { + var encoder = PatternLayoutEncoder() + encoder.pattern = "encoderadded %msg" + fixture = Fixture(minimumEventLevel = Level.DEBUG, encoder = encoder, sendDefaultPii = false) + fixture.logger.info("testing encoding {}", "param1") + + verify(fixture.transport).send( + checkEvent { event -> + assertNotNull(event.message) { message -> + assertEquals("encoderadded testing encoding param1", message.formatted) + assertNull(message.message) + assertNull(message.params) } assertEquals("io.sentry.logback.SentryAppenderTest", event.logger) }, @@ -151,7 +173,7 @@ class SentryAppenderTest { @Test fun `fallsback when encoder throws`() { var encoder = ThrowingEncoder() - fixture = Fixture(minimumEventLevel = Level.DEBUG, encoder = encoder) + fixture = Fixture(minimumEventLevel = Level.DEBUG, encoder = encoder, sendDefaultPii = true) fixture.logger.info("testing when encoder throws") verify(fixture.transport).send( From 1f3f5dc2bc73af4711f8fbb30c5c2cb348a96a09 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 23 Oct 2023 21:28:48 +0200 Subject: [PATCH 27/55] Use `getMyMemoryState()` instead of `getRunningAppProcesses()` (#3004) --- CHANGELOG.md | 7 + .../core/ActivityLifecycleIntegration.java | 2 +- .../io/sentry/android/core/ContextUtils.java | 28 +-- .../io/sentry/android/core/SentryAndroid.java | 3 +- .../core/ActivityLifecycleIntegrationTest.kt | 15 +- .../core/AndroidOptionsInitializerTest.kt | 10 +- .../sentry/android/core/ContextUtilsTest.kt | 233 +++++++++++++++--- .../android/core/ContextUtilsTestHelper.kt | 41 +++ .../android/core/ContextUtilsUnitTests.kt | 192 --------------- .../core/ManifestMetadataReaderTest.kt | 2 +- .../sentry/android/core/SentryAndroidTest.kt | 8 +- .../android/core/SentryInitProviderTest.kt | 14 +- .../android/core/SentryLogcatAdapterTest.kt | 2 +- .../core/SentryPerformanceProviderTest.kt | 8 +- .../android/okhttp/SentryOkHttpInterceptor.kt | 1 + 15 files changed, 292 insertions(+), 274 deletions(-) create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTestHelper.kt delete mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f7a2a26a3..5e84151345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,19 @@ ## Unreleased +### Features + **Breaking changes:** - Cleanup `startTransaction` overloads ([#2964](https://github.com/getsentry/sentry-java/pull/2964)) - We have reduce the number of overloads by allowing to pass in `TransactionOptions` instead of having separate parameters for certain options. - `TransactionOptions` has defaults set and can be customized - Raw logback message and parameters are now guarded by `sendDefaultPii` if an `encoder` has been configured ([#2976](https://github.com/getsentry/sentry-java/pull/2976)) +### Fixes + +- Use `getMyMemoryState()` instead of `getRunningAppProcesses()` to retrieve process importance ([#3004](https://github.com/getsentry/sentry-java/pull/3004)) + - This should prevent some app stores from flagging apps as violating their privacy + ## 7.0.0-beta.1 ### Features 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 2826366b4b..1af77bdd9d 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 @@ -102,7 +102,7 @@ public ActivityLifecycleIntegration( // we only track app start for processes that will show an Activity (full launch). // Here we check the process importance which will tell us that. - foregroundImportance = ContextUtils.isForegroundImportance(this.application); + foregroundImportance = ContextUtils.isForegroundImportance(); } @Override 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 04cf1e6fc6..824148416c 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 @@ -15,7 +15,6 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; -import android.os.Process; import android.provider.Settings; import android.util.DisplayMetrics; import io.sentry.ILogger; @@ -27,7 +26,6 @@ import java.io.FileReader; import java.io.IOException; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -169,28 +167,12 @@ static String getVersionName(final @NotNull PackageInfo packageInfo) { * * @return true if IMPORTANCE_FOREGROUND and false otherwise */ - static boolean isForegroundImportance(final @NotNull Context context) { + static boolean isForegroundImportance() { try { - final Object service = context.getSystemService(Context.ACTIVITY_SERVICE); - if (service instanceof ActivityManager) { - final ActivityManager activityManager = (ActivityManager) service; - final List runningAppProcesses = - activityManager.getRunningAppProcesses(); - - if (runningAppProcesses != null) { - final int myPid = Process.myPid(); - for (final ActivityManager.RunningAppProcessInfo processInfo : runningAppProcesses) { - if (processInfo.pid == myPid) { - if (processInfo.importance == IMPORTANCE_FOREGROUND) { - return true; - } - break; - } - } - } - } - } catch (SecurityException ignored) { - // happens for isolated processes + final ActivityManager.RunningAppProcessInfo appProcessInfo = + new ActivityManager.RunningAppProcessInfo(); + ActivityManager.getMyMemoryState(appProcessInfo); + return appProcessInfo.importance == IMPORTANCE_FOREGROUND; } catch (Throwable ignored) { // should never happen } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index 044e727a5c..f4e1539a58 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -132,8 +132,7 @@ public static synchronized void init( true); final @NotNull IHub hub = Sentry.getCurrentHub(); - if (hub.getOptions().isEnableAutoSessionTracking() - && ContextUtils.isForegroundImportance(context)) { + if (hub.getOptions().isEnableAutoSessionTracking() && ContextUtils.isForegroundImportance()) { hub.addBreadcrumb(BreadcrumbFactory.forSession("session.start")); hub.startSession(); } 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 d74cb6d090..c1fb101596 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.content.Context import android.os.Build import android.os.Bundle import android.os.Looper @@ -47,6 +48,8 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.Shadows.shadowOf +import org.robolectric.shadow.api.Shadow +import org.robolectric.shadows.ShadowActivityManager import java.util.Date import java.util.concurrent.Callable import java.util.concurrent.Future @@ -68,7 +71,6 @@ class ActivityLifecycleIntegrationTest { private class Fixture { val application = mock() - val am = mock() val hub = mock() val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" @@ -78,6 +80,7 @@ class ActivityLifecycleIntegrationTest { val activityFramesTracker = mock() val fullyDisplayedReporter = FullyDisplayedReporter.getInstance() val transactionFinishedCallback = mock() + lateinit var shadowActivityManager: ShadowActivityManager // we init the transaction with a mock to avoid errors when finishing it after tests that don't start it var transaction: SentryTracer = mock() @@ -101,14 +104,11 @@ class ActivityLifecycleIntegrationTest { } whenever(buildInfo.sdkInfoVersion).thenReturn(apiVersion) - whenever(application.getSystemService(any())).thenReturn(am) - val process = RunningAppProcessInfo().apply { this.importance = importance } val processes = mutableListOf(process) - - whenever(am.runningAppProcesses).thenReturn(processes) + shadowActivityManager.setProcesses(processes) return ActivityLifecycleIntegration(application, buildInfo, activityFramesTracker) } @@ -126,10 +126,15 @@ class ActivityLifecycleIntegrationTest { } private val fixture = Fixture() + private lateinit var context: Context @BeforeTest fun `reset instance`() { AppStartState.getInstance().resetInstance() + + context = ApplicationProvider.getApplicationContext() + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? + fixture.shadowActivityManager = Shadow.extract(activityManager) } @AfterTest 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 1b147b106a..3b9c012c38 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 @@ -55,13 +55,13 @@ class AndroidOptionsInitializerTest { assets: AssetManager? = null ) { mockContext = if (metadata != null) { - ContextUtilsTest.mockMetaData( - mockContext = ContextUtilsTest.createMockContext(hasAppContext), + ContextUtilsTestHelper.mockMetaData( + mockContext = ContextUtilsTestHelper.createMockContext(hasAppContext), metaData = metadata, assets = assets ) } else { - ContextUtilsTest.createMockContext(hasAppContext) + ContextUtilsTestHelper.createMockContext(hasAppContext) } whenever(mockContext.cacheDir).thenReturn(file) if (mockContext.applicationContext != null) { @@ -101,8 +101,8 @@ class AndroidOptionsInitializerTest { isFragmentAvailable: Boolean = false, isTimberAvailable: Boolean = false ) { - mockContext = ContextUtilsTest.mockMetaData( - mockContext = ContextUtilsTest.createMockContext(hasAppContext = true), + mockContext = ContextUtilsTestHelper.mockMetaData( + mockContext = ContextUtilsTestHelper.createMockContext(hasAppContext = true), metaData = Bundle().apply { putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt index c05d71e2b4..b758fae1f8 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTest.kt @@ -1,41 +1,216 @@ package io.sentry.android.core -import android.app.Application +import android.app.ActivityManager +import android.app.ActivityManager.MemoryInfo +import android.app.ActivityManager.RunningAppProcessInfo +import android.content.BroadcastReceiver import android.content.Context -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.content.res.AssetManager -import android.os.Bundle +import android.content.IntentFilter +import android.os.Build +import android.os.Process +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.ILogger +import io.sentry.NoOpLogger +import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.io.FileNotFoundException - -object ContextUtilsTest { - fun mockMetaData(mockContext: Context = createMockContext(hasAppContext = false), metaData: Bundle, assets: AssetManager? = null): Context { - val mockPackageManager = mock() - val mockApplicationInfo = mock() - - whenever(mockContext.packageName).thenReturn("io.sentry.sample.test") - whenever(mockContext.packageManager).thenReturn(mockPackageManager) - whenever(mockPackageManager.getApplicationInfo(mockContext.packageName, PackageManager.GET_META_DATA)) - .thenReturn(mockApplicationInfo) - - if (assets == null) { - val mockAssets = mock() - whenever(mockAssets.open(any())).thenThrow(FileNotFoundException()) - whenever(mockContext.assets).thenReturn(mockAssets) - } else { - whenever(mockContext.assets).thenReturn(assets) +import org.robolectric.annotation.Config +import org.robolectric.shadow.api.Shadow +import org.robolectric.shadows.ShadowActivityManager +import org.robolectric.shadows.ShadowBuild +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@Config(sdk = [33]) +@RunWith(AndroidJUnit4::class) +class ContextUtilsTest { + + private lateinit var shadowActivityManager: ShadowActivityManager + private lateinit var context: Context + private lateinit var logger: ILogger + + @BeforeTest + fun `set up`() { + context = ApplicationProvider.getApplicationContext() + logger = NoOpLogger.getInstance() + ShadowBuild.reset() + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? + shadowActivityManager = Shadow.extract(activityManager) + } + + @Test + fun `Given a valid context, returns a valid PackageInfo`() { + val packageInfo = ContextUtils.getPackageInfo(context, mock(), mock()) + assertNotNull(packageInfo) + } + + @Test + fun `Given an invalid context, do not throw Error`() { + // as Context is not fully mocked, it'll throw NPE but catch it and return null + val packageInfo = ContextUtils.getPackageInfo(mock(), mock(), mock()) + assertNull(packageInfo) + } + + @Test + fun `Given a valid PackageInfo, returns a valid versionCode`() { + val packageInfo = ContextUtils.getPackageInfo(context, mock(), mock()) + val versionCode = ContextUtils.getVersionCode(packageInfo!!, mock()) + + assertNotNull(versionCode) + } + + @Test + fun `Given a valid PackageInfo, returns a valid versionName`() { + // VersionName is null during tests, so we mock it the second time + val packageInfo = ContextUtils.getPackageInfo(context, mock(), mock())!! + val versionName = ContextUtils.getVersionName(packageInfo) + assertNull(versionName) + val mockedPackageInfo = spy(packageInfo) { it.versionName = "" } + val mockedVersionName = ContextUtils.getVersionName(mockedPackageInfo) + assertNotNull(mockedVersionName) + } + + @Test + fun `when context is valid, getApplicationName returns application name`() { + val appName = ContextUtils.getApplicationName(context, logger) + assertEquals("io.sentry.android.core.test", appName) + } + + @Test + fun `when context is invalid, getApplicationName returns null`() { + val appName = ContextUtils.getApplicationName(mock(), logger) + assertNull(appName) + } + + @Test + fun `isSideLoaded returns true for test context`() { + val sideLoadedInfo = + ContextUtils.retrieveSideLoadedInfo(context, logger, BuildInfoProvider(logger)) + assertTrue(sideLoadedInfo!!.isSideLoaded) + } + + @Test + fun `when installerPackageName is not null, sideLoadedInfo returns false and installerStore`() { + val mockedContext = spy(context) { + val mockedPackageManager = spy(mock.packageManager) { + whenever(mock.getInstallerPackageName(any())).thenReturn("play.google.com") + } + whenever(mock.packageManager).thenReturn(mockedPackageManager) } + val sideLoadedInfo = + ContextUtils.retrieveSideLoadedInfo(mockedContext, logger, BuildInfoProvider(logger)) + assertFalse(sideLoadedInfo!!.isSideLoaded) + assertEquals("play.google.com", sideLoadedInfo.installerStore) + } + + @Test + @Config(qualifiers = "w360dp-h640dp-xxhdpi") + fun `when display metrics specified, getDisplayMetrics returns correct values`() { + val displayMetrics = ContextUtils.getDisplayMetrics(context, logger) + assertEquals(1080, displayMetrics!!.widthPixels) + assertEquals(1920, displayMetrics.heightPixels) + assertEquals(3.0f, displayMetrics.density) + assertEquals(480, displayMetrics.densityDpi) + } + + @Test + fun `when display metrics are not specified, getDisplayMetrics returns null`() { + val displayMetrics = ContextUtils.getDisplayMetrics(mock(), logger) + assertNull(displayMetrics) + } + + @Test + fun `when Build MODEL specified, getFamily returns correct value`() { + ShadowBuild.setModel("Pixel 3XL") + val family = ContextUtils.getFamily(logger) + assertEquals("Pixel", family) + } + + @Test + fun `when Build MODEL is not specified, getFamily returns null`() { + ShadowBuild.setModel(null) + val family = ContextUtils.getFamily(logger) + assertNull(family) + } + + @Test + fun `when supported abis is specified, getArchitectures returns correct values`() { + val architectures = ContextUtils.getArchitectures(BuildInfoProvider(logger)) + assertEquals("armeabi-v7a", architectures[0]) + } + + @Test + fun `when memory info is specified, returns correct values`() { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val shadowActivityManager = Shadow.extract(activityManager) + + shadowActivityManager.setMemoryInfo( + MemoryInfo().apply { + availMem = 128 + totalMem = 2048 + lowMemory = true + } + ) + val memInfo = ContextUtils.getMemInfo(context, logger) + assertEquals(128, memInfo!!.availMem) + assertEquals(2048, memInfo.totalMem) + assertTrue(memInfo.lowMemory) + } + + @Test + fun `when memory info is not specified, returns null`() { + val memInfo = ContextUtils.getMemInfo(mock(), logger) + assertNull(memInfo) + } + + @Test + fun `registerReceiver calls context_registerReceiver without exported flag on API 32-`() { + val buildInfo = mock() + val receiver = mock() + val filter = mock() + val context = mock() + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.S) + ContextUtils.registerReceiver(context, buildInfo, receiver, filter) + verify(context).registerReceiver(eq(receiver), eq(filter)) + } + + @Test + fun `registerReceiver calls context_registerReceiver with exported flag on API 33+`() { + val buildInfo = mock() + val receiver = mock() + val filter = mock() + val context = mock() + whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.TIRAMISU) + ContextUtils.registerReceiver(context, buildInfo, receiver, filter) + verify(context).registerReceiver(eq(receiver), eq(filter), eq(Context.RECEIVER_EXPORTED)) + } - mockApplicationInfo.metaData = metaData - return mockContext + @Test + fun `returns true when app started with foreground importance`() { + assertTrue(ContextUtils.isForegroundImportance()) } - fun createMockContext(hasAppContext: Boolean = true): Context { - val mockApp = mock() - whenever(mockApp.applicationContext).thenReturn(if (hasAppContext) mock() else null) - return mockApp + @Test + fun `returns false when app started with importance different than foreground`() { + shadowActivityManager.setProcesses( + listOf( + RunningAppProcessInfo().apply { + processName = "io.sentry.android.core.test" + pid = Process.myPid() + importance = RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING + } + ) + ) + assertFalse(ContextUtils.isForegroundImportance()) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTestHelper.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTestHelper.kt new file mode 100644 index 0000000000..032bf6a996 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsTestHelper.kt @@ -0,0 +1,41 @@ +package io.sentry.android.core + +import android.app.Application +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.res.AssetManager +import android.os.Bundle +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.io.FileNotFoundException + +object ContextUtilsTestHelper { + fun mockMetaData(mockContext: Context = createMockContext(hasAppContext = false), metaData: Bundle, assets: AssetManager? = null): Context { + val mockPackageManager = mock() + val mockApplicationInfo = mock() + + whenever(mockContext.packageName).thenReturn("io.sentry.sample.test") + whenever(mockContext.packageManager).thenReturn(mockPackageManager) + whenever(mockPackageManager.getApplicationInfo(mockContext.packageName, PackageManager.GET_META_DATA)) + .thenReturn(mockApplicationInfo) + + if (assets == null) { + val mockAssets = mock() + whenever(mockAssets.open(any())).thenThrow(FileNotFoundException()) + whenever(mockContext.assets).thenReturn(mockAssets) + } else { + whenever(mockContext.assets).thenReturn(assets) + } + + mockApplicationInfo.metaData = metaData + return mockContext + } + + fun createMockContext(hasAppContext: Boolean = true): Context { + val mockApp = mock() + whenever(mockApp.applicationContext).thenReturn(if (hasAppContext) mock() else null) + return mockApp + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt deleted file mode 100644 index 00e56d0fa3..0000000000 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ContextUtilsUnitTests.kt +++ /dev/null @@ -1,192 +0,0 @@ -package io.sentry.android.core - -import android.app.ActivityManager -import android.app.ActivityManager.MemoryInfo -import android.content.BroadcastReceiver -import android.content.Context -import android.content.IntentFilter -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.ILogger -import io.sentry.NoOpLogger -import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.spy -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import org.robolectric.annotation.Config -import org.robolectric.shadow.api.Shadow -import org.robolectric.shadows.ShadowActivityManager -import org.robolectric.shadows.ShadowBuild -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue - -@Config(sdk = [33]) -@RunWith(AndroidJUnit4::class) -class ContextUtilsUnitTests { - - private lateinit var context: Context - private lateinit var logger: ILogger - - @BeforeTest - fun `set up`() { - context = ApplicationProvider.getApplicationContext() - logger = NoOpLogger.getInstance() - ShadowBuild.reset() - } - - @Test - fun `Given a valid context, returns a valid PackageInfo`() { - val packageInfo = ContextUtils.getPackageInfo(context, mock(), mock()) - assertNotNull(packageInfo) - } - - @Test - fun `Given an invalid context, do not throw Error`() { - // as Context is not fully mocked, it'll throw NPE but catch it and return null - val packageInfo = ContextUtils.getPackageInfo(mock(), mock(), mock()) - assertNull(packageInfo) - } - - @Test - fun `Given a valid PackageInfo, returns a valid versionCode`() { - val packageInfo = ContextUtils.getPackageInfo(context, mock(), mock()) - val versionCode = ContextUtils.getVersionCode(packageInfo!!, mock()) - - assertNotNull(versionCode) - } - - @Test - fun `Given a valid PackageInfo, returns a valid versionName`() { - // VersionName is null during tests, so we mock it the second time - val packageInfo = ContextUtils.getPackageInfo(context, mock(), mock())!! - val versionName = ContextUtils.getVersionName(packageInfo) - assertNull(versionName) - val mockedPackageInfo = spy(packageInfo) { it.versionName = "" } - val mockedVersionName = ContextUtils.getVersionName(mockedPackageInfo) - assertNotNull(mockedVersionName) - } - - @Test - fun `when context is valid, getApplicationName returns application name`() { - val appName = ContextUtils.getApplicationName(context, logger) - assertEquals("io.sentry.android.core.test", appName) - } - - @Test - fun `when context is invalid, getApplicationName returns null`() { - val appName = ContextUtils.getApplicationName(mock(), logger) - assertNull(appName) - } - - @Test - fun `isSideLoaded returns true for test context`() { - val sideLoadedInfo = - ContextUtils.retrieveSideLoadedInfo(context, logger, BuildInfoProvider(logger)) - assertTrue(sideLoadedInfo!!.isSideLoaded) - } - - @Test - fun `when installerPackageName is not null, sideLoadedInfo returns false and installerStore`() { - val mockedContext = spy(context) { - val mockedPackageManager = spy(mock.packageManager) { - whenever(mock.getInstallerPackageName(any())).thenReturn("play.google.com") - } - whenever(mock.packageManager).thenReturn(mockedPackageManager) - } - val sideLoadedInfo = - ContextUtils.retrieveSideLoadedInfo(mockedContext, logger, BuildInfoProvider(logger)) - assertFalse(sideLoadedInfo!!.isSideLoaded) - assertEquals("play.google.com", sideLoadedInfo.installerStore) - } - - @Test - @Config(qualifiers = "w360dp-h640dp-xxhdpi") - fun `when display metrics specified, getDisplayMetrics returns correct values`() { - val displayMetrics = ContextUtils.getDisplayMetrics(context, logger) - assertEquals(1080, displayMetrics!!.widthPixels) - assertEquals(1920, displayMetrics.heightPixels) - assertEquals(3.0f, displayMetrics.density) - assertEquals(480, displayMetrics.densityDpi) - } - - @Test - fun `when display metrics are not specified, getDisplayMetrics returns null`() { - val displayMetrics = ContextUtils.getDisplayMetrics(mock(), logger) - assertNull(displayMetrics) - } - - @Test - fun `when Build MODEL specified, getFamily returns correct value`() { - ShadowBuild.setModel("Pixel 3XL") - val family = ContextUtils.getFamily(logger) - assertEquals("Pixel", family) - } - - @Test - fun `when Build MODEL is not specified, getFamily returns null`() { - ShadowBuild.setModel(null) - val family = ContextUtils.getFamily(logger) - assertNull(family) - } - - @Test - fun `when supported abis is specified, getArchitectures returns correct values`() { - val architectures = ContextUtils.getArchitectures(BuildInfoProvider(logger)) - assertEquals("armeabi-v7a", architectures[0]) - } - - @Test - fun `when memory info is specified, returns correct values`() { - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val shadowActivityManager = Shadow.extract(activityManager) - - shadowActivityManager.setMemoryInfo( - MemoryInfo().apply { - availMem = 128 - totalMem = 2048 - lowMemory = true - } - ) - val memInfo = ContextUtils.getMemInfo(context, logger) - assertEquals(128, memInfo!!.availMem) - assertEquals(2048, memInfo.totalMem) - assertTrue(memInfo.lowMemory) - } - - @Test - fun `when memory info is not specified, returns null`() { - val memInfo = ContextUtils.getMemInfo(mock(), logger) - assertNull(memInfo) - } - - @Test - fun `registerReceiver calls context_registerReceiver without exported flag on API 32-`() { - val buildInfo = mock() - val receiver = mock() - val filter = mock() - val context = mock() - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.S) - ContextUtils.registerReceiver(context, buildInfo, receiver, filter) - verify(context).registerReceiver(eq(receiver), eq(filter)) - } - - @Test - fun `registerReceiver calls context_registerReceiver with exported flag on API 33+`() { - val buildInfo = mock() - val receiver = mock() - val filter = mock() - val context = mock() - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.TIRAMISU) - ContextUtils.registerReceiver(context, buildInfo, receiver, filter) - verify(context).registerReceiver(eq(receiver), eq(filter), eq(Context.RECEIVER_EXPORTED)) - } -} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 62d829403b..9b01348a59 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -30,7 +30,7 @@ class ManifestMetadataReaderTest { val buildInfoProvider = mock() fun getContext(metaData: Bundle = Bundle()): Context { - return ContextUtilsTest.mockMetaData(metaData = metaData) + return ContextUtilsTestHelper.mockMetaData(metaData = metaData) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index b441c7789a..a8cc62d208 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -83,7 +83,7 @@ class SentryAndroidTest { putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") putBoolean(ManifestMetadataReader.AUTO_INIT, autoInit) } - val mockContext = context ?: ContextUtilsTest.mockMetaData(metaData = metadata) + val mockContext = context ?: ContextUtilsTestHelper.mockMetaData(metaData = metadata) when { logger != null -> SentryAndroid.init(mockContext, logger) options != null -> SentryAndroid.init(mockContext, options) @@ -268,7 +268,7 @@ class SentryAndroidTest { fun `When initializing Sentry manually and changing both cache dir and dsn, the corresponding options should reflect that change`() { var options: SentryOptions? = null - val mockContext = ContextUtilsTest.createMockContext(true) + val mockContext = ContextUtilsTestHelper.createMockContext(true) val cacheDirPath = Files.createTempDirectory("new_cache").absolutePathString() SentryAndroid.init(mockContext) { it.dsn = "https://key@sentry.io/123" @@ -303,10 +303,10 @@ class SentryAndroidTest { inForeground: Boolean, callback: (session: Session?) -> Unit ) { - val context = ContextUtilsTest.createMockContext() + val context = ContextUtilsTestHelper.createMockContext() Mockito.mockStatic(ContextUtils::class.java).use { mockedContextUtils -> - mockedContextUtils.`when` { ContextUtils.isForegroundImportance(context) } + mockedContextUtils.`when` { ContextUtils.isForegroundImportance() } .thenReturn(inForeground) SentryAndroid.init(context) { options -> options.release = "prod" diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryInitProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryInitProviderTest.kt index 65086b8aa0..a83076efb0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryInitProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryInitProviderTest.kt @@ -26,7 +26,7 @@ class SentryInitProviderTest { fun `when missing applicationId, SentryInitProvider throws`() { val providerInfo = ProviderInfo() - val mockContext = ContextUtilsTest.createMockContext() + val mockContext = ContextUtilsTestHelper.createMockContext() providerInfo.authority = SentryInitProvider::class.java.name assertFailsWith { sentryInitProvider.attachInfo(mockContext, providerInfo) } } @@ -39,7 +39,7 @@ class SentryInitProviderTest { providerInfo.authority = AUTHORITY val metaData = Bundle() - val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) + val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metaData) metaData.putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") @@ -56,7 +56,7 @@ class SentryInitProviderTest { providerInfo.authority = AUTHORITY val metaData = Bundle() - val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) + val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metaData) metaData.putString(ManifestMetadataReader.DSN, "") @@ -73,7 +73,7 @@ class SentryInitProviderTest { providerInfo.authority = AUTHORITY val metaData = Bundle() - val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) + val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metaData) metaData.putString(ManifestMetadataReader.DSN, null) @@ -88,7 +88,7 @@ class SentryInitProviderTest { providerInfo.authority = AUTHORITY val metaData = Bundle() - val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) + val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metaData) metaData.putString(ManifestMetadataReader.DSN, "invalid dsn") @@ -103,7 +103,7 @@ class SentryInitProviderTest { providerInfo.authority = AUTHORITY val metaData = Bundle() - val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) + val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metaData) metaData.putBoolean(ManifestMetadataReader.AUTO_INIT, false) @@ -129,7 +129,7 @@ class SentryInitProviderTest { val sentryOptions = SentryAndroidOptions() val metaData = Bundle() - val mockContext = ContextUtilsTest.mockMetaData(metaData = metaData) + val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metaData) metaData.putBoolean(ManifestMetadataReader.NDK_ENABLE, false) AndroidOptionsInitializer.loadDefaultAndMetadataOptions(sentryOptions, mockContext) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index 89bc9d6037..96390c7ef9 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -27,7 +27,7 @@ class SentryLogcatAdapterTest { val metadata = Bundle().apply { putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") } - val mockContext = ContextUtilsTest.mockMetaData(metaData = metadata) + val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metadata) when { options != null -> SentryAndroid.init(mockContext, options) else -> SentryAndroid.init(mockContext) 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 aa9d9cc26b..dec045af85 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 @@ -37,7 +37,7 @@ class SentryPerformanceProviderTest { fun `provider sets app start`() { val providerInfo = ProviderInfo() - val mockContext = ContextUtilsTest.createMockContext() + val mockContext = ContextUtilsTestHelper.createMockContext() providerInfo.authority = AUTHORITY val providerAppStartMillis = 10L @@ -59,7 +59,7 @@ class SentryPerformanceProviderTest { fun `provider sets first activity as cold start`() { val providerInfo = ProviderInfo() - val mockContext = ContextUtilsTest.createMockContext() + val mockContext = ContextUtilsTestHelper.createMockContext() providerInfo.authority = AUTHORITY val provider = SentryPerformanceProvider() @@ -74,7 +74,7 @@ class SentryPerformanceProviderTest { fun `provider sets first activity as warm start`() { val providerInfo = ProviderInfo() - val mockContext = ContextUtilsTest.createMockContext() + val mockContext = ContextUtilsTestHelper.createMockContext() providerInfo.authority = AUTHORITY val provider = SentryPerformanceProvider() @@ -89,7 +89,7 @@ class SentryPerformanceProviderTest { fun `provider sets app start end on first activity resume, and unregisters afterwards`() { val providerInfo = ProviderInfo() - val mockContext = ContextUtilsTest.createMockContext(true) + val mockContext = ContextUtilsTestHelper.createMockContext(true) providerInfo.authority = AUTHORITY val provider = SentryPerformanceProvider( 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 c9ae1d197e..ddefaabbde 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 @@ -57,6 +57,7 @@ class SentryOkHttpInterceptor( .addPackage("maven:io.sentry:sentry-android-okhttp", BuildConfig.VERSION_NAME) } + @Suppress("LongMethod") override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() From 607e89b6c0ae1544b5c25181294656ea6c049bc8 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 3 Nov 2023 11:27:15 +0100 Subject: [PATCH 28/55] Send SDK frames in case of crashes (#3021) --- CHANGELOG.md | 2 + sentry/api/sentry.api | 2 +- .../io/sentry/SentryExceptionFactory.java | 5 ++- .../io/sentry/SentryStackTraceFactory.java | 12 ++--- .../java/io/sentry/SentryThreadFactory.java | 2 +- .../io/sentry/SentryExceptionFactoryTest.kt | 26 +++++++++-- .../io/sentry/SentryStackTraceFactoryTest.kt | 45 ++++++++++++------- 7 files changed, 67 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e84151345..1cf26d02bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features +- Do not filter out Sentry SDK frames in case of uncaught exceptions ([#3021](https://github.com/getsentry/sentry-java/pull/3021)) + **Breaking changes:** - Cleanup `startTransaction` overloads ([#2964](https://github.com/getsentry/sentry-java/pull/2964)) - We have reduce the number of overloads by allowing to pass in `TransactionOptions` instead of having separate parameters for certain options. diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index be2d5ee297..b70840f2d7 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2181,7 +2181,7 @@ public final class io/sentry/SentrySpanStorage { public final class io/sentry/SentryStackTraceFactory { public fun (Lio/sentry/SentryOptions;)V public fun getInAppCallStack ()Ljava/util/List; - public fun getStackFrames ([Ljava/lang/StackTraceElement;)Ljava/util/List; + public fun getStackFrames ([Ljava/lang/StackTraceElement;Z)Ljava/util/List; public fun isInApp (Ljava/lang/String;)Ljava/lang/Boolean; } diff --git a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java index f268a11bf6..6652ebb504 100644 --- a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java +++ b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java @@ -159,8 +159,11 @@ Deque extractExceptionQueue(final @NotNull Throwable throwable) thread = Thread.currentThread(); } + final boolean includeSentryFrames = + exceptionMechanism != null && Boolean.FALSE.equals(exceptionMechanism.isHandled()); final List frames = - sentryStackTraceFactory.getStackFrames(currentThrowable.getStackTrace()); + sentryStackTraceFactory.getStackFrames( + currentThrowable.getStackTrace(), includeSentryFrames); SentryException exception = getSentryException( currentThrowable, exceptionMechanism, thread.getId(), frames, snapshot); diff --git a/sentry/src/main/java/io/sentry/SentryStackTraceFactory.java b/sentry/src/main/java/io/sentry/SentryStackTraceFactory.java index 9e63e133f3..cec6107eb3 100644 --- a/sentry/src/main/java/io/sentry/SentryStackTraceFactory.java +++ b/sentry/src/main/java/io/sentry/SentryStackTraceFactory.java @@ -26,7 +26,8 @@ public SentryStackTraceFactory(final @NotNull SentryOptions options) { * @return list of SentryStackFrames or null if none */ @Nullable - public List getStackFrames(@Nullable final StackTraceElement[] elements) { + public List getStackFrames( + @Nullable final StackTraceElement[] elements, final boolean includeSentryFrames) { List sentryStackFrames = null; if (elements != null && elements.length > 0) { @@ -36,9 +37,10 @@ public List getStackFrames(@Nullable final StackTraceElement[] // we don't want to add our own frames final String className = item.getClassName(); - if (className.startsWith("io.sentry.") - && !className.startsWith("io.sentry.samples.") - && !className.startsWith("io.sentry.mobile.")) { + if (!includeSentryFrames + && (className.startsWith("io.sentry.") + && !className.startsWith("io.sentry.samples.") + && !className.startsWith("io.sentry.mobile."))) { continue; } @@ -102,7 +104,7 @@ public Boolean isInApp(final @Nullable String className) { @NotNull List getInAppCallStack(final @NotNull Throwable exception) { final StackTraceElement[] stacktrace = exception.getStackTrace(); - final List frames = getStackFrames(stacktrace); + final List frames = getStackFrames(stacktrace, false); if (frames == null) { return Collections.emptyList(); } diff --git a/sentry/src/main/java/io/sentry/SentryThreadFactory.java b/sentry/src/main/java/io/sentry/SentryThreadFactory.java index 9d27741d53..832ec8ea72 100644 --- a/sentry/src/main/java/io/sentry/SentryThreadFactory.java +++ b/sentry/src/main/java/io/sentry/SentryThreadFactory.java @@ -136,7 +136,7 @@ List getCurrentThreads( sentryThread.setCrashed(crashed); final List frames = - sentryStackTraceFactory.getStackFrames(stackFramesElements); + sentryStackTraceFactory.getStackFrames(stackFramesElements, false); if (options.isAttachStacktrace() && frames != null && !frames.isEmpty()) { final SentryStackTrace sentryStackTrace = new SentryStackTrace(frames); diff --git a/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt b/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt index c4794afd39..888f17e0a3 100644 --- a/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt @@ -6,6 +6,7 @@ import io.sentry.protocol.SentryStackFrame import io.sentry.protocol.SentryStackTrace import io.sentry.protocol.SentryThread import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import kotlin.test.Test @@ -20,7 +21,7 @@ class SentryExceptionFactoryTest { fun getSut( stackTraceFactory: SentryStackTraceFactory = SentryStackTraceFactory( - SentryOptions().apply { addInAppExclude("io.sentry") } + SentryOptions() ) ): SentryExceptionFactory { return SentryExceptionFactory(stackTraceFactory) @@ -50,7 +51,7 @@ class SentryExceptionFactoryTest { @Test fun `when frames are null, do not set a stack trace object`() { val stackTraceFactory = mock() - whenever(stackTraceFactory.getStackFrames(any())).thenReturn(null) + whenever(stackTraceFactory.getStackFrames(any(), eq(false))).thenReturn(null) val sut = fixture.getSut(stackTraceFactory) val exception = Exception("Exception") @@ -63,7 +64,7 @@ class SentryExceptionFactoryTest { @Test fun `when frames are empty, do not set a stack trace object`() { val stackTraceFactory = mock() - whenever(stackTraceFactory.getStackFrames(any())).thenReturn(emptyList()) + whenever(stackTraceFactory.getStackFrames(any(), eq(false))).thenReturn(emptyList()) val sut = fixture.getSut(stackTraceFactory) val exception = Exception("Exception") @@ -146,6 +147,25 @@ class SentryExceptionFactoryTest { assertEquals(thread.id, queue.first.threadId) } + @Test + fun `when exception has an unhandled mechanism, it should include sentry frames`() { + val exception = Exception("message") + val mechanism = Mechanism().apply { + isHandled = false + type = "UncaughtExceptionHandler" + } + val thread = Thread() + val throwable = ExceptionMechanismException(mechanism, exception, thread) + + val queue = fixture.getSut().extractExceptionQueue(throwable) + + assertTrue( + queue.first.stacktrace!!.frames!!.any { + it.module != null && it.module!!.startsWith("io.sentry") + } + ) + } + @Test fun `returns empty list if stacktrace is not available for SentryThread`() { val thread = SentryThread() diff --git a/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt b/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt index 8a3a90e6d3..7b01912a6c 100644 --- a/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt @@ -16,38 +16,38 @@ class SentryStackTraceFactoryTest { val stacktrace = Thread.currentThread().stackTrace // count the stack traces but ignores the test class which is io.sentry package val count = stacktrace.size - 1 - assertEquals(count, sut.getStackFrames(stacktrace)!!.count()) + assertEquals(count, sut.getStackFrames(stacktrace, false)!!.count()) } @Test fun `when line number is negative, not added to sentry stacktrace`() { val stacktrace = StackTraceElement("class", "method", "fileName", -2) - val actual = sut.getStackFrames(arrayOf(stacktrace)) + val actual = sut.getStackFrames(arrayOf(stacktrace), false) assertNull(actual!![0].lineno) } @Test fun `when line number is positive, gets added to sentry stacktrace`() { val stacktrace = StackTraceElement("class", "method", "fileName", 1) - val actual = sut.getStackFrames(arrayOf(stacktrace)) + val actual = sut.getStackFrames(arrayOf(stacktrace), false) assertEquals(stacktrace.lineNumber, actual!![0].lineno) } @Test fun `when getStackFrames is called passing empty elements, return null`() { - assertNull(sut.getStackFrames(arrayOf())) + assertNull(sut.getStackFrames(arrayOf(), false)) } @Test fun `when getStackFrames is called passing null, return null`() { - assertNull(sut.getStackFrames(null)) + assertNull(sut.getStackFrames(null, false)) } @Test fun `when getStackFrames is called passing a valid array, fields should be set`() { val element = generateStackTrace("class") val stacktrace = arrayOf(element) - val stackFrames = sut.getStackFrames(stacktrace) + val stackFrames = sut.getStackFrames(stacktrace, false) assertEquals("class", stackFrames!![0].module) assertEquals("method", stackFrames[0].function) assertEquals("fileName", stackFrames[0].filename) @@ -63,7 +63,7 @@ class SentryStackTraceFactoryTest { val sentryStackTraceFactory = SentryStackTraceFactory( SentryOptions().apply { addInAppExclude("io.mysentry") } ) - val sentryElements = sentryStackTraceFactory.getStackFrames(elements) + val sentryElements = sentryStackTraceFactory.getStackFrames(elements, false) assertFalse(sentryElements!!.first().isInApp!!) } @@ -76,7 +76,7 @@ class SentryStackTraceFactoryTest { SentryOptions().apply { addInAppExclude("io.mysentry") } ) - val sentryElements = sentryStackTraceFactory.getStackFrames(elements) + val sentryElements = sentryStackTraceFactory.getStackFrames(elements, false) assertNull(sentryElements!!.first().isInApp) } @@ -86,7 +86,7 @@ class SentryStackTraceFactoryTest { val element = generateStackTrace("io.mysentry.MyActivity") val elements = arrayOf(element) val sentryStackTraceFactory = SentryStackTraceFactory(SentryOptions()) - val sentryElements = sentryStackTraceFactory.getStackFrames(elements) + val sentryElements = sentryStackTraceFactory.getStackFrames(elements, false) assertNull(sentryElements!!.first().isInApp) } @@ -100,7 +100,7 @@ class SentryStackTraceFactoryTest { val sentryStackTraceFactory = SentryStackTraceFactory( SentryOptions().apply { addInAppInclude("io.mysentry") } ) - val sentryElements = sentryStackTraceFactory.getStackFrames(elements) + val sentryElements = sentryStackTraceFactory.getStackFrames(elements, false) assertTrue(sentryElements!!.first().isInApp!!) } @@ -112,7 +112,7 @@ class SentryStackTraceFactoryTest { val sentryStackTraceFactory = SentryStackTraceFactory( SentryOptions().apply { addInAppInclude("io.mysentry") } ) - val sentryElements = sentryStackTraceFactory.getStackFrames(elements) + val sentryElements = sentryStackTraceFactory.getStackFrames(elements, false) assertNull(sentryElements!!.first().isInApp) } @@ -122,7 +122,7 @@ class SentryStackTraceFactoryTest { val element = generateStackTrace("io.mysentry.MyActivity") val elements = arrayOf(element) val sentryStackTraceFactory = SentryStackTraceFactory(SentryOptions()) - val sentryElements = sentryStackTraceFactory.getStackFrames(elements) + val sentryElements = sentryStackTraceFactory.getStackFrames(elements, false) assertNull(sentryElements!!.first().isInApp) } @@ -138,7 +138,7 @@ class SentryStackTraceFactoryTest { addInAppInclude("io.mysentry") } ) - val sentryElements = sentryStackTraceFactory.getStackFrames(elements) + val sentryElements = sentryStackTraceFactory.getStackFrames(elements, false) assertTrue(sentryElements!!.first().isInApp!!) } @@ -173,7 +173,20 @@ class SentryStackTraceFactoryTest { stacktrace = stacktrace.plusElement(sentryElement) assertNull( - sut.getStackFrames(stacktrace)!!.find { + sut.getStackFrames(stacktrace, false)!!.find { + it.module != null && it.module!!.startsWith("io.sentry") + } + ) + } + + @Test + fun `when getStackFrames is called with includeSentryFrames, does not remove sentry classes`() { + var stacktrace = Thread.currentThread().stackTrace + val sentryElement = StackTraceElement("io.sentry.element", "test", "test.java", 1) + stacktrace = stacktrace.plusElement(sentryElement) + + assertTrue( + sut.getStackFrames(stacktrace, true)!!.any { it.module != null && it.module!!.startsWith("io.sentry") } ) @@ -186,7 +199,7 @@ class SentryStackTraceFactoryTest { stacktrace = stacktrace.plusElement(sentryElement) assertNotNull( - sut.getStackFrames(stacktrace)!!.find { + sut.getStackFrames(stacktrace, false)!!.find { it.module != null && it.module!!.startsWith("io.sentry") } ) @@ -199,7 +212,7 @@ class SentryStackTraceFactoryTest { stacktrace = stacktrace.plusElement(sentryElement) assertNotNull( - sut.getStackFrames(stacktrace)!!.find { + sut.getStackFrames(stacktrace, false)!!.find { it.module != null && it.module!!.startsWith("io.sentry") } ) From 5f02b63d9c6c0055052cffb4bca3a8ab06c8700e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 7 Nov 2023 12:31:02 +0100 Subject: [PATCH 29/55] Fix build --- .../io/sentry/android/core/internal/util/ScreenshotUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 dd86891990..45e9d56877 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,5 +1,6 @@ package io.sentry.android.core.internal.util; +import android.annotation.SuppressLint; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -43,7 +44,7 @@ public class ScreenshotUtils { // We are keeping BuildInfoProvider param for compatibility, as it's being used by // cross-platform SDKs - if (!isActivityValid(activity, buildInfoProvider)) { + if (!isActivityValid(activity)) { logger.log(SentryLevel.DEBUG, "Activity isn't valid, not taking screenshot."); return null; } From 7fec1663e769903f90a876e32c8fc0d97641547f Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 7 Nov 2023 13:00:08 +0100 Subject: [PATCH 30/55] Fix test --- sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt b/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt index 66bf30916f..92033bf64c 100644 --- a/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryStackTraceFactoryTest.kt @@ -296,7 +296,7 @@ class SentryStackTraceFactoryTest { repeat(120) { exception.stackTrace += generateStackTrace("com.me.stackoverflow") } val sut = SentryStackTraceFactory(SentryOptions()) - val sentryFrames = sut.getStackFrames(exception.stackTrace) + val sentryFrames = sut.getStackFrames(exception.stackTrace, false) assertEquals(100, sentryFrames!!.size) } From 3f122f3d9c3637630544de25ea81f9b1a5fd5415 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 8 Nov 2023 09:54:55 +0000 Subject: [PATCH 31/55] release: 7.0.0-rc.1 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c06a09f3a3..1c570959a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 7.0.0-rc.1 ### Features diff --git a/gradle.properties b/gradle.properties index a3566cfdb7..b96bca1610 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ android.useAndroidX=true android.defaults.buildfeatures.buildconfig=true # Release information -versionName=7.0.0-beta.1 +versionName=7.0.0-rc.1 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android From 209ee177e46d828086a93c489e72282e1e7385aa Mon Sep 17 00:00:00 2001 From: Richard Harrah <1672786+ToppleTheNun@users.noreply.github.com> Date: Fri, 24 Nov 2023 08:56:27 -0500 Subject: [PATCH 32/55] add sentry-okhttp module (#3005) Co-authored-by: Roman Zavarnitsyn --- .craft.yml | 1 + .github/ISSUE_TEMPLATE/bug_report_java.yml | 1 + CHANGELOG.md | 8 + buildSrc/src/main/java/Config.kt | 1 + .../api/sentry-android-okhttp.api | 9 - sentry-android-okhttp/build.gradle.kts | 5 +- .../okhttp/SentryOkHttpEventListener.kt | 338 +++----------- .../android/okhttp/SentryOkHttpInterceptor.kt | 185 +------- sentry-okhttp/api/sentry-okhttp.api | 63 +++ sentry-okhttp/build.gradle.kts | 92 ++++ .../io/sentry}/okhttp/SentryOkHttpEvent.kt | 16 +- .../okhttp/SentryOkHttpEventListener.kt | 411 ++++++++++++++++++ .../sentry/okhttp/SentryOkHttpInterceptor.kt | 228 ++++++++++ .../io/sentry}/okhttp/SentryOkHttpUtils.kt | 6 +- .../META-INF/proguard/sentry-okhttp.pro | 13 + .../okhttp/SentryOkHttpEventListenerTest.kt | 2 +- .../sentry}/okhttp/SentryOkHttpEventTest.kt | 16 +- .../okhttp/SentryOkHttpInterceptorTest.kt | 2 +- .../sentry}/okhttp/SentryOkHttpUtilsTest.kt | 2 +- .../org.mockito.plugin.MockMaker | 0 .../io/sentry/samples/android/GithubAPI.kt | 4 +- settings.gradle.kts | 1 + 22 files changed, 927 insertions(+), 477 deletions(-) create mode 100644 sentry-okhttp/api/sentry-okhttp.api create mode 100644 sentry-okhttp/build.gradle.kts rename {sentry-android-okhttp/src/main/java/io/sentry/android => sentry-okhttp/src/main/java/io/sentry}/okhttp/SentryOkHttpEvent.kt (92%) create mode 100644 sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt create mode 100644 sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt rename {sentry-android-okhttp/src/main/java/io/sentry/android => sentry-okhttp/src/main/java/io/sentry}/okhttp/SentryOkHttpUtils.kt (95%) create mode 100644 sentry-okhttp/src/main/resources/META-INF/proguard/sentry-okhttp.pro rename {sentry-android-okhttp/src/test/java/io/sentry/android => sentry-okhttp/src/test/java/io/sentry}/okhttp/SentryOkHttpEventListenerTest.kt (99%) rename {sentry-android-okhttp/src/test/java/io/sentry/android => sentry-okhttp/src/test/java/io/sentry}/okhttp/SentryOkHttpEventTest.kt (97%) rename {sentry-android-okhttp/src/test/java/io/sentry/android => sentry-okhttp/src/test/java/io/sentry}/okhttp/SentryOkHttpInterceptorTest.kt (99%) rename {sentry-android-okhttp/src/test/java/io/sentry/android => sentry-okhttp/src/test/java/io/sentry}/okhttp/SentryOkHttpUtilsTest.kt (99%) rename sentry-android-okhttp/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker => sentry-okhttp/src/test/resources/mockito-extensions/org.mockito.plugin.MockMaker (100%) diff --git a/.craft.yml b/.craft.yml index f3f0f52c9a..28c7191380 100644 --- a/.craft.yml +++ b/.craft.yml @@ -49,6 +49,7 @@ targets: maven:io.sentry:sentry-jdbc: maven:io.sentry:sentry-graphql: maven:io.sentry:sentry-quartz: + # maven:io.sentry:sentry-okhttp: maven:io.sentry:sentry-android-navigation: maven:io.sentry:sentry-compose: maven:io.sentry:sentry-compose-android: diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index 429fbdee73..f802c3a0cc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -30,6 +30,7 @@ body: - sentry-quartz - sentry-openfeign - sentry-apache-http-client-5 + - sentry-okhttp - other validations: required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c570959a5..00d3494b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +### Features + +- Add `sentry-okhttp` module to support instrumenting OkHttp in non-Android projects ([#3005](https://github.com/getsentry/sentry-java/pull/3005)) + - This deprecates `sentry-android-okhttp` classes. Make sure to replace `io.sentry.android.okhttp` package name with `io.sentry.okhttp` before the next major, where the classes will be removed + - `SentryOkHttpUtils` was removed from public API as it's been exposed by mistake + ## 7.0.0-rc.1 ### Features diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index e2cbf460af..6c81e13a20 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -236,6 +236,7 @@ object Config { val SENTRY_SERVLET_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet" val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta" val SENTRY_COMPOSE_HELPER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.compose.helper" + val SENTRY_OKHTTP_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.okhttp" val group = "io.sentry" val description = "SDK for sentry.io" val versionNameProp = "versionName" diff --git a/sentry-android-okhttp/api/sentry-android-okhttp.api b/sentry-android-okhttp/api/sentry-android-okhttp.api index 099534ea61..a1ad9114a2 100644 --- a/sentry-android-okhttp/api/sentry-android-okhttp.api +++ b/sentry-android-okhttp/api/sentry-android-okhttp.api @@ -7,7 +7,6 @@ public final class io/sentry/android/okhttp/BuildConfig { } public final class io/sentry/android/okhttp/SentryOkHttpEventListener : okhttp3/EventListener { - public static final field Companion Lio/sentry/android/okhttp/SentryOkHttpEventListener$Companion; public fun ()V public fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;)V public synthetic fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -48,9 +47,6 @@ public final class io/sentry/android/okhttp/SentryOkHttpEventListener : okhttp3/ public fun secureConnectStart (Lokhttp3/Call;)V } -public final class io/sentry/android/okhttp/SentryOkHttpEventListener$Companion { -} - public final class io/sentry/android/okhttp/SentryOkHttpInterceptor : okhttp3/Interceptor { public fun ()V public fun (Lio/sentry/IHub;)V @@ -64,8 +60,3 @@ public abstract interface class io/sentry/android/okhttp/SentryOkHttpInterceptor public abstract fun execute (Lio/sentry/ISpan;Lokhttp3/Request;Lokhttp3/Response;)Lio/sentry/ISpan; } -public final class io/sentry/android/okhttp/SentryOkHttpUtils { - public static final field INSTANCE Lio/sentry/android/okhttp/SentryOkHttpUtils; - public final fun captureClientError (Lio/sentry/IHub;Lokhttp3/Request;Lokhttp3/Response;)V -} - diff --git a/sentry-android-okhttp/build.gradle.kts b/sentry-android-okhttp/build.gradle.kts index 0f98011fac..f3eaa59303 100644 --- a/sentry-android-okhttp/build.gradle.kts +++ b/sentry-android-okhttp/build.gradle.kts @@ -24,9 +24,7 @@ android { buildTypes { getByName("debug") - getByName("release") { - consumerProguardFiles("proguard-rules.pro") - } + getByName("release") } kotlinOptions { @@ -63,6 +61,7 @@ kotlin { dependencies { api(projects.sentry) + api(projects.sentryOkhttp) compileOnly(Config.Libs.okhttp) diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt index da13ee110a..7ca5313d8f 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt @@ -2,8 +2,6 @@ package io.sentry.android.okhttp import io.sentry.HubAdapter import io.sentry.IHub -import io.sentry.SpanDataConvention -import io.sentry.SpanStatus import okhttp3.Call import okhttp3.Connection import okhttp3.EventListener @@ -16,12 +14,11 @@ import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy -import java.util.concurrent.ConcurrentHashMap /** * Logs network performance event metrics to Sentry * - * Usage - add instance of [SentryOkHttpEventListener] in [OkHttpClient.eventListener] + * Usage - add instance of [SentryOkHttpEventListener] in [okhttp3.OkHttpClient.Builder.eventListener] * * ``` * val client = OkHttpClient.Builder() @@ -30,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap * .build() * ``` * - * If you already use a [OkHttpClient.eventListener], you can pass it in the constructor. + * If you already use a [okhttp3.EventListener], you can pass it in the constructor. * * ``` * val client = OkHttpClient.Builder() @@ -39,28 +36,15 @@ import java.util.concurrent.ConcurrentHashMap * .build() * ``` */ +@Deprecated( + "Use SentryOkHttpEventListener from sentry-okhttp instead", + ReplaceWith("SentryOkHttpEventListener", "io.sentry.okhttp.SentryOkHttpEventListener") +) @Suppress("TooManyFunctions") class SentryOkHttpEventListener( - private val hub: IHub = HubAdapter.getInstance(), - private val originalEventListenerCreator: ((call: Call) -> EventListener)? = null + hub: IHub = HubAdapter.getInstance(), + originalEventListenerCreator: ((call: Call) -> EventListener)? = null ) : EventListener() { - - private var originalEventListener: EventListener? = null - - companion object { - internal const val PROXY_SELECT_EVENT = "proxy_select" - internal const val DNS_EVENT = "dns" - internal const val SECURE_CONNECT_EVENT = "secure_connect" - internal const val CONNECT_EVENT = "connect" - internal const val CONNECTION_EVENT = "connection" - internal const val REQUEST_HEADERS_EVENT = "request_headers" - internal const val REQUEST_BODY_EVENT = "request_body" - internal const val RESPONSE_HEADERS_EVENT = "response_headers" - internal const val RESPONSE_BODY_EVENT = "response_body" - - internal val eventMap: MutableMap = ConcurrentHashMap() - } - constructor() : this( HubAdapter.getInstance(), originalEventListenerCreator = null @@ -86,98 +70,34 @@ class SentryOkHttpEventListener( originalEventListenerCreator = { originalEventListenerFactory.create(it) } ) - override fun callStart(call: Call) { - originalEventListener = originalEventListenerCreator?.invoke(call) - originalEventListener?.callStart(call) - // If the wrapped EventListener is ours, we can just delegate the calls, - // without creating other events that would create duplicates - if (canCreateEventSpan()) { - eventMap[call] = SentryOkHttpEvent(hub, call.request()) - } - } + private val delegate = io.sentry.okhttp.SentryOkHttpEventListener(hub, originalEventListenerCreator) - override fun proxySelectStart(call: Call, url: HttpUrl) { - originalEventListener?.proxySelectStart(call, url) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(PROXY_SELECT_EVENT) + override fun cacheConditionalHit(call: Call, cachedResponse: Response) { + delegate.cacheConditionalHit(call, cachedResponse) } - override fun proxySelectEnd( - call: Call, - url: HttpUrl, - proxies: List - ) { - originalEventListener?.proxySelectEnd(call, url, proxies) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(PROXY_SELECT_EVENT) { - if (proxies.isNotEmpty()) { - it.setData("proxies", proxies.joinToString { proxy -> proxy.toString() }) - } - } + override fun cacheHit(call: Call, response: Response) { + delegate.cacheHit(call, response) } - override fun dnsStart(call: Call, domainName: String) { - originalEventListener?.dnsStart(call, domainName) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(DNS_EVENT) + override fun cacheMiss(call: Call) { + delegate.cacheMiss(call) } - override fun dnsEnd( - call: Call, - domainName: String, - inetAddressList: List - ) { - originalEventListener?.dnsEnd(call, domainName, inetAddressList) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(DNS_EVENT) { - it.setData("domain_name", domainName) - if (inetAddressList.isNotEmpty()) { - it.setData("dns_addresses", inetAddressList.joinToString { address -> address.toString() }) - } - } + override fun callEnd(call: Call) { + delegate.callEnd(call) } - override fun connectStart( - call: Call, - inetSocketAddress: InetSocketAddress, - proxy: Proxy - ) { - originalEventListener?.connectStart(call, inetSocketAddress, proxy) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(CONNECT_EVENT) + override fun callFailed(call: Call, ioe: IOException) { + delegate.callFailed(call, ioe) } - override fun secureConnectStart(call: Call) { - originalEventListener?.secureConnectStart(call) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(SECURE_CONNECT_EVENT) + override fun callStart(call: Call) { + delegate.callStart(call) } - override fun secureConnectEnd(call: Call, handshake: Handshake?) { - originalEventListener?.secureConnectEnd(call, handshake) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(SECURE_CONNECT_EVENT) + override fun canceled(call: Call) { + delegate.canceled(call) } override fun connectEnd( @@ -186,13 +106,7 @@ class SentryOkHttpEventListener( proxy: Proxy, protocol: Protocol? ) { - originalEventListener?.connectEnd(call, inetSocketAddress, proxy, protocol) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setProtocol(protocol?.name) - okHttpEvent.finishSpan(CONNECT_EVENT) + delegate.connectEnd(call, inetSocketAddress, proxy, protocol) } override fun connectFailed( @@ -202,210 +116,86 @@ class SentryOkHttpEventListener( protocol: Protocol?, ioe: IOException ) { - originalEventListener?.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setProtocol(protocol?.name) - okHttpEvent.setError(ioe.message) - okHttpEvent.finishSpan(CONNECT_EVENT) { - it.throwable = ioe - it.status = SpanStatus.INTERNAL_ERROR - } + delegate.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) + } + + override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) { + delegate.connectStart(call, inetSocketAddress, proxy) } override fun connectionAcquired(call: Call, connection: Connection) { - originalEventListener?.connectionAcquired(call, connection) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(CONNECTION_EVENT) + delegate.connectionAcquired(call, connection) } override fun connectionReleased(call: Call, connection: Connection) { - originalEventListener?.connectionReleased(call, connection) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(CONNECTION_EVENT) + delegate.connectionReleased(call, connection) } - override fun requestHeadersStart(call: Call) { - originalEventListener?.requestHeadersStart(call) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(REQUEST_HEADERS_EVENT) + override fun dnsEnd(call: Call, domainName: String, inetAddressList: List) { + delegate.dnsEnd(call, domainName, inetAddressList) } - override fun requestHeadersEnd(call: Call, request: Request) { - originalEventListener?.requestHeadersEnd(call, request) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(REQUEST_HEADERS_EVENT) + override fun dnsStart(call: Call, domainName: String) { + delegate.dnsStart(call, domainName) } - override fun requestBodyStart(call: Call) { - originalEventListener?.requestBodyStart(call) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(REQUEST_BODY_EVENT) + override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List) { + delegate.proxySelectEnd(call, url, proxies) + } + + override fun proxySelectStart(call: Call, url: HttpUrl) { + delegate.proxySelectStart(call, url) } override fun requestBodyEnd(call: Call, byteCount: Long) { - originalEventListener?.requestBodyEnd(call, byteCount) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(REQUEST_BODY_EVENT) { - if (byteCount > 0) { - it.setData("http.request_content_length", byteCount) - } - } - okHttpEvent.setRequestBodySize(byteCount) + delegate.requestBodyEnd(call, byteCount) } - override fun requestFailed(call: Call, ioe: IOException) { - originalEventListener?.requestFailed(call, ioe) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setError(ioe.message) - // requestFailed can happen after requestHeaders or requestBody. - // If requestHeaders already finished, we don't change its status. - okHttpEvent.finishSpan(REQUEST_HEADERS_EVENT) { - if (!it.isFinished) { - it.status = SpanStatus.INTERNAL_ERROR - it.throwable = ioe - } - } - okHttpEvent.finishSpan(REQUEST_BODY_EVENT) { - it.status = SpanStatus.INTERNAL_ERROR - it.throwable = ioe - } + override fun requestBodyStart(call: Call) { + delegate.requestBodyStart(call) } - override fun responseHeadersStart(call: Call) { - originalEventListener?.responseHeadersStart(call) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(RESPONSE_HEADERS_EVENT) + override fun requestFailed(call: Call, ioe: IOException) { + delegate.requestFailed(call, ioe) } - override fun responseHeadersEnd(call: Call, response: Response) { - originalEventListener?.responseHeadersEnd(call, response) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setResponse(response) - val responseHeadersSpan = okHttpEvent.finishSpan(RESPONSE_HEADERS_EVENT) { - it.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.code) - // Let's not override the status of a span that was set - if (it.status == null) { - it.status = SpanStatus.fromHttpStatusCode(response.code) - } - } - okHttpEvent.scheduleFinish(responseHeadersSpan?.finishDate ?: hub.options.dateProvider.now()) + override fun requestHeadersEnd(call: Call, request: Request) { + delegate.requestHeadersEnd(call, request) } - override fun responseBodyStart(call: Call) { - originalEventListener?.responseBodyStart(call) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(RESPONSE_BODY_EVENT) + override fun requestHeadersStart(call: Call) { + delegate.requestHeadersStart(call) } override fun responseBodyEnd(call: Call, byteCount: Long) { - originalEventListener?.responseBodyEnd(call, byteCount) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setResponseBodySize(byteCount) - okHttpEvent.finishSpan(RESPONSE_BODY_EVENT) { - if (byteCount > 0) { - it.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, byteCount) - } - } + delegate.responseBodyEnd(call, byteCount) } - override fun responseFailed(call: Call, ioe: IOException) { - originalEventListener?.responseFailed(call, ioe) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setError(ioe.message) - // responseFailed can happen after responseHeaders or responseBody. - // If responseHeaders already finished, we don't change its status. - okHttpEvent.finishSpan(RESPONSE_HEADERS_EVENT) { - if (!it.isFinished) { - it.status = SpanStatus.INTERNAL_ERROR - it.throwable = ioe - } - } - okHttpEvent.finishSpan(RESPONSE_BODY_EVENT) { - it.status = SpanStatus.INTERNAL_ERROR - it.throwable = ioe - } + override fun responseBodyStart(call: Call) { + delegate.responseBodyStart(call) } - override fun callEnd(call: Call) { - originalEventListener?.callEnd(call) - val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return - okHttpEvent.finishEvent() + override fun responseFailed(call: Call, ioe: IOException) { + delegate.responseFailed(call, ioe) } - override fun callFailed(call: Call, ioe: IOException) { - originalEventListener?.callFailed(call, ioe) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return - okHttpEvent.setError(ioe.message) - okHttpEvent.finishEvent { - it.status = SpanStatus.INTERNAL_ERROR - it.throwable = ioe - } + override fun responseHeadersEnd(call: Call, response: Response) { + delegate.responseHeadersEnd(call, response) } - override fun canceled(call: Call) { - originalEventListener?.canceled(call) + override fun responseHeadersStart(call: Call) { + delegate.responseHeadersStart(call) } override fun satisfactionFailure(call: Call, response: Response) { - originalEventListener?.satisfactionFailure(call, response) - } - - override fun cacheHit(call: Call, response: Response) { - originalEventListener?.cacheHit(call, response) - } - - override fun cacheMiss(call: Call) { - originalEventListener?.cacheMiss(call) + delegate.satisfactionFailure(call, response) } - override fun cacheConditionalHit(call: Call, cachedResponse: Response) { - originalEventListener?.cacheConditionalHit(call, cachedResponse) + override fun secureConnectEnd(call: Call, handshake: Handshake?) { + delegate.secureConnectEnd(call, handshake) } - private fun canCreateEventSpan(): Boolean { - // If the wrapped EventListener is ours, we shouldn't create spans, as the originalEventListener already did it - return originalEventListener !is SentryOkHttpEventListener + override fun secureConnectStart(call: Call) { + delegate.secureConnectStart(call) } } 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 ddefaabbde..678434f1db 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 @@ -1,27 +1,16 @@ package io.sentry.android.okhttp -import io.sentry.BaggageHeader -import io.sentry.Breadcrumb -import io.sentry.Hint import io.sentry.HttpStatusCodeRange import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS -import io.sentry.SpanDataConvention -import io.sentry.SpanStatus -import io.sentry.TypeCheckHint.OKHTTP_REQUEST -import io.sentry.TypeCheckHint.OKHTTP_RESPONSE +import io.sentry.android.okhttp.SentryOkHttpInterceptor.BeforeSpanCallback import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion -import io.sentry.util.Platform -import io.sentry.util.PropagationTargetsUtils -import io.sentry.util.TracingUtils -import io.sentry.util.UrlUtils import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response -import java.io.IOException /** * The Sentry's [SentryOkHttpInterceptor], it will automatically add a breadcrumb and start a span @@ -37,6 +26,10 @@ import java.io.IOException * @param failedRequestTargets The SDK will only capture HTTP Client errors if the HTTP Request URL * is a match for any of the defined targets. */ +@Deprecated( + "Use SentryOkHttpInterceptor from sentry-okhttp instead", + ReplaceWith("SentryOkHttpInterceptor", "io.sentry.okhttp.SentryOkHttpInterceptor") +) class SentryOkHttpInterceptor( private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null, @@ -45,7 +38,15 @@ class SentryOkHttpInterceptor( HttpStatusCodeRange(HttpStatusCodeRange.DEFAULT_MIN, HttpStatusCodeRange.DEFAULT_MAX) ), private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) -) : Interceptor { +) : Interceptor by io.sentry.okhttp.SentryOkHttpInterceptor( + hub, + { span, request, response -> + beforeSpan?.execute(span, request, response) + }, + captureFailedRequests, + failedRequestStatusCodes, + failedRequestTargets +) { constructor() : this(HubAdapter.getInstance()) constructor(hub: IHub) : this(hub, null) @@ -57,163 +58,13 @@ class SentryOkHttpInterceptor( .addPackage("maven:io.sentry:sentry-android-okhttp", BuildConfig.VERSION_NAME) } - @Suppress("LongMethod") - override fun intercept(chain: Interceptor.Chain): Response { - var request = chain.request() - - val urlDetails = UrlUtils.parse(request.url.toString()) - val url = urlDetails.urlOrFallback - val method = request.method - - val span: ISpan? - val okHttpEvent: SentryOkHttpEvent? - - if (SentryOkHttpEventListener.eventMap.containsKey(chain.call())) { - // read the span from the event listener - okHttpEvent = SentryOkHttpEventListener.eventMap[chain.call()] - span = okHttpEvent?.callRootSpan - } else { - // read the span from the bound scope - okHttpEvent = null - val parentSpan = if (Platform.isAndroid()) hub.transaction else hub.span - span = parentSpan?.startChild("http.client", "$method $url") - } - - span?.spanContext?.origin = TRACE_ORIGIN - - urlDetails.applyToSpan(span) - - val isFromEventListener = okHttpEvent != null - var response: Response? = null - var code: Int? = null - - try { - val requestBuilder = request.newBuilder() - - TracingUtils.traceIfAllowed( - hub, - request.url.toString(), - request.headers(BaggageHeader.BAGGAGE_HEADER), - span - )?.let { tracingHeaders -> - requestBuilder.addHeader(tracingHeaders.sentryTraceHeader.name, tracingHeaders.sentryTraceHeader.value) - tracingHeaders.baggageHeader?.let { - requestBuilder.removeHeader(BaggageHeader.BAGGAGE_HEADER) - requestBuilder.addHeader(it.name, it.value) - } - } - - request = requestBuilder.build() - response = chain.proceed(request) - code = response.code - span?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, code) - span?.status = SpanStatus.fromHttpStatusCode(code) - - // OkHttp errors (4xx, 5xx) don't throw, so it's safe to call within this block. - // breadcrumbs are added on the finally block because we'd like to know if the device - // had an unstable connection or something similar - if (shouldCaptureClientError(request, response)) { - // If we capture the client error directly, it could be associated with the - // currently running span by the backend. In case the listener is in use, that is - // an inner span. So, if the listener is in use, we let it capture the client - // error, to shown it in the http root call span in the dashboard. - if (isFromEventListener && okHttpEvent != null) { - okHttpEvent.setClientErrorResponse(response) - } else { - SentryOkHttpUtils.captureClientError(hub, request, response) - } - } - - return response - } catch (e: IOException) { - span?.apply { - this.throwable = e - this.status = SpanStatus.INTERNAL_ERROR - } - throw e - } finally { - finishSpan(span, request, response, isFromEventListener) - - // The SentryOkHttpEventListener will send the breadcrumb itself if used for this call - if (!isFromEventListener) { - sendBreadcrumb(request, code, response) - } - } - } - - private fun sendBreadcrumb(request: Request, code: Int?, response: Response?) { - val breadcrumb = Breadcrumb.http(request.url.toString(), request.method, code) - request.body?.contentLength().ifHasValidLength { - breadcrumb.setData("http.request_content_length", it) - } - - val hint = Hint().also { it.set(OKHTTP_REQUEST, request) } - response?.let { - it.body?.contentLength().ifHasValidLength { responseBodySize -> - breadcrumb.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, responseBodySize) - } - - hint[OKHTTP_RESPONSE] = it - } - - hub.addBreadcrumb(breadcrumb, hint) - } - - private fun finishSpan(span: ISpan?, request: Request, response: Response?, isFromEventListener: Boolean) { - if (span == null) { - return - } - if (beforeSpan != null) { - val result = beforeSpan.execute(span, request, response) - if (result == null) { - // span is dropped - span.spanContext.sampled = false - } else { - // The SentryOkHttpEventListener will finish the span itself if used for this call - if (!isFromEventListener) { - span.finish() - } - } - } else { - // The SentryOkHttpEventListener will finish the span itself if used for this call - if (!isFromEventListener) { - span.finish() - } - } - } - - private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { - if (this != null && this != -1L) { - fn.invoke(this) - } - } - - private fun shouldCaptureClientError(request: Request, response: Response): Boolean { - // return if the feature is disabled or its not within the range - if (!captureFailedRequests || !containsStatusCode(response.code)) { - return false - } - - // return if its not a target match - if (!PropagationTargetsUtils.contain(failedRequestTargets, request.url.toString())) { - return false - } - - return true - } - - private fun containsStatusCode(statusCode: Int): Boolean { - for (item in failedRequestStatusCodes) { - if (item.isInRange(statusCode)) { - return true - } - } - return false - } - /** * The BeforeSpan callback */ + @Deprecated( + "Use BeforeSpanCallback from sentry-okhttp instead", + ReplaceWith("BeforeSpanCallback", "io.sentry.okhttp.SentryOkHttpInterceptor.BeforeSpanCallback") + ) fun interface BeforeSpanCallback { /** * Mutates or drops span before being added diff --git a/sentry-okhttp/api/sentry-okhttp.api b/sentry-okhttp/api/sentry-okhttp.api new file mode 100644 index 0000000000..3095659c88 --- /dev/null +++ b/sentry-okhttp/api/sentry-okhttp.api @@ -0,0 +1,63 @@ +public final class io/sentry/okhttp/BuildConfig { + public static final field SENTRY_OKHTTP_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public class io/sentry/okhttp/SentryOkHttpEventListener : okhttp3/EventListener { + public static final field Companion Lio/sentry/okhttp/SentryOkHttpEventListener$Companion; + public fun ()V + public fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IHub;Lokhttp3/EventListener$Factory;)V + public synthetic fun (Lio/sentry/IHub;Lokhttp3/EventListener$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IHub;Lokhttp3/EventListener;)V + public synthetic fun (Lio/sentry/IHub;Lokhttp3/EventListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lokhttp3/EventListener$Factory;)V + public fun (Lokhttp3/EventListener;)V + public fun cacheConditionalHit (Lokhttp3/Call;Lokhttp3/Response;)V + public fun cacheHit (Lokhttp3/Call;Lokhttp3/Response;)V + public fun cacheMiss (Lokhttp3/Call;)V + public fun callEnd (Lokhttp3/Call;)V + public fun callFailed (Lokhttp3/Call;Ljava/io/IOException;)V + public fun callStart (Lokhttp3/Call;)V + public fun canceled (Lokhttp3/Call;)V + public fun connectEnd (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;)V + public fun connectFailed (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;Ljava/io/IOException;)V + public fun connectStart (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;)V + public fun connectionAcquired (Lokhttp3/Call;Lokhttp3/Connection;)V + public fun connectionReleased (Lokhttp3/Call;Lokhttp3/Connection;)V + public fun dnsEnd (Lokhttp3/Call;Ljava/lang/String;Ljava/util/List;)V + public fun dnsStart (Lokhttp3/Call;Ljava/lang/String;)V + public fun proxySelectEnd (Lokhttp3/Call;Lokhttp3/HttpUrl;Ljava/util/List;)V + public fun proxySelectStart (Lokhttp3/Call;Lokhttp3/HttpUrl;)V + public fun requestBodyEnd (Lokhttp3/Call;J)V + public fun requestBodyStart (Lokhttp3/Call;)V + public fun requestFailed (Lokhttp3/Call;Ljava/io/IOException;)V + public fun requestHeadersEnd (Lokhttp3/Call;Lokhttp3/Request;)V + public fun requestHeadersStart (Lokhttp3/Call;)V + public fun responseBodyEnd (Lokhttp3/Call;J)V + public fun responseBodyStart (Lokhttp3/Call;)V + public fun responseFailed (Lokhttp3/Call;Ljava/io/IOException;)V + public fun responseHeadersEnd (Lokhttp3/Call;Lokhttp3/Response;)V + public fun responseHeadersStart (Lokhttp3/Call;)V + public fun satisfactionFailure (Lokhttp3/Call;Lokhttp3/Response;)V + public fun secureConnectEnd (Lokhttp3/Call;Lokhttp3/Handshake;)V + public fun secureConnectStart (Lokhttp3/Call;)V +} + +public final class io/sentry/okhttp/SentryOkHttpEventListener$Companion { +} + +public class io/sentry/okhttp/SentryOkHttpInterceptor : okhttp3/Interceptor { + public fun ()V + public fun (Lio/sentry/IHub;)V + public fun (Lio/sentry/IHub;Lio/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;)V + public synthetic fun (Lio/sentry/IHub;Lio/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;)V + public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; +} + +public abstract interface class io/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback { + public abstract fun execute (Lio/sentry/ISpan;Lokhttp3/Request;Lokhttp3/Response;)Lio/sentry/ISpan; +} + diff --git a/sentry-okhttp/build.gradle.kts b/sentry-okhttp/build.gradle.kts new file mode 100644 index 0000000000..a30e2d0594 --- /dev/null +++ b/sentry-okhttp/build.gradle.kts @@ -0,0 +1,92 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) + id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +kotlin { + explicitApi() +} + +dependencies { + api(projects.sentry) + + compileOnly(Config.Libs.okhttp) + + implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + + // tests + testImplementation(projects.sentryTestSupport) + testImplementation(Config.Libs.okhttp) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) + testImplementation(Config.TestLibs.mockWebserver) +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { + rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } + } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.okhttp") + buildConfigField("String", "SENTRY_OKHTTP_SDK_NAME", "\"${Config.Sentry.SENTRY_OKHTTP_SDK_NAME}\"") + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} + +val generateBuildConfig by tasks +tasks.withType().configureEach { + dependsOn(generateBuildConfig) + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt similarity index 92% rename from sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt rename to sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt index e38445afac..4870c8bb64 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt @@ -1,4 +1,4 @@ -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.Breadcrumb import io.sentry.Hint @@ -9,13 +9,13 @@ import io.sentry.SentryLevel import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TypeCheckHint -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT -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.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT import io.sentry.util.Platform import io.sentry.util.UrlUtils import okhttp3.Request diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt new file mode 100644 index 0000000000..67a8cd8b56 --- /dev/null +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt @@ -0,0 +1,411 @@ +package io.sentry.okhttp + +import io.sentry.HubAdapter +import io.sentry.IHub +import io.sentry.SpanDataConvention +import io.sentry.SpanStatus +import okhttp3.Call +import okhttp3.Connection +import okhttp3.EventListener +import okhttp3.Handshake +import okhttp3.HttpUrl +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.concurrent.ConcurrentHashMap + +/** + * Logs network performance event metrics to Sentry + * + * Usage - add instance of [SentryOkHttpEventListener] in [okhttp3.OkHttpClient.Builder.eventListener] + * + * ``` + * val client = OkHttpClient.Builder() + * .eventListener(SentryOkHttpEventListener()) + * .addInterceptor(SentryOkHttpInterceptor()) + * .build() + * ``` + * + * If you already use a [okhttp3.EventListener], you can pass it in the constructor. + * + * ``` + * val client = OkHttpClient.Builder() + * .eventListener(SentryOkHttpEventListener(myEventListener)) + * .addInterceptor(SentryOkHttpInterceptor()) + * .build() + * ``` + */ +@Suppress("TooManyFunctions") +public open class SentryOkHttpEventListener( + private val hub: IHub = HubAdapter.getInstance(), + private val originalEventListenerCreator: ((call: Call) -> EventListener)? = null +) : EventListener() { + + private var originalEventListener: EventListener? = null + + public companion object { + internal const val PROXY_SELECT_EVENT = "proxy_select" + internal const val DNS_EVENT = "dns" + internal const val SECURE_CONNECT_EVENT = "secure_connect" + internal const val CONNECT_EVENT = "connect" + internal const val CONNECTION_EVENT = "connection" + internal const val REQUEST_HEADERS_EVENT = "request_headers" + internal const val REQUEST_BODY_EVENT = "request_body" + internal const val RESPONSE_HEADERS_EVENT = "response_headers" + internal const val RESPONSE_BODY_EVENT = "response_body" + + internal val eventMap: MutableMap = ConcurrentHashMap() + } + + public constructor() : this( + HubAdapter.getInstance(), + originalEventListenerCreator = null + ) + + public constructor(originalEventListener: EventListener) : this( + HubAdapter.getInstance(), + originalEventListenerCreator = { originalEventListener } + ) + + public constructor(originalEventListenerFactory: Factory) : this( + HubAdapter.getInstance(), + originalEventListenerCreator = { originalEventListenerFactory.create(it) } + ) + + public constructor(hub: IHub = HubAdapter.getInstance(), originalEventListener: EventListener) : this( + hub, + originalEventListenerCreator = { originalEventListener } + ) + + public constructor(hub: IHub = HubAdapter.getInstance(), originalEventListenerFactory: Factory) : this( + hub, + originalEventListenerCreator = { originalEventListenerFactory.create(it) } + ) + + override fun callStart(call: Call) { + originalEventListener = originalEventListenerCreator?.invoke(call) + originalEventListener?.callStart(call) + // If the wrapped EventListener is ours, we can just delegate the calls, + // without creating other events that would create duplicates + if (canCreateEventSpan()) { + eventMap[call] = SentryOkHttpEvent(hub, call.request()) + } + } + + override fun proxySelectStart(call: Call, url: HttpUrl) { + originalEventListener?.proxySelectStart(call, url) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(PROXY_SELECT_EVENT) + } + + override fun proxySelectEnd( + call: Call, + url: HttpUrl, + proxies: List + ) { + originalEventListener?.proxySelectEnd(call, url, proxies) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(PROXY_SELECT_EVENT) { + if (proxies.isNotEmpty()) { + it.setData("proxies", proxies.joinToString { proxy -> proxy.toString() }) + } + } + } + + override fun dnsStart(call: Call, domainName: String) { + originalEventListener?.dnsStart(call, domainName) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(DNS_EVENT) + } + + override fun dnsEnd( + call: Call, + domainName: String, + inetAddressList: List + ) { + originalEventListener?.dnsEnd(call, domainName, inetAddressList) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(DNS_EVENT) { + it.setData("domain_name", domainName) + if (inetAddressList.isNotEmpty()) { + it.setData("dns_addresses", inetAddressList.joinToString { address -> address.toString() }) + } + } + } + + override fun connectStart( + call: Call, + inetSocketAddress: InetSocketAddress, + proxy: Proxy + ) { + originalEventListener?.connectStart(call, inetSocketAddress, proxy) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(CONNECT_EVENT) + } + + override fun secureConnectStart(call: Call) { + originalEventListener?.secureConnectStart(call) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(SECURE_CONNECT_EVENT) + } + + override fun secureConnectEnd(call: Call, handshake: Handshake?) { + originalEventListener?.secureConnectEnd(call, handshake) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(SECURE_CONNECT_EVENT) + } + + override fun connectEnd( + call: Call, + inetSocketAddress: InetSocketAddress, + proxy: Proxy, + protocol: Protocol? + ) { + originalEventListener?.connectEnd(call, inetSocketAddress, proxy, protocol) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setProtocol(protocol?.name) + okHttpEvent.finishSpan(CONNECT_EVENT) + } + + override fun connectFailed( + call: Call, + inetSocketAddress: InetSocketAddress, + proxy: Proxy, + protocol: Protocol?, + ioe: IOException + ) { + originalEventListener?.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setProtocol(protocol?.name) + okHttpEvent.setError(ioe.message) + okHttpEvent.finishSpan(CONNECT_EVENT) { + it.throwable = ioe + it.status = SpanStatus.INTERNAL_ERROR + } + } + + override fun connectionAcquired(call: Call, connection: Connection) { + originalEventListener?.connectionAcquired(call, connection) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(CONNECTION_EVENT) + } + + override fun connectionReleased(call: Call, connection: Connection) { + originalEventListener?.connectionReleased(call, connection) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(CONNECTION_EVENT) + } + + override fun requestHeadersStart(call: Call) { + originalEventListener?.requestHeadersStart(call) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(REQUEST_HEADERS_EVENT) + } + + override fun requestHeadersEnd(call: Call, request: Request) { + originalEventListener?.requestHeadersEnd(call, request) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(REQUEST_HEADERS_EVENT) + } + + override fun requestBodyStart(call: Call) { + originalEventListener?.requestBodyStart(call) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(REQUEST_BODY_EVENT) + } + + override fun requestBodyEnd(call: Call, byteCount: Long) { + originalEventListener?.requestBodyEnd(call, byteCount) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(REQUEST_BODY_EVENT) { + if (byteCount > 0) { + it.setData("http.request_content_length", byteCount) + } + } + okHttpEvent.setRequestBodySize(byteCount) + } + + override fun requestFailed(call: Call, ioe: IOException) { + originalEventListener?.requestFailed(call, ioe) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setError(ioe.message) + // requestFailed can happen after requestHeaders or requestBody. + // If requestHeaders already finished, we don't change its status. + okHttpEvent.finishSpan(REQUEST_HEADERS_EVENT) { + if (!it.isFinished) { + it.status = SpanStatus.INTERNAL_ERROR + it.throwable = ioe + } + } + okHttpEvent.finishSpan(REQUEST_BODY_EVENT) { + it.status = SpanStatus.INTERNAL_ERROR + it.throwable = ioe + } + } + + override fun responseHeadersStart(call: Call) { + originalEventListener?.responseHeadersStart(call) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(RESPONSE_HEADERS_EVENT) + } + + override fun responseHeadersEnd(call: Call, response: Response) { + originalEventListener?.responseHeadersEnd(call, response) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setResponse(response) + val responseHeadersSpan = okHttpEvent.finishSpan(RESPONSE_HEADERS_EVENT) { + it.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.code) + // Let's not override the status of a span that was set + if (it.status == null) { + it.status = SpanStatus.fromHttpStatusCode(response.code) + } + } + okHttpEvent.scheduleFinish(responseHeadersSpan?.finishDate ?: hub.options.dateProvider.now()) + } + + override fun responseBodyStart(call: Call) { + originalEventListener?.responseBodyStart(call) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(RESPONSE_BODY_EVENT) + } + + override fun responseBodyEnd(call: Call, byteCount: Long) { + originalEventListener?.responseBodyEnd(call, byteCount) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setResponseBodySize(byteCount) + okHttpEvent.finishSpan(RESPONSE_BODY_EVENT) { + if (byteCount > 0) { + it.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, byteCount) + } + } + } + + override fun responseFailed(call: Call, ioe: IOException) { + originalEventListener?.responseFailed(call, ioe) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setError(ioe.message) + // responseFailed can happen after responseHeaders or responseBody. + // If responseHeaders already finished, we don't change its status. + okHttpEvent.finishSpan(RESPONSE_HEADERS_EVENT) { + if (!it.isFinished) { + it.status = SpanStatus.INTERNAL_ERROR + it.throwable = ioe + } + } + okHttpEvent.finishSpan(RESPONSE_BODY_EVENT) { + it.status = SpanStatus.INTERNAL_ERROR + it.throwable = ioe + } + } + + override fun callEnd(call: Call) { + originalEventListener?.callEnd(call) + val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return + okHttpEvent.finishEvent() + } + + override fun callFailed(call: Call, ioe: IOException) { + originalEventListener?.callFailed(call, ioe) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return + okHttpEvent.setError(ioe.message) + okHttpEvent.finishEvent { + it.status = SpanStatus.INTERNAL_ERROR + it.throwable = ioe + } + } + + override fun canceled(call: Call) { + originalEventListener?.canceled(call) + } + + override fun satisfactionFailure(call: Call, response: Response) { + originalEventListener?.satisfactionFailure(call, response) + } + + override fun cacheHit(call: Call, response: Response) { + originalEventListener?.cacheHit(call, response) + } + + override fun cacheMiss(call: Call) { + originalEventListener?.cacheMiss(call) + } + + override fun cacheConditionalHit(call: Call, cachedResponse: Response) { + originalEventListener?.cacheConditionalHit(call, cachedResponse) + } + + private fun canCreateEventSpan(): Boolean { + // If the wrapped EventListener is ours, we shouldn't create spans, as the originalEventListener already did it + return originalEventListener !is SentryOkHttpEventListener + } +} diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt new file mode 100644 index 0000000000..1f1aaf8c4e --- /dev/null +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt @@ -0,0 +1,228 @@ +package io.sentry.okhttp + +import io.sentry.BaggageHeader +import io.sentry.Breadcrumb +import io.sentry.Hint +import io.sentry.HttpStatusCodeRange +import io.sentry.HubAdapter +import io.sentry.IHub +import io.sentry.ISpan +import io.sentry.SentryIntegrationPackageStorage +import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS +import io.sentry.SpanDataConvention +import io.sentry.SpanStatus +import io.sentry.TypeCheckHint.OKHTTP_REQUEST +import io.sentry.TypeCheckHint.OKHTTP_RESPONSE +import io.sentry.okhttp.SentryOkHttpInterceptor.BeforeSpanCallback +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion +import io.sentry.util.Platform +import io.sentry.util.PropagationTargetsUtils +import io.sentry.util.TracingUtils +import io.sentry.util.UrlUtils +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import java.io.IOException + +/** + * The Sentry's [SentryOkHttpInterceptor], it will automatically add a breadcrumb and start a span + * out of the active span bound to the scope for each HTTP Request. + * If [captureFailedRequests] is enabled, the SDK will capture HTTP Client errors as well. + * + * @param hub The [IHub], internal and only used for testing. + * @param beforeSpan The [ISpan] can be customized or dropped with the [BeforeSpanCallback]. + * @param captureFailedRequests The SDK will only capture HTTP Client errors if it is enabled, + * Defaults to false. + * @param failedRequestStatusCodes The SDK will only capture HTTP Client errors if the HTTP Response + * status code is within the defined ranges. + * @param failedRequestTargets The SDK will only capture HTTP Client errors if the HTTP Request URL + * is a match for any of the defined targets. + */ +public open class SentryOkHttpInterceptor( + private val hub: IHub = HubAdapter.getInstance(), + private val beforeSpan: BeforeSpanCallback? = null, + private val captureFailedRequests: Boolean = true, + private val failedRequestStatusCodes: List = listOf( + HttpStatusCodeRange(HttpStatusCodeRange.DEFAULT_MIN, HttpStatusCodeRange.DEFAULT_MAX) + ), + private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) +) : Interceptor { + + public constructor() : this(HubAdapter.getInstance()) + public constructor(hub: IHub) : this(hub, null) + public constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) + + init { + addIntegrationToSdkVersion(javaClass) + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-okhttp", BuildConfig.VERSION_NAME) + } + + @Suppress("LongMethod") + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + + val urlDetails = UrlUtils.parse(request.url.toString()) + val url = urlDetails.urlOrFallback + val method = request.method + + val span: ISpan? + val okHttpEvent: SentryOkHttpEvent? + + if (SentryOkHttpEventListener.eventMap.containsKey(chain.call())) { + // read the span from the event listener + okHttpEvent = SentryOkHttpEventListener.eventMap[chain.call()] + span = okHttpEvent?.callRootSpan + } else { + // read the span from the bound scope + okHttpEvent = null + val parentSpan = if (Platform.isAndroid()) hub.transaction else hub.span + span = parentSpan?.startChild("http.client", "$method $url") + } + + span?.spanContext?.origin = TRACE_ORIGIN + + urlDetails.applyToSpan(span) + + val isFromEventListener = okHttpEvent != null + var response: Response? = null + var code: Int? = null + + try { + val requestBuilder = request.newBuilder() + + TracingUtils.traceIfAllowed( + hub, + request.url.toString(), + request.headers(BaggageHeader.BAGGAGE_HEADER), + span + )?.let { tracingHeaders -> + requestBuilder.addHeader(tracingHeaders.sentryTraceHeader.name, tracingHeaders.sentryTraceHeader.value) + tracingHeaders.baggageHeader?.let { + requestBuilder.removeHeader(BaggageHeader.BAGGAGE_HEADER) + requestBuilder.addHeader(it.name, it.value) + } + } + + request = requestBuilder.build() + response = chain.proceed(request) + code = response.code + span?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, code) + span?.status = SpanStatus.fromHttpStatusCode(code) + + // OkHttp errors (4xx, 5xx) don't throw, so it's safe to call within this block. + // breadcrumbs are added on the finally block because we'd like to know if the device + // had an unstable connection or something similar + if (shouldCaptureClientError(request, response)) { + // If we capture the client error directly, it could be associated with the + // currently running span by the backend. In case the listener is in use, that is + // an inner span. So, if the listener is in use, we let it capture the client + // error, to shown it in the http root call span in the dashboard. + if (isFromEventListener && okHttpEvent != null) { + okHttpEvent.setClientErrorResponse(response) + } else { + SentryOkHttpUtils.captureClientError(hub, request, response) + } + } + + return response + } catch (e: IOException) { + span?.apply { + this.throwable = e + this.status = SpanStatus.INTERNAL_ERROR + } + throw e + } finally { + finishSpan(span, request, response, isFromEventListener) + + // The SentryOkHttpEventListener will send the breadcrumb itself if used for this call + if (!isFromEventListener) { + sendBreadcrumb(request, code, response) + } + } + } + + private fun sendBreadcrumb(request: Request, code: Int?, response: Response?) { + val breadcrumb = Breadcrumb.http(request.url.toString(), request.method, code) + request.body?.contentLength().ifHasValidLength { + breadcrumb.setData("http.request_content_length", it) + } + + val hint = Hint().also { it.set(OKHTTP_REQUEST, request) } + response?.let { + it.body?.contentLength().ifHasValidLength { responseBodySize -> + breadcrumb.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, responseBodySize) + } + + hint[OKHTTP_RESPONSE] = it + } + + hub.addBreadcrumb(breadcrumb, hint) + } + + private fun finishSpan(span: ISpan?, request: Request, response: Response?, isFromEventListener: Boolean) { + if (span == null) { + return + } + if (beforeSpan != null) { + val result = beforeSpan.execute(span, request, response) + if (result == null) { + // span is dropped + span.spanContext.sampled = false + } else { + // The SentryOkHttpEventListener will finish the span itself if used for this call + if (!isFromEventListener) { + span.finish() + } + } + } else { + // The SentryOkHttpEventListener will finish the span itself if used for this call + if (!isFromEventListener) { + span.finish() + } + } + } + + private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { + if (this != null && this != -1L) { + fn.invoke(this) + } + } + + private fun shouldCaptureClientError(request: Request, response: Response): Boolean { + // return if the feature is disabled or its not within the range + if (!captureFailedRequests || !containsStatusCode(response.code)) { + return false + } + + // return if its not a target match + if (!PropagationTargetsUtils.contain(failedRequestTargets, request.url.toString())) { + return false + } + + return true + } + + private fun containsStatusCode(statusCode: Int): Boolean { + for (item in failedRequestStatusCodes) { + if (item.isInRange(statusCode)) { + return true + } + } + return false + } + + /** + * The BeforeSpan callback + */ + public fun interface BeforeSpanCallback { + /** + * Mutates or drops span before being added + * + * @param span the span to mutate or drop + * @param request the HTTP request executed by okHttp + * @param response the HTTP response received by okHttp + */ + public fun execute(span: ISpan, request: Request, response: Response?): ISpan? + } +} diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpUtils.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt similarity index 95% rename from sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpUtils.kt rename to sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt index 8d9a24edbc..0cfc1c5a75 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpUtils.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt @@ -1,4 +1,4 @@ -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.Hint import io.sentry.IHub @@ -13,9 +13,9 @@ import okhttp3.Headers import okhttp3.Request import okhttp3.Response -object SentryOkHttpUtils { +internal object SentryOkHttpUtils { - fun captureClientError(hub: IHub, request: Request, response: Response) { + internal fun captureClientError(hub: IHub, request: Request, response: Response) { // not possible to get a parameterized url, but we remove at least the // query string and the fragment. // url example: https://api.github.com/users/getsentry/repos/#fragment?query=query diff --git a/sentry-okhttp/src/main/resources/META-INF/proguard/sentry-okhttp.pro b/sentry-okhttp/src/main/resources/META-INF/proguard/sentry-okhttp.pro new file mode 100644 index 0000000000..3f9ea4feb2 --- /dev/null +++ b/sentry-okhttp/src/main/resources/META-INF/proguard/sentry-okhttp.pro @@ -0,0 +1,13 @@ +##---------------Begin: proguard configuration for OkHttp ---------- + +# To ensure that stack traces is unambiguous +# https://developer.android.com/studio/build/shrink-code#decode-stack-trace +-keepattributes LineNumberTable,SourceFile + +# https://square.github.io/okhttp/features/r8_proguard/ +# If you use OkHttp as a dependency in an Android project which uses R8 as a default compiler you +# don’t have to do anything. The specific rules are already bundled into the JAR which can +# be interpreted by R8 automatically. +# https://raw.githubusercontent.com/square/okhttp/master/okhttp/src/jvmMain/resources/META-INF/proguard/okhttp3.pro + +##---------------End: proguard configuration for OkHttp ---------- diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventListenerTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt similarity index 99% rename from sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventListenerTest.kt rename to sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt index a1cea5e3d7..c6d10fce10 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventListenerTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt @@ -1,4 +1,4 @@ -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.BaggageHeader import io.sentry.IHub diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt similarity index 97% rename from sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventTest.kt rename to sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt index 9fab3b475c..1363c23785 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt @@ -1,4 +1,4 @@ -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.Breadcrumb import io.sentry.Hint @@ -15,14 +15,14 @@ import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.TypeCheckHint -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT -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.exception.SentryHttpClientException +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT import io.sentry.test.getProperty import okhttp3.Protocol import okhttp3.Request diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt similarity index 99% rename from sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt rename to sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt index b01ab8edd2..b856d93fb1 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt @@ -1,6 +1,6 @@ @file:Suppress("MaxLineLength") -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.BaggageHeader import io.sentry.Breadcrumb diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpUtilsTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpUtilsTest.kt similarity index 99% rename from sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpUtilsTest.kt rename to sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpUtilsTest.kt index 6f9ea500a7..ec19454327 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpUtilsTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpUtilsTest.kt @@ -1,4 +1,4 @@ -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.Hint import io.sentry.IHub diff --git a/sentry-android-okhttp/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sentry-okhttp/src/test/resources/mockito-extensions/org.mockito.plugin.MockMaker similarity index 100% rename from sentry-android-okhttp/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to sentry-okhttp/src/test/resources/mockito-extensions/org.mockito.plugin.MockMaker diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt index 09ad9a2d07..66e455a190 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt @@ -1,8 +1,8 @@ package io.sentry.samples.android import io.sentry.HttpStatusCodeRange -import io.sentry.android.okhttp.SentryOkHttpEventListener -import io.sentry.android.okhttp.SentryOkHttpInterceptor +import io.sentry.okhttp.SentryOkHttpEventListener +import io.sentry.okhttp.SentryOkHttpInterceptor import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory diff --git a/settings.gradle.kts b/settings.gradle.kts index f87b9e0126..cdec3d7181 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -46,6 +46,7 @@ include( "sentry-opentelemetry:sentry-opentelemetry-agentcustomization", "sentry-opentelemetry:sentry-opentelemetry-agent", "sentry-quartz", + "sentry-okhttp", "sentry-samples:sentry-samples-android", "sentry-samples:sentry-samples-console", "sentry-samples:sentry-samples-jul", From 3a42bf370be720d3ef5dba9c3e32e01d846a9083 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 27 Nov 2023 15:48:02 +0100 Subject: [PATCH 33/55] Prepare changelog for v7 --- CHANGELOG.md | 83 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dbf77db7e..93b9ec9ef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,67 +2,80 @@ ## Unreleased -### Features +Version 7 of the Sentry Java/Android SDK brings a variety of features and fixes. The most notable changes are: +- Bumping `minSdk` level to 19 (Android 4.4) +- The SDK will now listen to connectivity changes and try to re-upload cached events when internet connection is re-established additionally to uploading events on app restart +- `Sentry.getSpan` now returns the root transaction, which should improve the span hierarchy and make it leaner +- Multiple improvements to reduce probability of the SDK causing ANRs +- New `sentry-okhttp` artifact is unbundled from Android and can be used in pure JVM-only apps -- Add `sentry-okhttp` module to support instrumenting OkHttp in non-Android projects ([#3005](https://github.com/getsentry/sentry-java/pull/3005)) - - This deprecates `sentry-android-okhttp` classes. Make sure to replace `io.sentry.android.okhttp` package name with `io.sentry.okhttp` before the next major, where the classes will be removed - - `SentryOkHttpUtils` was removed from public API as it's been exposed by mistake +**Note: The v7 version of the Android/Java SDK requires a self-hosted version of Sentry 22.12.0 or higher. If you are using a version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka onpremise) older than `22.12.0` then you will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/). If you're using `sentry.io` no action needed.** -## 7.0.0-rc.1 +## Sentry Integrations Version Compatibility -### Features +Make sure to align _all_ 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) -- Do not filter out Sentry SDK frames in case of uncaught exceptions ([#3021](https://github.com/getsentry/sentry-java/pull/3021)) +For example, if you're using the [Sentry Android Gradle plugin](https://github.com/getsentry/sentry-android-gradle-plugin) with the `autoInstallation` [feature](https://docs.sentry.io/platforms/android/configuration/gradle/#auto-installation) (enabled by default), make sure to use version 4.+ of the gradle plugin together with version 7.+ of the SDK. If you can't do that for some reason, you can specify sentry version via the plugin config block: -**Breaking changes:** -- Cleanup `startTransaction` overloads ([#2964](https://github.com/getsentry/sentry-java/pull/2964)) - - We have reduce the number of overloads by allowing to pass in `TransactionOptions` instead of having separate parameters for certain options. - - `TransactionOptions` has defaults set and can be customized -- Raw logback message and parameters are now guarded by `sendDefaultPii` if an `encoder` has been configured ([#2976](https://github.com/getsentry/sentry-java/pull/2976)) +```kotlin +sentry { + autoInstallation { + sentryVersion.set("7.0.0") + } +} +``` -### Fixes +Similarly, if you have a Sentry SDK (e.g. `sentry-android-core`) dependency on one of your Gradle modules and you're updating it to 7.+, make sure the Gradle plugin is at 4.+ or specify the SDK version as shown in the snippet above. -- Use `getMyMemoryState()` instead of `getRunningAppProcesses()` to retrieve process importance ([#3004](https://github.com/getsentry/sentry-java/pull/3004)) - - This should prevent some app stores from flagging apps as violating their privacy +## Breaking Changes -## 7.0.0-beta.1 +- Bump min API to 19 ([#2883](https://github.com/getsentry/sentry-java/pull/2883)) +- If you're using `sentry-kotlin-extensions`, it requires `kotlinx-coroutines-core` version `1.6.1` or higher now ([#2838](https://github.com/getsentry/sentry-java/pull/2838)) +- Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) +- Apollo v2 BeforeSpanCallback now allows returning null ([#2890](https://github.com/getsentry/sentry-java/pull/2890)) +- `SentryOkHttpUtils` was removed from public API as it's been exposed by mistake ([#3005](https://github.com/getsentry/sentry-java/pull/3005)) -### Features +## Behavioural Changes -**Breaking changes:** +- Android only: `Sentry.getSpan()` returns the root span/transaction instead of the latest span ([#2855](https://github.com/getsentry/sentry-java/pull/2855)) - 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)) +- Measure AppStart time till First Draw instead of `onResume` ([#2851](https://github.com/getsentry/sentry-java/pull/2851)) - 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)) +- 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, meaning the automatic transaction will be force-finished with status `deadline_exceeded` when reaching the deadline +- 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 +- Raw logback message and parameters are now guarded by `sendDefaultPii` if an `encoder` has been configured ([#2976](https://github.com/getsentry/sentry-java/pull/2976)) + +## Deprecations + +- `sentry-android-okhttp` was deprecated in favour of the new `sentry-okhttp` module. Make sure to replace `io.sentry.android.okhttp` package name with `io.sentry.okhttp` before the next major, where the classes will be removed ([#3005](https://github.com/getsentry/sentry-java/pull/3005)) + +## Other Changes + +### Features + - 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` +- Add `sentry-okhttp` module to support instrumenting OkHttp in non-Android projects ([#3005](https://github.com/getsentry/sentry-java/pull/3005)) +- Do not filter out Sentry SDK frames in case of uncaught exceptions ([#3021](https://github.com/getsentry/sentry-java/pull/3021)) - Do not try to send and drop cached envelopes when rate-limiting is active ([#2937](https://github.com/getsentry/sentry-java/pull/2937)) ### Fixes -- Measure AppStart time till First Draw instead of `onResume` ([#2851](https://github.com/getsentry/sentry-java/pull/2851)) +- Use `getMyMemoryState()` instead of `getRunningAppProcesses()` to retrieve process importance ([#3004](https://github.com/getsentry/sentry-java/pull/3004)) + - This should prevent some app stores from flagging apps as violating their privacy +- Reduce flush timeout to 4s on Android to avoid ANRs ([#2858](https://github.com/getsentry/sentry-java/pull/2858)) +- Reduce timeout of AsyncHttpTransport to avoid ANR ([#2879](https://github.com/getsentry/sentry-java/pull/2879)) - 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)) -- 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 Coroutine Context Propagation using CopyableThreadContextElement ([#2838](https://github.com/getsentry/sentry-java/pull/2838)) - 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.34.0 From eca1a76a43d6d1fc61910db314b1eca0cd2f9149 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 27 Nov 2023 15:48:15 +0100 Subject: [PATCH 34/55] wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b9ec9ef1..43bbc5b668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -Version 7 of the Sentry Java/Android SDK brings a variety of features and fixes. The most notable changes are: +Version 7 of the Sentry Android/Java SDK brings a variety of features and fixes. The most notable changes are: - Bumping `minSdk` level to 19 (Android 4.4) - The SDK will now listen to connectivity changes and try to re-upload cached events when internet connection is re-established additionally to uploading events on app restart - `Sentry.getSpan` now returns the root transaction, which should improve the span hierarchy and make it leaner From eb245c4abdd2e4c977a24c17497f5f1a18c3bb21 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 27 Nov 2023 17:13:16 +0100 Subject: [PATCH 35/55] Respect maxSpans for nested child spans (#3065) --- CHANGELOG.md | 1 + .../core/ActivityLifecycleIntegrationTest.kt | 1 + .../src/main/java/io/sentry/SentryTracer.java | 77 +++++++++++-------- .../test/java/io/sentry/SentryTracerTest.kt | 15 ++++ 4 files changed, 61 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43bbc5b668..1d7e11b43c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ Similarly, if you have a Sentry SDK (e.g. `sentry-android-core`) dependency on o - 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 - Raw logback message and parameters are now guarded by `sendDefaultPii` if an `encoder` has been configured ([#2976](https://github.com/getsentry/sentry-java/pull/2976)) +- The `maxSpans` setting (defaults to 1000) is enforced for nested child spans which means a single transaction can have `maxSpans` number of children (nested or not) at most ([#3065](https://github.com/getsentry/sentry-java/pull/3065)) ## Deprecations 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 bd949b1ace..463b9adf89 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 @@ -45,6 +45,7 @@ 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 diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index f4d03dc641..925bab45b9 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -421,40 +421,51 @@ private ISpan createChild( return NoOpSpan.getInstance(); } - Objects.requireNonNull(parentSpanId, "parentSpanId is required"); - Objects.requireNonNull(operation, "operation is required"); - cancelIdleTimer(); - final Span span = - new Span( - root.getTraceId(), - parentSpanId, - this, - operation, - this.hub, - timestamp, - spanOptions, - __ -> { - final FinishStatus finishStatus = this.finishStatus; - if (transactionOptions.getIdleTimeout() != null) { - // if it's an idle transaction, no matter the status, we'll reset the timeout here - // so the transaction will either idle and finish itself, or a new child will be - // added and we'll wait for it again - if (!transactionOptions.isWaitForChildren() || hasAllChildrenFinished()) { - scheduleFinish(); + if (children.size() < hub.getOptions().getMaxSpans()) { + Objects.requireNonNull(parentSpanId, "parentSpanId is required"); + Objects.requireNonNull(operation, "operation is required"); + cancelIdleTimer(); + final Span span = + new Span( + root.getTraceId(), + parentSpanId, + this, + operation, + this.hub, + timestamp, + spanOptions, + __ -> { + final FinishStatus finishStatus = this.finishStatus; + if (transactionOptions.getIdleTimeout() != null) { + // if it's an idle transaction, no matter the status, we'll reset the timeout here + // so the transaction will either idle and finish itself, or a new child will be + // added and we'll wait for it again + if (!transactionOptions.isWaitForChildren() || hasAllChildrenFinished()) { + scheduleFinish(); + } + } else if (finishStatus.isFinishing) { + finish(finishStatus.spanStatus); } - } else if (finishStatus.isFinishing) { - finish(finishStatus.spanStatus); - } - }); - span.setDescription(description); - span.setData(SpanDataConvention.THREAD_ID, String.valueOf(Thread.currentThread().getId())); - span.setData( - SpanDataConvention.THREAD_NAME, - hub.getOptions().getMainThreadChecker().isMainThread() - ? "main" - : Thread.currentThread().getName()); - this.children.add(span); - return span; + }); + span.setDescription(description); + span.setData(SpanDataConvention.THREAD_ID, String.valueOf(Thread.currentThread().getId())); + span.setData( + SpanDataConvention.THREAD_NAME, + hub.getOptions().getMainThreadChecker().isMainThread() + ? "main" + : Thread.currentThread().getName()); + this.children.add(span); + return span; + } else { + hub.getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Span operation: %s, description: %s dropped due to limit reached. Returning NoOpSpan.", + operation, + description); + return NoOpSpan.getInstance(); + } } @Override diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index cfe5ed7aa5..ba9cf11955 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -1332,4 +1332,19 @@ class SentryTracerTest { assertNotNull(span.getData(SpanDataConvention.THREAD_ID)) assertNotEquals("main", span.getData(SpanDataConvention.THREAD_NAME)) } + + @Test + fun `maxSpans is respected by nested child spans`() { + val tracer = fixture.getSut(optionsConfiguration = { it.maxSpans = 5 }) + + val nested = tracer.startChild("task", "parent span") + repeat(10) { + val child = nested.startChild("task", "span number $it") + child.finish() + } + nested.finish() + tracer.finish() + + assertEquals(5, tracer.children.size) + } } From 3e6db94e613fc549b5ed51f0afecc11c97716935 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 27 Nov 2023 20:28:01 +0100 Subject: [PATCH 36/55] Capture failed requests for Apollo by default --- .../apollo3/SentryApollo3HttpInterceptor.kt | 6 ++-- .../SentryApollo3InterceptorClientErrors.kt | 33 +++++++++---------- .../apollo3/SentryApollo3InterceptorTest.kt | 4 +-- ...ntryApollo3InterceptorWithVariablesTest.kt | 2 +- 4 files changed, 22 insertions(+), 23 deletions(-) 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 c6aeb8755a..08cab179a5 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 @@ -29,11 +29,11 @@ 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.Platform 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 import java.util.Locale @@ -65,7 +65,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( request: HttpRequest, chain: HttpInterceptorChain ): HttpResponse { - val activeSpan = if (io.sentry.util.Platform.isAndroid()) hub.transaction else hub.span + val activeSpan = if (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) @@ -446,6 +446,6 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( companion object { const val SENTRY_APOLLO_3_VARIABLES = "SENTRY-APOLLO-3-VARIABLES" const val SENTRY_APOLLO_3_OPERATION_TYPE = "SENTRY-APOLLO-3-OPERATION-TYPE" - const val DEFAULT_CAPTURE_FAILED_REQUESTS = false + const val DEFAULT_CAPTURE_FAILED_REQUESTS = true } } diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt index 2bf0bece69..40406b77b5 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorClientErrors.kt @@ -120,7 +120,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `does not capture errors if captureFailedRequests is disabled`() { - val sut = fixture.getSut(responseBody = fixture.responseBodyNotOk) + val sut = fixture.getSut(captureFailedRequests = false, responseBody = fixture.responseBodyNotOk) executeQuery(sut) verify(fixture.hub, never()).captureEvent(any(), any()) @@ -129,7 +129,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors if captureFailedRequests is enabled`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) verify(fixture.hub).captureEvent(any(), any()) @@ -141,14 +141,14 @@ class SentryApollo3InterceptorClientErrors { @Test fun `does not add Apollo3ClientError integration if captureFailedRequests is disabled`() { - fixture.getSut() + fixture.getSut(captureFailedRequests = false) assertFalse(SentryIntegrationPackageStorage.getInstance().integrations.contains("Apollo3ClientError")) } @Test fun `adds Apollo3ClientError integration if captureFailedRequests is enabled`() { - fixture.getSut(captureFailedRequests = true) + fixture.getSut() assertTrue(SentryIntegrationPackageStorage.getInstance().integrations.contains("Apollo3ClientError")) } @@ -160,7 +160,6 @@ class SentryApollo3InterceptorClientErrors { @Test fun `does not capture errors if failedRequestTargets does not match`() { val sut = fixture.getSut( - captureFailedRequests = true, failedRequestTargets = listOf("nope.com"), responseBody = fixture.responseBodyNotOk ) @@ -172,7 +171,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors if failedRequestTargets matches`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) verify(fixture.hub).captureEvent(any(), any()) @@ -185,7 +184,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors with SentryApollo3Interceptor mechanism`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) verify(fixture.hub).captureEvent( @@ -200,7 +199,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors with title`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) verify(fixture.hub).captureEvent( @@ -215,7 +214,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors with snapshot flag set`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) verify(fixture.hub).captureEvent( @@ -232,7 +231,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors with request context`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) val body = """ @@ -260,7 +259,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors with more request context if sendDefaultPii is enabled`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk, sendDefaultPii = true) + fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true) executeQuery(sut) verify(fixture.hub).captureEvent( @@ -278,7 +277,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors with response context`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) verify(fixture.hub).captureEvent( @@ -298,7 +297,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors with more response context if sendDefaultPii is enabled`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk, sendDefaultPii = true) + fixture.getSut(responseBody = fixture.responseBodyNotOk, sendDefaultPii = true) executeQuery(sut) verify(fixture.hub).captureEvent( @@ -316,7 +315,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors with specific fingerprints`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) verify(fixture.hub).captureEvent( @@ -334,7 +333,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors if response code is equal or higher than 400`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk, httpStatusCode = 500) + fixture.getSut(responseBody = fixture.responseBodyNotOk, httpStatusCode = 500) executeQuery(sut) // HttpInterceptor does not throw for >= 400 @@ -344,7 +343,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `capture errors swallow any exception during the error transformation`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + fixture.getSut(responseBody = fixture.responseBodyNotOk) whenever(fixture.hub.captureEvent(any(), any())).thenThrow(RuntimeException()) @@ -358,7 +357,7 @@ class SentryApollo3InterceptorClientErrors { @Test fun `hints are set when capturing errors`() { val sut = - fixture.getSut(captureFailedRequests = true, responseBody = fixture.responseBodyNotOk) + fixture.getSut(responseBody = fixture.responseBodyNotOk) executeQuery(sut) verify(fixture.hub).captureEvent( 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 68454efc28..44d8bfd624 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 @@ -61,7 +61,7 @@ class SentryApollo3InterceptorTest { whenever(it.options).thenReturn(options) doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }.whenever(it).configureScope(any()) } - private var httpInterceptor = SentryApollo3HttpInterceptor(hub) + private var httpInterceptor = SentryApollo3HttpInterceptor(hub, captureFailedRequests = false) @SuppressWarnings("LongParameterList") fun getSut( @@ -93,7 +93,7 @@ class SentryApollo3InterceptorTest { ) if (beforeSpan != null) { - httpInterceptor = SentryApollo3HttpInterceptor(hub, beforeSpan) + httpInterceptor = SentryApollo3HttpInterceptor(hub, beforeSpan, captureFailedRequests = false) } val builder = ApolloClient.Builder() diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt index 12db8f50e5..81775efc18 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorWithVariablesTest.kt @@ -68,7 +68,7 @@ class SentryApollo3InterceptorWithVariablesTest { ) return ApolloClient.Builder().serverUrl(server.url("/").toString()) - .sentryTracing(hub = hub, beforeSpan = beforeSpan) + .sentryTracing(hub = hub, beforeSpan = beforeSpan, captureFailedRequests = false) .build() } } From 1a2e334ebb88a4247ffed7011750f19bd9e8570f Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 27 Nov 2023 20:40:58 +0100 Subject: [PATCH 37/55] Address review --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d7e11b43c..ff2c4d2e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ Version 7 of the Sentry Android/Java SDK brings a variety of features and fixes. - Multiple improvements to reduce probability of the SDK causing ANRs - New `sentry-okhttp` artifact is unbundled from Android and can be used in pure JVM-only apps -**Note: The v7 version of the Android/Java SDK requires a self-hosted version of Sentry 22.12.0 or higher. If you are using a version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka onpremise) older than `22.12.0` then you will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/). If you're using `sentry.io` no action needed.** +## Sentry Self-hosted Compatibility + +This SDK version is compatible with a self-hosted version of Sentry `22.12.0` or higher. If you are using an older version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka onpremise), you will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/). If you're using `sentry.io` no action is required. ## Sentry Integrations Version Compatibility @@ -38,7 +40,8 @@ Similarly, if you have a Sentry SDK (e.g. `sentry-android-core`) dependency on o ## Behavioural Changes - Android only: `Sentry.getSpan()` returns the root span/transaction instead of the latest span ([#2855](https://github.com/getsentry/sentry-java/pull/2855)) -- Capture failed HTTP requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794)) +- Capture failed HTTP and GraphQL (Apollo) requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794)) + - This can increase your event consumption and may affect your quota, because we will report failed network requests as Sentry events by default, if you're using the `sentry-android-okhttp` or `sentry-apollo-3` integrations. You can customize what errors you want/don't want to have reported for [OkHttp](https://docs.sentry.io/platforms/android/integrations/okhttp#http-client-errors) and [Apollo3](https://docs.sentry.io/platforms/android/integrations/apollo3#graphql-client-errors) respectively. - Measure AppStart time till First Draw instead of `onResume` ([#2851](https://github.com/getsentry/sentry-java/pull/2851)) - 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 From b3170f42074d6941e74a11ac42c472d61dd053cb Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 27 Nov 2023 23:50:35 +0100 Subject: [PATCH 38/55] Move more stuff to background for SendEnvelopeIntegrations --- .../core/SendCachedEnvelopeIntegration.java | 74 +++++++++--------- ...achedEnvelopeFireAndForgetIntegration.java | 76 +++++++++++-------- 2 files changed, 82 insertions(+), 68 deletions(-) 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 fd9a082c7a..4f45778c74 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 @@ -31,6 +31,8 @@ final class SendCachedEnvelopeIntegration private @Nullable IHub hub; private @Nullable SentryAndroidOptions options; private @Nullable SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget sender; + private final AtomicBoolean isInitialized = new AtomicBoolean(false); + private final AtomicBoolean isClosed = new AtomicBoolean(false); public SendCachedEnvelopeIntegration( final @NotNull SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory factory, @@ -53,16 +55,12 @@ public void register(@NotNull IHub hub, @NotNull SentryOptions options) { return; } - connectionStatusProvider = options.getConnectionStatusProvider(); - connectionStatusProvider.addConnectionStatusObserver(this); - - sender = factory.create(hub, options); - sendCachedEnvelopes(hub, this.options); } @Override public void close() throws IOException { + isClosed.set(true); if (connectionStatusProvider != null) { connectionStatusProvider.removeConnectionStatusObserver(this); } @@ -79,44 +77,48 @@ public void onConnectionStatusChanged( @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) { - options.getLogger().log(SentryLevel.ERROR, "SendCachedEnvelopeIntegration factory is null."); - return; - } - try { final Future future = options .getExecutorService() .submit( () -> { - final SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget sender = - factory.create(hub, androidOptions); - - if (sender == null) { - androidOptions - .getLogger() - .log(SentryLevel.ERROR, "SendFireAndForget factory is null."); - return; - } try { + if (isClosed.get()) { + options + .getLogger() + .log(SentryLevel.INFO, "SendCachedEnvelopeIntegration, not trying to send after closing."); + return; + } + + if (!isInitialized.getAndSet(true)) { + connectionStatusProvider = options.getConnectionStatusProvider(); + connectionStatusProvider.addConnectionStatusObserver(this); + + sender = factory.create(hub, 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) { + options.getLogger().log(SentryLevel.ERROR, "SendCachedEnvelopeIntegration factory is null."); + return; + } + sender.send(); } catch (Throwable e) { options diff --git a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java index d13fbf7fcb..0c0022f2ae 100644 --- a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java +++ b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java @@ -8,6 +8,7 @@ import java.io.File; import java.io.IOException; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -20,6 +21,8 @@ public final class SendCachedEnvelopeFireAndForgetIntegration private @Nullable IHub hub; private @Nullable SentryOptions options; private @Nullable SendFireAndForget sender; + private final AtomicBoolean isInitialized = new AtomicBoolean(false); + private final AtomicBoolean isClosed = new AtomicBoolean(false); public interface SendFireAndForget { void send(); @@ -78,16 +81,12 @@ public void register(final @NotNull IHub hub, final @NotNull SentryOptions optio .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 { + isClosed.set(true); if (connectionStatusProvider != null) { connectionStatusProvider.removeConnectionStatusObserver(this); } @@ -104,39 +103,52 @@ public void onConnectionStatusChanged( @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."); - return; - } - try { options .getExecutorService() .submit( () -> { try { + if (isClosed.get()) { + options + .getLogger() + .log(SentryLevel.INFO, "SendCachedEnvelopeFireAndForgetIntegration, not trying to send after closing."); + return; + } + + if (!isInitialized.getAndSet(true)) { + connectionStatusProvider = options.getConnectionStatusProvider(); + connectionStatusProvider.addConnectionStatusObserver(this); + + sender = factory.create(hub, 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."); + return; + } + sender.send(); } catch (Throwable e) { options From 29b704b0c94fdba63563bca56e63c7cad0c39bd8 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 27 Nov 2023 23:51:11 +0100 Subject: [PATCH 39/55] spotless --- .../core/SendCachedEnvelopeIntegration.java | 29 +++++++++++++------ ...achedEnvelopeFireAndForgetIntegration.java | 28 +++++++++++------- 2 files changed, 37 insertions(+), 20 deletions(-) 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 4f45778c74..66e534bb7d 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 @@ -86,8 +86,10 @@ private synchronized void sendCachedEnvelopes( try { if (isClosed.get()) { options - .getLogger() - .log(SentryLevel.INFO, "SendCachedEnvelopeIntegration, not trying to send after closing."); + .getLogger() + .log( + SentryLevel.INFO, + "SendCachedEnvelopeIntegration, not trying to send after closing."); return; } @@ -99,23 +101,32 @@ private synchronized void sendCachedEnvelopes( } if (connectionStatusProvider != null - && connectionStatusProvider.getConnectionStatus() - == IConnectionStatusProvider.ConnectionStatus.DISCONNECTED) { - options.getLogger().log(SentryLevel.INFO, "SendCachedEnvelopeIntegration, no connection."); + && 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)) { + if (rateLimiter != null + && rateLimiter.isActiveForCategory(DataCategory.All)) { options - .getLogger() - .log(SentryLevel.INFO, "SendCachedEnvelopeIntegration, rate limiting active."); + .getLogger() + .log( + SentryLevel.INFO, + "SendCachedEnvelopeIntegration, rate limiting active."); return; } if (sender == null) { - options.getLogger().log(SentryLevel.ERROR, "SendCachedEnvelopeIntegration factory is null."); + options + .getLogger() + .log( + SentryLevel.ERROR, + "SendCachedEnvelopeIntegration factory is null."); return; } diff --git a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java index 0c0022f2ae..bc813fdd1e 100644 --- a/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java +++ b/sentry/src/main/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegration.java @@ -111,8 +111,10 @@ private synchronized void sendCachedEnvelopes( try { if (isClosed.get()) { options - .getLogger() - .log(SentryLevel.INFO, "SendCachedEnvelopeFireAndForgetIntegration, not trying to send after closing."); + .getLogger() + .log( + SentryLevel.INFO, + "SendCachedEnvelopeFireAndForgetIntegration, not trying to send after closing."); return; } @@ -125,11 +127,13 @@ private synchronized void sendCachedEnvelopes( // skip run only if we're certainly disconnected if (connectionStatusProvider != null - && connectionStatusProvider.getConnectionStatus() - == IConnectionStatusProvider.ConnectionStatus.DISCONNECTED) { + && connectionStatusProvider.getConnectionStatus() + == IConnectionStatusProvider.ConnectionStatus.DISCONNECTED) { options - .getLogger() - .log(SentryLevel.INFO, "SendCachedEnvelopeFireAndForgetIntegration, no connection."); + .getLogger() + .log( + SentryLevel.INFO, + "SendCachedEnvelopeFireAndForgetIntegration, no connection."); return; } @@ -137,15 +141,17 @@ private synchronized void sendCachedEnvelopes( final @Nullable RateLimiter rateLimiter = hub.getRateLimiter(); if (rateLimiter != null && rateLimiter.isActiveForCategory(DataCategory.All)) { options - .getLogger() - .log( - SentryLevel.INFO, - "SendCachedEnvelopeFireAndForgetIntegration, rate limiting active."); + .getLogger() + .log( + SentryLevel.INFO, + "SendCachedEnvelopeFireAndForgetIntegration, rate limiting active."); return; } if (sender == null) { - options.getLogger().log(SentryLevel.ERROR, "SendFireAndForget factory is null."); + options + .getLogger() + .log(SentryLevel.ERROR, "SendFireAndForget factory is null."); return; } From 12cde1a6ac3967a2e9b25188638c33da7d28d73e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 28 Nov 2023 00:40:19 +0100 Subject: [PATCH 40/55] Fix tests --- .../core/SendCachedEnvelopeIntegrationTest.kt | 18 +++++++++++-- ...hedEnvelopeFireAndForgetIntegrationTest.kt | 25 +++++++++++++++---- 2 files changed, 36 insertions(+), 7 deletions(-) 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 2be757c7ac..403f40ee70 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 @@ -9,6 +9,7 @@ import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory import io.sentry.SentryLevel.DEBUG import io.sentry.SentryOptions +import io.sentry.test.DeferredExecutorService import io.sentry.test.ImmediateExecutorService import io.sentry.transport.RateLimiter import io.sentry.util.LazyEvaluator @@ -138,7 +139,7 @@ class SendCachedEnvelopeIntegrationTest { @Test fun `registers for network connection changes`() { - val sut = fixture.getSut(hasStartupCrashMarker = false) + val sut = fixture.getSut(hasStartupCrashMarker = false, mockExecutorService = ImmediateExecutorService()) val connectionStatusProvider = mock() fixture.options.connectionStatusProvider = connectionStatusProvider @@ -164,7 +165,7 @@ class SendCachedEnvelopeIntegrationTest { @Test fun `when the network is not disconnected the factory is initialized`() { - val sut = fixture.getSut(hasStartupCrashMarker = false) + val sut = fixture.getSut(hasStartupCrashMarker = false, mockExecutorService = ImmediateExecutorService()) val connectionStatusProvider = mock() fixture.options.connectionStatusProvider = connectionStatusProvider @@ -221,4 +222,17 @@ class SendCachedEnvelopeIntegrationTest { // no factory call should be done if there's rate limiting active verify(fixture.sender, never()).send() } + + @Test + fun `when closed after register, does nothing`() { + val deferredExecutorService = DeferredExecutorService() + val sut = fixture.getSut(mockExecutorService = deferredExecutorService) + + sut.register(fixture.hub, fixture.options) + verify(fixture.sender, never()).send() + sut.close() + + deferredExecutorService.runAll() + verify(fixture.sender, never()).send() + } } diff --git a/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt b/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt index 8461147826..78623f90a7 100644 --- a/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt +++ b/sentry/src/test/java/io/sentry/SendCachedEnvelopeFireAndForgetIntegrationTest.kt @@ -2,6 +2,7 @@ package io.sentry import io.sentry.SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget import io.sentry.protocol.SdkVersion +import io.sentry.test.DeferredExecutorService import io.sentry.test.ImmediateExecutorService import io.sentry.transport.RateLimiter import org.mockito.kotlin.any @@ -33,10 +34,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { options.sdkVersion = SdkVersion("test", "1.2.3") } - fun getSut(useImmediateExecutor: Boolean = true): SendCachedEnvelopeFireAndForgetIntegration { - if (useImmediateExecutor) { - options.executorService = ImmediateExecutorService() - } + fun getSut(): SendCachedEnvelopeFireAndForgetIntegration { return SendCachedEnvelopeFireAndForgetIntegration(callback) } } @@ -97,7 +95,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { fixture.options.cacheDirPath = "cache" fixture.options.executorService.close(0) whenever(fixture.callback.create(any(), any())).thenReturn(mock()) - val sut = fixture.getSut(useImmediateExecutor = false) + val sut = fixture.getSut() 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()) } @@ -105,6 +103,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { @Test fun `registers for network connection changes`() { val connectionStatusProvider = mock() + fixture.options.executorService = ImmediateExecutorService() fixture.options.connectionStatusProvider = connectionStatusProvider fixture.options.cacheDirPath = "cache" @@ -135,6 +134,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { whenever(connectionStatusProvider.connectionStatus).thenReturn( IConnectionStatusProvider.ConnectionStatus.UNKNOWN ) + fixture.options.executorService = ImmediateExecutorService() fixture.options.connectionStatusProvider = connectionStatusProvider fixture.options.cacheDirPath = "cache" @@ -150,6 +150,7 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { whenever(connectionStatusProvider.connectionStatus).thenReturn( IConnectionStatusProvider.ConnectionStatus.DISCONNECTED ) + fixture.options.executorService = ImmediateExecutorService() fixture.options.connectionStatusProvider = connectionStatusProvider fixture.options.cacheDirPath = "cache" @@ -208,6 +209,20 @@ class SendCachedEnvelopeFireAndForgetIntegrationTest { verify(fixture.callback, never()).create(any(), any()) } + @Test + fun `when closed after register, does nothing`() { + val deferredExecutorService = DeferredExecutorService() + fixture.options.executorService = deferredExecutorService + val sut = fixture.getSut() + + sut.register(fixture.hub, fixture.options) + verify(fixture.sender, never()).send() + sut.close() + + deferredExecutorService.runAll() + verify(fixture.sender, never()).send() + } + private class CustomFactory : SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForgetFactory { override fun create(hub: IHub, options: SentryOptions): SendCachedEnvelopeFireAndForgetIntegration.SendFireAndForget? { return null From 741b219b2bc9abf45fb60a2d49d5b837c96c6629 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 28 Nov 2023 17:03:42 +0100 Subject: [PATCH 41/55] Always execute callback in withScope (#3066) * execute withScopeCallback in noopHub, add tests to SentryTest * extract IScope interface * initial migration to IScope * fix usages of IScope constructor and replace with Scope, add fromScope static Method, fix tests, add tests, run withScope even on disabled hub * replace static fromScope method with clone * add changelog entries * revert unintended changes * Format code --------- Co-authored-by: Sentry Github Bot --- CHANGELOG.md | 2 + .../api/sentry-android-core.api | 4 +- .../core/ActivityLifecycleIntegration.java | 6 +- .../android/core/InternalSentrySdk.java | 10 +- .../android/core/SentryAndroidOptions.java | 8 +- .../gestures/SentryGestureListener.java | 6 +- .../core/ActivityLifecycleIntegrationTest.kt | 3 +- .../android/core/InternalSentrySdkTest.kt | 5 +- .../android/core/LifecycleWatcherTest.kt | 4 +- .../core/SessionTrackingIntegrationTest.kt | 10 +- .../SentryGestureListenerClickTest.kt | 4 +- .../SentryGestureListenerScrollTest.kt | 4 +- .../SentryGestureListenerTracingTest.kt | 3 +- .../SentryFragmentLifecycleCallbacksTest.kt | 4 +- .../SentryNavigationListenerTest.kt | 3 +- .../sentry/graphql/ExceptionReporterTest.kt | 3 +- .../okhttp/SentryOkHttpInterceptorTest.kt | 3 +- .../SentryWebfluxAutoConfiguration.java | 5 +- .../SentrySpanWebClientCustomizerTest.kt | 3 +- .../boot/SentryWebfluxAutoConfiguration.java | 3 +- .../boot/SentrySpanWebClientCustomizerTest.kt | 3 +- .../spring/jakarta/SentryUserFilter.java | 4 +- .../webflux/AbstractSentryWebFilter.java | 3 +- .../jakarta/webflux/SentryWebFilter.java | 3 +- ...entryWebFilterWithThreadLocalAccessor.java | 3 +- .../spring/jakarta/SentrySpringFilterTest.kt | 3 +- .../io/sentry/spring/SentryUserFilter.java | 4 +- .../spring/webflux/SentryWebFilter.java | 3 +- .../sentry/spring/SentrySpringFilterTest.kt | 3 +- sentry/api/sentry.api | 151 ++++++- sentry/src/main/java/io/sentry/Baggage.java | 3 +- sentry/src/main/java/io/sentry/Hub.java | 26 +- sentry/src/main/java/io/sentry/IScope.java | 373 ++++++++++++++++++ .../main/java/io/sentry/ISentryClient.java | 18 +- sentry/src/main/java/io/sentry/NoOpHub.java | 4 +- sentry/src/main/java/io/sentry/NoOpScope.java | 248 ++++++++++++ .../main/java/io/sentry/NoOpSentryClient.java | 6 +- sentry/src/main/java/io/sentry/Scope.java | 85 +++- .../main/java/io/sentry/ScopeCallback.java | 2 +- .../src/main/java/io/sentry/SentryClient.java | 16 +- sentry/src/main/java/io/sentry/Stack.java | 8 +- .../java/io/sentry/util/TracingUtils.java | 4 +- sentry/src/test/java/io/sentry/HubTest.kt | 52 +-- sentry/src/test/java/io/sentry/NoOpHubTest.kt | 9 + .../src/test/java/io/sentry/NoOpScopeTest.kt | 124 ++++++ sentry/src/test/java/io/sentry/ScopeTest.kt | 10 +- .../test/java/io/sentry/SentryClientTest.kt | 26 +- sentry/src/test/java/io/sentry/SentryTest.kt | 22 ++ sentry/src/test/java/io/sentry/StackTest.kt | 2 +- 49 files changed, 1145 insertions(+), 166 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/IScope.java create mode 100644 sentry/src/main/java/io/sentry/NoOpScope.java create mode 100644 sentry/src/test/java/io/sentry/NoOpScopeTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index d951849f1c..6cf9bb5bf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ Similarly, if you have a Sentry SDK (e.g. `sentry-android-core`) dependency on o - Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) - Apollo v2 BeforeSpanCallback now allows returning null ([#2890](https://github.com/getsentry/sentry-java/pull/2890)) - `SentryOkHttpUtils` was removed from public API as it's been exposed by mistake ([#3005](https://github.com/getsentry/sentry-java/pull/3005)) +- `IScope` and `NoOpScope` were introduced. ([#3066](https://github.com/getsentry/sentry-java/pull/3066)) ## Behavioural Changes @@ -51,6 +52,7 @@ Similarly, if you have a Sentry SDK (e.g. `sentry-android-core`) dependency on o - Instead use the "Prevent Storing of IP Addresses" option in the "Security & Privacy" project settings on sentry.io - Raw logback message and parameters are now guarded by `sendDefaultPii` if an `encoder` has been configured ([#2976](https://github.com/getsentry/sentry-java/pull/2976)) - The `maxSpans` setting (defaults to 1000) is enforced for nested child spans which means a single transaction can have `maxSpans` number of children (nested or not) at most ([#3065](https://github.com/getsentry/sentry-java/pull/3065)) +- The `ScopeCallback` in `withScope` is now always executed ([#3066](https://github.com/getsentry/sentry-java/pull/3066)) ## Deprecations diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 52b2b2e2e4..42aca9b5cc 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -201,8 +201,8 @@ public abstract interface class io/sentry/android/core/IDebugImagesLoader { public final class io/sentry/android/core/InternalSentrySdk { public fun ()V public static fun captureEnvelope ([B)Lio/sentry/protocol/SentryId; - public static fun getCurrentScope ()Lio/sentry/Scope; - public static fun serializeScope (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/Scope;)Ljava/util/Map; + public static fun getCurrentScope ()Lio/sentry/IScope; + public static fun serializeScope (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/IScope;)Ljava/util/Map; } public final class io/sentry/android/core/LoadClass { 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 0d7b976d5e..425d26ad32 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 @@ -16,12 +16,12 @@ import io.sentry.FullyDisplayedReporter; import io.sentry.Hint; import io.sentry.IHub; +import io.sentry.IScope; import io.sentry.ISpan; import io.sentry.ITransaction; import io.sentry.Instrumenter; import io.sentry.Integration; import io.sentry.NoOpTransaction; -import io.sentry.Scope; import io.sentry.SentryDate; import io.sentry.SentryLevel; import io.sentry.SentryOptions; @@ -299,7 +299,7 @@ private void setSpanOrigin(ISpan span) { } @VisibleForTesting - void applyScope(final @NotNull Scope scope, final @NotNull ITransaction transaction) { + void applyScope(final @NotNull IScope scope, final @NotNull ITransaction transaction) { scope.withTransaction( scopeTransaction -> { // we'd not like to overwrite existent transactions bound to the Scope @@ -318,7 +318,7 @@ void applyScope(final @NotNull Scope scope, final @NotNull ITransaction transact } @VisibleForTesting - void clearScope(final @NotNull Scope scope, final @NotNull ITransaction transaction) { + void clearScope(final @NotNull IScope scope, final @NotNull ITransaction transaction) { scope.withTransaction( scopeTransaction -> { if (scopeTransaction == transaction) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index aa25a4e745..637247133b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -7,9 +7,9 @@ import io.sentry.HubAdapter; import io.sentry.IHub; import io.sentry.ILogger; +import io.sentry.IScope; import io.sentry.ISerializer; import io.sentry.ObjectWriter; -import io.sentry.Scope; import io.sentry.SentryEnvelope; import io.sentry.SentryEnvelopeItem; import io.sentry.SentryEvent; @@ -40,12 +40,12 @@ public final class InternalSentrySdk { * @return a copy of the current hub's topmost scope, or null in case the hub is disabled */ @Nullable - public static Scope getCurrentScope() { - final @NotNull AtomicReference scopeRef = new AtomicReference<>(); + public static IScope getCurrentScope() { + final @NotNull AtomicReference scopeRef = new AtomicReference<>(); HubAdapter.getInstance() .configureScope( scope -> { - scopeRef.set(new Scope(scope)); + scopeRef.set(scope.clone()); }); return scopeRef.get(); } @@ -64,7 +64,7 @@ public static Scope getCurrentScope() { public static Map serializeScope( final @NotNull Context context, final @NotNull SentryAndroidOptions options, - final @Nullable Scope scope) { + final @Nullable IScope scope) { final @NotNull Map data = new HashMap<>(); if (scope == null) { 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 d57a467c53..8110ca442a 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 @@ -3,8 +3,8 @@ import android.app.ActivityManager; import android.app.ApplicationExitInfo; import io.sentry.Hint; +import io.sentry.IScope; import io.sentry.ISpan; -import io.sentry.Scope; import io.sentry.Sentry; import io.sentry.SentryEvent; import io.sentry.SentryOptions; @@ -77,8 +77,8 @@ public final class SentryAndroidOptions extends SentryOptions { *

  • The transaction status will be {@link SpanStatus#OK} if none is set. * * - * The transaction is automatically bound to the {@link Scope}, but only if there's no transaction - * already bound to the Scope. + * The transaction is automatically bound to the {@link IScope}, but only if there's no + * transaction already bound to the Scope. */ private boolean enableAutoActivityLifecycleTracing = true; @@ -201,7 +201,7 @@ public interface BeforeCaptureCallback { * reporting only the latest one. * *

    These events do not affect ANR rate nor are they enriched with additional information from - * {@link Scope} like breadcrumbs. The events are reported with 'HistoricalAppExitInfo' {@link + * {@link IScope} like breadcrumbs. The events are reported with 'HistoricalAppExitInfo' {@link * Mechanism}. */ private boolean reportHistoricalAnrs = false; 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 32a1ffb06a..0ec0d83258 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 @@ -11,8 +11,8 @@ import io.sentry.Breadcrumb; import io.sentry.Hint; import io.sentry.IHub; +import io.sentry.IScope; import io.sentry.ITransaction; -import io.sentry.Scope; import io.sentry.SentryLevel; import io.sentry.SpanStatus; import io.sentry.TransactionContext; @@ -292,7 +292,7 @@ void stopTracing(final @NotNull SpanStatus status) { } @VisibleForTesting - void clearScope(final @NotNull Scope scope) { + void clearScope(final @NotNull IScope scope) { scope.withTransaction( transaction -> { if (transaction == activeTransaction) { @@ -302,7 +302,7 @@ void clearScope(final @NotNull Scope scope) { } @VisibleForTesting - void applyScope(final @NotNull Scope scope, final @NotNull ITransaction transaction) { + void applyScope(final @NotNull IScope scope, final @NotNull ITransaction transaction) { scope.withTransaction( scopeTransaction -> { // we'd not like to overwrite existent transactions bound to the Scope manually 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 309773c0a4..1e280d173b 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 @@ -16,6 +16,7 @@ import io.sentry.Breadcrumb import io.sentry.DateUtils import io.sentry.FullyDisplayedReporter import io.sentry.Hub +import io.sentry.IScope import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.Sentry @@ -1414,7 +1415,7 @@ class ActivityLifecycleIntegrationTest { fixture.options.enableTracing = false val argumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ScopeCallback::class.java) - val scope = mock() + val scope = mock() whenever(fixture.hub.configureScope(argumentCaptor.capture())).thenAnswer { argumentCaptor.value.run(scope) } 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 c2ffb9489e..766ee95f5c 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 @@ -6,6 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb import io.sentry.Hint import io.sentry.Hub +import io.sentry.IScope import io.sentry.Scope import io.sentry.Sentry import io.sentry.SentryEnvelope @@ -188,7 +189,7 @@ class InternalSentrySdkTest { @Test fun `serializeScope returns empty map in case scope serialization fails`() { val options = SentryAndroidOptions() - val scope = mock() + val scope = mock() whenever(scope.contexts).thenReturn(Contexts()) whenever(scope.user).thenThrow(IllegalStateException("something is off")) @@ -291,7 +292,7 @@ class InternalSentrySdkTest { assertEquals(Session.State.Crashed, capturedSession.status) // and the local session should be marked as crashed too - val scopeRef = AtomicReference() + val scopeRef = AtomicReference() Sentry.configureScope { scope -> scopeRef.set(scope) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt index 7a06147f66..be30993142 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/LifecycleWatcherTest.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.LifecycleOwner import io.sentry.Breadcrumb import io.sentry.DateUtils import io.sentry.IHub -import io.sentry.Scope +import io.sentry.IScope import io.sentry.ScopeCallback import io.sentry.SentryLevel import io.sentry.Session @@ -42,7 +42,7 @@ class LifecycleWatcherTest { session: Session? = null ): LifecycleWatcher { val argumentCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ScopeCallback::class.java) - val scope = mock() + val scope = mock() whenever(scope.session).thenReturn(session) whenever(hub.configureScope(argumentCaptor.capture())).thenAnswer { argumentCaptor.value.run(scope) 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 a25e7411c7..af32fa3714 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 @@ -8,16 +8,14 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.CheckIn import io.sentry.Hint +import io.sentry.IScope import io.sentry.ISentryClient import io.sentry.ProfilingTraceData -import io.sentry.Scope import io.sentry.Sentry import io.sentry.SentryEnvelope import io.sentry.SentryEvent import io.sentry.SentryOptions import io.sentry.Session -import io.sentry.Session.State.Exited -import io.sentry.Session.State.Ok import io.sentry.TraceContext import io.sentry.UserFeedback import io.sentry.protocol.SentryId @@ -131,7 +129,7 @@ class SessionTrackingIntegrationTest { override fun isEnabled(): Boolean = true - override fun captureEvent(event: SentryEvent, scope: Scope?, hint: Hint?): SentryId { + override fun captureEvent(event: SentryEvent, scope: IScope?, hint: Hint?): SentryId { TODO("Not yet implemented") } @@ -158,14 +156,14 @@ class SessionTrackingIntegrationTest { override fun captureTransaction( transaction: SentryTransaction, traceContext: TraceContext?, - scope: Scope?, + scope: IScope?, hint: Hint?, profilingTraceData: ProfilingTraceData? ): SentryId { TODO("Not yet implemented") } - override fun captureCheckIn(checkIn: CheckIn, scope: Scope?, hint: Hint?): SentryId { + override fun captureCheckIn(checkIn: CheckIn, scope: IScope?, hint: Hint?): SentryId { TODO("Not yet implemented") } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt index 2fdbc9dc07..1e6652276a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerClickTest.kt @@ -11,8 +11,8 @@ import android.widget.CheckBox import android.widget.RadioButton import io.sentry.Breadcrumb import io.sentry.IHub +import io.sentry.IScope import io.sentry.PropagationContext -import io.sentry.Scope import io.sentry.Scope.IWithPropagationContext import io.sentry.ScopeCallback import io.sentry.SentryLevel.INFO @@ -41,7 +41,7 @@ class SentryGestureListenerClickTest { dsn = "https://key@sentry.io/proj" } val hub = mock() - val scope = mock() + val scope = mock() val propagationContext = PropagationContext() lateinit var target: View lateinit var invalidTarget: View diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt index dd78e75d6c..5d39b64753 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/gestures/SentryGestureListenerScrollTest.kt @@ -12,6 +12,7 @@ import android.widget.ListAdapter import androidx.core.view.ScrollingView import io.sentry.Breadcrumb import io.sentry.IHub +import io.sentry.IScope import io.sentry.PropagationContext import io.sentry.Scope import io.sentry.ScopeCallback @@ -25,7 +26,6 @@ import org.mockito.kotlin.doAnswer import org.mockito.kotlin.inOrder 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 @@ -45,7 +45,7 @@ class SentryGestureListenerScrollTest { gestureTargetLocators = listOf(AndroidViewGestureTargetLocator(true)) } val hub = mock() - val scope = mock() + val scope = mock() val propagationContext = PropagationContext() val firstEvent = mock() 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 308632c6ed..c7ada69c88 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 @@ -10,6 +10,7 @@ import android.view.Window import android.widget.AbsListView import android.widget.ListAdapter import io.sentry.IHub +import io.sentry.IScope import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryTracer @@ -47,7 +48,7 @@ class SentryGestureListenerTracingTest { } val hub = mock() val event = mock() - val scope = mock() + val scope = mock() lateinit var target: View lateinit var transaction: SentryTracer diff --git a/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt index 2c7b8f2e58..9b7fd5c5f2 100644 --- a/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt +++ b/sentry-android-fragment/src/test/java/io/sentry/android/fragment/SentryFragmentLifecycleCallbacksTest.kt @@ -6,9 +6,9 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import io.sentry.Breadcrumb import io.sentry.Hub +import io.sentry.IScope import io.sentry.ISpan import io.sentry.ITransaction -import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryLevel.INFO import io.sentry.SentryOptions @@ -35,7 +35,7 @@ class SentryFragmentLifecycleCallbacksTest { val hub = mock() val fragment = mock() val context = mock() - val scope = mock() + val scope = mock() val transaction = mock() val span = mock() 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 e1c899d136..76c57159c3 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 @@ -8,6 +8,7 @@ import androidx.navigation.NavDestination import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb import io.sentry.IHub +import io.sentry.IScope import io.sentry.Scope import io.sentry.Scope.IWithTransaction import io.sentry.ScopeCallback @@ -44,7 +45,7 @@ class SentryNavigationListenerTest { val context = mock() val resources = mock() - val scope = mock() + val scope = mock() lateinit var options: SentryOptions lateinit var transaction: SentryTracer diff --git a/sentry-graphql/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt b/sentry-graphql/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt index af469f8e02..a2b2b0f101 100644 --- a/sentry-graphql/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt +++ b/sentry-graphql/src/test/kotlin/io/sentry/graphql/ExceptionReporterTest.kt @@ -13,6 +13,7 @@ import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLSchema import io.sentry.Hint import io.sentry.IHub +import io.sentry.IScope import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions @@ -41,7 +42,7 @@ class ExceptionReporterTest { val hub = mock() lateinit var instrumentationExecutionParameters: InstrumentationExecutionParameters lateinit var executionResult: ExecutionResult - lateinit var scope: Scope + lateinit var scope: IScope val query = """query greeting(name: "somename")""" val variables = mapOf("variableA" to "value a") diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt index b856d93fb1..ad88330962 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt @@ -7,6 +7,7 @@ import io.sentry.Breadcrumb import io.sentry.Hint import io.sentry.HttpStatusCodeRange import io.sentry.IHub +import io.sentry.IScope import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions @@ -50,7 +51,7 @@ class SentryOkHttpInterceptorTest { val server = MockWebServer() lateinit var sentryTracer: SentryTracer lateinit var options: SentryOptions - lateinit var scope: Scope + lateinit var scope: IScope @SuppressWarnings("LongParameterList") fun getSut( diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java index 8b68d3435c..d1cda8b4d2 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java @@ -2,6 +2,7 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.IHub; +import io.sentry.IScope; import io.sentry.spring.jakarta.webflux.SentryScheduleHook; import io.sentry.spring.jakarta.webflux.SentryWebExceptionHandler; import io.sentry.spring.jakarta.webflux.SentryWebFilter; @@ -40,7 +41,7 @@ public class SentryWebfluxAutoConfiguration { static class SentryWebfluxFilterThreadLocalAccessorConfiguration { /** - * Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request. + * Configures a filter that sets up Sentry {@link IScope} for each request. * *

    Makes use of newer reactor-core and context-propagation library feature * ThreadLocalAccessor to propagate the Sentry hub. @@ -67,7 +68,7 @@ static class SentryWebfluxFilterConfiguration { }; } - /** Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request. */ + /** Configures a filter that sets up Sentry {@link IScope} for each request. */ @Bean @Order(SENTRY_SPRING_FILTER_PRECEDENCE) public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IHub hub) { diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt index 2aaea06efc..51f0a6cb3d 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt @@ -3,6 +3,7 @@ package io.sentry.spring.boot.jakarta import io.sentry.BaggageHeader import io.sentry.Breadcrumb import io.sentry.IHub +import io.sentry.IScope import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions @@ -37,7 +38,7 @@ import kotlin.test.assertNull class SentrySpanWebClientCustomizerTest { class Fixture { lateinit var sentryOptions: SentryOptions - lateinit var scope: Scope + lateinit var scope: IScope val hub = mock() var mockServer = MockWebServer() lateinit var transaction: SentryTracer diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryWebfluxAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryWebfluxAutoConfiguration.java index 2d83b77635..3d507f1b6b 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryWebfluxAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryWebfluxAutoConfiguration.java @@ -2,6 +2,7 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.IHub; +import io.sentry.IScope; import io.sentry.spring.webflux.SentryScheduleHook; import io.sentry.spring.webflux.SentryWebExceptionHandler; import io.sentry.spring.webflux.SentryWebFilter; @@ -35,7 +36,7 @@ public class SentryWebfluxAutoConfiguration { }; } - /** Configures a filter that sets up Sentry {@link io.sentry.Scope} for each request. */ + /** Configures a filter that sets up Sentry {@link IScope} for each request. */ @Bean @Order(SENTRY_SPRING_FILTER_PRECEDENCE) public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IHub hub) { diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanWebClientCustomizerTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanWebClientCustomizerTest.kt index ba26fef3e9..f925435fd3 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanWebClientCustomizerTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanWebClientCustomizerTest.kt @@ -3,6 +3,7 @@ package io.sentry.spring.boot import io.sentry.BaggageHeader import io.sentry.Breadcrumb import io.sentry.IHub +import io.sentry.IScope import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions @@ -37,7 +38,7 @@ import kotlin.test.assertNull class SentrySpanWebClientCustomizerTest { class Fixture { lateinit var sentryOptions: SentryOptions - lateinit var scope: Scope + lateinit var scope: IScope val hub = mock() var mockServer = MockWebServer() lateinit var transaction: SentryTracer diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java index 4572831f26..f7b8bc62d6 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java @@ -2,8 +2,8 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.IHub; +import io.sentry.IScope; import io.sentry.IpAddressUtils; -import io.sentry.Scope; import io.sentry.protocol.User; import io.sentry.util.Objects; import jakarta.servlet.FilterChain; @@ -21,7 +21,7 @@ import org.springframework.web.filter.OncePerRequestFilter; /** - * Sets the {@link User} on the {@link Scope} with information retrieved from {@link + * Sets the {@link User} on the {@link IScope} with information retrieved from {@link * SentryUserProvider}s. */ @Open diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java index 0b6b387e45..bf25aa5f49 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java @@ -8,6 +8,7 @@ import io.sentry.CustomSamplingContext; import io.sentry.Hint; import io.sentry.IHub; +import io.sentry.IScope; import io.sentry.ITransaction; import io.sentry.NoOpHub; import io.sentry.Sentry; @@ -29,7 +30,7 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; -/** Manages {@link io.sentry.Scope} in Webflux request processing. */ +/** Manages {@link IScope} in Webflux request processing. */ @ApiStatus.Experimental public abstract class AbstractSentryWebFilter implements WebFilter { private final @NotNull SentryRequestResolver sentryRequestResolver; diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java index 1b3a54a389..a57a389499 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java @@ -2,6 +2,7 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.IHub; +import io.sentry.IScope; import io.sentry.ITransaction; import io.sentry.Sentry; import org.jetbrains.annotations.ApiStatus; @@ -12,7 +13,7 @@ import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; -/** Manages {@link io.sentry.Scope} in Webflux request processing. */ +/** Manages {@link IScope} in Webflux request processing. */ @ApiStatus.Experimental @Open public class SentryWebFilter extends AbstractSentryWebFilter { diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java index 7d102c1ee7..278c2b8e7e 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java @@ -1,6 +1,7 @@ package io.sentry.spring.jakarta.webflux; import io.sentry.IHub; +import io.sentry.IScope; import io.sentry.ITransaction; import io.sentry.Sentry; import org.jetbrains.annotations.ApiStatus; @@ -10,7 +11,7 @@ import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; -/** Manages {@link io.sentry.Scope} in Webflux request processing. */ +/** Manages {@link IScope} in Webflux request processing. */ @ApiStatus.Experimental public final class SentryWebFilterWithThreadLocalAccessor extends AbstractSentryWebFilter { diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt index 4d26fbcb24..4e1bbb0ee5 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt @@ -2,6 +2,7 @@ package io.sentry.spring.jakarta import io.sentry.Breadcrumb import io.sentry.IHub +import io.sentry.IScope import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions @@ -39,7 +40,7 @@ class SentrySpringFilterTest { val hub = mock() val response = MockHttpServletResponse() val chain = mock() - lateinit var scope: Scope + lateinit var scope: IScope lateinit var request: HttpServletRequest fun getSut(request: HttpServletRequest? = null, options: SentryOptions = SentryOptions()): SentrySpringFilter { diff --git a/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java b/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java index 9c20c486fc..55c5826d40 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java +++ b/sentry-spring/src/main/java/io/sentry/spring/SentryUserFilter.java @@ -2,8 +2,8 @@ import com.jakewharton.nopen.annotation.Open; import io.sentry.IHub; +import io.sentry.IScope; import io.sentry.IpAddressUtils; -import io.sentry.Scope; import io.sentry.protocol.User; import io.sentry.util.Objects; import java.io.IOException; @@ -21,7 +21,7 @@ import org.springframework.web.filter.OncePerRequestFilter; /** - * Sets the {@link User} on the {@link Scope} with information retrieved from {@link + * Sets the {@link User} on the {@link IScope} with information retrieved from {@link * SentryUserProvider}s. */ @Open diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java index 5dedc9dbba..f68a87ae0f 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryWebFilter.java @@ -8,6 +8,7 @@ import io.sentry.CustomSamplingContext; import io.sentry.Hint; import io.sentry.IHub; +import io.sentry.IScope; import io.sentry.ITransaction; import io.sentry.NoOpHub; import io.sentry.Sentry; @@ -30,7 +31,7 @@ import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; -/** Manages {@link io.sentry.Scope} in Webflux request processing. */ +/** Manages {@link IScope} in Webflux request processing. */ @ApiStatus.Experimental public final class SentryWebFilter implements WebFilter { public static final String SENTRY_HUB_KEY = "sentry-hub"; diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/SentrySpringFilterTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/SentrySpringFilterTest.kt index 8043e69f67..ce83c4b9b7 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/SentrySpringFilterTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/SentrySpringFilterTest.kt @@ -2,6 +2,7 @@ package io.sentry.spring import io.sentry.Breadcrumb import io.sentry.IHub +import io.sentry.IScope import io.sentry.Scope import io.sentry.ScopeCallback import io.sentry.SentryOptions @@ -39,7 +40,7 @@ class SentrySpringFilterTest { val hub = mock() val response = MockHttpServletResponse() val chain = mock() - lateinit var scope: Scope + lateinit var scope: IScope lateinit var request: HttpServletRequest fun getSut(request: HttpServletRequest? = null, options: SentryOptions = SentryOptions()): SentrySpringFilter { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 8a77bb0af4..3096090f81 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -65,7 +65,7 @@ public final class io/sentry/Baggage { public fun setTransaction (Ljava/lang/String;)V public fun setUserId (Ljava/lang/String;)V public fun setUserSegment (Ljava/lang/String;)V - public fun setValuesFromScope (Lio/sentry/Scope;Lio/sentry/SentryOptions;)V + public fun setValuesFromScope (Lio/sentry/IScope;Lio/sentry/SentryOptions;)V public fun setValuesFromTransaction (Lio/sentry/ITransaction;Lio/sentry/protocol/User;Lio/sentry/SentryOptions;Lio/sentry/TracesSamplingDecision;)V public fun toHeaderString (Ljava/lang/String;)Ljava/lang/String; public fun toTraceContext ()Lio/sentry/TraceContext; @@ -620,6 +620,60 @@ public abstract interface class io/sentry/IOptionsObserver { public abstract fun setTags (Ljava/util/Map;)V } +public abstract interface class io/sentry/IScope { + public abstract fun addAttachment (Lio/sentry/Attachment;)V + public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;)V + public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V + public abstract fun addEventProcessor (Lio/sentry/EventProcessor;)V + public abstract fun clear ()V + public abstract fun clearAttachments ()V + public abstract fun clearBreadcrumbs ()V + public abstract fun clearTransaction ()V + public abstract fun clone ()Lio/sentry/IScope; + public abstract fun endSession ()Lio/sentry/Session; + public abstract fun getAttachments ()Ljava/util/List; + public abstract fun getBreadcrumbs ()Ljava/util/Queue; + public abstract fun getContexts ()Lio/sentry/protocol/Contexts; + public abstract fun getEventProcessors ()Ljava/util/List; + public abstract fun getExtras ()Ljava/util/Map; + public abstract fun getFingerprint ()Ljava/util/List; + public abstract fun getLevel ()Lio/sentry/SentryLevel; + public abstract fun getOptions ()Lio/sentry/SentryOptions; + public abstract fun getPropagationContext ()Lio/sentry/PropagationContext; + public abstract fun getRequest ()Lio/sentry/protocol/Request; + public abstract fun getScreen ()Ljava/lang/String; + public abstract fun getSession ()Lio/sentry/Session; + public abstract fun getSpan ()Lio/sentry/ISpan; + public abstract fun getTags ()Ljava/util/Map; + public abstract fun getTransaction ()Lio/sentry/ITransaction; + public abstract fun getTransactionName ()Ljava/lang/String; + public abstract fun getUser ()Lio/sentry/protocol/User; + public abstract fun removeContexts (Ljava/lang/String;)V + public abstract fun removeExtra (Ljava/lang/String;)V + public abstract fun removeTag (Ljava/lang/String;)V + public abstract fun setContexts (Ljava/lang/String;Ljava/lang/Boolean;)V + public abstract fun setContexts (Ljava/lang/String;Ljava/lang/Character;)V + public abstract fun setContexts (Ljava/lang/String;Ljava/lang/Number;)V + public abstract fun setContexts (Ljava/lang/String;Ljava/lang/Object;)V + public abstract fun setContexts (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun setContexts (Ljava/lang/String;Ljava/util/Collection;)V + public abstract fun setContexts (Ljava/lang/String;[Ljava/lang/Object;)V + public abstract fun setExtra (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun setFingerprint (Ljava/util/List;)V + public abstract fun setLevel (Lio/sentry/SentryLevel;)V + public abstract fun setPropagationContext (Lio/sentry/PropagationContext;)V + public abstract fun setRequest (Lio/sentry/protocol/Request;)V + public abstract fun setScreen (Ljava/lang/String;)V + public abstract fun setTag (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun setTransaction (Lio/sentry/ITransaction;)V + public abstract fun setTransaction (Ljava/lang/String;)V + public abstract fun setUser (Lio/sentry/protocol/User;)V + public abstract fun startSession ()Lio/sentry/Scope$SessionPair; + public abstract fun withPropagationContext (Lio/sentry/Scope$IWithPropagationContext;)Lio/sentry/PropagationContext; + public abstract fun withSession (Lio/sentry/Scope$IWithSession;)Lio/sentry/Session; + public abstract fun withTransaction (Lio/sentry/Scope$IWithTransaction;)V +} + public abstract interface class io/sentry/IScopeObserver { public abstract fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public abstract fun removeExtra (Ljava/lang/String;)V @@ -639,26 +693,26 @@ public abstract interface class io/sentry/IScopeObserver { } public abstract interface class io/sentry/ISentryClient { - public abstract fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/Scope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public abstract fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;)Lio/sentry/protocol/SentryId; public abstract fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; - public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Scope;)Lio/sentry/protocol/SentryId; - public abstract fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Scope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; + public abstract fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; - public fun captureException (Ljava/lang/Throwable;Lio/sentry/Scope;)Lio/sentry/protocol/SentryId; - public fun captureException (Ljava/lang/Throwable;Lio/sentry/Scope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public fun captureException (Ljava/lang/Throwable;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; + public fun captureException (Ljava/lang/Throwable;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; - public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/Scope;)Lio/sentry/protocol/SentryId; + public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;)V public abstract fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;)Lio/sentry/protocol/SentryId; - public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Scope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;)Lio/sentry/protocol/SentryId; - public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Scope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; - public abstract fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Scope;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; + public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public abstract fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/IScope;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public abstract fun captureUserFeedback (Lio/sentry/UserFeedback;)V public abstract fun close ()V public abstract fun flush (J)V @@ -1091,6 +1145,62 @@ public final class io/sentry/NoOpLogger : io/sentry/ILogger { public fun log (Lio/sentry/SentryLevel;Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V } +public final class io/sentry/NoOpScope : io/sentry/IScope { + public fun addAttachment (Lio/sentry/Attachment;)V + public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V + public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V + public fun addEventProcessor (Lio/sentry/EventProcessor;)V + public fun clear ()V + public fun clearAttachments ()V + public fun clearBreadcrumbs ()V + public fun clearTransaction ()V + public fun clone ()Lio/sentry/IScope; + public synthetic fun clone ()Ljava/lang/Object; + public fun endSession ()Lio/sentry/Session; + public fun getAttachments ()Ljava/util/List; + public fun getBreadcrumbs ()Ljava/util/Queue; + public fun getContexts ()Lio/sentry/protocol/Contexts; + public fun getEventProcessors ()Ljava/util/List; + public fun getExtras ()Ljava/util/Map; + public fun getFingerprint ()Ljava/util/List; + public static fun getInstance ()Lio/sentry/NoOpScope; + public fun getLevel ()Lio/sentry/SentryLevel; + public fun getOptions ()Lio/sentry/SentryOptions; + public fun getPropagationContext ()Lio/sentry/PropagationContext; + public fun getRequest ()Lio/sentry/protocol/Request; + public fun getScreen ()Ljava/lang/String; + public fun getSession ()Lio/sentry/Session; + public fun getSpan ()Lio/sentry/ISpan; + public fun getTags ()Ljava/util/Map; + public fun getTransaction ()Lio/sentry/ITransaction; + public fun getTransactionName ()Ljava/lang/String; + public fun getUser ()Lio/sentry/protocol/User; + public fun removeContexts (Ljava/lang/String;)V + public fun removeExtra (Ljava/lang/String;)V + public fun removeTag (Ljava/lang/String;)V + public fun setContexts (Ljava/lang/String;Ljava/lang/Boolean;)V + public fun setContexts (Ljava/lang/String;Ljava/lang/Character;)V + public fun setContexts (Ljava/lang/String;Ljava/lang/Number;)V + public fun setContexts (Ljava/lang/String;Ljava/lang/Object;)V + public fun setContexts (Ljava/lang/String;Ljava/lang/String;)V + public fun setContexts (Ljava/lang/String;Ljava/util/Collection;)V + public fun setContexts (Ljava/lang/String;[Ljava/lang/Object;)V + public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V + public fun setFingerprint (Ljava/util/List;)V + public fun setLevel (Lio/sentry/SentryLevel;)V + public fun setPropagationContext (Lio/sentry/PropagationContext;)V + public fun setRequest (Lio/sentry/protocol/Request;)V + public fun setScreen (Ljava/lang/String;)V + public fun setTag (Ljava/lang/String;Ljava/lang/String;)V + public fun setTransaction (Lio/sentry/ITransaction;)V + public fun setTransaction (Ljava/lang/String;)V + public fun setUser (Lio/sentry/protocol/User;)V + public fun startSession ()Lio/sentry/Scope$SessionPair; + public fun withPropagationContext (Lio/sentry/Scope$IWithPropagationContext;)Lio/sentry/PropagationContext; + public fun withSession (Lio/sentry/Scope$IWithSession;)Lio/sentry/Session; + public fun withTransaction (Lio/sentry/Scope$IWithTransaction;)V +} + public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V @@ -1396,8 +1506,7 @@ public final class io/sentry/SamplingContext { public fun getTransactionContext ()Lio/sentry/TransactionContext; } -public final class io/sentry/Scope { - public fun (Lio/sentry/Scope;)V +public final class io/sentry/Scope : io/sentry/IScope { public fun (Lio/sentry/SentryOptions;)V public fun addAttachment (Lio/sentry/Attachment;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V @@ -1407,11 +1516,17 @@ public final class io/sentry/Scope { public fun clearAttachments ()V public fun clearBreadcrumbs ()V public fun clearTransaction ()V + public fun clone ()Lio/sentry/IScope; + public synthetic fun clone ()Ljava/lang/Object; + public fun endSession ()Lio/sentry/Session; + public fun getAttachments ()Ljava/util/List; public fun getBreadcrumbs ()Ljava/util/Queue; public fun getContexts ()Lio/sentry/protocol/Contexts; + public fun getEventProcessors ()Ljava/util/List; public fun getExtras ()Ljava/util/Map; public fun getFingerprint ()Ljava/util/List; public fun getLevel ()Lio/sentry/SentryLevel; + public fun getOptions ()Lio/sentry/SentryOptions; public fun getPropagationContext ()Lio/sentry/PropagationContext; public fun getRequest ()Lio/sentry/protocol/Request; public fun getScreen ()Ljava/lang/String; @@ -1441,7 +1556,9 @@ public final class io/sentry/Scope { public fun setTransaction (Lio/sentry/ITransaction;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startSession ()Lio/sentry/Scope$SessionPair; public fun withPropagationContext (Lio/sentry/Scope$IWithPropagationContext;)Lio/sentry/PropagationContext; + public fun withSession (Lio/sentry/Scope$IWithSession;)Lio/sentry/Session; public fun withTransaction (Lio/sentry/Scope$IWithTransaction;)V } @@ -1454,7 +1571,7 @@ public abstract interface class io/sentry/Scope$IWithTransaction { } public abstract interface class io/sentry/ScopeCallback { - public abstract fun run (Lio/sentry/Scope;)V + public abstract fun run (Lio/sentry/IScope;)V } public abstract class io/sentry/ScopeObserverAdapter : io/sentry/IScopeObserver { @@ -1654,11 +1771,11 @@ public final class io/sentry/SentryBaseEvent$Serializer { } public final class io/sentry/SentryClient : io/sentry/ISentryClient { - public fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/Scope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; - public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Scope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V - public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Scope;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; + public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/IScope;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V public fun close ()V public fun flush (J)V @@ -4583,7 +4700,7 @@ public final class io/sentry/util/StringUtils { public final class io/sentry/util/TracingUtils { public fun ()V - public static fun maybeUpdateBaggage (Lio/sentry/Scope;Lio/sentry/SentryOptions;)Lio/sentry/PropagationContext; + public static fun maybeUpdateBaggage (Lio/sentry/IScope;Lio/sentry/SentryOptions;)Lio/sentry/PropagationContext; public static fun startNewTrace (Lio/sentry/IHub;)V public static fun trace (Lio/sentry/IHub;Ljava/util/List;Lio/sentry/ISpan;)Lio/sentry/util/TracingUtils$TracingHeaders; public static fun traceIfAllowed (Lio/sentry/IHub;Ljava/lang/String;Ljava/util/List;Lio/sentry/ISpan;)Lio/sentry/util/TracingUtils$TracingHeaders; diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index 945183fa83..8e19fceaf8 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -389,7 +389,8 @@ public void setValuesFromTransaction( } @ApiStatus.Internal - public void setValuesFromScope(final @NotNull Scope scope, final @NotNull SentryOptions options) { + public void setValuesFromScope( + final @NotNull IScope scope, final @NotNull SentryOptions options) { final @NotNull PropagationContext propagationContext = scope.getPropagationContext(); final @Nullable User user = scope.getUser(); setTraceId(propagationContext.getTraceId().toString()); diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index a85b9d4f77..dcd01dec73 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -68,7 +68,7 @@ private static void validateOptions(final @NotNull SentryOptions options) { private static StackItem createRootStackItem(final @NotNull SentryOptions options) { validateOptions(options); - final Scope scope = new Scope(options); + final IScope scope = new Scope(options); final ISentryClient client = new SentryClient(options); return new StackItem(options, client, scope); } @@ -109,7 +109,7 @@ public boolean isEnabled() { assignTraceContext(event); final StackItem item = stack.peek(); - final Scope scope = buildLocalScope(item.getScope(), scopeCallback); + final IScope scope = buildLocalScope(item.getScope(), scopeCallback); sentryId = item.getClient().captureEvent(event, scope, hint); this.lastEventId = sentryId; @@ -154,7 +154,7 @@ public boolean isEnabled() { try { final StackItem item = stack.peek(); - final Scope scope = buildLocalScope(item.getScope(), scopeCallback); + final IScope scope = buildLocalScope(item.getScope(), scopeCallback); sentryId = item.getClient().captureMessage(message, level, scope); } catch (Throwable e) { @@ -226,7 +226,7 @@ public boolean isEnabled() { final SentryEvent event = new SentryEvent(throwable); assignTraceContext(event); - final Scope scope = buildLocalScope(item.getScope(), scopeCallback); + final IScope scope = buildLocalScope(item.getScope(), scopeCallback); sentryId = item.getClient().captureEvent(event, scope, hint); } catch (Throwable e) { @@ -515,8 +515,7 @@ public void pushScope() { .log(SentryLevel.WARNING, "Instance is disabled and this 'pushScope' call is a no-op."); } else { final StackItem item = stack.peek(); - final StackItem newItem = - new StackItem(options, item.getClient(), new Scope(item.getScope())); + final StackItem newItem = new StackItem(options, item.getClient(), item.getScope().clone()); stack.push(newItem); } } @@ -553,9 +552,12 @@ public void popScope() { @Override public void withScope(final @NotNull ScopeCallback callback) { if (!isEnabled()) { - options - .getLogger() - .log(SentryLevel.WARNING, "Instance is disabled and this 'withScope' call is a no-op."); + try { + callback.run(NoOpScope.getInstance()); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error in the 'withScope' callback.", e); + } + } else { pushScope(); try { @@ -811,11 +813,11 @@ SpanContext getSpanContext(final @NotNull Throwable throwable) { return null; } - private Scope buildLocalScope( - final @NotNull Scope scope, final @Nullable ScopeCallback callback) { + private IScope buildLocalScope( + final @NotNull IScope scope, final @Nullable ScopeCallback callback) { if (callback != null) { try { - final Scope localScope = new Scope(scope); + final IScope localScope = scope.clone(); callback.run(localScope); return localScope; } catch (Throwable t) { diff --git a/sentry/src/main/java/io/sentry/IScope.java b/sentry/src/main/java/io/sentry/IScope.java new file mode 100644 index 0000000000..3842fb2c3a --- /dev/null +++ b/sentry/src/main/java/io/sentry/IScope.java @@ -0,0 +1,373 @@ +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.List; +import java.util.Map; +import java.util.Queue; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface IScope { + @Nullable + SentryLevel getLevel(); + + /** + * Sets the Scope's SentryLevel Level from scope exceptionally take precedence over the event + * + * @param level the SentryLevel + */ + void setLevel(final @Nullable SentryLevel level); + + /** + * Returns the Scope's transaction name. + * + * @return the transaction + */ + @Nullable + String getTransactionName(); + + /** + * Sets the Scope's transaction. + * + * @param transaction the transaction + */ + void setTransaction(final @NotNull String transaction); + + /** + * Returns current active Span or Transaction. + * + * @return current active Span or Transaction or null if transaction has not been set. + */ + @Nullable + ISpan getSpan(); + + /** + * Sets the current active transaction + * + * @param transaction the transaction + */ + void setTransaction(final @Nullable ITransaction transaction); + + /** + * Returns the Scope's user + * + * @return the user + */ + @Nullable + User getUser(); + + /** + * Sets the Scope's user + * + * @param user the user + */ + void setUser(final @Nullable User user); + + /** + * Returns the Scope's current screen, previously set by {@link IScope#setScreen(String)} + * + * @return the name of the screen + */ + @ApiStatus.Internal + @Nullable + String getScreen(); + + /** + * Sets the Scope's current screen + * + * @param screen the name of the screen + */ + @ApiStatus.Internal + void setScreen(final @Nullable String screen); + + /** + * Returns the Scope's request + * + * @return the request + */ + @Nullable + Request getRequest(); + + /** + * Sets the Scope's request + * + * @param request the request + */ + void setRequest(final @Nullable Request request); + + /** + * Returns the Scope's fingerprint list + * + * @return the fingerprint list + */ + @ApiStatus.Internal + @NotNull + List getFingerprint(); + + /** + * Sets the Scope's fingerprint list + * + * @param fingerprint the fingerprint list + */ + void setFingerprint(final @NotNull List fingerprint); + + /** + * Returns the Scope's breadcrumbs queue + * + * @return the breadcrumbs queue + */ + @ApiStatus.Internal + @NotNull + Queue getBreadcrumbs(); + + /** + * Adds a breadcrumb to the breadcrumbs queue. It also executes the BeforeBreadcrumb callback if + * set + * + * @param breadcrumb the breadcrumb + * @param hint the hint + */ + void addBreadcrumb(@NotNull Breadcrumb breadcrumb, @Nullable Hint hint); + + /** + * Adds a breadcrumb to the breadcrumbs queue It also executes the BeforeBreadcrumb callback if + * set + * + * @param breadcrumb the breadcrumb + */ + void addBreadcrumb(final @NotNull Breadcrumb breadcrumb); + + /** Clear all the breadcrumbs */ + void clearBreadcrumbs(); + + /** Clears the transaction. */ + void clearTransaction(); + + /** + * Returns active transaction or null if there is no active transaction. + * + * @return the transaction + */ + @Nullable + ITransaction getTransaction(); + + /** Resets the Scope to its default state */ + void clear(); + + /** + * Returns the Scope's tags + * + * @return the tags map + */ + @ApiStatus.Internal + @SuppressWarnings("NullAway") // tags are never null + @NotNull + Map getTags(); + + /** + * Sets a tag to Scope's tags + * + * @param key the key + * @param value the value + */ + void setTag(final @NotNull String key, final @NotNull String value); + + /** + * Removes a tag from the Scope's tags + * + * @param key the key + */ + void removeTag(final @NotNull String key); + + /** + * Returns the Scope's extra map + * + * @return the extra map + */ + @ApiStatus.Internal + @NotNull + Map getExtras(); + + /** + * Sets an extra to the Scope's extra map + * + * @param key the key + * @param value the value + */ + void setExtra(final @NotNull String key, final @NotNull String value); + + /** + * Removes an extra from the Scope's extras + * + * @param key the key + */ + void removeExtra(final @NotNull String key); + + /** + * Returns the Scope's contexts + * + * @return the contexts + */ + @NotNull + Contexts getContexts(); + + /** + * Sets the Scope's contexts + * + * @param key the context key + * @param value the context value + */ + void setContexts(final @NotNull String key, final @NotNull Object value); + + /** + * Sets the Scope's contexts + * + * @param key the context key + * @param value the context value + */ + void setContexts(final @NotNull String key, final @NotNull Boolean value); + + /** + * Sets the Scope's contexts + * + * @param key the context key + * @param value the context value + */ + void setContexts(final @NotNull String key, final @NotNull String value); + + /** + * Sets the Scope's contexts + * + * @param key the context key + * @param value the context value + */ + void setContexts(final @NotNull String key, final @NotNull Number value); + + /** + * Sets the Scope's contexts + * + * @param key the context key + * @param value the context value + */ + void setContexts(final @NotNull String key, final @NotNull Collection value); + + /** + * Sets the Scope's contexts + * + * @param key the context key + * @param value the context value + */ + void setContexts(final @NotNull String key, final @NotNull Object[] value); + + /** + * Sets the Scope's contexts + * + * @param key the context key + * @param value the context value + */ + void setContexts(final @NotNull String key, final @NotNull Character value); + + /** + * Removes a value from the Scope's contexts + * + * @param key the Key + */ + void removeContexts(final @NotNull String key); + + /** + * Returns the Scopes's attachments + * + * @return the attachments + */ + @NotNull + List getAttachments(); + + /** + * Adds an attachment to the Scope's list of attachments. The SDK adds the attachment to every + * event and transaction sent to Sentry. + * + * @param attachment The attachment to add to the Scope's list of attachments. + */ + void addAttachment(final @NotNull Attachment attachment); + + /** Clear all attachments. */ + void clearAttachments(); + + /** + * Returns the Scope's event processors + * + * @return the event processors list + */ + @NotNull + List getEventProcessors(); + + /** + * Adds an event processor to the Scope's event processors list + * + * @param eventProcessor the event processor + */ + void addEventProcessor(final @NotNull EventProcessor eventProcessor); + + /** + * Callback to do atomic operations on session + * + * @param sessionCallback the IWithSession callback + * @return a clone of the Session after executing the callback and mutating the session + */ + @Nullable + Session withSession(final @NotNull Scope.IWithSession sessionCallback); + + /** + * Returns a previous session (now closed) bound to this scope together with the newly created one + * + * @return the SessionPair with the previous closed session if exists and the current session + */ + @Nullable + Scope.SessionPair startSession(); + + /** + * Ends a session, unbinds it from the scope and returns it. + * + * @return the previous session + */ + @Nullable + Session endSession(); + + /** + * Mutates the current transaction atomically + * + * @param callback the IWithTransaction callback + */ + @ApiStatus.Internal + void withTransaction(final @NotNull Scope.IWithTransaction callback); + + @NotNull + SentryOptions getOptions(); + + @ApiStatus.Internal + @Nullable + Session getSession(); + + @ApiStatus.Internal + void setPropagationContext(final @NotNull PropagationContext propagationContext); + + @ApiStatus.Internal + @NotNull + PropagationContext getPropagationContext(); + + @ApiStatus.Internal + @NotNull + PropagationContext withPropagationContext(final @NotNull Scope.IWithPropagationContext callback); + + /** + * Clones the Scope + * + * @return the cloned Scope + */ + @NotNull + IScope clone(); +} diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 561f6ea6a4..f6387ea6b4 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -27,7 +27,7 @@ public interface ISentryClient { * @return The Id (SentryId object) of the event. */ @NotNull - SentryId captureEvent(@NotNull SentryEvent event, @Nullable Scope scope, @Nullable Hint hint); + SentryId captureEvent(@NotNull SentryEvent event, @Nullable IScope scope, @Nullable Hint hint); /** Flushes out the queue for up to timeout seconds and disable the client. */ void close(); @@ -56,7 +56,7 @@ public interface ISentryClient { * @param scope An optional scope to be applied to the event. * @return The Id (SentryId object) of the event */ - default @NotNull SentryId captureEvent(@NotNull SentryEvent event, @Nullable Scope scope) { + default @NotNull SentryId captureEvent(@NotNull SentryEvent event, @Nullable IScope scope) { return captureEvent(event, scope, null); } @@ -80,7 +80,7 @@ public interface ISentryClient { * @return The Id (SentryId object) of the event */ default @NotNull SentryId captureMessage( - @NotNull String message, @NotNull SentryLevel level, @Nullable Scope scope) { + @NotNull String message, @NotNull SentryLevel level, @Nullable IScope scope) { SentryEvent event = new SentryEvent(); Message sentryMessage = new Message(); sentryMessage.setFormatted(message); @@ -120,7 +120,7 @@ public interface ISentryClient { * @return The Id (SentryId object) of the event */ default @NotNull SentryId captureException( - @NotNull Throwable throwable, @Nullable Scope scope, @Nullable Hint hint) { + @NotNull Throwable throwable, @Nullable IScope scope, @Nullable Hint hint) { SentryEvent event = new SentryEvent(throwable); return captureEvent(event, scope, hint); } @@ -143,7 +143,7 @@ public interface ISentryClient { * @param scope An optional scope to be applied to the event. * @return The Id (SentryId object) of the event */ - default @NotNull SentryId captureException(@NotNull Throwable throwable, @Nullable Scope scope) { + default @NotNull SentryId captureException(@NotNull Throwable throwable, @Nullable IScope scope) { return captureException(throwable, scope, null); } @@ -203,7 +203,7 @@ default void captureSession(@NotNull Session session) { */ @NotNull default SentryId captureTransaction( - @NotNull SentryTransaction transaction, @Nullable Scope scope, @Nullable Hint hint) { + @NotNull SentryTransaction transaction, @Nullable IScope scope, @Nullable Hint hint) { return captureTransaction(transaction, null, scope, hint); } @@ -219,7 +219,7 @@ default SentryId captureTransaction( default SentryId captureTransaction( @NotNull SentryTransaction transaction, @Nullable TraceContext traceContext, - @Nullable Scope scope, + @Nullable IScope scope, @Nullable Hint hint) { return captureTransaction(transaction, traceContext, scope, hint, null); } @@ -239,7 +239,7 @@ default SentryId captureTransaction( SentryId captureTransaction( @NotNull SentryTransaction transaction, @Nullable TraceContext traceContext, - @Nullable Scope scope, + @Nullable IScope scope, @Nullable Hint hint, @Nullable ProfilingTraceData profilingTraceData); @@ -268,7 +268,7 @@ SentryId captureTransaction( @NotNull @ApiStatus.Experimental - SentryId captureCheckIn(@NotNull CheckIn checkIn, @Nullable Scope scope, @Nullable Hint hint); + SentryId captureCheckIn(@NotNull CheckIn checkIn, @Nullable IScope scope, @Nullable Hint hint); @ApiStatus.Internal @Nullable diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index d5ecf6c4f0..e4c93a10ec 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -121,7 +121,9 @@ public void pushScope() {} public void popScope() {} @Override - public void withScope(@NotNull ScopeCallback callback) {} + public void withScope(@NotNull ScopeCallback callback) { + callback.run(NoOpScope.getInstance()); + } @Override public void configureScope(@NotNull ScopeCallback callback) {} diff --git a/sentry/src/main/java/io/sentry/NoOpScope.java b/sentry/src/main/java/io/sentry/NoOpScope.java new file mode 100644 index 0000000000..c756fb49a3 --- /dev/null +++ b/sentry/src/main/java/io/sentry/NoOpScope.java @@ -0,0 +1,248 @@ +package io.sentry; + +import io.sentry.protocol.Contexts; +import io.sentry.protocol.Request; +import io.sentry.protocol.User; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class NoOpScope implements IScope { + + private static final NoOpScope instance = new NoOpScope(); + + private final @NotNull SentryOptions emptyOptions = SentryOptions.empty(); + + private NoOpScope() {} + + public static NoOpScope getInstance() { + return instance; + } + + @Override + public @Nullable SentryLevel getLevel() { + return null; + } + + @Override + public void setLevel(@Nullable SentryLevel level) {} + + @Override + public @Nullable String getTransactionName() { + return null; + } + + @Override + public void setTransaction(@NotNull String transaction) {} + + @Override + public @Nullable ISpan getSpan() { + return null; + } + + @Override + public void setTransaction(@Nullable ITransaction transaction) {} + + @Override + public @Nullable User getUser() { + return null; + } + + @Override + public void setUser(@Nullable User user) {} + + @ApiStatus.Internal + @Override + public @Nullable String getScreen() { + return null; + } + + @ApiStatus.Internal + @Override + public void setScreen(@Nullable String screen) {} + + @Override + public @Nullable Request getRequest() { + return null; + } + + @Override + public void setRequest(@Nullable Request request) {} + + @ApiStatus.Internal + @Override + public @NotNull List getFingerprint() { + return new ArrayList<>(); + } + + @Override + public void setFingerprint(@NotNull List fingerprint) {} + + @ApiStatus.Internal + @Override + public @NotNull Queue getBreadcrumbs() { + return new ArrayDeque<>(); + } + + @Override + public void addBreadcrumb(@NotNull Breadcrumb breadcrumb, @Nullable Hint hint) {} + + @Override + public void addBreadcrumb(@NotNull Breadcrumb breadcrumb) {} + + @Override + public void clearBreadcrumbs() {} + + @Override + public void clearTransaction() {} + + @Override + public @Nullable ITransaction getTransaction() { + return null; + } + + @Override + public void clear() {} + + @ApiStatus.Internal + @Override + public @NotNull Map getTags() { + return new HashMap<>(); + } + + @Override + public void setTag(@NotNull String key, @NotNull String value) {} + + @Override + public void removeTag(@NotNull String key) {} + + @ApiStatus.Internal + @Override + public @NotNull Map getExtras() { + return new HashMap<>(); + } + + @Override + public void setExtra(@NotNull String key, @NotNull String value) {} + + @Override + public void removeExtra(@NotNull String key) {} + + @Override + public @NotNull Contexts getContexts() { + return new Contexts(); + } + + @Override + public void setContexts(@NotNull String key, @NotNull Object value) {} + + @Override + public void setContexts(@NotNull String key, @NotNull Boolean value) {} + + @Override + public void setContexts(@NotNull String key, @NotNull String value) {} + + @Override + public void setContexts(@NotNull String key, @NotNull Number value) {} + + @Override + public void setContexts(@NotNull String key, @NotNull Collection value) {} + + @Override + public void setContexts(@NotNull String key, @NotNull Object[] value) {} + + @Override + public void setContexts(@NotNull String key, @NotNull Character value) {} + + @Override + public void removeContexts(@NotNull String key) {} + + @ApiStatus.Internal + @Override + public @NotNull List getAttachments() { + return new ArrayList<>(); + } + + @Override + public void addAttachment(@NotNull Attachment attachment) {} + + @Override + public void clearAttachments() {} + + @ApiStatus.Internal + @Override + public @NotNull List getEventProcessors() { + return new ArrayList<>(); + } + + @Override + public void addEventProcessor(@NotNull EventProcessor eventProcessor) {} + + @ApiStatus.Internal + @Override + public @Nullable Session withSession(Scope.@NotNull IWithSession sessionCallback) { + return null; + } + + @ApiStatus.Internal + @Override + public @Nullable Scope.SessionPair startSession() { + return null; + } + + @ApiStatus.Internal + @Override + public @Nullable Session endSession() { + return null; + } + + @ApiStatus.Internal + @Override + public void withTransaction(Scope.@NotNull IWithTransaction callback) {} + + @ApiStatus.Internal + @Override + public @NotNull SentryOptions getOptions() { + return emptyOptions; + } + + @ApiStatus.Internal + @Override + public @Nullable Session getSession() { + return null; + } + + @ApiStatus.Internal + @Override + public void setPropagationContext(@NotNull PropagationContext propagationContext) {} + + @ApiStatus.Internal + @Override + public @NotNull PropagationContext getPropagationContext() { + return new PropagationContext(); + } + + @ApiStatus.Internal + @Override + public @NotNull PropagationContext withPropagationContext( + Scope.@NotNull IWithPropagationContext callback) { + return new PropagationContext(); + } + + /** + * Clones the Scope + * + * @return the cloned Scope + */ + @Override + public @NotNull IScope clone() { + return NoOpScope.getInstance(); + } +} diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 35de56aa67..a37d09eb89 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -24,7 +24,7 @@ public boolean isEnabled() { @Override public @NotNull SentryId captureEvent( - @NotNull SentryEvent event, @Nullable Scope scope, @Nullable Hint hint) { + @NotNull SentryEvent event, @Nullable IScope scope, @Nullable Hint hint) { return SentryId.EMPTY_ID; } @@ -49,7 +49,7 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint public @NotNull SentryId captureTransaction( @NotNull SentryTransaction transaction, @Nullable TraceContext traceContext, - @Nullable Scope scope, + @Nullable IScope scope, @Nullable Hint hint, @Nullable ProfilingTraceData profilingTraceData) { return SentryId.EMPTY_ID; @@ -58,7 +58,7 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint @Override @ApiStatus.Experimental public @NotNull SentryId captureCheckIn( - @NotNull CheckIn checkIn, @Nullable Scope scope, @Nullable Hint hint) { + @NotNull CheckIn checkIn, @Nullable IScope scope, @Nullable Hint hint) { return SentryId.EMPTY_ID; } diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 780b08721a..91c9fcd8cf 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -20,7 +20,7 @@ import org.jetbrains.annotations.Nullable; /** Scope data to be sent with the event */ -public final class Scope { +public final class Scope implements IScope { /** Scope's SentryLevel */ private @Nullable SentryLevel level; @@ -91,8 +91,7 @@ public Scope(final @NotNull SentryOptions options) { this.propagationContext = new PropagationContext(); } - @ApiStatus.Internal - public Scope(final @NotNull Scope scope) { + private Scope(final @NotNull Scope scope) { this.transaction = scope.transaction; this.transactionName = scope.transactionName; this.session = scope.session; @@ -155,6 +154,7 @@ public Scope(final @NotNull Scope scope) { * * @return the SentryLevel */ + @Override public @Nullable SentryLevel getLevel() { return level; } @@ -164,6 +164,7 @@ public Scope(final @NotNull Scope scope) { * * @param level the SentryLevel */ + @Override public void setLevel(final @Nullable SentryLevel level) { this.level = level; @@ -177,6 +178,7 @@ public void setLevel(final @Nullable SentryLevel level) { * * @return the transaction */ + @Override public @Nullable String getTransactionName() { final ITransaction tx = this.transaction; return tx != null ? tx.getName() : transactionName; @@ -187,6 +189,7 @@ public void setLevel(final @Nullable SentryLevel level) { * * @param transaction the transaction */ + @Override public void setTransaction(final @NotNull String transaction) { if (transaction != null) { final ITransaction tx = this.transaction; @@ -209,6 +212,7 @@ public void setTransaction(final @NotNull String transaction) { * @return current active Span or Transaction or null if transaction has not been set. */ @Nullable + @Override public ISpan getSpan() { final ITransaction tx = transaction; if (tx != null) { @@ -226,6 +230,7 @@ public ISpan getSpan() { * * @param transaction the transaction */ + @Override public void setTransaction(final @Nullable ITransaction transaction) { synchronized (transactionLock) { this.transaction = transaction; @@ -247,6 +252,7 @@ public void setTransaction(final @Nullable ITransaction transaction) { * * @return the user */ + @Override public @Nullable User getUser() { return user; } @@ -256,6 +262,7 @@ public void setTransaction(final @Nullable ITransaction transaction) { * * @param user the user */ + @Override public void setUser(final @Nullable User user) { this.user = user; @@ -265,11 +272,12 @@ public void setUser(final @Nullable User user) { } /** - * Returns the Scope's current screen, previously set by {@link Scope#setScreen(String)} + * Returns the Scope's current screen, previously set by {@link IScope#setScreen(String)} * * @return the name of the screen */ @ApiStatus.Internal + @Override public @Nullable String getScreen() { return screen; } @@ -280,6 +288,7 @@ public void setUser(final @Nullable User user) { * @param screen the name of the screen */ @ApiStatus.Internal + @Override public void setScreen(final @Nullable String screen) { this.screen = screen; @@ -308,6 +317,7 @@ public void setScreen(final @Nullable String screen) { * * @return the request */ + @Override public @Nullable Request getRequest() { return request; } @@ -317,6 +327,7 @@ public void setScreen(final @Nullable String screen) { * * @param request the request */ + @Override public void setRequest(final @Nullable Request request) { this.request = request; @@ -332,6 +343,7 @@ public void setRequest(final @Nullable Request request) { */ @ApiStatus.Internal @NotNull + @Override public List getFingerprint() { return fingerprint; } @@ -341,6 +353,7 @@ public List getFingerprint() { * * @param fingerprint the fingerprint list */ + @Override public void setFingerprint(final @NotNull List fingerprint) { if (fingerprint == null) { return; @@ -359,6 +372,7 @@ public void setFingerprint(final @NotNull List fingerprint) { */ @ApiStatus.Internal @NotNull + @Override public Queue getBreadcrumbs() { return breadcrumbs; } @@ -399,6 +413,7 @@ public Queue getBreadcrumbs() { * @param breadcrumb the breadcrumb * @param hint the hint */ + @Override public void addBreadcrumb(@NotNull Breadcrumb breadcrumb, @Nullable Hint hint) { if (breadcrumb == null) { return; @@ -429,11 +444,13 @@ public void addBreadcrumb(@NotNull Breadcrumb breadcrumb, @Nullable Hint hint) { * * @param breadcrumb the breadcrumb */ + @Override public void addBreadcrumb(final @NotNull Breadcrumb breadcrumb) { addBreadcrumb(breadcrumb, null); } /** Clear all the breadcrumbs */ + @Override public void clearBreadcrumbs() { breadcrumbs.clear(); @@ -443,6 +460,7 @@ public void clearBreadcrumbs() { } /** Clears the transaction. */ + @Override public void clearTransaction() { synchronized (transactionLock) { transaction = null; @@ -461,11 +479,13 @@ public void clearTransaction() { * @return the transaction */ @Nullable + @Override public ITransaction getTransaction() { return this.transaction; } /** Resets the Scope to its default state */ + @Override public void clear() { level = null; user = null; @@ -487,6 +507,7 @@ public void clear() { */ @ApiStatus.Internal @SuppressWarnings("NullAway") // tags are never null + @Override public @NotNull Map getTags() { return CollectionUtils.newConcurrentHashMap(tags); } @@ -497,6 +518,7 @@ public void clear() { * @param key the key * @param value the value */ + @Override public void setTag(final @NotNull String key, final @NotNull String value) { this.tags.put(key, value); @@ -511,6 +533,7 @@ public void setTag(final @NotNull String key, final @NotNull String value) { * * @param key the key */ + @Override public void removeTag(final @NotNull String key) { this.tags.remove(key); @@ -527,6 +550,7 @@ public void removeTag(final @NotNull String key) { */ @ApiStatus.Internal @NotNull + @Override public Map getExtras() { return extra; } @@ -537,6 +561,7 @@ public Map getExtras() { * @param key the key * @param value the value */ + @Override public void setExtra(final @NotNull String key, final @NotNull String value) { this.extra.put(key, value); @@ -551,6 +576,7 @@ public void setExtra(final @NotNull String key, final @NotNull String value) { * * @param key the key */ + @Override public void removeExtra(final @NotNull String key) { this.extra.remove(key); @@ -565,6 +591,7 @@ public void removeExtra(final @NotNull String key) { * * @return the contexts */ + @Override public @NotNull Contexts getContexts() { return contexts; } @@ -575,6 +602,7 @@ public void removeExtra(final @NotNull String key) { * @param key the context key * @param value the context value */ + @Override public void setContexts(final @NotNull String key, final @NotNull Object value) { this.contexts.put(key, value); @@ -589,6 +617,7 @@ public void setContexts(final @NotNull String key, final @NotNull Object value) * @param key the context key * @param value the context value */ + @Override public void setContexts(final @NotNull String key, final @NotNull Boolean value) { final Map map = new HashMap<>(); map.put("value", value); @@ -601,6 +630,7 @@ public void setContexts(final @NotNull String key, final @NotNull Boolean value) * @param key the context key * @param value the context value */ + @Override public void setContexts(final @NotNull String key, final @NotNull String value) { final Map map = new HashMap<>(); map.put("value", value); @@ -613,6 +643,7 @@ public void setContexts(final @NotNull String key, final @NotNull String value) * @param key the context key * @param value the context value */ + @Override public void setContexts(final @NotNull String key, final @NotNull Number value) { final Map map = new HashMap<>(); map.put("value", value); @@ -625,6 +656,7 @@ public void setContexts(final @NotNull String key, final @NotNull Number value) * @param key the context key * @param value the context value */ + @Override public void setContexts(final @NotNull String key, final @NotNull Collection value) { final Map> map = new HashMap<>(); map.put("value", value); @@ -637,6 +669,7 @@ public void setContexts(final @NotNull String key, final @NotNull Collection * @param key the context key * @param value the context value */ + @Override public void setContexts(final @NotNull String key, final @NotNull Object[] value) { final Map map = new HashMap<>(); map.put("value", value); @@ -649,6 +682,7 @@ public void setContexts(final @NotNull String key, final @NotNull Object[] value * @param key the context key * @param value the context value */ + @Override public void setContexts(final @NotNull String key, final @NotNull Character value) { final Map map = new HashMap<>(); map.put("value", value); @@ -660,6 +694,7 @@ public void setContexts(final @NotNull String key, final @NotNull Character valu * * @param key the Key */ + @Override public void removeContexts(final @NotNull String key) { contexts.remove(key); } @@ -669,8 +704,10 @@ public void removeContexts(final @NotNull String key) { * * @return the attachments */ + @ApiStatus.Internal @NotNull - List getAttachments() { + @Override + public List getAttachments() { return new CopyOnWriteArrayList<>(attachments); } @@ -680,11 +717,13 @@ List getAttachments() { * * @param attachment The attachment to add to the Scope's list of attachments. */ + @Override public void addAttachment(final @NotNull Attachment attachment) { attachments.add(attachment); } /** Clear all attachments. */ + @Override public void clearAttachments() { attachments.clear(); } @@ -704,8 +743,10 @@ public void clearAttachments() { * * @return the event processors list */ + @ApiStatus.Internal @NotNull - List getEventProcessors() { + @Override + public List getEventProcessors() { return eventProcessors; } @@ -714,6 +755,7 @@ List getEventProcessors() { * * @param eventProcessor the event processor */ + @Override public void addEventProcessor(final @NotNull EventProcessor eventProcessor) { eventProcessors.add(eventProcessor); } @@ -724,8 +766,10 @@ public void addEventProcessor(final @NotNull EventProcessor eventProcessor) { * @param sessionCallback the IWithSession callback * @return a clone of the Session after executing the callback and mutating the session */ + @ApiStatus.Internal @Nullable - Session withSession(final @NotNull IWithSession sessionCallback) { + @Override + public Session withSession(final @NotNull IWithSession sessionCallback) { Session cloneSession = null; synchronized (sessionLock) { sessionCallback.accept(session); @@ -753,8 +797,10 @@ interface IWithSession { * * @return the SessionPair with the previous closed session if exists and the current session */ + @ApiStatus.Internal @Nullable - SessionPair startSession() { + @Override + public SessionPair startSession() { Session previousSession; SessionPair pair = null; synchronized (sessionLock) { @@ -826,8 +872,10 @@ public SessionPair(final @NotNull Session current, final @Nullable Session previ * * @return the previous session */ + @ApiStatus.Internal @Nullable - Session endSession() { + @Override + public Session endSession() { Session previousSession = null; synchronized (sessionLock) { if (session != null) { @@ -845,33 +893,40 @@ Session endSession() { * @param callback the IWithTransaction callback */ @ApiStatus.Internal + @Override public void withTransaction(final @NotNull IWithTransaction callback) { synchronized (transactionLock) { callback.accept(transaction); } } + @ApiStatus.Internal @NotNull - SentryOptions getOptions() { + @Override + public SentryOptions getOptions() { return options; } @ApiStatus.Internal + @Override public @Nullable Session getSession() { return session; } @ApiStatus.Internal + @Override public void setPropagationContext(final @NotNull PropagationContext propagationContext) { this.propagationContext = propagationContext; } @ApiStatus.Internal + @Override public @NotNull PropagationContext getPropagationContext() { return propagationContext; } @ApiStatus.Internal + @Override public @NotNull PropagationContext withPropagationContext( final @NotNull IWithPropagationContext callback) { synchronized (propagationContextLock) { @@ -880,6 +935,16 @@ public void setPropagationContext(final @NotNull PropagationContext propagationC } } + /** + * Clones the Scope + * + * @return the cloned Scope + */ + @Override + public @NotNull IScope clone() { + return new Scope(this); + } + /** The IWithTransaction callback */ @ApiStatus.Internal public interface IWithTransaction { diff --git a/sentry/src/main/java/io/sentry/ScopeCallback.java b/sentry/src/main/java/io/sentry/ScopeCallback.java index 344b2835c1..96d15c29f7 100644 --- a/sentry/src/main/java/io/sentry/ScopeCallback.java +++ b/sentry/src/main/java/io/sentry/ScopeCallback.java @@ -3,5 +3,5 @@ import org.jetbrains.annotations.NotNull; public interface ScopeCallback { - void run(@NotNull Scope scope); + void run(@NotNull IScope scope); } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 88fc6ecd51..f6b7fbf947 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -90,7 +90,7 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul @Override public @NotNull SentryId captureEvent( - @NotNull SentryEvent event, final @Nullable Scope scope, @Nullable Hint hint) { + @NotNull SentryEvent event, final @Nullable IScope scope, @Nullable Hint hint) { Objects.requireNonNull(event, "SentryEvent is required."); if (hint == null) { @@ -246,7 +246,7 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul return sentryId; } - private void addScopeAttachmentsToHint(@Nullable Scope scope, @NotNull Hint hint) { + private void addScopeAttachmentsToHint(@Nullable IScope scope, @NotNull Hint hint) { if (scope != null) { hint.addAttachments(scope.getAttachments()); } @@ -495,7 +495,7 @@ public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { @TestOnly @Nullable Session updateSessionData( - final @NotNull SentryEvent event, final @NotNull Hint hint, final @Nullable Scope scope) { + final @NotNull SentryEvent event, final @NotNull Hint hint, final @Nullable IScope scope) { Session clonedSession = null; if (HintUtils.shouldApplyScopeData(hint)) { @@ -599,7 +599,7 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint public @NotNull SentryId captureTransaction( @NotNull SentryTransaction transaction, @Nullable TraceContext traceContext, - final @Nullable Scope scope, + final @Nullable IScope scope, @Nullable Hint hint, final @Nullable ProfilingTraceData profilingTraceData) { Objects.requireNonNull(transaction, "Transaction is required."); @@ -681,7 +681,7 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint @Override @ApiStatus.Experimental public @NotNull SentryId captureCheckIn( - @NotNull CheckIn checkIn, final @Nullable Scope scope, @Nullable Hint hint) { + @NotNull CheckIn checkIn, final @Nullable IScope scope, @Nullable Hint hint) { if (hint == null) { hint = new Hint(); } @@ -758,7 +758,7 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint } private @Nullable SentryEvent applyScope( - @NotNull SentryEvent event, final @Nullable Scope scope, final @NotNull Hint hint) { + @NotNull SentryEvent event, final @Nullable IScope scope, final @NotNull Hint hint) { if (scope != null) { applyScope(event, scope); @@ -789,7 +789,7 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint return event; } - private @NotNull CheckIn applyScope(@NotNull CheckIn checkIn, final @Nullable Scope scope) { + private @NotNull CheckIn applyScope(@NotNull CheckIn checkIn, final @Nullable IScope scope) { if (scope != null) { // Set trace data from active span to connect events with transactions final ISpan span = scope.getSpan(); @@ -807,7 +807,7 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint } private @NotNull T applyScope( - final @NotNull T sentryBaseEvent, final @Nullable Scope scope) { + final @NotNull T sentryBaseEvent, final @Nullable IScope scope) { if (scope != null) { if (sentryBaseEvent.getRequest() == null) { sentryBaseEvent.setRequest(scope.getRequest()); diff --git a/sentry/src/main/java/io/sentry/Stack.java b/sentry/src/main/java/io/sentry/Stack.java index f73b72d7ef..adb43f8212 100644 --- a/sentry/src/main/java/io/sentry/Stack.java +++ b/sentry/src/main/java/io/sentry/Stack.java @@ -11,12 +11,12 @@ final class Stack { static final class StackItem { private final SentryOptions options; private volatile @NotNull ISentryClient client; - private volatile @NotNull Scope scope; + private volatile @NotNull IScope scope; StackItem( final @NotNull SentryOptions options, final @NotNull ISentryClient client, - final @NotNull Scope scope) { + final @NotNull IScope scope) { this.client = Objects.requireNonNull(client, "ISentryClient is required."); this.scope = Objects.requireNonNull(scope, "Scope is required."); this.options = Objects.requireNonNull(options, "Options is required"); @@ -25,7 +25,7 @@ static final class StackItem { StackItem(final @NotNull StackItem item) { options = item.options; client = item.client; - scope = new Scope(item.scope); + scope = item.scope.clone(); } public @NotNull ISentryClient getClient() { @@ -36,7 +36,7 @@ public void setClient(final @NotNull ISentryClient client) { this.client = client; } - public @NotNull Scope getScope() { + public @NotNull IScope getScope() { return scope; } diff --git a/sentry/src/main/java/io/sentry/util/TracingUtils.java b/sentry/src/main/java/io/sentry/util/TracingUtils.java index fbb8ccb775..2aeb613f2d 100644 --- a/sentry/src/main/java/io/sentry/util/TracingUtils.java +++ b/sentry/src/main/java/io/sentry/util/TracingUtils.java @@ -3,9 +3,9 @@ import io.sentry.Baggage; import io.sentry.BaggageHeader; import io.sentry.IHub; +import io.sentry.IScope; import io.sentry.ISpan; import io.sentry.PropagationContext; -import io.sentry.Scope; import io.sentry.SentryOptions; import io.sentry.SentryTraceHeader; import java.util.List; @@ -73,7 +73,7 @@ public static void startNewTrace(final @NotNull IHub hub) { } public static @NotNull PropagationContext maybeUpdateBaggage( - final @NotNull Scope scope, final @NotNull SentryOptions sentryOptions) { + final @NotNull IScope scope, final @NotNull SentryOptions sentryOptions) { return scope.withPropagationContext( propagationContext -> { @Nullable Baggage baggage = propagationContext.getBaggage(); diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index 44cffe2977..6e4ff587a1 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -91,12 +91,12 @@ class HubTest { options.dsn = "https://key@sentry.io/proj" options.setSerializer(mock()) val hub = Hub(options) - var firstScope: Scope? = null + var firstScope: IScope? = null hub.configureScope { firstScope = it it.setTag("hub", "a") } - var cloneScope: Scope? = null + var cloneScope: IScope? = null val clone = hub.clone() clone.configureScope { cloneScope = it @@ -419,7 +419,7 @@ class HubTest { @Test fun `when captureEvent is called with a ScopeCallback then subsequent calls to captureEvent send the unmodified Scope to the client`() { val (sut, mockClient) = getEnabledHub() - val argumentCaptor = argumentCaptor() + val argumentCaptor = argumentCaptor() sut.captureEvent(SentryEvent(), null) { it.setTag("test", "testValue") @@ -512,7 +512,7 @@ class HubTest { @Test fun `when captureMessage is called with a ScopeCallback then subsequent calls to captureMessage send the unmodified Scope to the client`() { val (sut, mockClient) = getEnabledHub() - val argumentCaptor = argumentCaptor() + val argumentCaptor = argumentCaptor() sut.captureMessage("testMessage") { it.setTag("test", "testValue") @@ -644,7 +644,7 @@ class HubTest { @Test fun `when captureException is called with a ScopeCallback then subsequent calls to captureException send the unmodified Scope to the client`() { val (sut, mockClient) = getEnabledHub() - val argumentCaptor = argumentCaptor() + val argumentCaptor = argumentCaptor() sut.captureException(Throwable(), null) { it.setTag("test", "testValue") @@ -787,14 +787,14 @@ class HubTest { //region withScope tests @Test - fun `when withScope is called on disabled client, do nothing`() { + fun `when withScope is called on disabled client, execute on NoOpScope`() { val (sut) = getEnabledHub() val scopeCallback = mock() sut.close() sut.withScope(scopeCallback) - verify(scopeCallback, never()).run(any()) + verify(scopeCallback).run(NoOpScope.getInstance()) } @Test @@ -885,7 +885,7 @@ class HubTest { @Test fun `when setLevel is called on disabled client, do nothing`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -898,7 +898,7 @@ class HubTest { @Test fun `when setLevel is called, level is set`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -912,7 +912,7 @@ class HubTest { @Test fun `when setTransaction is called on disabled client, do nothing`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -925,7 +925,7 @@ class HubTest { @Test fun `when setTransaction is called, and transaction is not set, transaction name is changed`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -937,7 +937,7 @@ class HubTest { @Test fun `when setTransaction is called, and transaction is set, transaction name is changed`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -988,7 +988,7 @@ class HubTest { @Test fun `when setUser is called on disabled client, do nothing`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1001,7 +1001,7 @@ class HubTest { @Test fun `when setUser is called, user is set`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1016,7 +1016,7 @@ class HubTest { @Test fun `when setFingerprint is called on disabled client, do nothing`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1030,7 +1030,7 @@ class HubTest { @Test fun `when setFingerprint is called with null parameter, do nothing`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1042,7 +1042,7 @@ class HubTest { @Test fun `when setFingerprint is called, fingerprint is set`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1057,7 +1057,7 @@ class HubTest { @Test fun `when clearBreadcrumbs is called on disabled client, do nothing`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1072,7 +1072,7 @@ class HubTest { @Test fun `when clearBreadcrumbs is called, clear breadcrumbs`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1088,7 +1088,7 @@ class HubTest { @Test fun `when setTag is called on disabled client, do nothing`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1101,7 +1101,7 @@ class HubTest { @Test fun `when setTag is called with null parameters, do nothing`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1113,7 +1113,7 @@ class HubTest { @Test fun `when setTag is called, tag is set`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1127,7 +1127,7 @@ class HubTest { @Test fun `when setExtra is called on disabled client, do nothing`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1140,7 +1140,7 @@ class HubTest { @Test fun `when setExtra is called with null parameters, do nothing`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1152,7 +1152,7 @@ class HubTest { @Test fun `when setExtra is called, extra is set`() { val hub = generateHub() - var scope: Scope? = null + var scope: IScope? = null hub.configureScope { scope = it } @@ -1601,7 +1601,7 @@ class HubTest { // we have to clone the scope, so its isEnabled returns true, but it's still built up from // the old scope preserving its data val clone = sut.clone() - var oldScope: Scope? = null + var oldScope: IScope? = null clone.configureScope { scope -> oldScope = scope } assertNull(oldScope!!.transaction) assertTrue(oldScope!!.breadcrumbs.isEmpty()) diff --git a/sentry/src/test/java/io/sentry/NoOpHubTest.kt b/sentry/src/test/java/io/sentry/NoOpHubTest.kt index c7b985e9fd..868ebaa81e 100644 --- a/sentry/src/test/java/io/sentry/NoOpHubTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpHubTest.kt @@ -2,6 +2,7 @@ package io.sentry import io.sentry.protocol.SentryId import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -94,4 +95,12 @@ class NoOpHubTest { fun `captureCheckIn returns empty id`() { assertEquals(SentryId.EMPTY_ID, sut.captureCheckIn(mock())) } + + @Test + fun `withScopeCallback is executed on NoOpScope`() { + val scopeCallback = mock() + + sut.withScope(scopeCallback) + verify(scopeCallback).run(NoOpScope.getInstance()) + } } diff --git a/sentry/src/test/java/io/sentry/NoOpScopeTest.kt b/sentry/src/test/java/io/sentry/NoOpScopeTest.kt new file mode 100644 index 0000000000..f735e369e3 --- /dev/null +++ b/sentry/src/test/java/io/sentry/NoOpScopeTest.kt @@ -0,0 +1,124 @@ +package io.sentry + +import io.sentry.Scope.IWithSession +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertSame + +class NoOpScopeTest { + private var sut: NoOpScope = NoOpScope.getInstance() + + @Test + fun `getLevel returns null`() { + assertNull(sut.level) + } + + @Test + fun `getTransactionName returns null`() { + assertNull(sut.transactionName) + } + + @Test + fun `getSpan returns null`() { + assertNull(sut.span) + } + + @Test + fun `getUser returns null`() { + assertNull(sut.user) + } + + @Test + fun `getScreen returns null`() { + assertNull(sut.screen) + } + + @Test + fun `getRequest returns null`() { + assertNull(sut.request) + } + + @Test + fun `getFingerprint returns empty list`() { + assertEquals(0, sut.fingerprint.size) + } + + @Test + fun `getBreadcrumbs returns empty queue`() { + assertEquals(0, sut.breadcrumbs.size) + } + + @Test + fun `getTransaction returns null`() { + assertNull(sut.transaction) + } + + @Test + fun `getTags returns empty map`() { + assertEquals(0, sut.tags.size) + } + + @Test + fun `getExtras returns empty map`() { + assertEquals(0, sut.extras.size) + } + + @Test + fun `getContexts returns empty contexts`() { + assertEquals(0, sut.contexts.size) + } + + @Test + fun `getAttachments returns empty list`() { + assertEquals(0, sut.attachments.size) + } + + @Test + fun `getEventProcessors returns empty list`() { + assertEquals(0, sut.eventProcessors.size) + } + + @Test + fun `withSession is NoOp`() { + val withSessionCallback = mock() + sut.withSession(withSessionCallback) + verify(withSessionCallback, never()).accept(any()) + } + + @Test + fun `startSession returns null`() { + assertNull(sut.startSession()) + } + + @Test + fun `endSession returns null`() { + assertNull(sut.endSession()) + } + + @Test + fun `withTransaction is NoOp`() { + val withTransactionCallback = mock() + sut.withTransaction(withTransactionCallback) + verify(withTransactionCallback, never()).accept(any()) + } + + @Test + fun `getSession returns null`() { + assertNull(sut.session) + } + + @Test + fun `withPropagationContext is NoOp`() { + val withPropagationContextCallback = mock() + sut.withPropagationContext(withPropagationContextCallback) + verify(withPropagationContextCallback, never()).accept(any()) + } + + @Test + fun `clone returns the same instance`() = assertSame(NoOpScope.getInstance(), sut.clone()) +} diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index a7043d1208..906c897c62 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -74,7 +74,7 @@ class ScopeTest { scope.setContexts("key", "value") scope.addAttachment(Attachment("file name")) - val clone = Scope(scope) + val clone = scope.clone() assertNotNull(clone) assertNotSame(scope, clone) @@ -124,7 +124,7 @@ class ScopeTest { scope.setContexts("contexts", "contexts") - val clone = Scope(scope) + val clone = scope.clone() assertEquals(SentryLevel.DEBUG, clone.level) @@ -183,7 +183,7 @@ class ScopeTest { val attachment = Attachment("path/log.txt") scope.addAttachment(attachment) - val clone = Scope(scope) + val clone = scope.clone() scope.level = SentryLevel.FATAL user.id = "456" @@ -255,7 +255,7 @@ class ScopeTest { // clone in the meantime while (scope.breadcrumbs.isNotEmpty()) { - Scope(scope) + scope.clone() } // expect no exception to be thrown ¯\_(ツ)_/¯ @@ -886,7 +886,7 @@ class ScopeTest { scope.clear() assertTrue(scope.attachments is CopyOnWriteArrayList) - val cloned = Scope(scope) + val cloned = scope.clone() assertTrue(cloned.attachments is CopyOnWriteArrayList) } diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 8bdb7dd850..8fab30790f 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -239,7 +239,7 @@ class SentryClientTest { parameterTypes = arrayOf( String::class.java, SentryLevel::class.java, - Scope::class.java + IScope::class.java ), actual, null, @@ -2246,7 +2246,7 @@ class SentryClientTest { whenever(transaction.spanContext).thenReturn(spanContext) // scope - val scope = mock() + val scope = mock() whenever(scope.transaction).thenReturn(transaction) whenever(scope.breadcrumbs).thenReturn(LinkedList()) whenever(scope.extras).thenReturn(emptyMap()) @@ -2323,7 +2323,7 @@ class SentryClientTest { whenever(transaction.spanContext).thenReturn(spanContext) // scope - val scope = mock() + val scope = mock() whenever(scope.transaction).thenReturn(transaction) whenever(scope.breadcrumbs).thenReturn(LinkedList()) whenever(scope.extras).thenReturn(emptyMap()) @@ -2354,7 +2354,7 @@ class SentryClientTest { val sut = fixture.getSut() // scope - val scope = mock() + val scope = mock() whenever(scope.breadcrumbs).thenReturn(LinkedList()) whenever(scope.extras).thenReturn(emptyMap()) whenever(scope.contexts).thenReturn(Contexts()) @@ -2387,7 +2387,7 @@ class SentryClientTest { whenever(transaction.spanContext).thenReturn(spanContext) // scope - val scope = mock() + val scope = mock() whenever(scope.transaction).thenReturn(transaction) whenever(scope.breadcrumbs).thenReturn(LinkedList()) whenever(scope.extras).thenReturn(emptyMap()) @@ -2422,7 +2422,7 @@ class SentryClientTest { val sut = fixture.getSut() // scope - val scope = mock() + val scope = mock() whenever(scope.breadcrumbs).thenReturn(LinkedList()) whenever(scope.extras).thenReturn(emptyMap()) whenever(scope.contexts).thenReturn(Contexts()) @@ -2456,7 +2456,7 @@ class SentryClientTest { whenever(transaction.traceContext()).thenReturn(transactionTraceContext) // scope - val scope = mock() + val scope = mock() whenever(scope.transaction).thenReturn(transaction) whenever(scope.breadcrumbs).thenReturn(LinkedList()) whenever(scope.extras).thenReturn(emptyMap()) @@ -2480,7 +2480,7 @@ class SentryClientTest { ) } - private fun givenScopeWithStartedSession(errored: Boolean = false, crashed: Boolean = false): Scope { + private fun givenScopeWithStartedSession(errored: Boolean = false, crashed: Boolean = false): IScope { val scope = createScope(fixture.sentryOptions) scope.startSession() @@ -2511,19 +2511,19 @@ class SentryClientTest { assertEquals(transactionCount, envelopeItemTypes.count { it == SentryItemType.Transaction }) } - private fun thenSessionIsStillOK(scope: Scope) { + private fun thenSessionIsStillOK(scope: IScope) { val sessionAfterCapture = scope.withSession { }!! assertEquals(0, sessionAfterCapture.errorCount()) assertEquals(Session.State.Ok, sessionAfterCapture.status) } - private fun thenSessionIsErrored(scope: Scope) { + private fun thenSessionIsErrored(scope: IScope) { val sessionAfterCapture = scope.withSession { }!! assertTrue(sessionAfterCapture.errorCount() > 0) assertEquals(Session.State.Ok, sessionAfterCapture.status) } - private fun thenSessionIsCrashed(scope: Scope) { + private fun thenSessionIsCrashed(scope: IScope) { val sessionAfterCapture = scope.withSession { }!! assertTrue(sessionAfterCapture.errorCount() > 0) assertEquals(Session.State.Crashed, sessionAfterCapture.status) @@ -2538,7 +2538,7 @@ class SentryClientTest { } } - private fun createScope(options: SentryOptions = SentryOptions()): Scope { + private fun createScope(options: SentryOptions = SentryOptions()): IScope { return Scope(options).apply { addBreadcrumb( Breadcrumb().apply { @@ -2558,7 +2558,7 @@ class SentryClientTest { } } - private fun createScopeWithAttachments(): Scope { + private fun createScopeWithAttachments(): IScope { return createScope().apply { addAttachment(fixture.attachment) addAttachment(fixture.attachment) diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 099109505a..65cf88d31b 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -839,6 +839,28 @@ class SentryTest { assertIs(sentryOptions?.modulesLoader) } + @Test + fun `if Sentry is disabled through options with scope callback is executed`() { + Sentry.init { + it.isEnabled = false + } + + val scopeCallback = mock() + + Sentry.withScope(scopeCallback) + + verify(scopeCallback).run(any()) + } + + @Test + fun `if Sentry is not initialized with scope callback is executed`() { + val scopeCallback = mock() + + Sentry.withScope(scopeCallback) + + verify(scopeCallback).run(any()) + } + @Test fun `getSpan calls hub getSpan`() { val hub = mock() diff --git a/sentry/src/test/java/io/sentry/StackTest.kt b/sentry/src/test/java/io/sentry/StackTest.kt index 06a7a43bec..13089ab6a4 100644 --- a/sentry/src/test/java/io/sentry/StackTest.kt +++ b/sentry/src/test/java/io/sentry/StackTest.kt @@ -20,7 +20,7 @@ class StackTest { return Stack(options.logger, rootItem) } - fun createStackItem(scope: Scope = Scope(options)) = + fun createStackItem(scope: IScope = Scope(options)) = StackItem(this.options, this.client, scope) } From 9f185cbc0c2da387b060f05b9b60db2f74003cc8 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 28 Nov 2023 23:48:49 +0100 Subject: [PATCH 42/55] Mention startTransaction overloads removal --- CHANGELOG.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cf9bb5bf7..06d9197ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ Version 7 of the Sentry Android/Java SDK brings a variety of features and fixes. This SDK version is compatible with a self-hosted version of Sentry `22.12.0` or higher. If you are using an older version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka onpremise), you will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/). If you're using `sentry.io` no action is required. -## Sentry Integrations Version Compatibility +## Sentry Integrations Version Compatibility (Android) Make sure to align _all_ 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) @@ -36,7 +36,17 @@ Similarly, if you have a Sentry SDK (e.g. `sentry-android-core`) dependency on o - Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) - Apollo v2 BeforeSpanCallback now allows returning null ([#2890](https://github.com/getsentry/sentry-java/pull/2890)) - `SentryOkHttpUtils` was removed from public API as it's been exposed by mistake ([#3005](https://github.com/getsentry/sentry-java/pull/3005)) -- `IScope` and `NoOpScope` were introduced. ([#3066](https://github.com/getsentry/sentry-java/pull/3066)) +- `Scope` now implements the `IScope` interface, therefore some methods like `ScopeCallback.run` accept `IScope` now ([#3066](https://github.com/getsentry/sentry-java/pull/3066)) +- Cleanup `startTransaction` overloads ([#2964](https://github.com/getsentry/sentry-java/pull/2964)) + - We have reduced the number of overloads by allowing to pass in a `TransactionOptions` object instead of having separate parameters for certain options + - `TransactionOptions` has defaults set and can be customized, for example: + +```kotlin +// old +val transaction = Sentry.startTransaction("name", "op", bindToScope = true) +// new +val transaction = Sentry.startTransaction("name", "op", TransactionOptions().apply { isBindToScope = true }) +``` ## Behavioural Changes From bcc769f8ede89b3575e8431ce659c6386d8cee02 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 1 Dec 2023 10:51:23 +0100 Subject: [PATCH 43/55] Backpressure WIP --- 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 | 7 ++ .../main/java/io/sentry/ISentryClient.java | 5 ++ sentry/src/main/java/io/sentry/NoOpHub.java | 5 ++ sentry/src/main/java/io/sentry/Sentry.java | 10 +++ .../src/main/java/io/sentry/SentryClient.java | 5 ++ .../main/java/io/sentry/SentryOptions.java | 16 +++++ .../main/java/io/sentry/TracesSampler.java | 13 +++- .../backpressure/BackpressureMonitor.java | 65 +++++++++++++++++++ .../backpressure/IBackpressureMonitor.java | 9 +++ .../backpressure/NoOpBackpressureMonitor.java | 22 +++++++ .../sentry/transport/AsyncHttpTransport.java | 9 +++ .../java/io/sentry/transport/ITransport.java | 4 ++ .../java/io/sentry/transport/RateLimiter.java | 24 +++++++ 15 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java create mode 100644 sentry/src/main/java/io/sentry/backpressure/IBackpressureMonitor.java create mode 100644 sentry/src/main/java/io/sentry/backpressure/NoOpBackpressureMonitor.java diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index dcd01dec73..c648ccf9be 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -604,6 +604,11 @@ public void bindClient(final @NotNull ISentryClient client) { } } + @Override + public boolean isHealthy() { + return stack.peek().getClient().isHealthy(); + } + @Override public void flush(long timeoutMillis) { if (!isEnabled()) { diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index b655336314..68a9bdf11d 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -168,6 +168,11 @@ public void bindClient(@NotNull ISentryClient client) { Sentry.bindClient(client); } + @Override + public boolean isHealthy() { + return Sentry.isHealthy(); + } + @Override public void flush(long timeoutMillis) { Sentry.flush(timeoutMillis); diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index a6700df70e..01431043f0 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -329,6 +329,13 @@ default void addBreadcrumb(@NotNull String message, @NotNull String category) { */ void bindClient(@NotNull ISentryClient client); + /** + * Whether the transport is healthy. + * + * @return true if the transport is healthy + */ + boolean isHealthy(); + /** * Flushes events queued up, but keeps the Hub enabled. Not implemented yet. * diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index f6387ea6b4..15b5f25c4b 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -273,4 +273,9 @@ SentryId captureTransaction( @ApiStatus.Internal @Nullable RateLimiter getRateLimiter(); + + @ApiStatus.Internal + default boolean isHealthy() { + return true; + } } diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index e4c93a10ec..aca0562f77 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -131,6 +131,11 @@ public void configureScope(@NotNull ScopeCallback callback) {} @Override public void bindClient(@NotNull ISentryClient client) {} + @Override + public boolean isHealthy() { + return false; + } + @Override public void flush(long timeoutMillis) {} diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index e58ddf9d04..bfa0bdb7c5 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.backpressure.BackpressureMonitor; import io.sentry.cache.EnvelopeCache; import io.sentry.cache.IEnvelopeCache; import io.sentry.config.PropertiesProviderFactory; @@ -241,6 +242,11 @@ private static synchronized void init( notifyOptionsObservers(options); finalizePreviousSession(options, HubAdapter.getInstance()); + + // TODO move start into an integration? + + options.setBackpressureMonitor(new BackpressureMonitor(options)); + options.getBackpressureMonitor().start(); } @SuppressWarnings("FutureReturnValueIgnored") @@ -731,6 +737,10 @@ public static void bindClient(final @NotNull ISentryClient client) { getCurrentHub().bindClient(client); } + public static boolean isHealthy() { + return getCurrentHub().isHealthy(); + } + /** * Flushes events queued up to the current hub. Not implemented yet. * diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index f6b7fbf947..2973c1f8bc 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -941,6 +941,11 @@ public void flush(final long timeoutMillis) { return transport.getRateLimiter(); } + @Override + public boolean isHealthy() { + return transport.isHealthy(); + } + 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 7fdbf8d9cf..06208b5c19 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -1,6 +1,9 @@ package io.sentry; import com.jakewharton.nopen.annotation.Open; + +import io.sentry.backpressure.IBackpressureMonitor; +import io.sentry.backpressure.NoOpBackpressureMonitor; import io.sentry.cache.IEnvelopeCache; import io.sentry.clientreport.ClientReportRecorder; import io.sentry.clientreport.IClientReportRecorder; @@ -440,6 +443,8 @@ public class SentryOptions { /** Contains a list of monitor slugs for which check-ins should not be sent. */ @ApiStatus.Experimental private @Nullable List ignoredCheckIns = null; + @ApiStatus.Experimental private @NotNull IBackpressureMonitor backpressureMonitor = NoOpBackpressureMonitor.getInstance(); + /** * Adds an event processor * @@ -2188,6 +2193,17 @@ public void setConnectionStatusProvider( this.connectionStatusProvider = connectionStatusProvider; } + @ApiStatus.Internal + @NotNull + public IBackpressureMonitor getBackpressureMonitor() { + return backpressureMonitor; + } + + @ApiStatus.Internal + public void setBackpressureMonitor(final @NotNull IBackpressureMonitor backpressureMonitor) { + this.backpressureMonitor = backpressureMonitor; + } + /** The BeforeSend callback */ public interface BeforeSendCallback { diff --git a/sentry/src/main/java/io/sentry/TracesSampler.java b/sentry/src/main/java/io/sentry/TracesSampler.java index 750a728fa7..ca8c52a545 100644 --- a/sentry/src/main/java/io/sentry/TracesSampler.java +++ b/sentry/src/main/java/io/sentry/TracesSampler.java @@ -72,11 +72,18 @@ TracesSamplingDecision sample(final @NotNull SamplingContext samplingContext) { Boolean.TRUE.equals(isEnableTracing) ? DEFAULT_TRACES_SAMPLE_RATE : null; final @Nullable Double tracesSampleRateOrDefault = tracesSampleRateFromOptions == null ? defaultSampleRate : tracesSampleRateFromOptions; + final @NotNull Double downsampleFactor = Math.pow(2, options.getBackpressureMonitor().getDownsampleFactor()); + final @Nullable Double downsampledTracesSampleRate = + tracesSampleRateOrDefault == null ? null : tracesSampleRateOrDefault / downsampleFactor; - if (tracesSampleRateOrDefault != null) { +// System.out.println("tracesSampleRateOrDefault=" + tracesSampleRateOrDefault); +// System.out.println("downsampleFactor=" + downsampleFactor); +// System.out.println("downsampledTracesSampleRate=" + downsampledTracesSampleRate); + + if (downsampledTracesSampleRate != null) { return new TracesSamplingDecision( - sample(tracesSampleRateOrDefault), - tracesSampleRateOrDefault, + sample(downsampledTracesSampleRate), + downsampledTracesSampleRate, profilesSampled, profilesSampleRate); } diff --git a/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java new file mode 100644 index 0000000000..6a7d81a037 --- /dev/null +++ b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java @@ -0,0 +1,65 @@ +package io.sentry.backpressure; + +import org.jetbrains.annotations.NotNull; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; + +import io.sentry.ISentryExecutorService; +import io.sentry.Sentry; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; + +public final class BackpressureMonitor implements IBackpressureMonitor, Runnable { + private static final int MAX_DOWNSAMPLE_FACTOR = 10; + private static final int CHECK_INTERVAL_IN_MS = 10 * 1000; + + private final @NotNull SentryOptions sentryOptions; + private int downsampleFactor = 0; + private boolean didEverDownsample = false; + + public BackpressureMonitor(final @NotNull SentryOptions sentryOptions) { + this.sentryOptions = sentryOptions; + } + + @Override + public void start() { + reschedule(); + } + + @SuppressWarnings("FutureReturnValueIgnored") + private void reschedule() { + final @NotNull ISentryExecutorService executorService = sentryOptions.getExecutorService(); + if (!executorService.isClosed()) { + executorService.schedule(this, CHECK_INTERVAL_IN_MS); + } + } + + @Override + public int getDownsampleFactor() { + return downsampleFactor; + } + + @Override + public void run() { + if (isHealthy()) { + if (downsampleFactor > 0) { + sentryOptions.getLogger().log(SentryLevel.DEBUG, "Health check positive, reverting to normal sampling."); + } + downsampleFactor = 0; + } else { + if (downsampleFactor < MAX_DOWNSAMPLE_FACTOR) { + downsampleFactor++; + didEverDownsample = true; + sentryOptions.getLogger().log(SentryLevel.DEBUG, "Health check negative, downsampling with a factor of %d", downsampleFactor); + } + } + System.out.println("hello from backpressure monitor, downsamplingFactor is now " + downsampleFactor + " and it is " + ZonedDateTime.now(ZoneId.systemDefault()).toString() + " didEverDownsample? " + didEverDownsample); + reschedule(); + } + + private boolean isHealthy() { + return Sentry.isHealthy(); + } +} diff --git a/sentry/src/main/java/io/sentry/backpressure/IBackpressureMonitor.java b/sentry/src/main/java/io/sentry/backpressure/IBackpressureMonitor.java new file mode 100644 index 0000000000..e55de9bc4d --- /dev/null +++ b/sentry/src/main/java/io/sentry/backpressure/IBackpressureMonitor.java @@ -0,0 +1,9 @@ +package io.sentry.backpressure; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public interface IBackpressureMonitor { + void start(); + int getDownsampleFactor(); +} diff --git a/sentry/src/main/java/io/sentry/backpressure/NoOpBackpressureMonitor.java b/sentry/src/main/java/io/sentry/backpressure/NoOpBackpressureMonitor.java new file mode 100644 index 0000000000..edbf660e24 --- /dev/null +++ b/sentry/src/main/java/io/sentry/backpressure/NoOpBackpressureMonitor.java @@ -0,0 +1,22 @@ +package io.sentry.backpressure; + +public final class NoOpBackpressureMonitor implements IBackpressureMonitor { + + private static final NoOpBackpressureMonitor instance = new NoOpBackpressureMonitor(); + + private NoOpBackpressureMonitor() {} + + public static NoOpBackpressureMonitor getInstance() { + return instance; + } + + @Override + public void start() { + // do nothing + } + + @Override + public int getDownsampleFactor() { + return 0; + } +} diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index 7efbcbcaf3..f9d920bf51 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -1,5 +1,6 @@ package io.sentry.transport; +import io.sentry.DataCategory; import io.sentry.DateUtils; import io.sentry.Hint; import io.sentry.ILogger; @@ -149,6 +150,14 @@ private static QueuedThreadPoolExecutor initExecutor( return rateLimiter; } + @Override + public boolean isHealthy() { + boolean anyRateLimitActive = rateLimiter.isAnyRateLimitActive(); + boolean schedulingAllowed = executor.isSchedulingAllowed(); + System.out.println("rate limit " + anyRateLimitActive + ", scheduling allowed " + schedulingAllowed); + return !anyRateLimitActive && schedulingAllowed; + } + @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 09fc034246..b7a38d20ed 100644 --- a/sentry/src/main/java/io/sentry/transport/ITransport.java +++ b/sentry/src/main/java/io/sentry/transport/ITransport.java @@ -15,6 +15,10 @@ default void send(@NotNull SentryEnvelope envelope) throws IOException { send(envelope, new Hint()); } + default boolean isHealthy() { + return true; + } + /** * Flushes events queued up, but keeps the client enabled. Not implemented yet. * diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index ed4c04c630..7cb3c53f5f 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -113,6 +113,30 @@ public boolean isActiveForCategory(final @NotNull DataCategory dataCategory) { return false; } + + @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) + public boolean isAnyRateLimitActive() { + final Date currentDate = new Date(currentDateProvider.getCurrentTimeMillis()); + + // check all categories + final Date dateAllCategories = sentryRetryAfterLimit.get(DataCategory.All); + if (dateAllCategories != null) { + if (!currentDate.after(dateAllCategories)) { + return true; + } + } + + for (DataCategory dataCategory : sentryRetryAfterLimit.keySet()) { + // check for specific dataCategory + final Date dateCategory = sentryRetryAfterLimit.get(dataCategory); + if (dateCategory != null) { + return !currentDate.after(dateCategory); + } + } + + return false; + } + /** * It marks the hint when sending has failed, so it's not necessary to wait the timeout * From 0d3bb1b5f70a0afd931277479bff0f1394d26ef9 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 1 Dec 2023 11:32:40 +0100 Subject: [PATCH 44/55] fix changelog --- CHANGELOG.md | 77 ---------------------------------------------------- 1 file changed, 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0349e509d3..7024eec963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,83 +79,6 @@ val transaction = Sentry.startTransaction("name", "op", TransactionOptions().app - Do not filter out Sentry SDK frames in case of uncaught exceptions ([#3021](https://github.com/getsentry/sentry-java/pull/3021)) - Do not try to send and drop cached envelopes when rate-limiting is active ([#2937](https://github.com/getsentry/sentry-java/pull/2937)) -Version 7 of the Sentry Android/Java SDK brings a variety of features and fixes. The most notable changes are: -- Bumping `minSdk` level to 19 (Android 4.4) -- The SDK will now listen to connectivity changes and try to re-upload cached events when internet connection is re-established additionally to uploading events on app restart -- `Sentry.getSpan` now returns the root transaction, which should improve the span hierarchy and make it leaner -- Multiple improvements to reduce probability of the SDK causing ANRs -- New `sentry-okhttp` artifact is unbundled from Android and can be used in pure JVM-only apps - -## Sentry Self-hosted Compatibility - -This SDK version is compatible with a self-hosted version of Sentry `22.12.0` or higher. If you are using an older version of [self-hosted Sentry](https://develop.sentry.dev/self-hosted/) (aka onpremise), you will need to [upgrade](https://develop.sentry.dev/self-hosted/releases/). If you're using `sentry.io` no action is required. - -## Sentry Integrations Version Compatibility (Android) - -Make sure to align _all_ 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) - -For example, if you're using the [Sentry Android Gradle plugin](https://github.com/getsentry/sentry-android-gradle-plugin) with the `autoInstallation` [feature](https://docs.sentry.io/platforms/android/configuration/gradle/#auto-installation) (enabled by default), make sure to use version 4.+ of the gradle plugin together with version 7.+ of the SDK. If you can't do that for some reason, you can specify sentry version via the plugin config block: - -```kotlin -sentry { - autoInstallation { - sentryVersion.set("7.0.0") - } -} -``` - -Similarly, if you have a Sentry SDK (e.g. `sentry-android-core`) dependency on one of your Gradle modules and you're updating it to 7.+, make sure the Gradle plugin is at 4.+ or specify the SDK version as shown in the snippet above. - -## Breaking Changes - -- Bump min API to 19 ([#2883](https://github.com/getsentry/sentry-java/pull/2883)) -- If you're using `sentry-kotlin-extensions`, it requires `kotlinx-coroutines-core` version `1.6.1` or higher now ([#2838](https://github.com/getsentry/sentry-java/pull/2838)) -- Move enableNdk from SentryOptions to SentryAndroidOptions ([#2793](https://github.com/getsentry/sentry-java/pull/2793)) -- Apollo v2 BeforeSpanCallback now allows returning null ([#2890](https://github.com/getsentry/sentry-java/pull/2890)) -- `SentryOkHttpUtils` was removed from public API as it's been exposed by mistake ([#3005](https://github.com/getsentry/sentry-java/pull/3005)) -- `Scope` now implements the `IScope` interface, therefore some methods like `ScopeCallback.run` accept `IScope` now ([#3066](https://github.com/getsentry/sentry-java/pull/3066)) -- Cleanup `startTransaction` overloads ([#2964](https://github.com/getsentry/sentry-java/pull/2964)) - - We have reduced the number of overloads by allowing to pass in a `TransactionOptions` object instead of having separate parameters for certain options - - `TransactionOptions` has defaults set and can be customized, for example: - -```kotlin -// old -val transaction = Sentry.startTransaction("name", "op", bindToScope = true) -// new -val transaction = Sentry.startTransaction("name", "op", TransactionOptions().apply { isBindToScope = true }) -``` - -## Behavioural Changes - -- Android only: `Sentry.getSpan()` returns the root span/transaction instead of the latest span ([#2855](https://github.com/getsentry/sentry-java/pull/2855)) -- Capture failed HTTP and GraphQL (Apollo) requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794)) - - This can increase your event consumption and may affect your quota, because we will report failed network requests as Sentry events by default, if you're using the `sentry-android-okhttp` or `sentry-apollo-3` integrations. You can customize what errors you want/don't want to have reported for [OkHttp](https://docs.sentry.io/platforms/android/integrations/okhttp#http-client-errors) and [Apollo3](https://docs.sentry.io/platforms/android/integrations/apollo3#graphql-client-errors) respectively. -- Measure AppStart time till First Draw instead of `onResume` ([#2851](https://github.com/getsentry/sentry-java/pull/2851)) -- 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 -- 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, meaning the automatic transaction will be force-finished with status `deadline_exceeded` when reaching the deadline -- 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 -- Raw logback message and parameters are now guarded by `sendDefaultPii` if an `encoder` has been configured ([#2976](https://github.com/getsentry/sentry-java/pull/2976)) -- The `maxSpans` setting (defaults to 1000) is enforced for nested child spans which means a single transaction can have `maxSpans` number of children (nested or not) at most ([#3065](https://github.com/getsentry/sentry-java/pull/3065)) -- The `ScopeCallback` in `withScope` is now always executed ([#3066](https://github.com/getsentry/sentry-java/pull/3066)) - -## Deprecations - -- `sentry-android-okhttp` was deprecated in favour of the new `sentry-okhttp` module. Make sure to replace `io.sentry.android.okhttp` package name with `io.sentry.okhttp` before the next major, where the classes will be removed ([#3005](https://github.com/getsentry/sentry-java/pull/3005)) - -## Other Changes - -### Features - -- 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` -- Add `sentry-okhttp` module to support instrumenting OkHttp in non-Android projects ([#3005](https://github.com/getsentry/sentry-java/pull/3005)) -- Do not filter out Sentry SDK frames in case of uncaught exceptions ([#3021](https://github.com/getsentry/sentry-java/pull/3021)) -- Do not try to send and drop cached envelopes when rate-limiting is active ([#2937](https://github.com/getsentry/sentry-java/pull/2937)) - ### Fixes - Use `getMyMemoryState()` instead of `getRunningAppProcesses()` to retrieve process importance ([#3004](https://github.com/getsentry/sentry-java/pull/3004)) From 1ad36f18e9a0aa4093ed7fe072edf5ce7e6757b2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 1 Dec 2023 12:40:35 +0100 Subject: [PATCH 45/55] check if queue recently rejected instead of checking if it is currently full --- .../io/sentry/transport/AsyncHttpTransport.java | 12 +++++++++--- .../transport/QueuedThreadPoolExecutor.java | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index 59aedb8bfe..6d5f8bf1fb 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -152,10 +152,16 @@ private static QueuedThreadPoolExecutor initExecutor( @Override public boolean isHealthy() { boolean anyRateLimitActive = rateLimiter.isAnyRateLimitActive(); - boolean schedulingAllowed = executor.isSchedulingAllowed(); + boolean didRejectRecently = executor.didRejectRecently(); + boolean isSchedulingAllowed = executor.isSchedulingAllowed(); System.out.println( - "rate limit " + anyRateLimitActive + ", scheduling allowed " + schedulingAllowed); - return !anyRateLimitActive && schedulingAllowed; + "rate limit " + + anyRateLimitActive + + ", did reject recently " + + didRejectRecently + + ", isSchedulingAllowed " + + isSchedulingAllowed); + return !anyRateLimitActive && !didRejectRecently; } @Override diff --git a/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java b/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java index 3ddad78908..c9f5ce7326 100644 --- a/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java +++ b/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java @@ -1,6 +1,9 @@ package io.sentry.transport; +import io.sentry.DateUtils; import io.sentry.ILogger; +import io.sentry.Sentry; +import io.sentry.SentryDate; import io.sentry.SentryLevel; import java.util.concurrent.CancellationException; import java.util.concurrent.Future; @@ -20,10 +23,13 @@ * *

    This class is not public because it is used solely in {@link AsyncHttpTransport}. */ +@SuppressWarnings("UnusedVariable") final class QueuedThreadPoolExecutor extends ThreadPoolExecutor { private final int maxQueueSize; + private @Nullable SentryDate lastRejectTimestamp = null; private final @NotNull ILogger logger; private final @NotNull ReusableCountLatch unfinishedTasksCount = new ReusableCountLatch(); + private static final long RECENT_THRESHOLD = DateUtils.millisToNanos(10 * 1000); /** * Creates a new instance of the thread pool. @@ -58,6 +64,7 @@ public Future submit(final @NotNull Runnable task) { unfinishedTasksCount.increment(); return super.submit(task); } else { + lastRejectTimestamp = Sentry.getCurrentHub().getOptions().getDateProvider().now(); // if the thread pool is full, we don't cache it logger.log(SentryLevel.WARNING, "Submit cancelled"); return new CancelledFuture<>(); @@ -88,6 +95,16 @@ public boolean isSchedulingAllowed() { return unfinishedTasksCount.getCount() < maxQueueSize; } + public boolean didRejectRecently() { + final @Nullable SentryDate lastReject = this.lastRejectTimestamp; + if (lastReject == null) { + return false; + } + + long diff = Sentry.getCurrentHub().getOptions().getDateProvider().now().diff(lastReject); + return diff < RECENT_THRESHOLD; + } + static final class CancelledFuture implements Future { @Override public boolean cancel(final boolean mayInterruptIfRunning) { From 94b09a2e86471e52cd2abc506a319e5f9d81dfa3 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 5 Dec 2023 11:21:54 +0100 Subject: [PATCH 46/55] CR changes; tests; cleanup --- .../jakarta/it/SentrySpringIntegrationTest.kt | 4 +- .../boot/it/SentrySpringIntegrationTest.kt | 4 +- .../sentry/spring/jakarta/EnableSentryTest.kt | 7 +- .../webflux/SentryWebfluxIntegrationTest.kt | 4 +- .../io/sentry/spring/EnableSentryTest.kt | 7 +- .../webflux/SentryWebfluxIntegrationTest.kt | 4 +- .../src/main/kotlin/io/sentry/test/Mocks.kt | 9 +- sentry/api/sentry.api | 2 +- sentry/src/main/java/io/sentry/Sentry.java | 2 +- .../backpressure/BackpressureMonitor.java | 34 ++++---- .../sentry/transport/AsyncHttpTransport.java | 11 ++- .../transport/QueuedThreadPoolExecutor.java | 10 ++- .../java/io/sentry/transport/RateLimiter.java | 18 ++-- .../backpressure/BackpressureMonitorTest.kt | 83 +++++++++++++++++++ .../transport/AsyncHttpTransportTest.kt | 32 +++++++ .../transport/QueuedThreadPoolExecutorTest.kt | 15 +++- .../io/sentry/transport/RateLimiterTest.kt | 16 ++++ 17 files changed, 222 insertions(+), 40 deletions(-) create mode 100644 sentry/src/test/java/io/sentry/backpressure/BackpressureMonitorTest.kt diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt index 3d7fb26993..4b8a0c26be 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt @@ -210,7 +210,9 @@ class SentrySpringIntegrationTest { @SpringBootApplication open class App { - private val transport = mock() + private val transport = mock().also { + whenever(it.isHealthy).thenReturn(true) + } @Bean open fun mockTransportFactory(): ITransportFactory { diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/it/SentrySpringIntegrationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/it/SentrySpringIntegrationTest.kt index 5cb1dc9d72..eb6d159a7c 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/it/SentrySpringIntegrationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/it/SentrySpringIntegrationTest.kt @@ -210,7 +210,9 @@ class SentrySpringIntegrationTest { @SpringBootApplication open class App { - private val transport = mock() + private val transport = mock().also { + whenever(it.isHealthy).thenReturn(true) + } @Bean open fun mockTransportFactory(): ITransportFactory { diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt index 03e8ad1f99..7b51bbc1e7 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt @@ -6,8 +6,11 @@ import io.sentry.ITransportFactory import io.sentry.Integration import io.sentry.Sentry import io.sentry.SentryOptions +import io.sentry.transport.ITransport import org.assertj.core.api.Assertions.assertThat +import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import org.springframework.boot.context.annotation.UserConfigurations import org.springframework.boot.test.context.runner.ApplicationContextRunner import org.springframework.context.annotation.Bean @@ -185,7 +188,9 @@ class EnableSentryTest { class AppConfigWithCustomTransportFactory { @Bean - fun transport() = mock() + fun transport() = mock().also { + whenever(it.create(any(), any())).thenReturn(mock()) + } } @EnableSentry(dsn = "http://key@localhost/proj") diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt index 8dbbd05ebd..3f4628ea3c 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt @@ -145,7 +145,9 @@ class SentryWebfluxIntegrationTest { @SpringBootApplication(exclude = [ReactiveSecurityAutoConfiguration::class, SecurityAutoConfiguration::class]) open class App { - private val transport = mock() + private val transport = mock().also { + whenever(it.isHealthy).thenReturn(true) + } @Bean open fun mockTransportFactory(): ITransportFactory { diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/EnableSentryTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/EnableSentryTest.kt index 2d7cabae3e..5a8fec3053 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/EnableSentryTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/EnableSentryTest.kt @@ -6,8 +6,11 @@ import io.sentry.ITransportFactory import io.sentry.Integration import io.sentry.Sentry import io.sentry.SentryOptions +import io.sentry.transport.ITransport import org.assertj.core.api.Assertions.assertThat +import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import org.springframework.boot.context.annotation.UserConfigurations import org.springframework.boot.test.context.runner.ApplicationContextRunner import org.springframework.context.annotation.Bean @@ -185,7 +188,9 @@ class EnableSentryTest { class AppConfigWithCustomTransportFactory { @Bean - fun transport() = mock() + fun transport() = mock().also { + whenever(it.create(any(), any())).thenReturn(mock()) + } } @EnableSentry(dsn = "http://key@localhost/proj") diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt index 94cd22f505..0d4346c5ae 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebfluxIntegrationTest.kt @@ -145,7 +145,9 @@ class SentryWebfluxIntegrationTest { @SpringBootApplication(exclude = [ReactiveSecurityAutoConfiguration::class, SecurityAutoConfiguration::class]) open class App { - private val transport = mock() + private val transport = mock().also { + whenever(it.isHealthy).thenReturn(true) + } @Bean open fun mockTransportFactory(): ITransportFactory { diff --git a/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt b/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt index 8d0466cfaf..2ff144fd9b 100644 --- a/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt +++ b/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt @@ -2,19 +2,24 @@ package io.sentry.test import io.sentry.ISentryExecutorService +import io.sentry.backpressure.IBackpressureMonitor import org.mockito.kotlin.mock import java.util.concurrent.Callable import java.util.concurrent.Future class ImmediateExecutorService : ISentryExecutorService { override fun submit(runnable: Runnable): Future<*> { - runnable.run() + if (runnable !is IBackpressureMonitor) { + runnable.run() + } return mock() } override fun submit(callable: Callable): Future = mock() override fun schedule(runnable: Runnable, delayMillis: Long): Future<*> { - runnable.run() + if (runnable !is IBackpressureMonitor) { + runnable.run() + } return mock>() } override fun close(timeoutMillis: Long) {} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4d404bc2a6..9ba27f1586 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2831,7 +2831,7 @@ public final class io/sentry/UserFeedback$JsonKeys { } public final class io/sentry/backpressure/BackpressureMonitor : io/sentry/backpressure/IBackpressureMonitor, java/lang/Runnable { - public fun (Lio/sentry/SentryOptions;)V + public fun (Lio/sentry/SentryOptions;Lio/sentry/IHub;)V public fun getDownsampleFactor ()I public fun run ()V public fun start ()V diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index bfa0bdb7c5..96ab903390 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -245,7 +245,7 @@ private static synchronized void init( // TODO move start into an integration? - options.setBackpressureMonitor(new BackpressureMonitor(options)); + options.setBackpressureMonitor(new BackpressureMonitor(options, HubAdapter.getInstance())); options.getBackpressureMonitor().start(); } diff --git a/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java index d736d0c322..2eaad3a656 100644 --- a/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java +++ b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java @@ -1,7 +1,7 @@ package io.sentry.backpressure; +import io.sentry.IHub; import io.sentry.ISentryExecutorService; -import io.sentry.Sentry; import io.sentry.SentryLevel; import io.sentry.SentryOptions; import java.time.ZoneId; @@ -9,28 +9,28 @@ import org.jetbrains.annotations.NotNull; public final class BackpressureMonitor implements IBackpressureMonitor, Runnable { - private static final int MAX_DOWNSAMPLE_FACTOR = 10; + static final int MAX_DOWNSAMPLE_FACTOR = 10; private static final int CHECK_INTERVAL_IN_MS = 10 * 1000; private final @NotNull SentryOptions sentryOptions; + private final @NotNull IHub hub; private int downsampleFactor = 0; private boolean didEverDownsample = false; - public BackpressureMonitor(final @NotNull SentryOptions sentryOptions) { + public BackpressureMonitor(final @NotNull SentryOptions sentryOptions, final @NotNull IHub hub) { this.sentryOptions = sentryOptions; + this.hub = hub; } @Override public void start() { - reschedule(); + run(); } - @SuppressWarnings("FutureReturnValueIgnored") - private void reschedule() { - final @NotNull ISentryExecutorService executorService = sentryOptions.getExecutorService(); - if (!executorService.isClosed()) { - executorService.schedule(this, CHECK_INTERVAL_IN_MS); - } + @Override + public void run() { + checkHealth(); + reschedule(); } @Override @@ -38,8 +38,7 @@ public int getDownsampleFactor() { return downsampleFactor; } - @Override - public void run() { + void checkHealth() { if (isHealthy()) { if (downsampleFactor > 0) { sentryOptions @@ -66,10 +65,17 @@ public void run() { + ZonedDateTime.now(ZoneId.systemDefault()).toString() + " didEverDownsample? " + didEverDownsample); - reschedule(); + } + + @SuppressWarnings("FutureReturnValueIgnored") + private void reschedule() { + final @NotNull ISentryExecutorService executorService = sentryOptions.getExecutorService(); + if (!executorService.isClosed()) { + executorService.schedule(this, CHECK_INTERVAL_IN_MS); + } } private boolean isHealthy() { - return Sentry.isHealthy(); + return hub.isHealthy(); } } diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index 6d5f8bf1fb..2ac292d37e 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -5,6 +5,7 @@ import io.sentry.ILogger; import io.sentry.RequestDetails; import io.sentry.SentryDate; +import io.sentry.SentryDateProvider; import io.sentry.SentryEnvelope; import io.sentry.SentryLevel; import io.sentry.SentryOptions; @@ -46,7 +47,10 @@ public AsyncHttpTransport( final @NotNull RequestDetails requestDetails) { this( initExecutor( - options.getMaxQueueSize(), options.getEnvelopeDiskCache(), options.getLogger()), + options.getMaxQueueSize(), + options.getEnvelopeDiskCache(), + options.getLogger(), + options.getDateProvider()), options, rateLimiter, transportGate, @@ -124,7 +128,8 @@ public void flush(long timeoutMillis) { private static QueuedThreadPoolExecutor initExecutor( final int maxQueueSize, final @NotNull IEnvelopeCache envelopeCache, - final @NotNull ILogger logger) { + final @NotNull ILogger logger, + final @NotNull SentryDateProvider dateProvider) { final RejectedExecutionHandler storeEvents = (r, executor) -> { @@ -141,7 +146,7 @@ private static QueuedThreadPoolExecutor initExecutor( }; return new QueuedThreadPoolExecutor( - 1, maxQueueSize, new AsyncConnectionThreadFactory(), storeEvents, logger); + 1, maxQueueSize, new AsyncConnectionThreadFactory(), storeEvents, logger, dateProvider); } @Override diff --git a/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java b/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java index c9f5ce7326..a0919cfcd4 100644 --- a/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java +++ b/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java @@ -2,8 +2,8 @@ import io.sentry.DateUtils; import io.sentry.ILogger; -import io.sentry.Sentry; import io.sentry.SentryDate; +import io.sentry.SentryDateProvider; import io.sentry.SentryLevel; import java.util.concurrent.CancellationException; import java.util.concurrent.Future; @@ -44,7 +44,8 @@ public QueuedThreadPoolExecutor( final int maxQueueSize, final @NotNull ThreadFactory threadFactory, final @NotNull RejectedExecutionHandler rejectedExecutionHandler, - final @NotNull ILogger logger) { + final @NotNull ILogger logger, + final @NotNull SentryDateProvider dateProvider) { // similar to Executors.newSingleThreadExecutor, but with a max queue size control super( corePoolSize, @@ -56,6 +57,7 @@ public QueuedThreadPoolExecutor( rejectedExecutionHandler); this.maxQueueSize = maxQueueSize; this.logger = logger; + this.dateProvider = dateProvider; } @Override @@ -64,7 +66,7 @@ public Future submit(final @NotNull Runnable task) { unfinishedTasksCount.increment(); return super.submit(task); } else { - lastRejectTimestamp = Sentry.getCurrentHub().getOptions().getDateProvider().now(); + lastRejectTimestamp = dateProvider.now(); // if the thread pool is full, we don't cache it logger.log(SentryLevel.WARNING, "Submit cancelled"); return new CancelledFuture<>(); @@ -101,7 +103,7 @@ public boolean didRejectRecently() { return false; } - long diff = Sentry.getCurrentHub().getOptions().getDateProvider().now().diff(lastReject); + long diff = dateProvider.now().diff(lastReject); return diff < RECENT_THRESHOLD; } diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index d5d377f3d2..45796aa384 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -117,19 +117,21 @@ public boolean isActiveForCategory(final @NotNull DataCategory dataCategory) { public boolean isAnyRateLimitActive() { final Date currentDate = new Date(currentDateProvider.getCurrentTimeMillis()); - // check all categories - final Date dateAllCategories = sentryRetryAfterLimit.get(DataCategory.All); - if (dateAllCategories != null) { - if (!currentDate.after(dateAllCategories)) { - return true; - } - } + // // check all categories + // final Date dateAllCategories = sentryRetryAfterLimit.get(DataCategory.All); + // if (dateAllCategories != null) { + // if (!currentDate.after(dateAllCategories)) { + // return true; + // } + // } for (DataCategory dataCategory : sentryRetryAfterLimit.keySet()) { // check for specific dataCategory final Date dateCategory = sentryRetryAfterLimit.get(dataCategory); if (dateCategory != null) { - return !currentDate.after(dateCategory); + if (!currentDate.after(dateCategory)) { + return true; + } } } diff --git a/sentry/src/test/java/io/sentry/backpressure/BackpressureMonitorTest.kt b/sentry/src/test/java/io/sentry/backpressure/BackpressureMonitorTest.kt new file mode 100644 index 0000000000..c010c97238 --- /dev/null +++ b/sentry/src/test/java/io/sentry/backpressure/BackpressureMonitorTest.kt @@ -0,0 +1,83 @@ +package io.sentry.backpressure + +import io.sentry.IHub +import io.sentry.ISentryExecutorService +import io.sentry.SentryOptions +import io.sentry.backpressure.BackpressureMonitor.MAX_DOWNSAMPLE_FACTOR +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.util.concurrent.Future +import kotlin.test.Test +import kotlin.test.assertEquals + +class BackpressureMonitorTest { + + class Fixture { + + val options = SentryOptions() + val hub = mock() + val executor = mock() + fun getSut(): BackpressureMonitor { + options.executorService = executor + whenever(executor.isClosed).thenReturn(false) + whenever(executor.schedule(any(), any())).thenReturn(mock>()) + return BackpressureMonitor(options, hub) + } + } + + val fixture = Fixture() + + @Test + fun `starts off with downsampleFactor 0`() { + val sut = fixture.getSut() + assertEquals(0, sut.downsampleFactor) + } + + @Test + fun `downsampleFactor increases with negative health checks up to max`() { + val sut = fixture.getSut() + whenever(fixture.hub.isHealthy).thenReturn(false) + assertEquals(0, sut.downsampleFactor) + + (1..MAX_DOWNSAMPLE_FACTOR).forEach { i -> + sut.checkHealth() + assertEquals(i, sut.downsampleFactor) + } + + assertEquals(MAX_DOWNSAMPLE_FACTOR, sut.downsampleFactor) + sut.checkHealth() + assertEquals(MAX_DOWNSAMPLE_FACTOR, sut.downsampleFactor) + } + + @Test + fun `downsampleFactor goes back to 0 after positive health check`() { + val sut = fixture.getSut() + whenever(fixture.hub.isHealthy).thenReturn(false) + assertEquals(0, sut.downsampleFactor) + + sut.checkHealth() + assertEquals(1, sut.downsampleFactor) + + whenever(fixture.hub.isHealthy).thenReturn(true) + sut.checkHealth() + assertEquals(0, sut.downsampleFactor) + } + + @Test + fun `schedules on start`() { + val sut = fixture.getSut() + sut.start() + + verify(fixture.executor).schedule(any(), any()) + } + + @Test + fun `reschedules on run`() { + val sut = fixture.getSut() + sut.run() + + verify(fixture.executor).schedule(any(), any()) + } +} diff --git a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt index 2982e9567b..182763c161 100644 --- a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt +++ b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt @@ -402,6 +402,38 @@ class AsyncHttpTransportTest { assertTrue(called) } + @Test + fun `is healthy if not rate limited and not rejected recently`() { + whenever(fixture.rateLimiter.isAnyRateLimitActive()).thenReturn(false) + whenever(fixture.executor.didRejectRecently()).thenReturn(false) + + assertTrue(fixture.getSUT().isHealthy) + } + + @Test + fun `is unhealthy if rate limited and not rejected recently`() { + whenever(fixture.rateLimiter.isAnyRateLimitActive()).thenReturn(true) + whenever(fixture.executor.didRejectRecently()).thenReturn(false) + + assertFalse(fixture.getSUT().isHealthy) + } + + @Test + fun `is unhealthy if not rate limited but rejected recently`() { + whenever(fixture.rateLimiter.isAnyRateLimitActive()).thenReturn(false) + whenever(fixture.executor.didRejectRecently()).thenReturn(true) + + assertFalse(fixture.getSUT().isHealthy) + } + + @Test + fun `is unhealthy if rate limited and rejected recently`() { + whenever(fixture.rateLimiter.isAnyRateLimitActive()).thenReturn(true) + whenever(fixture.executor.didRejectRecently()).thenReturn(true) + + assertFalse(fixture.getSUT().isHealthy) + } + private fun createSession(): Session { return Session("123", User(), "env", "release") } diff --git a/sentry/src/test/java/io/sentry/transport/QueuedThreadPoolExecutorTest.kt b/sentry/src/test/java/io/sentry/transport/QueuedThreadPoolExecutorTest.kt index 7ce0da4eb1..d73ba41b27 100644 --- a/sentry/src/test/java/io/sentry/transport/QueuedThreadPoolExecutorTest.kt +++ b/sentry/src/test/java/io/sentry/transport/QueuedThreadPoolExecutorTest.kt @@ -1,5 +1,6 @@ package io.sentry.transport +import io.sentry.SentryNanotimeDateProvider import org.mockito.kotlin.mock import java.util.concurrent.CountDownLatch import java.util.concurrent.ThreadFactory @@ -29,7 +30,14 @@ class QueuedThreadPoolExecutorTest { } fun getSut(): QueuedThreadPoolExecutor = - QueuedThreadPoolExecutor(maxQueueSize + 1, maxQueueSize, threadFactory, DiscardPolicy(), mock()) + QueuedThreadPoolExecutor( + maxQueueSize + 1, + maxQueueSize, + threadFactory, + DiscardPolicy(), + mock(), + SentryNanotimeDateProvider() + ) } private val fixture = Fixture() @@ -79,6 +87,9 @@ class QueuedThreadPoolExecutorTest { @Test fun `limits the queue size`() { val sut = fixture.getSut() + + assertFalse(sut.didRejectRecently()) + // using this we're waiting for the submitted jobs to be unblocked val jobBlocker = Object() @@ -111,6 +122,8 @@ class QueuedThreadPoolExecutorTest { var f = sut.submit { synchronized(jobBlocker) { jobBlocker.wait() } } assertTrue(f.isCancelled, "A task above the queue size should have been cancelled.") + assertTrue(sut.didRejectRecently()) + // wake up a single job and wait on the main thread for that to finish synchronized(jobBlocker) { jobBlocker.notify() } atLeastOneFinished.await() diff --git a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt index fa00a79f78..063f0b9b26 100644 --- a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt +++ b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt @@ -32,8 +32,10 @@ import java.io.File import java.util.UUID import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue class RateLimiterTest { @@ -265,4 +267,18 @@ class RateLimiterTest { verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileItem)) verifyNoMoreInteractions(fixture.clientReportRecorder) } + + @Test + fun `any limit can be checked`() { + val rateLimiter = fixture.getSUT() + whenever(fixture.currentDateProvider.currentTimeMillis).thenReturn(0) + val eventItem = SentryEnvelopeItem.fromEvent(fixture.serializer, SentryEvent()) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem)) + + assertFalse(rateLimiter.isAnyRateLimitActive) + + rateLimiter.updateRetryAfterLimits("50:transaction:key, 1:default;error;security:organization", null, 1) + + assertTrue(rateLimiter.isAnyRateLimitActive) + } } From 0e1b15a2118c615be6c84c4cd52f8f883dd3bfd5 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 5 Dec 2023 15:22:35 +0100 Subject: [PATCH 47/55] Make opt-in; reduce recent reject check to 2s --- sentry/api/sentry.api | 2 ++ sentry/src/main/java/io/sentry/Sentry.java | 10 +++++----- sentry/src/main/java/io/sentry/SentryOptions.java | 12 ++++++++++++ .../sentry/transport/QueuedThreadPoolExecutor.java | 3 ++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 9ba27f1586..2c59d43932 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2164,6 +2164,7 @@ public class io/sentry/SentryOptions { public fun isAttachThreads ()Z public fun isDebug ()Z public fun isEnableAutoSessionTracking ()Z + public fun isEnableBackpressureHandling ()Z public fun isEnableDeduplication ()Z public fun isEnableExternalConfiguration ()Z public fun isEnablePrettySerializationOutput ()Z @@ -2200,6 +2201,7 @@ public class io/sentry/SentryOptions { public fun setDistinctId (Ljava/lang/String;)V public fun setDsn (Ljava/lang/String;)V public fun setEnableAutoSessionTracking (Z)V + public fun setEnableBackpressureHandling (Z)V public fun setEnableDeduplication (Z)V public fun setEnableExternalConfiguration (Z)V public fun setEnablePrettySerializationOutput (Z)V diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 96ab903390..b76888da76 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -242,11 +242,6 @@ private static synchronized void init( notifyOptionsObservers(options); finalizePreviousSession(options, HubAdapter.getInstance()); - - // TODO move start into an integration? - - options.setBackpressureMonitor(new BackpressureMonitor(options, HubAdapter.getInstance())); - options.getBackpressureMonitor().start(); } @SuppressWarnings("FutureReturnValueIgnored") @@ -397,6 +392,11 @@ private static boolean initConfigurations(final @NotNull SentryOptions options) options.addCollector(new JavaMemoryCollector()); } + if (options.isEnableBackpressureHandling()) { + options.setBackpressureMonitor(new BackpressureMonitor(options, HubAdapter.getInstance())); + options.getBackpressureMonitor().start(); + } + return true; } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 5abf89286a..6fed56bf63 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -445,6 +445,8 @@ public class SentryOptions { @ApiStatus.Experimental private @NotNull IBackpressureMonitor backpressureMonitor = NoOpBackpressureMonitor.getInstance(); + @ApiStatus.Experimental private boolean enableBackpressureHandling = false; + /** * Adds an event processor * @@ -2204,6 +2206,16 @@ public void setBackpressureMonitor(final @NotNull IBackpressureMonitor backpress this.backpressureMonitor = backpressureMonitor; } + @ApiStatus.Experimental + public void setEnableBackpressureHandling(final boolean enableBackpressureHandling) { + this.enableBackpressureHandling = enableBackpressureHandling; + } + + @ApiStatus.Experimental + public boolean isEnableBackpressureHandling() { + return enableBackpressureHandling; + } + /** The BeforeSend callback */ public interface BeforeSendCallback { diff --git a/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java b/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java index a0919cfcd4..f8d35a6888 100644 --- a/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java +++ b/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java @@ -28,8 +28,9 @@ final class QueuedThreadPoolExecutor extends ThreadPoolExecutor { private final int maxQueueSize; private @Nullable SentryDate lastRejectTimestamp = null; private final @NotNull ILogger logger; + private final @NotNull SentryDateProvider dateProvider; private final @NotNull ReusableCountLatch unfinishedTasksCount = new ReusableCountLatch(); - private static final long RECENT_THRESHOLD = DateUtils.millisToNanos(10 * 1000); + private static final long RECENT_THRESHOLD = DateUtils.millisToNanos(2 * 1000); /** * Creates a new instance of the thread pool. From c989dee8a1c47a4664278d38c4f79bdce24a0920 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 12 Dec 2023 08:22:35 +0100 Subject: [PATCH 48/55] add changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7024eec963..eadb743cf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +### Features + +- Automatically downsample transactions when system is under load ([#3072](https://github.com/getsentry/sentry-java/pull/3072)) + - You can opt into this behaviour by setting `enable-backpressure-handling=true`. + - We're happy to receive feedback, e.g. [in this GitHub issue](https://github.com/getsentry/sentry-java/issues/2829) + - When the system is under load we start reducing the `tracesSampleRate` automatically. + - Once the system goes back to healthy, we reset the `tracesSampleRate` to its original value. + ## 7.0.0 Version 7 of the Sentry Android/Java SDK brings a variety of features and fixes. The most notable changes are: From 7b22a0981fea1fc487a6526c27362f36ecb2fa92 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 12 Dec 2023 08:23:20 +0100 Subject: [PATCH 49/55] external option; add tests for options --- .../jakarta/SentryAutoConfigurationTest.kt | 4 ++- .../boot/SentryAutoConfigurationTest.kt | 4 ++- sentry/api/sentry.api | 2 ++ .../main/java/io/sentry/ExternalOptions.java | 14 +++++++++++ .../main/java/io/sentry/SentryOptions.java | 3 +++ .../java/io/sentry/ExternalOptionsTest.kt | 7 ++++++ .../test/java/io/sentry/SentryOptionsTest.kt | 9 +++++++ sentry/src/test/java/io/sentry/SentryTest.kt | 25 +++++++++++++++++++ 8 files changed, 66 insertions(+), 2 deletions(-) diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index 8e5221203f..2c1e118ebf 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -162,7 +162,8 @@ class SentryAutoConfigurationTest { "sentry.trace-propagation-targets=localhost,^(http|https)://api\\..*\$", "sentry.enabled=false", "sentry.send-modules=false", - "sentry.ignored-checkins=slug1,slugB" + "sentry.ignored-checkins=slug1,slugB", + "sentry.enable-backpressure-handling=true" ).run { val options = it.getBean(SentryProperties::class.java) assertThat(options.readTimeoutMillis).isEqualTo(10) @@ -194,6 +195,7 @@ class SentryAutoConfigurationTest { assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") + assertThat(options.isEnableBackpressureHandling).isEqualTo(true) } } diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index 71369d6f2d..fdf135f99d 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -162,7 +162,8 @@ class SentryAutoConfigurationTest { "sentry.trace-propagation-targets=localhost,^(http|https)://api\\..*\$", "sentry.enabled=false", "sentry.send-modules=false", - "sentry.ignored-checkins=slug1,slugB" + "sentry.ignored-checkins=slug1,slugB", + "sentry.enable-backpressure-handling=true" ).run { val options = it.getBean(SentryProperties::class.java) assertThat(options.readTimeoutMillis).isEqualTo(10) @@ -194,6 +195,7 @@ class SentryAutoConfigurationTest { assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") + assertThat(options.isEnableBackpressureHandling).isEqualTo(true) } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 2c59d43932..dc12937026 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -336,12 +336,14 @@ public final class io/sentry/ExternalOptions { public fun getTracePropagationTargets ()Ljava/util/List; public fun getTracesSampleRate ()Ljava/lang/Double; public fun getTracingOrigins ()Ljava/util/List; + public fun isEnableBackpressureHandling ()Ljava/lang/Boolean; public fun isEnablePrettySerializationOutput ()Ljava/lang/Boolean; public fun isEnabled ()Ljava/lang/Boolean; public fun isSendModules ()Ljava/lang/Boolean; public fun setDebug (Ljava/lang/Boolean;)V public fun setDist (Ljava/lang/String;)V public fun setDsn (Ljava/lang/String;)V + public fun setEnableBackpressureHandling (Ljava/lang/Boolean;)V public fun setEnableDeduplication (Ljava/lang/Boolean;)V public fun setEnablePrettySerializationOutput (Ljava/lang/Boolean;)V public fun setEnableTracing (Ljava/lang/Boolean;)V diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index adb25811e0..a34f24df85 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -49,6 +49,7 @@ public final class ExternalOptions { private @Nullable List ignoredCheckIns; private @Nullable Boolean sendModules; + private @Nullable Boolean enableBackpressureHandling; @SuppressWarnings("unchecked") public static @NotNull ExternalOptions from( @@ -131,6 +132,9 @@ public final class ExternalOptions { options.setIgnoredCheckIns(propertiesProvider.getList("ignored-checkins")); + options.setEnableBackpressureHandling( + propertiesProvider.getBooleanProperty("enable-backpressure-handling")); + for (final String ignoredExceptionType : propertiesProvider.getList("ignored-exceptions-for-type")) { try { @@ -398,4 +402,14 @@ public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { public @Nullable List getIgnoredCheckIns() { return ignoredCheckIns; } + + @ApiStatus.Experimental + public void setEnableBackpressureHandling(final @Nullable Boolean enableBackpressureHandling) { + this.enableBackpressureHandling = enableBackpressureHandling; + } + + @ApiStatus.Experimental + public @Nullable Boolean isEnableBackpressureHandling() { + return enableBackpressureHandling; + } } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 6fed56bf63..9a6a962dc9 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -2431,6 +2431,9 @@ public void merge(final @NotNull ExternalOptions options) { final List ignoredCheckIns = new ArrayList<>(options.getIgnoredCheckIns()); setIgnoredCheckIns(ignoredCheckIns); } + if (options.isEnableBackpressureHandling() != null) { + setEnableBackpressureHandling(options.isEnableBackpressureHandling()); + } } private @NotNull SdkVersion createSdkVersion() { diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 929db8ad06..7abc5de474 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -268,6 +268,13 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with enableBackpressureHandling set to true`() { + withPropertiesFile("enable-backpressure-handling=true") { options -> + assertTrue(options.isEnableBackpressureHandling == true) + } + } + private fun withPropertiesFile(textLines: List = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) { // create a sentry.properties file in temporary folder val temporaryFolder = TemporaryFolder() diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 3fcc9fa88c..989103507f 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.backpressure.NoOpBackpressureMonitor import io.sentry.util.StringUtils import org.mockito.kotlin.mock import java.io.File @@ -367,6 +368,7 @@ class SentryOptionsTest { externalOptions.isEnablePrettySerializationOutput = false externalOptions.isSendModules = false externalOptions.ignoredCheckIns = listOf("slug1", "slug-B") + externalOptions.isEnableBackpressureHandling = true val options = SentryOptions() @@ -396,6 +398,7 @@ class SentryOptionsTest { assertFalse(options.isEnablePrettySerializationOutput) assertFalse(options.isSendModules) assertEquals(listOf("slug1", "slug-B"), options.ignoredCheckIns) + assertTrue(options.isEnableBackpressureHandling) } @Test @@ -527,4 +530,10 @@ class SentryOptionsTest { fun `when options are initialized, sendModules is set to true by default`() { assertTrue(SentryOptions().isSendModules) } + + @Test + fun `when options are initialized, enableBackpressureHandling is set to false by default`() { + assertFalse(SentryOptions().isEnableBackpressureHandling) + assertTrue(SentryOptions().backpressureMonitor is NoOpBackpressureMonitor) + } } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 65cf88d31b..1b5383e461 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -1,5 +1,7 @@ package io.sentry +import io.sentry.backpressure.BackpressureMonitor +import io.sentry.backpressure.NoOpBackpressureMonitor import io.sentry.cache.EnvelopeCache import io.sentry.cache.IEnvelopeCache import io.sentry.internal.debugmeta.IDebugMetaLoader @@ -920,6 +922,29 @@ class SentryTest { assertEquals("op-child", span.operation) } + @Test + fun `backpressure monitor is a NoOp if handling is disabled`() { + var sentryOptions: SentryOptions? = null + Sentry.init({ + it.dsn = dsn + it.isEnableBackpressureHandling = false + sentryOptions = it + }) + assertIs(sentryOptions?.backpressureMonitor) + } + + @Test + fun `backpressure monitor is set if handling is enabled`() { + var sentryOptions: SentryOptions? = null + + Sentry.init({ + it.dsn = dsn + it.isEnableBackpressureHandling = true + sentryOptions = it + }) + assertIs(sentryOptions?.backpressureMonitor) + } + private class InMemoryOptionsObserver : IOptionsObserver { var release: String? = null private set From 51d7297432b4f36d345206e98ea818a0ca47e0cf Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 12 Dec 2023 14:40:21 +0100 Subject: [PATCH 50/55] add backpressure discard reason --- sentry/api/sentry.api | 1 + sentry/src/main/java/io/sentry/Hub.java | 12 +++++++--- .../io/sentry/clientreport/DiscardReason.java | 4 +++- sentry/src/test/java/io/sentry/HubTest.kt | 23 +++++++++++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index dc12937026..e7f22574fb 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2953,6 +2953,7 @@ public final class io/sentry/clientreport/ClientReportRecorder : io/sentry/clien } public final class io/sentry/clientreport/DiscardReason : java/lang/Enum { + public static final field BACKPRESSURE Lio/sentry/clientreport/DiscardReason; public static final field BEFORE_SEND Lio/sentry/clientreport/DiscardReason; public static final field CACHE_OVERFLOW Lio/sentry/clientreport/DiscardReason; public static final field EVENT_PROCESSOR Lio/sentry/clientreport/DiscardReason; diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index c648ccf9be..a1396cbcaf 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -665,9 +665,15 @@ public void flush(long timeoutMillis) { SentryLevel.DEBUG, "Transaction %s was dropped due to sampling decision.", transaction.getEventId()); - options - .getClientReportRecorder() - .recordLostEvent(DiscardReason.SAMPLE_RATE, DataCategory.Transaction); + if (options.getBackpressureMonitor().getDownsampleFactor() > 0) { + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.BACKPRESSURE, DataCategory.Transaction); + } else { + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.SAMPLE_RATE, DataCategory.Transaction); + } } else { StackItem item = null; try { diff --git a/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java b/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java index 887dadc502..cc63f8d01c 100644 --- a/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java +++ b/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java @@ -10,7 +10,9 @@ public enum DiscardReason { NETWORK_ERROR("network_error"), SAMPLE_RATE("sample_rate"), BEFORE_SEND("before_send"), - EVENT_PROCESSOR("event_processor"); // also for ignored exceptions + EVENT_PROCESSOR("event_processor"), // also for ignored exceptions + + BACKPRESSURE("backpressure"); private final String reason; diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index 6e4ff587a1..69950001fc 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.backpressure.IBackpressureMonitor import io.sentry.cache.EnvelopeCache import io.sentry.clientreport.ClientReportTestHelper.Companion.assertClientReport import io.sentry.clientreport.DiscardReason @@ -1439,6 +1440,28 @@ class HubTest { listOf(DiscardedEvent(DiscardReason.SAMPLE_RATE.reason, DataCategory.Transaction.category, 1)) ) } + + @Test + fun `transactions lost due to sampling caused by backpressure are recorded as lost`() { + val options = SentryOptions() + options.cacheDirPath = file.absolutePath + options.dsn = "https://key@sentry.io/proj" + options.setSerializer(mock()) + val sut = Hub(options) + val mockClient = mock() + sut.bindClient(mockClient) + val mockBackpressureMonitor = mock() + options.backpressureMonitor = mockBackpressureMonitor + whenever(mockBackpressureMonitor.downsampleFactor).thenReturn(1) + + val sentryTracer = SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(false)), sut) + sentryTracer.finish() + + assertClientReport( + options.clientReportRecorder, + listOf(DiscardedEvent(DiscardReason.BACKPRESSURE.reason, DataCategory.Transaction.category, 1)) + ) + } //endregion //region profiling tests From 5b51964b13aa16e4dca74df35854e0cd9fd7b20b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 12 Dec 2023 14:42:23 +0100 Subject: [PATCH 51/55] Consider NoOpHub healthy so it does not cause downsampling in case a check is run against it --- sentry/src/main/java/io/sentry/NoOpHub.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index aca0562f77..d186c69ca2 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -133,7 +133,7 @@ public void bindClient(@NotNull ISentryClient client) {} @Override public boolean isHealthy() { - return false; + return true; } @Override From 8c69f98371575463b26fefef09ca95a54184d556 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 12 Dec 2023 14:44:43 +0100 Subject: [PATCH 52/55] remove sysos --- .../io/sentry/backpressure/BackpressureMonitor.java | 11 ----------- .../java/io/sentry/transport/AsyncHttpTransport.java | 8 -------- .../main/java/io/sentry/transport/RateLimiter.java | 9 --------- 3 files changed, 28 deletions(-) diff --git a/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java index 2eaad3a656..2416f0e0e1 100644 --- a/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java +++ b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java @@ -4,8 +4,6 @@ import io.sentry.ISentryExecutorService; import io.sentry.SentryLevel; import io.sentry.SentryOptions; -import java.time.ZoneId; -import java.time.ZonedDateTime; import org.jetbrains.annotations.NotNull; public final class BackpressureMonitor implements IBackpressureMonitor, Runnable { @@ -15,7 +13,6 @@ public final class BackpressureMonitor implements IBackpressureMonitor, Runnable private final @NotNull SentryOptions sentryOptions; private final @NotNull IHub hub; private int downsampleFactor = 0; - private boolean didEverDownsample = false; public BackpressureMonitor(final @NotNull SentryOptions sentryOptions, final @NotNull IHub hub) { this.sentryOptions = sentryOptions; @@ -49,7 +46,6 @@ void checkHealth() { } else { if (downsampleFactor < MAX_DOWNSAMPLE_FACTOR) { downsampleFactor++; - didEverDownsample = true; sentryOptions .getLogger() .log( @@ -58,13 +54,6 @@ void checkHealth() { downsampleFactor); } } - System.out.println( - "hello from backpressure monitor, downsamplingFactor is now " - + downsampleFactor - + " and it is " - + ZonedDateTime.now(ZoneId.systemDefault()).toString() - + " didEverDownsample? " - + didEverDownsample); } @SuppressWarnings("FutureReturnValueIgnored") diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index 2ac292d37e..6636ce517f 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -158,14 +158,6 @@ private static QueuedThreadPoolExecutor initExecutor( public boolean isHealthy() { boolean anyRateLimitActive = rateLimiter.isAnyRateLimitActive(); boolean didRejectRecently = executor.didRejectRecently(); - boolean isSchedulingAllowed = executor.isSchedulingAllowed(); - System.out.println( - "rate limit " - + anyRateLimitActive - + ", did reject recently " - + didRejectRecently - + ", isSchedulingAllowed " - + isSchedulingAllowed); return !anyRateLimitActive && !didRejectRecently; } diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index 45796aa384..8cc08fd8f6 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -117,16 +117,7 @@ public boolean isActiveForCategory(final @NotNull DataCategory dataCategory) { public boolean isAnyRateLimitActive() { final Date currentDate = new Date(currentDateProvider.getCurrentTimeMillis()); - // // check all categories - // final Date dateAllCategories = sentryRetryAfterLimit.get(DataCategory.All); - // if (dateAllCategories != null) { - // if (!currentDate.after(dateAllCategories)) { - // return true; - // } - // } - for (DataCategory dataCategory : sentryRetryAfterLimit.keySet()) { - // check for specific dataCategory final Date dateCategory = sentryRetryAfterLimit.get(dataCategory); if (dateCategory != null) { if (!currentDate.after(dateCategory)) { From 49876072c35902aa249ea544a4de489a8047da14 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 12 Dec 2023 14:46:49 +0100 Subject: [PATCH 53/55] delay initial health check so it runs against a real hub --- .../java/io/sentry/backpressure/BackpressureMonitor.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java index 2416f0e0e1..2008a38c76 100644 --- a/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java +++ b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java @@ -8,6 +8,7 @@ public final class BackpressureMonitor implements IBackpressureMonitor, Runnable { static final int MAX_DOWNSAMPLE_FACTOR = 10; + private static final int INITIAL_CHECK_DELAY_IN_MS = 500; private static final int CHECK_INTERVAL_IN_MS = 10 * 1000; private final @NotNull SentryOptions sentryOptions; @@ -21,13 +22,13 @@ public BackpressureMonitor(final @NotNull SentryOptions sentryOptions, final @No @Override public void start() { - run(); + reschedule(INITIAL_CHECK_DELAY_IN_MS); } @Override public void run() { checkHealth(); - reschedule(); + reschedule(CHECK_INTERVAL_IN_MS); } @Override @@ -57,10 +58,10 @@ void checkHealth() { } @SuppressWarnings("FutureReturnValueIgnored") - private void reschedule() { + private void reschedule(final int delay) { final @NotNull ISentryExecutorService executorService = sentryOptions.getExecutorService(); if (!executorService.isClosed()) { - executorService.schedule(this, CHECK_INTERVAL_IN_MS); + executorService.schedule(this, delay); } } From ee7cb9f173b72d4e311a01daa09d1935cd17c7f8 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 12 Dec 2023 14:48:23 +0100 Subject: [PATCH 54/55] enable backpressure handling for spring samples --- .../src/main/resources/application.properties | 1 + .../src/main/resources/application.properties | 1 + .../src/main/resources/application.properties | 1 + .../src/main/resources/application.properties | 1 + 4 files changed, 4 insertions(+) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties index d677399f7e..040a9bb208 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties @@ -13,6 +13,7 @@ sentry.enable-tracing=true sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 sentry.debug=true sentry.graphql.ignored-error-types=SOME_ERROR,ANOTHER_ERROR +sentry.enable-backpressure-handling=true in-app-includes="io.sentry.samples" # Uncomment and set to true to enable aot compatibility diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/resources/application.properties index cded2b5e60..007d8d8ef3 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/resources/application.properties @@ -9,3 +9,4 @@ sentry.logging.minimum-event-level=info sentry.logging.minimum-breadcrumb-level=debug sentry.reactive.thread-local-accessor-enabled=true sentry.enable-tracing=true +sentry.enable-backpressure-handling=true diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/resources/application.properties index a08a498bf2..3b6d041bda 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/resources/application.properties @@ -11,3 +11,4 @@ sentry.enable-tracing=true spring.graphql.graphiql.enabled=true spring.graphql.websocket.path=/graphql spring.graphql.schema.printer.enabled=true +sentry.enable-backpressure-handling=true diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties index 75461046db..f63478d9a0 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties @@ -13,6 +13,7 @@ sentry.enable-tracing=true sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 sentry.debug=true sentry.graphql.ignored-error-types=SOME_ERROR,ANOTHER_ERROR +sentry.enable-backpressure-handling=true in-app-includes="io.sentry.samples" # Database configuration From 3194e3fe7b162deac830647b80447ecb9c539b94 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 14 Dec 2023 09:30:17 +0100 Subject: [PATCH 55/55] remove blank line --- sentry/src/main/java/io/sentry/clientreport/DiscardReason.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java b/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java index cc63f8d01c..91a56cf313 100644 --- a/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java +++ b/sentry/src/main/java/io/sentry/clientreport/DiscardReason.java @@ -11,7 +11,6 @@ public enum DiscardReason { SAMPLE_RATE("sample_rate"), BEFORE_SEND("before_send"), EVENT_PROCESSOR("event_processor"), // also for ignored exceptions - BACKPRESSURE("backpressure"); private final String reason;