From a067ff7fb0aa256a30959f4bf390b8d7db616c62 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 23 Sep 2026 17:01:57 +0200 Subject: [PATCH 01/13] docs: Warn about sendDefaultPii removal (#6156) Make the upcoming removal visible in the data collection changelog entry so customers know to migrate before the next major SDK version. Co-authored-by: Claude --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e59606e335c..26aa987e7cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,11 @@ - Add `LocalSentrySpan` to `sentry-compose` so apps can provide a parent `ISpan` to a composable subtree and have nested `SentryTraced` spans attach to it ([#6112]https://github.com/getsentry/sentry-java/pull/6112) - Add `dataCollection`, a fine-grained replacement for `sendDefaultPii`, for controlling data collected automatically by SDK integrations ([#5759](https://github.com/getsentry/sentry-java/pull/5759)) - - `sendDefaultPii` remains supported for backwards compatibility. When `dataCollection` is not configured, the SDK preserves the existing `sendDefaultPii` behavior. + + > [!WARNING] + > `sendDefaultPii` will be removed in the next major SDK version. Migrate to `dataCollection` before upgrading. + + - Until then, when `dataCollection` is not configured, the SDK preserves the existing `sendDefaultPii` behavior. - Configuring any `dataCollection` option makes it the source of truth. `sendDefaultPii` is then ignored, and omitted `dataCollection` options use the defaults below. - The Logback appender is a compatibility exception. When an encoder is configured, `sendDefaultPii=true` continues to include the original message template and parameters. To opt in independently of `sendDefaultPii`, set `true` on the Sentry appender in `logback.xml` or `logback-spring.xml`. - Data explicitly supplied through APIs such as `Sentry.setUser`, scopes, event processors, or `beforeSend` is not affected. From e319d59207fa589b3cf7f507d4a7663c1e55b0a3 Mon Sep 17 00:00:00 2001 From: adinauer <2542832+adinauer@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:05:21 +0000 Subject: [PATCH 02/13] release: 8.58.0 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26aa987e7cf..1b7908da67d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 8.58.0 ### Features diff --git a/gradle.properties b/gradle.properties index bcd121eb063..a9cd715bc73 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ android.newDsl=false android.experimental.lint.version=9.4.0 # Release information -versionName=8.57.0 +versionName=8.58.0 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android From ceb45dcd4ed081329502269cbcca43ea43b59f71 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:15:58 +0000 Subject: [PATCH 03/13] docs(changelog): Remove warning indentation (#6157) Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Alexander Dinauer --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b7908da67d..c9a0864235d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,8 @@ - Add `LocalSentrySpan` to `sentry-compose` so apps can provide a parent `ISpan` to a composable subtree and have nested `SentryTraced` spans attach to it ([#6112]https://github.com/getsentry/sentry-java/pull/6112) - Add `dataCollection`, a fine-grained replacement for `sendDefaultPii`, for controlling data collected automatically by SDK integrations ([#5759](https://github.com/getsentry/sentry-java/pull/5759)) - > [!WARNING] - > `sendDefaultPii` will be removed in the next major SDK version. Migrate to `dataCollection` before upgrading. +> [!WARNING] +> `sendDefaultPii` will be removed in the next major SDK version. Migrate to `dataCollection` before upgrading. - Until then, when `dataCollection` is not configured, the SDK preserves the existing `sendDefaultPii` behavior. - Configuring any `dataCollection` option makes it the source of truth. `sendDefaultPii` is then ignored, and omitted `dataCollection` options use the defaults below. From a4fd77512d20304ee8a4adc3cc153826042a4ec4 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 24 Sep 2026 11:38:02 +0200 Subject: [PATCH 04/13] feat(core): Deprecate sendDefaultPii (#6158) * feat(core): Deprecate sendDefaultPii Mark the SentryOptions accessors as deprecated and scheduled for removal in 9.0. Direct users to dataCollection while retaining legacy fallback behavior throughout 8.x. Co-Authored-By: Claude * changelog --------- Co-authored-by: Claude --- CHANGELOG.md | 6 ++++++ .../sentry/apollo3/SentryApollo3HttpInterceptor.kt | 1 + .../sentry/apollo4/SentryApollo4HttpInterceptor.kt | 1 + .../io/sentry/ktorClient/SentryKtorClientUtils.kt | 1 + .../java/io/sentry/logback/SentryAppender.java | 1 + .../java/io/sentry/okhttp/SentryOkHttpUtils.kt | 1 + .../OpenTelemetryAttributesExtractor.java | 2 +- .../io/sentry/spring7/SentryRequestResolver.java | 1 + .../spring7/webflux/SentryRequestResolver.java | 1 + .../spring/jakarta/SentryRequestResolver.java | 1 + .../jakarta/webflux/SentryRequestResolver.java | 1 + .../io/sentry/spring/SentryRequestResolver.java | 1 + .../spring/webflux/SentryRequestResolver.java | 1 + .../java/io/sentry/DataCollectionResolver.java | 1 + sentry/src/main/java/io/sentry/SentryOptions.java | 14 ++++++++++++++ .../src/main/java/io/sentry/util/CookieUtils.java | 1 + 16 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9a0864235d..5abe7ece787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Deprecate `sendDefaultPii` in favor of `dataCollection` ahead of its removal in 9.0 ([#6158](https://github.com/getsentry/sentry-java/pull/6158)) + ## 8.58.0 ### Features 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 94ba52cb592..7aa8d693943 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 @@ -301,6 +301,7 @@ constructor( return getHeaders(headers) } + @Suppress("DEPRECATION") private fun getHeaders(headers: List): MutableMap? { // Headers are only sent if isSendDefaultPii is enabled due to PII if (!scopes.options.isSendDefaultPii) { diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt index 54ad1d50fdb..437edff82d7 100644 --- a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt @@ -300,6 +300,7 @@ constructor( return getHeaders(headers) } + @Suppress("DEPRECATION") private fun getHeaders(headers: List): MutableMap? { // Headers are only sent if isSendDefaultPii is enabled due to PII if (!scopes.options.isSendDefaultPii) { diff --git a/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt b/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt index b8c1385ed92..6632efce93f 100644 --- a/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt +++ b/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientUtils.kt @@ -92,6 +92,7 @@ internal object SentryKtorClientUtils { return getHeaders(scopes, headers) } + @Suppress("DEPRECATION") private fun getHeaders(scopes: IScopes, headers: Headers): MutableMap? { // Headers are only sent if isSendDefaultPii is enabled due to PII if (!scopes.options.isSendDefaultPii) { 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 5e08fee0134..722845e2d70 100644 --- a/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java +++ b/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java @@ -200,6 +200,7 @@ protected void captureLog(@NotNull ILoggingEvent loggingEvent) { Sentry.logger().log(sentryLevel, params, formattedMessage, arguments); } + @SuppressWarnings("deprecation") private boolean shouldIncludeUnencodedMessage() { return encoder == null || includeUnencodedMessage diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt index 1be993c9544..106baced8ca 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt @@ -102,6 +102,7 @@ internal object SentryOkHttpUtils { return getHeaders(scopes, responseHeaders) } + @Suppress("DEPRECATION") private fun getHeaders(scopes: IScopes, requestHeaders: Headers): MutableMap? { // Headers are only sent if isSendDefaultPii is enabled due to PII if (!scopes.options.isSendDefaultPii) { diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryAttributesExtractor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryAttributesExtractor.java index 8612f1132f8..cf0fb06c11a 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryAttributesExtractor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryAttributesExtractor.java @@ -74,7 +74,7 @@ private void addRequestAttributesToScope( } } - @SuppressWarnings("unchecked") + @SuppressWarnings({"deprecation", "unchecked"}) private static Map collectHeaders( final @NotNull Attributes attributes, final @NotNull SentryOptions options) { Map headers = new HashMap<>(); diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestResolver.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestResolver.java index c3d96eb8115..4995d4bfe09 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestResolver.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestResolver.java @@ -22,6 +22,7 @@ import org.jetbrains.annotations.Nullable; @Open +@SuppressWarnings("deprecation") public class SentryRequestResolver { protected static final @NotNull AutoClosableReentrantLock staticLock = new AutoClosableReentrantLock(); diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryRequestResolver.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryRequestResolver.java index 3a4366d481b..c9ea5c6c7de 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryRequestResolver.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryRequestResolver.java @@ -19,6 +19,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; @Open +@SuppressWarnings("deprecation") @ApiStatus.Experimental public class SentryRequestResolver { private final @NotNull IScopes scopes; diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java index d4f69cb8714..9fcb30665cb 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java @@ -22,6 +22,7 @@ import org.jetbrains.annotations.Nullable; @Open +@SuppressWarnings("deprecation") public class SentryRequestResolver { protected static final @NotNull AutoClosableReentrantLock staticLock = new AutoClosableReentrantLock(); diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java index 774de4d6b31..f188f7ec5a4 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java @@ -19,6 +19,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; @Open +@SuppressWarnings("deprecation") @ApiStatus.Experimental public class SentryRequestResolver { private final @NotNull IScopes scopes; diff --git a/sentry-spring/src/main/java/io/sentry/spring/SentryRequestResolver.java b/sentry-spring/src/main/java/io/sentry/spring/SentryRequestResolver.java index 607fb2b58be..1c8ec52ac88 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/SentryRequestResolver.java +++ b/sentry-spring/src/main/java/io/sentry/spring/SentryRequestResolver.java @@ -22,6 +22,7 @@ import org.jetbrains.annotations.Nullable; @Open +@SuppressWarnings("deprecation") public class SentryRequestResolver { protected static final @NotNull AutoClosableReentrantLock staticLock = new AutoClosableReentrantLock(); diff --git a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java index 76ad3ba1703..321f2a05d8d 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java +++ b/sentry-spring/src/main/java/io/sentry/spring/webflux/SentryRequestResolver.java @@ -19,6 +19,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest; @Open +@SuppressWarnings("deprecation") @ApiStatus.Experimental public class SentryRequestResolver { private final @NotNull IScopes scopes; diff --git a/sentry/src/main/java/io/sentry/DataCollectionResolver.java b/sentry/src/main/java/io/sentry/DataCollectionResolver.java index fe643d8317c..81ec4dd16b0 100644 --- a/sentry/src/main/java/io/sentry/DataCollectionResolver.java +++ b/sentry/src/main/java/io/sentry/DataCollectionResolver.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.Nullable; /** Resolves effective Data Collection policies for SDK integrations. */ +@SuppressWarnings("deprecation") @ApiStatus.Internal public final class DataCollectionResolver { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index f37e00db0fb..6dacb527276 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -1755,10 +1755,24 @@ public void setSdkVersion(final @Nullable SdkVersion sdkVersion) { this.sdkVersion = sdkVersion; } + /** + * Returns whether personally identifiable information is sent by default. + * + * @deprecated use {@link #getDataCollection()} instead. This option will be removed in 9.0. + */ + @ApiStatus.ScheduledForRemoval(inVersion = "9.0") + @Deprecated public boolean isSendDefaultPii() { return sendDefaultPii; } + /** + * Sets whether personally identifiable information is sent by default. + * + * @deprecated use {@link #getDataCollection()} instead. This option will be removed in 9.0. + */ + @ApiStatus.ScheduledForRemoval(inVersion = "9.0") + @Deprecated public void setSendDefaultPii(boolean sendDefaultPii) { this.sendDefaultPii = sendDefaultPii; } diff --git a/sentry/src/main/java/io/sentry/util/CookieUtils.java b/sentry/src/main/java/io/sentry/util/CookieUtils.java index 935f98aaf1a..0b12acc1768 100644 --- a/sentry/src/main/java/io/sentry/util/CookieUtils.java +++ b/sentry/src/main/java/io/sentry/util/CookieUtils.java @@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@SuppressWarnings("deprecation") @ApiStatus.Internal public final class CookieUtils { From b59e639f5ae48357f55171f8f5ac69f008c6f505 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 25 Sep 2026 12:43:41 +0200 Subject: [PATCH 05/13] feat(android): Make tombstone merge time threshold configurable (#6154) * feat(android): Make tombstone merge time threshold configurable * changelog * ref(android): Drop duplicate no-match log and document the merge threshold default * docs(android): Expand tombstone merge threshold javadoc --- CHANGELOG.md | 1 + .../api/sentry-android-core.api | 2 ++ .../android/core/ManifestMetadataReader.java | 8 +++++ .../android/core/NativeEventCollector.java | 18 +++++++--- .../android/core/SentryAndroidOptions.java | 35 +++++++++++++++++++ .../android/core/TombstoneIntegration.java | 2 +- .../core/ManifestMetadataReaderTest.kt | 25 +++++++++++++ .../android/core/NativeEventCollectorTest.kt | 29 +++++++++++++++ .../android/core/SentryAndroidOptionsTest.kt | 9 +++++ 9 files changed, 124 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5abe7ece787..ec63c894fc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Deprecate `sendDefaultPii` in favor of `dataCollection` ahead of its removal in 9.0 ([#6158](https://github.com/getsentry/sentry-java/pull/6158)) +- Make the tombstone merge time threshold configurable via `SentryAndroidOptions.setTombstoneMergeTimeThresholdMillis` and the `io.sentry.tombstone.merge-time-threshold-millis` manifest option ([#6154](https://github.com/getsentry/sentry-java/pull/6154)) ## 8.58.0 diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 9a8b9d835db..efd776cabb8 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -435,6 +435,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun getNdkHandlerStrategy ()I public fun getScreenshot ()Lio/sentry/android/core/SentryScreenshotOptions; public fun getStartupCrashDurationThresholdMillis ()J + public fun getTombstoneMergeTimeThresholdMillis ()J public fun isAnrEnabled ()Z public fun isAnrProfilingEnabled ()Z public fun isAnrReportInDebug ()Z @@ -505,6 +506,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setReportHistoricalMemoryLimiterExits (Z)V public fun setReportHistoricalTombstones (Z)V public fun setTombstoneEnabled (Z)V + public fun setTombstoneMergeTimeThresholdMillis (J)V } public abstract interface class io/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index b5ef861a1a4..7de5a0c2716 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -52,6 +52,8 @@ final class ManifestMetadataReader { static final String TOMBSTONE_ENABLE = "io.sentry.tombstone.enable"; static final String TOMBSTONE_ATTACH_RAW = "io.sentry.tombstone.attach-raw"; static final String TOMBSTONE_REPORT_HISTORICAL = "io.sentry.tombstone.report-historical"; + static final String TOMBSTONE_MERGE_TIME_THRESHOLD_MILLIS = + "io.sentry.tombstone.merge-time-threshold-millis"; static final String MEMORY_LIMITER_ENABLE = "io.sentry.memory-limiter.enable"; static final String MEMORY_LIMITER_REPORT_HISTORICAL = "io.sentry.memory-limiter.report-historical"; @@ -278,6 +280,12 @@ static void applyMetadata( logger, TOMBSTONE_REPORT_HISTORICAL, options.isReportHistoricalTombstones())); + options.setTombstoneMergeTimeThresholdMillis( + readLong( + metadata, + logger, + TOMBSTONE_MERGE_TIME_THRESHOLD_MILLIS, + options.getTombstoneMergeTimeThresholdMillis())); options.setMemoryLimiterEnabled( readBool(metadata, logger, MEMORY_LIMITER_ENABLE, options.isMemoryLimiterEnabled())); options.setReportHistoricalMemoryLimiterExits( diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NativeEventCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/NativeEventCollector.java index 2cf5acd05fa..69dfa9c16be 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NativeEventCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NativeEventCollector.java @@ -38,8 +38,6 @@ public final class NativeEventCollector { private static final String NATIVE_PLATFORM = "native"; - private static final long TIMESTAMP_TOLERANCE_MS = 5000; - private final @NotNull SentryAndroidOptions options; /** Lightweight metadata collected during scan phase. */ @@ -175,18 +173,30 @@ public void collect() { // Lazily collect on first use (runs on executor thread, not main thread) collect(); + final long thresholdMs = options.getTombstoneMergeTimeThresholdMillis(); for (final NativeEnvelopeMetadata metadata : nativeEnvelopes) { final long timeDiff = Math.abs(tombstoneTimestampMs - metadata.getTimestampMs()); - if (timeDiff <= TIMESTAMP_TOLERANCE_MS) { + if (timeDiff <= thresholdMs) { options .getLogger() - .log(SentryLevel.DEBUG, "Matched native event by timestamp (diff: %d ms)", timeDiff); + .log( + SentryLevel.DEBUG, + "Matched native event by timestamp (diff: %d ms, threshold: %d ms)", + timeDiff, + thresholdMs); nativeEnvelopes.remove(metadata); // Only load full event data when we have a match return loadFullNativeEventData(metadata.getFile()); } } + options + .getLogger() + .log( + SentryLevel.DEBUG, + "No native event matched the tombstone timestamp %d within %d ms.", + tombstoneTimestampMs, + thresholdMs); return 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 825d0cbedb9..c135172bc5c 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 @@ -305,6 +305,13 @@ public interface BeforeCaptureCallback { private boolean enableTombstone = false; + /** + * The maximum time difference, in milliseconds, between a tombstone from {@link + * ApplicationExitInfo} and a native crash event in the outbox for the two to be merged into a + * single event. + */ + private long tombstoneMergeTimeThresholdMillis = 5000; + /** * Screenshot masking options. Configure which views should be masked when capturing screenshots * on error events. @@ -752,6 +759,34 @@ public void setAttachAnrThreadDump(final boolean attachAnrThreadDump) { this.attachAnrThreadDump = attachAnrThreadDump; } + public long getTombstoneMergeTimeThresholdMillis() { + return tombstoneMergeTimeThresholdMillis; + } + + /** + * Sets the maximum time difference, in milliseconds, between a tombstone from {@link + * ApplicationExitInfo} and a native crash event in the outbox for the two to be merged into a + * single event. Defaults to 5000 ms. + * + *

The two timestamps come from different sources: the tombstone timestamp is recorded by the + * system when the process died, the native event timestamp is recorded by the SDK signal handler. + * Raise the threshold when crashes are reported as separate 'signalhandler' events instead of a + * merged event, because the gap between the two exceeded the threshold. + * + *

Do not raise it more than necessary. The threshold is the only criterion used to pair the + * two, so a high value can merge a tombstone with a native crash that belongs to a different + * process death. The merged event then reports the wrong stack trace, and the native crash it + * consumed is never sent on its own. + * + *

A value of 0 merges only events with identical timestamps. A negative value disables merging + * completely, so tombstone and native crash are both reported as separate events. + * + * @param tombstoneMergeTimeThresholdMillis the threshold in milliseconds + */ + public void setTombstoneMergeTimeThresholdMillis(final long tombstoneMergeTimeThresholdMillis) { + this.tombstoneMergeTimeThresholdMillis = tombstoneMergeTimeThresholdMillis; + } + public boolean isAttachRawTombstone() { return attachRawTombstone; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java index 48832ca8dce..43de4034ec5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java @@ -241,7 +241,7 @@ public void markReported(final long timestamp) { nativeEventCollector.findAndRemoveMatchingNativeEvent(tombstoneTimestamp); if (matchingNativeEvent == null) { - options.getLogger().log(SentryLevel.DEBUG, "No matching native event found for tombstone."); + // NativeEventCollector already logs why no event matched. return null; } 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 229e22d55f2..463b5d82ddb 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 @@ -464,6 +464,31 @@ class ManifestMetadataReaderTest { assertEquals(false, fixture.options.isReportHistoricalTombstones) } + @Test + fun `applyMetadata reads tombstone merge time threshold to options`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.TOMBSTONE_MERGE_TIME_THRESHOLD_MILLIS to 10000) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(10000, fixture.options.tombstoneMergeTimeThresholdMillis) + } + + @Test + fun `applyMetadata reads tombstone merge time threshold to options and keeps default`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(5000, fixture.options.tombstoneMergeTimeThresholdMillis) + } + @Test fun `applyMetadata reads anr report historical to options`() { // Arrange diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/NativeEventCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/NativeEventCollectorTest.kt index 243c20a2069..490847b3182 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/NativeEventCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/NativeEventCollectorTest.kt @@ -179,6 +179,35 @@ class NativeEventCollectorTest { assertNull(noMatch) } + @Test + fun `does not match when the gap exceeds the default threshold`() { + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("native-event.txt") + + val timestamp = DateUtils.getDateTime("2023-07-15T10:30:05.800Z").time + assertNull(sut.findAndRemoveMatchingNativeEvent(timestamp)) + } + + @Test + fun `matches when the gap is within a raised threshold`() { + fixture.options.tombstoneMergeTimeThresholdMillis = 10000 + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("native-event.txt") + + val timestamp = DateUtils.getDateTime("2023-07-15T10:30:05.800Z").time + assertNotNull(sut.findAndRemoveMatchingNativeEvent(timestamp)) + } + + @Test + fun `does not match when the gap exceeds a lowered threshold`() { + fixture.options.tombstoneMergeTimeThresholdMillis = 1000 + val sut = fixture.getSut(tmpDir) + copyEnvelopeToOutbox("native-event.txt") + + val timestamp = DateUtils.getDateTime("2023-07-15T10:30:02.000Z").time + assertNull(sut.findAndRemoveMatchingNativeEvent(timestamp)) + } + private fun copyEnvelopeToOutbox(name: String): File { val resourcePath = "envelopes/$name" val inputStream = 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 94857b91058..d580ff54cd0 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 @@ -246,6 +246,15 @@ class SentryAndroidOptionsTest { assertEquals(5000L, sentryOptions.ndkAppHangTimeoutIntervalMillis) } + @Test + fun `tombstone merge time threshold defaults to 5s and is configurable`() { + val sentryOptions = SentryAndroidOptions() + assertEquals(5000L, sentryOptions.tombstoneMergeTimeThresholdMillis) + + sentryOptions.tombstoneMergeTimeThresholdMillis = 10000L + assertEquals(10000L, sentryOptions.tombstoneMergeTimeThresholdMillis) + } + private class CustomDebugImagesLoader : IDebugImagesLoader { override fun loadDebugImages(): List? = null From cf43facdf3937dc9804b8b0ee2e29e14b9c78213 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 13:09:45 +0200 Subject: [PATCH 06/13] chore(deps): bump the github-actions group across 1 directory with 6 updates (#6169) Bumps the github-actions group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github/codeql-action/init](https://github.com/github/codeql-action) | `4.38.0` | `4.38.2` | | [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.38.0` | `4.38.2` | | [getsentry/github-workflows/danger](https://github.com/getsentry/github-workflows) | `3.4.0` | `3.4.1` | | [getsentry/craft](https://github.com/getsentry/craft) | `2.31.0` | `2.31.2` | | [getsentry/github-workflows/updater](https://github.com/getsentry/github-workflows) | `3.4.0` | `3.4.1` | | [getsentry/github-workflows/validate-pr](https://github.com/getsentry/github-workflows) | `3.4.0` | `3.4.1` | Updates `github/codeql-action/init` from 4.38.0 to 4.38.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2) Updates `github/codeql-action/analyze` from 4.38.0 to 4.38.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2) Updates `getsentry/github-workflows/danger` from 3.4.0 to 3.4.1 - [Release notes](https://github.com/getsentry/github-workflows/releases) - [Changelog](https://github.com/getsentry/github-workflows/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/github-workflows/compare/607fed74f812e69201531a5185b6c3c57caa4e89...959162c159b866489d3fabe93327f48f9b8cb7ee) Updates `getsentry/craft` from 2.31.0 to 2.31.2 - [Release notes](https://github.com/getsentry/craft/releases) - [Changelog](https://github.com/getsentry/craft/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/craft/compare/55694f8bf3a0fd051e21a877f57b4c0dd8e946bb...25028d0646040cc0a669ae3314cbf884f4c4347a) Updates `getsentry/github-workflows/updater` from 3.4.0 to 3.4.1 - [Release notes](https://github.com/getsentry/github-workflows/releases) - [Changelog](https://github.com/getsentry/github-workflows/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/github-workflows/compare/607fed74f812e69201531a5185b6c3c57caa4e89...959162c159b866489d3fabe93327f48f9b8cb7ee) Updates `getsentry/github-workflows/validate-pr` from 3.4.0 to 3.4.1 - [Release notes](https://github.com/getsentry/github-workflows/releases) - [Changelog](https://github.com/getsentry/github-workflows/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/github-workflows/compare/607fed74f812e69201531a5185b6c3c57caa4e89...959162c159b866489d3fabe93327f48f9b8cb7ee) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.38.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.38.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: getsentry/github-workflows/danger dependency-version: 3.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: getsentry/craft dependency-version: 2.31.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: getsentry/github-workflows/updater dependency-version: 3.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: getsentry/github-workflows/validate-pr dependency-version: 3.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/danger.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/update-deps.yml | 2 +- .github/workflows/validate-pr.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2efc786187c..3fc57ffad65 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Initialize CodeQL - uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # pin@v2 + uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # pin@v2 with: languages: 'java' @@ -45,4 +45,4 @@ jobs: ./gradlew buildForCodeQL --no-build-cache - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # pin@v2 + uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # pin@v2 diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index e40b4563b00..a39e3f8906a 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -8,4 +8,4 @@ jobs: danger: runs-on: ubuntu-latest steps: - - uses: getsentry/github-workflows/danger@607fed74f812e69201531a5185b6c3c57caa4e89 # v3 + - uses: getsentry/github-workflows/danger@959162c159b866489d3fabe93327f48f9b8cb7ee # v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2696d4f4758..6dd30d341b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: fetch-depth: 0 submodules: 'recursive' - name: Prepare release - uses: getsentry/craft@55694f8bf3a0fd051e21a877f57b4c0dd8e946bb # v2 + uses: getsentry/craft@25028d0646040cc0a669ae3314cbf884f4c4347a # v2 env: GITHUB_TOKEN: ${{ steps.token.outputs.token }} with: diff --git a/.github/workflows/update-deps.yml b/.github/workflows/update-deps.yml index 5b8d3d11628..ef51d5da084 100644 --- a/.github/workflows/update-deps.yml +++ b/.github/workflows/update-deps.yml @@ -18,7 +18,7 @@ jobs: native: runs-on: ubuntu-latest steps: - - uses: getsentry/github-workflows/updater@607fed74f812e69201531a5185b6c3c57caa4e89 # v3 + - uses: getsentry/github-workflows/updater@959162c159b866489d3fabe93327f48f9b8cb7ee # v3 with: path: scripts/update-sentry-native-ndk.sh name: Native SDK diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index ca5108943de..307c7474be9 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -10,7 +10,7 @@ jobs: permissions: pull-requests: write steps: - - uses: getsentry/github-workflows/validate-pr@607fed74f812e69201531a5185b6c3c57caa4e89 # v3 + - uses: getsentry/github-workflows/validate-pr@959162c159b866489d3fabe93327f48f9b8cb7ee # v3 with: app-id: ${{ vars.SDK_MAINTAINER_BOT_APP_ID }} private-key: ${{ secrets.SDK_MAINTAINER_BOT_PRIVATE_KEY }} From 893cf0a8fc0c5249e57b7d4345cc7e194f90621f Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 28 Sep 2026 14:11:36 +0200 Subject: [PATCH 07/13] build(detekt): Ignore FunctionNaming for Composable functions (#6176) Compose functions that emit UI are PascalCase by convention, which detekt's default FunctionNaming pattern flags. Follow detekt's Compose guide and skip the rule for @Composable functions. Fixes JAVA-748 Fixes #6175 Co-authored-by: Claude Opus 5.5 (1M context) --- detekt.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/detekt.yml b/detekt.yml index 49f91ae64fb..09d69d77761 100644 --- a/detekt.yml +++ b/detekt.yml @@ -1,3 +1,6 @@ style: ReturnCount: max: 3 +naming: + FunctionNaming: + ignoreAnnotated: ['Composable'] From 63926e523d29872ac5c6e2af61bcca1800af2a14 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 28 Sep 2026 14:14:01 +0200 Subject: [PATCH 08/13] chore(android): Compile Android modules against Android SDK 37.2 (#6172) Updates our compile SDK version from 37.0 to 37.2 for all Sentry Android modules, which lays the groundwork for accommodating new MemoryLimiter flags introduced in 37.2. Introduces a mechanism that lets us support compilation against minor SDK versions for any future release. --- gradle/libs.versions.toml | 1 + sentry-android-core/build.gradle.kts | 1 + sentry-android-distribution/build.gradle.kts | 1 + sentry-android-fragment/build.gradle.kts | 1 + .../sentry-uitest-android-critical/build.gradle.kts | 1 + .../sentry-uitest-android-macrobenchmark/build.gradle.kts | 1 + .../sentry-uitest-android/build.gradle.kts | 1 + sentry-android-integration-tests/test-app-size/build.gradle.kts | 1 + sentry-android-navigation/build.gradle.kts | 1 + sentry-android-ndk/build.gradle.kts | 1 + sentry-android-replay/build.gradle.kts | 1 + sentry-android-sqlite/build.gradle.kts | 1 + sentry-android-timber/build.gradle.kts | 1 + sentry-android/build.gradle.kts | 1 + sentry-compose/build.gradle.kts | 1 + sentry-launchdarkly-android/build.gradle.kts | 1 + sentry-samples/sentry-samples-android/build.gradle.kts | 1 + 17 files changed, 17 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fbe9ef0177a..bcab265e5a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,6 +49,7 @@ sqldelight = "2.3.2" # Android targetSdk = "37" compileSdk = "37" +compileSdkMinor = "2" minSdk = "21" [plugins] diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index 75b77752349..72a526a7a5e 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -12,6 +12,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.android.core" defaultConfig { diff --git a/sentry-android-distribution/build.gradle.kts b/sentry-android-distribution/build.gradle.kts index c699c364c3b..bd88690c1cc 100644 --- a/sentry-android-distribution/build.gradle.kts +++ b/sentry-android-distribution/build.gradle.kts @@ -8,6 +8,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.android.distribution" defaultConfig { minSdk = libs.versions.minSdk.get().toInt() } diff --git a/sentry-android-fragment/build.gradle.kts b/sentry-android-fragment/build.gradle.kts index 3ef1c1934f8..e613f1fd5bb 100644 --- a/sentry-android-fragment/build.gradle.kts +++ b/sentry-android-fragment/build.gradle.kts @@ -11,6 +11,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.android.fragment" defaultConfig { diff --git a/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts index 6f875e7a5e9..9ca0c1f6d9d 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts @@ -9,6 +9,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.uitest.android.critical" signingConfigs { diff --git a/sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/build.gradle.kts index a00d76d6029..18a5b6cb7f5 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android-macrobenchmark/build.gradle.kts @@ -8,6 +8,7 @@ plugins { android { namespace = "io.sentry.uitest.android.macrobenchmark" compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() defaultConfig { // Macrobenchmark requires API 23+. diff --git a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts index 52c17199e4d..85ff2b1a3c7 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts @@ -13,6 +13,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.uitest.android" defaultConfig { diff --git a/sentry-android-integration-tests/test-app-size/build.gradle.kts b/sentry-android-integration-tests/test-app-size/build.gradle.kts index 20af6dad6ae..7bebbc6cfd0 100644 --- a/sentry-android-integration-tests/test-app-size/build.gradle.kts +++ b/sentry-android-integration-tests/test-app-size/build.gradle.kts @@ -6,6 +6,7 @@ plugins { android { namespace = "io.sentry.tests.size" compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() defaultConfig { applicationId = "io.sentry.tests.size" diff --git a/sentry-android-navigation/build.gradle.kts b/sentry-android-navigation/build.gradle.kts index 6c1aa62a57d..19b9b3c753f 100644 --- a/sentry-android-navigation/build.gradle.kts +++ b/sentry-android-navigation/build.gradle.kts @@ -11,6 +11,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.android.navigation" defaultConfig { diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index 6867d964124..dff85906ca1 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -8,6 +8,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.android.ndk" defaultConfig { diff --git a/sentry-android-replay/build.gradle.kts b/sentry-android-replay/build.gradle.kts index 6d03ba771b0..0a40a920a7c 100644 --- a/sentry-android-replay/build.gradle.kts +++ b/sentry-android-replay/build.gradle.kts @@ -14,6 +14,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.android.replay" defaultConfig { diff --git a/sentry-android-sqlite/build.gradle.kts b/sentry-android-sqlite/build.gradle.kts index 9637b91546a..fd15526fcbe 100644 --- a/sentry-android-sqlite/build.gradle.kts +++ b/sentry-android-sqlite/build.gradle.kts @@ -11,6 +11,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.android.sqlite" defaultConfig { diff --git a/sentry-android-timber/build.gradle.kts b/sentry-android-timber/build.gradle.kts index 3c8ac1ea1e4..f66c1a8a96b 100644 --- a/sentry-android-timber/build.gradle.kts +++ b/sentry-android-timber/build.gradle.kts @@ -11,6 +11,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.android.timber" defaultConfig { diff --git a/sentry-android/build.gradle.kts b/sentry-android/build.gradle.kts index a2d92829b29..4c493e9e2d2 100644 --- a/sentry-android/build.gradle.kts +++ b/sentry-android/build.gradle.kts @@ -6,6 +6,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.android" defaultConfig { minSdk = libs.versions.minSdk.get().toInt() } diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index 28d88f28a2b..c5bb5715830 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -79,6 +79,7 @@ kotlin { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.compose" defaultConfig { diff --git a/sentry-launchdarkly-android/build.gradle.kts b/sentry-launchdarkly-android/build.gradle.kts index f201c57b97d..ddc982e9d6a 100644 --- a/sentry-launchdarkly-android/build.gradle.kts +++ b/sentry-launchdarkly-android/build.gradle.kts @@ -6,6 +6,7 @@ plugins { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.launchdarkly.android" defaultConfig { diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index 6cb2302d7c7..40e5bbc9822 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -43,6 +43,7 @@ plugins.withId("io.sentry.android.gradle") { android { compileSdk = libs.versions.compileSdk.get().toInt() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInt() namespace = "io.sentry.samples.android" defaultConfig { From f24a0018ace637ba1cf91d075d4157e3a4861cb5 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 28 Sep 2026 15:23:07 +0200 Subject: [PATCH 09/13] ref(android): Match MemoryLimiter app exit reason introduced in Android 37.2 (JAVA-687) (#6174) Android 37.2 introduced a new ApplicationExitInfo.REASON_MEMORY_LIMITER flag. This commit updates our MemoryLimiterIntegration code to match against it (while preserving ouro previous matching logic). No new Android API guard is needed for the new flag because: - we already check MemoryLimiterIntegration registration against API 37; and - REASON_MEMORY_LIMITER is a static integer constant and will be inlined as an integer by the Java compiler (meaning we don't need extra protection for devices on 37.0 or 37.1). --- CHANGELOG.md | 4 ++ .../core/MemoryLimiterIntegration.java | 30 +++++---- .../core/MemoryLimiterIntegrationTest.kt | 67 +++++++++++++------ .../io/sentry/samples/android/MainActivity.kt | 3 +- .../android/memory/MemoryLimiterActivity.kt | 22 ++++-- 5 files changed, 87 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec63c894fc8..7df4c039d03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Deprecate `sendDefaultPii` in favor of `dataCollection` ahead of its removal in 9.0 ([#6158](https://github.com/getsentry/sentry-java/pull/6158)) - Make the tombstone merge time threshold configurable via `SentryAndroidOptions.setTombstoneMergeTimeThresholdMillis` and the `io.sentry.tombstone.merge-time-threshold-millis` manifest option ([#6154](https://github.com/getsentry/sentry-java/pull/6154)) +### Improvements + +- Recover Android 17 `MemoryLimiter` app exits recorded as `ApplicationExitInfo.REASON_MEMORY_LIMITER` ([#6174](https://github.com/getsentry/sentry-java/pull/6174)) + ## 8.58.0 ### Features diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/MemoryLimiterIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/MemoryLimiterIntegration.java index 648d8a4adac..f259250a733 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/MemoryLimiterIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/MemoryLimiterIntegration.java @@ -190,24 +190,30 @@ private MemoryLimiterPolicy(final @NotNull SentryAndroidOptions options) { /** * Returns true if the provided {@code exitInfo} looks like it came from a MemoryLimiter-induced * process death. - * - *

Criteria taken from here. */ @Override @RequiresApi(api = Build.VERSION_CODES.R) public boolean matches(final @NotNull ApplicationExitInfo exitInfo) { - if (exitInfo.getReason() != ApplicationExitInfo.REASON_OTHER) { - return false; + final int reason = exitInfo.getReason(); + + // 1. Return true for explicit matches. + if (reason == ApplicationExitInfo.REASON_MEMORY_LIMITER) { + return true; } - final String description = exitInfo.getDescription(); - // We match on the "MemoryLimiter:" prefix rather than the full "MemoryLimiter:AnonSwap" - // string mentioned in the Android 17 release notes because we want to capture any future - // MemoryLimiter kill reason without a code change. (MemoryLimiter source already tracks - // MemoryLimiter:Memory and MemoryLimiter:Swap, but for now doesn't kill the process because - // of them.) - return description != null && description.contains(MEMORY_LIMITER_DESCRIPTION_PREFIX); + // 2. Return true for heuristic matches based on criteria discussed here: + // https://developer.android.com/about/versions/17/behavior-changes-all#app-memory-limits + if (reason == ApplicationExitInfo.REASON_OTHER) { + final String description = exitInfo.getDescription(); + // We match on the "MemoryLimiter:" prefix rather than the full "MemoryLimiter:AnonSwap" + // string mentioned in the Android 17 release notes because we want to capture any future + // MemoryLimiter kill reason without a code change. (MemoryLimiter source already tracks + // MemoryLimiter:Memory and MemoryLimiter:Swap, but for now doesn't kill the process because + // of them.) + return description != null && description.contains(MEMORY_LIMITER_DESCRIPTION_PREFIX); + } + + return false; } @Override diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/MemoryLimiterIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/MemoryLimiterIntegrationTest.kt index 183ca3dfbdd..0b72fb6e894 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/MemoryLimiterIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/MemoryLimiterIntegrationTest.kt @@ -214,8 +214,30 @@ class MemoryLimiterIntegrationTest { verify(fixture.options.executorService).submit(any()) } + // "Explicit records" are ApplicationExitInfo instances that use + // ApplicationExitInfo.REASON_MEMORY_LIMITER to indicate a MemoryLimiter kill. Contrast "heuristic + // records" below. @Test - fun `captures exit when reason and description match exits produced by MemoryLimiter`() { + fun `captures exit for explicit MemoryLimiter record`() { + val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) + fixture.addAppExitInfo( + reason = ApplicationExitInfo.REASON_MEMORY_LIMITER, + description = null, + timestamp = newTimestamp, + ) + + integration.register(fixture.scopes, fixture.options) + + verify(fixture.scopes).captureEvent(any(), anyOrNull()) + } + + // "Heuristic records" are ApplicationExitInfo instances that use something other than + // ApplicationExitInfo.REASON_MEMORY_LIMITER to indicate a MemoryLimiter kill. + // (REASON_MEMORY_LIMITER wasn't introduced until 37.2.) They consist of + // ApplicationExitInfo.REASON_OTHER + an ApplicationExitInfo.description that contains the prefix + // "MemoryLimiter:". + @Test + fun `captures exit for heuristic MemoryLimiter record`() { val integration = fixture.getSut( memoryLimiterEnabled = true, @@ -285,25 +307,34 @@ class MemoryLimiterIntegrationTest { } @Test - fun `ignores exit when reason does not match exits produced by MemoryLimiter`() { + fun `captures exit for heuristic record even if description suffix is not AnonSwap`() { val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) fixture.addAppExitInfo( - reason = ApplicationExitInfo.REASON_ANR, - description = MemoryLimiterIntegration.MEMORY_LIMITER_DESCRIPTION, + reason = ApplicationExitInfo.REASON_OTHER, + // Description suffix is "Memory" rather than "AnonSwap". + description = "MemoryLimiter:Memory", timestamp = newTimestamp, ) integration.register(fixture.scopes, fixture.options) - verify(fixture.scopes, never()).captureEvent(any(), anyOrNull()) + verify(fixture.scopes) + .captureEvent( + check { event -> + assertEquals("MemoryLimiter:Memory", event.exceptions!!.single().mechanism!!.description) + }, + anyOrNull(), + ) } + // "Heuristic candidates" are ApplicationExitInfo instances that may or may not be heuristic + // records. @Test - fun `ignores exit when description does not match exits produced by MemoryLimiter`() { + fun `ignores exit for heuristic candidate when reason does not match`() { val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) fixture.addAppExitInfo( - reason = ApplicationExitInfo.REASON_OTHER, - description = "LowSwapKiller", + reason = ApplicationExitInfo.REASON_ANR, + description = MemoryLimiterIntegration.MEMORY_LIMITER_DESCRIPTION, timestamp = newTimestamp, ) @@ -313,29 +344,21 @@ class MemoryLimiterIntegrationTest { } @Test - fun `captures exit for any MemoryLimiter sub-reason, not just AnonSwap`() { + fun `ignores exit for heuristic candidate when description does not match`() { val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) - // A future MemoryLimiter kill sub-reason (e.g. the memory or swap limits) still lives in the - // "MemoryLimiter:" namespace and must be captured, with its raw sub-reason preserved. fixture.addAppExitInfo( reason = ApplicationExitInfo.REASON_OTHER, - description = "MemoryLimiter:Memory", + description = "LowSwapKiller", timestamp = newTimestamp, ) integration.register(fixture.scopes, fixture.options) - verify(fixture.scopes) - .captureEvent( - check { event -> - assertEquals("MemoryLimiter:Memory", event.exceptions!!.single().mechanism!!.description) - }, - anyOrNull(), - ) + verify(fixture.scopes, never()).captureEvent(any(), anyOrNull()) } @Test - fun `ignores exit when description mentions MemoryLimiter without the namespace delimiter`() { + fun `ignores exit for heuristic candidate when description mentions MemoryLimiter without the namespace delimiter`() { val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) // "MemoryLimiter" without the ":" delimiter is not a MemoryLimiter kill; matching requires the // namespace prefix so we don't over-capture unrelated REASON_OTHER exits. @@ -351,7 +374,7 @@ class MemoryLimiterIntegrationTest { } @Test - fun `ignores exit when description is null`() { + fun `ignores exit for heuristic candidate when description is null`() { val integration = fixture.getSut(tmpDir, lastReportedTimestamp = oldTimestamp) fixture.addAppExitInfo( reason = ApplicationExitInfo.REASON_OTHER, @@ -481,7 +504,7 @@ class MemoryLimiterIntegrationTest { } @Test - fun `dedupes MemoryLimiter exists independently of ANR exits`() { + fun `dedupes MemoryLimiter exits independently of ANR exits`() { val integration = fixture.getSut( tmpDir, diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt index c923282527e..31bf86c7db2 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt @@ -76,6 +76,7 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -992,7 +993,7 @@ fun IntegrationsScreen() { tagSampleAction("memory_limiter") activity.startActivity(Intent(activity, MemoryLimiterActivity::class.java)) }, - modifier = Modifier, + modifier = Modifier.testTag("open_memory_limiter_demo"), ) { Text("MemoryLimiter Demo", maxLines = 2, overflow = TextOverflow.Ellipsis) } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/memory/MemoryLimiterActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/memory/MemoryLimiterActivity.kt index 6f9c650590b..087cfdbb8a1 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/memory/MemoryLimiterActivity.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/memory/MemoryLimiterActivity.kt @@ -47,6 +47,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -307,13 +308,25 @@ private fun MemoryLimiterScreen( horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - Button(onClick = onStartAllocating, enabled = isSupported && !isAllocating) { + Button( + onClick = onStartAllocating, + enabled = isSupported && !isAllocating, + modifier = Modifier.testTag("memory_limiter_start_pressure"), + ) { Text("Start memory pressure") } - OutlinedButton(onClick = onReleaseBuffers, enabled = allocatedMb > 0 || isAllocating) { + OutlinedButton( + onClick = onReleaseBuffers, + enabled = allocatedMb > 0 || isAllocating, + modifier = Modifier.testTag("memory_limiter_reset_run"), + ) { Text("Reset run") } - OutlinedButton(onClick = onMoveToBackground, enabled = isSupported) { + OutlinedButton( + onClick = onMoveToBackground, + enabled = isSupported, + modifier = Modifier.testTag("memory_limiter_move_background"), + ) { Text("Move app to background") } } @@ -431,7 +444,7 @@ private fun RuntimeCard( isAllocating: Boolean, statusLine: String, ) { - ElevatedCard { + ElevatedCard(modifier = Modifier.testTag("memory_limiter_runtime_card")) { Column(modifier = Modifier.padding(18.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( text = "Runtime", @@ -445,6 +458,7 @@ private fun RuntimeCard( RuntimeRow(label = "Retained", value = "$allocatedMb MiB") HorizontalDivider() Text( + modifier = Modifier.testTag("memory_limiter_status_line"), text = statusLine, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, From 42e4a2aadd9ecde9873a038241f624cce4ad0574 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Mon, 28 Sep 2026 17:17:04 +0200 Subject: [PATCH 10/13] build(detekt): Apply remaining Compose guide settings (#6179) Add the rest of detekt's Compose guide on top of the FunctionNaming change from #6176. Keep accepting SCREAMING_CASE top-level constants, because the guide's PascalCase-only pattern would flag every existing TRACE_ORIGIN-style constant. Refs JAVA-748 Co-authored-by: Claude Opus 5.5 (1M context) --- detekt.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/detekt.yml b/detekt.yml index 09d69d77761..e8a01a87135 100644 --- a/detekt.yml +++ b/detekt.yml @@ -1,6 +1,16 @@ style: ReturnCount: max: 3 + MagicNumber: + ignorePropertyDeclaration: true + UnusedPrivateMember: + ignoreAnnotated: ['Preview'] naming: FunctionNaming: ignoreAnnotated: ['Composable'] + TopLevelPropertyNaming: + # Accept both SCREAMING_CASE and Compose's PascalCase constants. + constantPattern: '[A-Z][_A-Za-z0-9]*' +complexity: + TooManyFunctions: + ignoreAnnotatedFunctions: ['Preview'] From 93d2004d674c08529725f4efe0531bb971cd913a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 29 Sep 2026 05:56:14 +0200 Subject: [PATCH 11/13] docs: Link Data Collection changelog to guides (#6180) Replace duplicated configuration details in the 8.58.0 changelog with links to the Android, Java, and Spring Boot documentation while retaining the sendDefaultPii removal warning. Co-authored-by: Claude --- CHANGELOG.md | 116 +-------------------------------------------------- 1 file changed, 1 insertion(+), 115 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7df4c039d03..02d7ed4a92e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,125 +16,11 @@ ### Features - Add `LocalSentrySpan` to `sentry-compose` so apps can provide a parent `ISpan` to a composable subtree and have nested `SentryTraced` spans attach to it ([#6112]https://github.com/getsentry/sentry-java/pull/6112) -- Add `dataCollection`, a fine-grained replacement for `sendDefaultPii`, for controlling data collected automatically by SDK integrations ([#5759](https://github.com/getsentry/sentry-java/pull/5759)) +- Add `dataCollection`, a fine-grained replacement for `sendDefaultPii`, for controlling data collected automatically by SDK integrations. See the Data Collection documentation for [Android](https://docs.sentry.io/platforms/android/configuration/options/#dataCollection), [Java](https://docs.sentry.io/platforms/java/configuration/options/#dataCollection), and [Spring Boot](https://docs.sentry.io/platforms/java/guides/spring-boot/configuration/options/#dataCollection) ([#5759](https://github.com/getsentry/sentry-java/pull/5759)) > [!WARNING] > `sendDefaultPii` will be removed in the next major SDK version. Migrate to `dataCollection` before upgrading. - - Until then, when `dataCollection` is not configured, the SDK preserves the existing `sendDefaultPii` behavior. - - Configuring any `dataCollection` option makes it the source of truth. `sendDefaultPii` is then ignored, and omitted `dataCollection` options use the defaults below. - - The Logback appender is a compatibility exception. When an encoder is configured, `sendDefaultPii=true` continues to include the original message template and parameters. To opt in independently of `sendDefaultPii`, set `true` on the Sentry appender in `logback.xml` or `logback-spring.xml`. - - Data explicitly supplied through APIs such as `Sentry.setUser`, scopes, event processors, or `beforeSend` is not affected. - - To opt in to the documented `dataCollection` defaults without configuring an individual option: - - ```java - Sentry.init(options -> options.getDataCollection().forceDataCollection()); - ``` - - | Option | Default | Behavior | - | --- | --- | --- | - | `userInfo` | `true` | Allows integrations to populate user identity and IP address information automatically. | - | `cookies` | `{ mode: DENY_LIST, terms: [] }` | Collects cookies while filtering sensitive values. | - | `httpHeaders.request` | `{ mode: DENY_LIST, terms: [] }` | Collects request headers while filtering sensitive values. | - | `httpHeaders.response` | `{ mode: DENY_LIST, terms: [] }` | Collects response headers while filtering sensitive values. | - | `httpBodies` | All supported body types | Collects supported incoming and outgoing request and response bodies. An empty set disables body collection. | - | `urlQueryParams` | `{ mode: DENY_LIST, terms: [] }` | Collects URL query parameters while filtering sensitive values. | - | `graphql.document` | `true` | Collects GraphQL documents. | - | `graphql.variables` | `true` | Collects GraphQL variables. | - | `databaseQueryData` | `true` | Allows collection of associated query data, such as bound parameters, write payloads, and results, where supported. Sanitized query statements and structural database metadata remain available. | - | `filePaths` | `true` | Allows file-system instrumentation to collect file and directory paths. File extensions and byte counts remain available when disabled. | - - Cookies, HTTP headers, and URL query parameters support three modes: - - - `OFF`: Do not collect the category. - - `DENY_LIST`: Collect values except those matching the built-in sensitive deny-list or additional configured terms. - - `ALLOW_LIST`: Only send plaintext values for matching terms. The built-in sensitive deny-list still applies. - - Matching is case-insensitive and partial. The built-in sensitive deny-list contains `auth`, `token`, `secret`, `password`, `passwd`, `pwd`, `key`, `jwt`, `bearer`, `sso`, `saml`, `csrf`, `xsrf`, `credentials`, `session`, `sid`, and `identity`. Filtered values are replaced with `"[Filtered]"`. Custom deny-list terms extend rather than replace this list. - - Configure all HTTP body types, a custom cookie deny-list, a request-header allow-list, and disable URL query parameter and file path collection in an options callback: - - ```java - Sentry.init( - options -> { - options - .getDataCollection() - .setHttpBodies( - EnumSet.of( - HttpBodyType.INCOMING_REQUEST, - HttpBodyType.OUTGOING_REQUEST, - HttpBodyType.INCOMING_RESPONSE, - HttpBodyType.OUTGOING_RESPONSE)); - options - .getDataCollection() - .setCookies( - KeyValueCollectionBehavior.denyList( - "forwarded", "-ip", "remote-", "via", "-user")); - options - .getDataCollection() - .getHttpHeaders() - .setRequest( - KeyValueCollectionBehavior.allowList("content-type", "x-request-id")); - options - .getDataCollection() - .setUrlQueryParams(KeyValueCollectionBehavior.off()); - options.getDataCollection().setFilePaths(false); - }); - ``` - - Configure the same options in `sentry.properties`: - - ```properties - data-collection.http-bodies=incoming_request,outgoing_request,incoming_response,outgoing_response - data-collection.cookies.mode=deny_list - data-collection.cookies.terms=forwarded,-ip,remote-,via,-user - data-collection.http-headers.request.mode=allow_list - data-collection.http-headers.request.terms=content-type,x-request-id - data-collection.url-query-params.mode=off - data-collection.file-paths=false - ``` - - Configure them with Spring Boot properties: - - ```properties - sentry.data-collection.http-bodies=incoming-request,outgoing-request,incoming-response,outgoing-response - sentry.data-collection.cookies.mode=deny-list - sentry.data-collection.cookies.terms=forwarded,-ip,remote-,via,-user - sentry.data-collection.http-headers.request.mode=allow-list - sentry.data-collection.http-headers.request.terms=content-type,x-request-id - sentry.data-collection.url-query-params.mode=off - sentry.data-collection.file-paths=false - ``` - - Configure them in `AndroidManifest.xml`: - - ```xml - - - - - - - - ``` - - See the [Data Collection documentation](https://docs.sentry.io/platforms/java/configuration/options/#dataCollection) for all configuration keys, supported integrations, and migration guidance. - ### Fixes - Disable URL caching when reading `META-INF/MANIFEST.MF` files during version detection so that the SDK no longer keeps jar file handles open for the life of the process ([#6124](https://github.com/getsentry/sentry-java/pull/6124) From 0f0be1c4182f0f02e0101602cf156ed86a69c2b3 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 29 Sep 2026 09:03:39 +0200 Subject: [PATCH 12/13] fix(core): Clear the persisted replay id when resetting the scope cache (#6033) * fix(core): Clear the persisted replay id when resetting the scope cache resetCache() clears every other persisted scope value on init but leaves replay.json in place, so a replay id written by a previous process can still be attached to events from the current one. The reset already runs after the integrations that consume those values, so deleting it here is safe. Co-Authored-By: Claude Opus 5 (1M context) * changelog * Remove comment Co-authored-by: Markus Hintersteiner --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Markus Hintersteiner --- CHANGELOG.md | 1 + .../sentry/cache/PersistingScopeObserver.java | 1 + .../PersistingScopeObserverBatchingTest.kt | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02d7ed4a92e..5476f6d9831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,7 @@ ### Fixes - Keep dropped tombstone and ANR events dropped, instead of reporting the same app exit again at every app start ([#6002](https://github.com/getsentry/sentry-java/pull/6002)) +- Clear the persisted replay id on SDK init, so a replay id from a previous process is no longer attached to ANR events from the current one ([#6033](https://github.com/getsentry/sentry-java/pull/6033)) - Apply `Sentry.withScope` and `Sentry.withIsolationScope` data to events captured inside the callback when `globalHubMode` is enabled ([#6004](https://github.com/getsentry/sentry-java/pull/6004)) - `globalHubMode` is enabled by default on Android, where tags, extras, contexts and level set inside the callback were silently dropped - Scopes that are explicitly made current, e.g. via `Sentry.setCurrentScopes` or the `SentryContext` coroutine integration, are now also honoured when `globalHubMode` is enabled diff --git a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java index d6137ae051a..8a097486db1 100644 --- a/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java +++ b/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java @@ -378,5 +378,6 @@ public void resetCache() { delete(TAGS_FILENAME); delete(TRACE_FILENAME); delete(TRANSACTION_FILENAME); + delete(REPLAY_FILENAME); } } diff --git a/sentry/src/test/java/io/sentry/cache/PersistingScopeObserverBatchingTest.kt b/sentry/src/test/java/io/sentry/cache/PersistingScopeObserverBatchingTest.kt index eec38e151ee..d893e52d4ce 100644 --- a/sentry/src/test/java/io/sentry/cache/PersistingScopeObserverBatchingTest.kt +++ b/sentry/src/test/java/io/sentry/cache/PersistingScopeObserverBatchingTest.kt @@ -6,7 +6,9 @@ import io.sentry.ISentryExecutorService import io.sentry.ISerializer import io.sentry.SentryOptions import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME +import io.sentry.cache.PersistingScopeObserver.REPLAY_FILENAME import io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME +import io.sentry.protocol.SentryId import io.sentry.test.DeferredExecutorService import java.io.Writer import java.util.concurrent.atomic.AtomicBoolean @@ -28,6 +30,9 @@ class PersistingScopeObserverBatchingTest { private fun PersistingScopeObserver.readTransaction(): String? = read(options, TRANSACTION_FILENAME, String::class.java) + private fun PersistingScopeObserver.readReplayId(): String? = + read(options, REPLAY_FILENAME, String::class.java) + @Suppress("UNCHECKED_CAST") private fun PersistingScopeObserver.readBreadcrumbs(): List = read(options, BREADCRUMBS_FILENAME, List::class.java) as List @@ -118,6 +123,20 @@ class PersistingScopeObserverBatchingTest { } } + @Test + fun `resetCache clears the replay id left behind by the previous process`() { + val executor = DeferredExecutorService() + val sut = getSut(executor) + + sut.setReplayId(SentryId("afcb46b1140ade5187c4bbb5daa804df")) + executor.runAll() + assertThat(sut.readReplayId()).isEqualTo("afcb46b1140ade5187c4bbb5daa804df") + + sut.resetCache() + + assertThat(sut.readReplayId()).isNull() + } + @Test fun `resetCache keeps pending mutations from the current process`() { val executor = DeferredExecutorService() From 91fbff18891b383877472b16e0adc93da9e5251e Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 29 Sep 2026 09:33:23 +0200 Subject: [PATCH 13/13] build(detekt): Lint all Kotlin modules (#6178) * build(detekt): Apply Compose guide and lint all Kotlin modules Add the remaining settings from detekt's Compose guide. Keep accepting SCREAMING_CASE top-level constants, because the guide's PascalCase-only pattern would flag every existing TRACE_ORIGIN-style constant. sentry-compose had the detekt plugin applied but the task was always NO-SOURCE, because the default source set is src/main and a multiplatform module keeps its code in src/androidMain. Point it at the Android source sets. Also enable detekt in sentry-android-replay, where it was commented out, and in the Kotlin modules that never applied it. The newly linted modules have 298 existing findings. Record them in per-module baselines so check passes and only new issues fail. They can be fixed in follow-ups. Refs JAVA-748 Co-Authored-By: Claude Opus 5.5 (1M context) * build(detekt): Move Compose guide settings to a separate PR Regenerate the baselines against main's config so this PR doesn't depend on the config change. Co-Authored-By: Claude Opus 5.5 (1M context) --------- Co-authored-by: Claude Opus 5.5 (1M context) --- build.gradle.kts | 1 + sentry-android-distribution/build.gradle.kts | 1 + .../detekt-baseline.xml | 24 +++ sentry-android-replay/build.gradle.kts | 3 +- sentry-android-replay/detekt-baseline.xml | 165 ++++++++++++++++++ sentry-apollo-3/build.gradle.kts | 1 + sentry-apollo-3/detekt-baseline.xml | 33 ++++ sentry-apollo-4/build.gradle.kts | 1 + sentry-apollo-4/detekt-baseline.xml | 34 ++++ sentry-apollo/build.gradle.kts | 1 + sentry-apollo/detekt-baseline.xml | 10 ++ sentry-compose/build.gradle.kts | 3 + sentry-compose/detekt-baseline.xml | 20 +++ sentry-ktor-client/build.gradle.kts | 1 + sentry-ktor-client/detekt-baseline.xml | 15 ++ sentry-okhttp/build.gradle.kts | 1 + sentry-okhttp/detekt-baseline.xml | 10 ++ 17 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 sentry-android-distribution/detekt-baseline.xml create mode 100644 sentry-android-replay/detekt-baseline.xml create mode 100644 sentry-apollo-3/detekt-baseline.xml create mode 100644 sentry-apollo-4/detekt-baseline.xml create mode 100644 sentry-apollo/detekt-baseline.xml create mode 100644 sentry-compose/detekt-baseline.xml create mode 100644 sentry-ktor-client/detekt-baseline.xml create mode 100644 sentry-okhttp/detekt-baseline.xml diff --git a/build.gradle.kts b/build.gradle.kts index 4f7cdb7cbb2..52e17b1d309 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -117,6 +117,7 @@ subprojects { buildUponDefaultConfig = true allRules = true config.setFrom("${rootProject.rootDir}/detekt.yml") + baseline = file("detekt-baseline.xml") } } diff --git a/sentry-android-distribution/build.gradle.kts b/sentry-android-distribution/build.gradle.kts index bd88690c1cc..074d56c08e6 100644 --- a/sentry-android-distribution/build.gradle.kts +++ b/sentry-android-distribution/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { id("com.android.library") alias(libs.plugins.kotlin.android) + alias(libs.plugins.detekt) } android { diff --git a/sentry-android-distribution/detekt-baseline.xml b/sentry-android-distribution/detekt-baseline.xml new file mode 100644 index 00000000000..78b4ed376de --- /dev/null +++ b/sentry-android-distribution/detekt-baseline.xml @@ -0,0 +1,24 @@ + + + + + MagicNumber:DistributionHttpClient.kt$DistributionHttpClient$200 + MagicNumber:DistributionHttpClient.kt$DistributionHttpClient$299 + MagicNumber:DistributionHttpClient.kt$DistributionHttpClient.HttpResponse$200 + MagicNumber:DistributionHttpClient.kt$DistributionHttpClient.HttpResponse$299 + MagicNumber:UpdateResponseParser.kt$UpdateResponseParser$200 + MagicNumber:UpdateResponseParser.kt$UpdateResponseParser$400 + MagicNumber:UpdateResponseParser.kt$UpdateResponseParser$499 + MagicNumber:UpdateResponseParser.kt$UpdateResponseParser$500 + MagicNumber:UpdateResponseParser.kt$UpdateResponseParser$599 + MaxLineLength:DistributionHttpClient.kt$DistributionHttpClient$"/api/0/projects/${URLEncoder.encode(orgSlug, "UTF-8")}/${URLEncoder.encode(projectSlug, "UTF-8")}/preprodartifacts/check-for-updates/" + SwallowedException:DistributionIntegration.kt$DistributionIntegration$e: android.content.ActivityNotFoundException + TooGenericExceptionCaught:DistributionIntegration.kt$DistributionIntegration$e: Exception + TooGenericExceptionCaught:UpdateResponseParser.kt$UpdateResponseParser$e: Exception + UseCheckOrError:DistributionHttpClient.kt$DistributionHttpClient$throw IllegalStateException( "Missing required distribution configuration: orgSlug, projectSlug, or orgAuthToken" ) + UseCheckOrError:DistributionHttpClient.kt$DistributionHttpClient$throw IllegalStateException("sentryClientName must be set") + UseCheckOrError:DistributionIntegration.kt$DistributionIntegration$throw IllegalStateException("buildConfiguration must be set in distribution options") + UseRequire:UpdateResponseParser.kt$UpdateResponseParser$throw IllegalArgumentException( "Missing required fields in API response: ${missingFields.joinToString(", ")}" ) + WildcardImport:DistributionHttpClientTest.kt$import org.junit.Assert.* + + diff --git a/sentry-android-replay/build.gradle.kts b/sentry-android-replay/build.gradle.kts index 0a40a920a7c..1318f7767c9 100644 --- a/sentry-android-replay/build.gradle.kts +++ b/sentry-android-replay/build.gradle.kts @@ -8,8 +8,7 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.gradle.versions) - // TODO: enable it later - // alias(libs.plugins.detekt) + alias(libs.plugins.detekt) } android { diff --git a/sentry-android-replay/detekt-baseline.xml b/sentry-android-replay/detekt-baseline.xml new file mode 100644 index 00000000000..54c6754e2d8 --- /dev/null +++ b/sentry-android-replay/detekt-baseline.xml @@ -0,0 +1,165 @@ + + + + + ComplexCondition:ReplayCache.kt$ReplayCache.Companion$height == null || width == null || frameRate == null || bitRate == null || (segmentId == null || segmentId == -1) || segmentTimestamp == null || replayType == null + ComplexCondition:ReplayIntegration.kt$ReplayIntegration$isManualPause || lastKnownConnectionStatus == DISCONNECTED || scopes?.rateLimiter?.isActiveForCategory(All) == true || scopes?.rateLimiter?.isActiveForCategory(Replay) == true + ComplexCondition:ReplayIntegration.kt$ReplayIntegration$name.startsWith("replay_") && !name.contains(replayId.toString()) && !(unfinishedReplayId.isNotBlank() && name.contains(unfinishedReplayId)) + ComplexCondition:ReplayIntegration.kt$ReplayIntegration$state.get().captureStrategy is SessionCaptureStrategy && (lastKnownConnectionStatus == DISCONNECTED || scopes?.rateLimiter?.isActiveForCategory(All) == true || scopes?.rateLimiter?.isActiveForCategory(Replay) == true) + ComplexCondition:ScreenshotRecorder.kt$ScreenshotRecorder$root == null || root.width <= 0 || root.height <= 0 || !root.isShown + ComplexCondition:SimpleVideoEncoder.kt$SimpleVideoEncoder$Build.MANUFACTURER.contains("xiaomi", ignoreCase = true) || Build.MANUFACTURER.contains("motorola", ignoreCase = true) || SystemProperties.get(SystemProperties.Property.SOC_MANUFACTURER) .equals("spreadtrum", ignoreCase = true) || SystemProperties.get(SystemProperties.Property.SOC_MANUFACTURER) .equals("unisoc", ignoreCase = true) + CyclomaticComplexMethod:CaptureStrategy.kt$CaptureStrategy.Companion$@Suppress("NewApi") private fun buildReplay( options: SentryOptions, video: File, currentReplayId: SentryId, segmentTimestamp: Date, segmentId: Int, height: Int, width: Int, frameCount: Int, frameRate: Int, videoDuration: Long, replayType: ReplayType, screenAtStart: String?, breadcrumbs: List<Breadcrumb>, events: Deque<RRWebEvent>, traceIds: List<String>, segmentNames: List<String>, ): ReplaySegment + CyclomaticComplexMethod:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$private fun fromComposeNode( node: LayoutNode, parent: ViewHierarchyNode?, distance: Int, isComposeRoot: Boolean, options: SentryMaskingOptions, logger: ILogger, ): ViewHierarchyNode? + CyclomaticComplexMethod:DefaultReplayBreadcrumbConverter.kt$DefaultReplayBreadcrumbConverter$override fun convert(breadcrumb: Breadcrumb): RRWebEvent? + CyclomaticComplexMethod:DefaultReplayBreadcrumbConverter.kt$DefaultReplayBreadcrumbConverter$private fun Breadcrumb.toRRWebSpanEvent(): RRWebSpanEvent + CyclomaticComplexMethod:ReplayCache.kt$ReplayCache$public fun createVideoOf( duration: Long, from: Long, segmentId: Int, height: Int, width: Int, frameRate: Int, bitRate: Int, videoFile: File = File(replayCacheDir, "$segmentId.mp4"), ): GeneratedVideo? + CyclomaticComplexMethod:ReplayCache.kt$ReplayCache.Companion$internal fun fromDisk( options: SentryOptions, replayId: SentryId, replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null, ): LastSegmentData? + CyclomaticComplexMethod:ReplayGestureConverter.kt$ReplayGestureConverter$fun convert( event: MotionEvent, recorderConfig: ScreenshotRecorderConfig, ): List<RRWebIncrementalSnapshotEvent>? + CyclomaticComplexMethod:ReplayLifecycle.kt$internal fun ReplayLifecycleState.isAllowed(newState: ReplayLifecycleState): Boolean + CyclomaticComplexMethod:SimpleVideoEncoder.kt$SimpleVideoEncoder$private fun drainCodec(endOfStream: Boolean) + EmptyCatchBlock:ReplaySmokeTest.kt$ReplaySmokeTest${} + EmptyDefaultConstructor:ScreenshotRecorderTest.kt$ScreenshotRecorderTest.Fixture$() + EmptyFunctionBlock:ReplayExecutorService.kt$CompletedFuture${} + ForbiddenComment:BufferCaptureStrategy.kt$BufferCaptureStrategy$// TODO: can be a single while-loop + ForbiddenComment:BufferCaptureStrategy.kt$BufferCaptureStrategy$// TODO: capture envelopes for buffered segments instead, but don't send them until buffer is + ForbiddenComment:CaptureStrategy.kt$CaptureStrategy.Companion$// TODO: support non-fullscreen windows later + ForbiddenComment:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$// TODO: if we get reports that it's slow, we can drop this, and just mask + ForbiddenComment:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$// TODO: support editable text (currently there's a way to get @Composable's padding only + ForbiddenComment:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$// TODO: the whole view instead of per-line + ForbiddenComment:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$// TODO: this currently does not support embedded AndroidViews, we'd have to + ForbiddenComment:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$// TODO: traverse the ViewHierarchyNode here again. For now we can recommend + ForbiddenComment:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$// TODO: using custom modifiers to obscure the entire node if it's sensitive + ForbiddenComment:Nodes.kt$// TODO: probably most of the below we can do via bytecode instrumentation and speed up at runtime + ForbiddenComment:Persistable.kt$PersistableLinkedList$// TODO: enable this back after we are able to serialize individual touches to disk to not overload + ForbiddenComment:PixelCopyStrategy.kt$PixelCopyStrategy$// TODO: disableAllMasking here and dont traverse? + ForbiddenComment:ReplayCache.kt$ReplayCache$// TODO: it's awful, choose a better serialization format + ForbiddenComment:ReplayIntegration.kt$ReplayIntegration$// TODO: previous run and set them directly to the ReplayEvent so they don't get overwritten in + ForbiddenComment:ReplayIntegration.kt$ReplayIntegration$// TODO: read persisted options/scope values form the + ForbiddenComment:ReplaySmokeTest.kt$ReplaySmokeTest$// TODO: figure out why there's more than 10 + ForbiddenComment:SimpleVideoEncoder.kt$SimpleVideoEncoder$// TODO: because the screenshot config has to match the video config + ForbiddenComment:SimpleVideoEncoder.kt$SimpleVideoEncoder$// TODO: if this ever becomes a problem, move this to ScreenshotRecorderConfig.from() + ForbiddenComment:SimpleVideoEncoder.kt$SimpleVideoEncoder$// TODO: maybe enable this back later, for now variable bitrate seems to provide much better + ForbiddenComment:SimpleVideoEncoder.kt$SimpleVideoEncoder$// TODO: quality with almost no overhead in terms of video size, let's monitor that + ForbiddenComment:Views.kt$// TODO: maybe find a way how to check if the drawable is coming from the apk or loaded from + ForbiddenComment:Views.kt$// TODO: otherwise maybe check for the bitmap size and don't mask those that take a lot of height + FunctionParameterNaming:PixelCopyStrategyTest.kt$DeferredWindowPixelCopyShadow.Companion$_dest: Bitmap + FunctionParameterNaming:PixelCopyStrategyTest.kt$DeferredWindowPixelCopyShadow.Companion$_source: Window + LargeClass:CanvasStrategy.kt$TextIgnoringDelegateCanvas : Canvas + LargeClass:ReplayIntegrationTest.kt$ReplayIntegrationTest + LongMethod:CaptureStrategy.kt$CaptureStrategy.Companion$@Suppress("NewApi") private fun buildReplay( options: SentryOptions, video: File, currentReplayId: SentryId, segmentTimestamp: Date, segmentId: Int, height: Int, width: Int, frameCount: Int, frameRate: Int, videoDuration: Long, replayType: ReplayType, screenAtStart: String?, breadcrumbs: List<Breadcrumb>, events: Deque<RRWebEvent>, traceIds: List<String>, segmentNames: List<String>, ): ReplaySegment + LongMethod:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$private fun fromComposeNode( node: LayoutNode, parent: ViewHierarchyNode?, distance: Int, isComposeRoot: Boolean, options: SentryMaskingOptions, logger: ILogger, ): ViewHierarchyNode? + LongMethod:DefaultReplayBreadcrumbConverter.kt$DefaultReplayBreadcrumbConverter$override fun convert(breadcrumb: Breadcrumb): RRWebEvent? + LongMethod:DefaultReplayBreadcrumbConverter.kt$DefaultReplayBreadcrumbConverter$private fun Breadcrumb.toRRWebSpanEvent(): RRWebSpanEvent + LongMethod:PixelCopyStrategy.kt$PixelCopyStrategy$@SuppressLint("NewApi") override fun capture(root: View) + LongMethod:ReplayCache.kt$ReplayCache$public fun createVideoOf( duration: Long, from: Long, segmentId: Int, height: Int, width: Int, frameRate: Int, bitRate: Int, videoFile: File = File(replayCacheDir, "$segmentId.mp4"), ): GeneratedVideo? + LongMethod:ReplayCache.kt$ReplayCache.Companion$internal fun fromDisk( options: SentryOptions, replayId: SentryId, replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null, ): LastSegmentData? + LongMethod:ReplayGestureConverter.kt$ReplayGestureConverter$fun convert( event: MotionEvent, recorderConfig: ScreenshotRecorderConfig, ): List<RRWebIncrementalSnapshotEvent>? + LongMethod:ReplayGestureConverterTest.kt$ReplayGestureConverterTest$@Test fun `convert multi-pointer events`() + LongMethod:ReplayIntegrationTest.kt$ReplayIntegrationTest$@Test fun `register finalizes previous replay`() + LongMethod:ReplayIntegrationWithRecorderTest.kt$ReplayIntegrationWithRecorderTest$@Test fun `works with different recorder`() + LongMethod:SimpleVideoEncoder.kt$SimpleVideoEncoder$private fun drainCodec(endOfStream: Boolean) + LongMethod:ViewHierarchyNode.kt$ViewHierarchyNode.Companion$@JvmStatic fun fromView( view: View, parent: ViewHierarchyNode?, distance: Int, options: SentryMaskingOptions, ): ViewHierarchyNode + LongParameterList:BaseCaptureStrategy.kt$BaseCaptureStrategy$( duration: Long, currentSegmentTimestamp: Date, replayId: SentryId, segmentId: Int, height: Int, width: Int, frameRate: Int, bitRate: Int, replayType: ReplayType = this.replayType, cache: ReplayCache? = this.cache, screenAtStart: String? = this.screenAtStart, breadcrumbs: List<Breadcrumb>? = null, events: Deque<RRWebEvent> = this.currentEvents, ) + LongParameterList:CaptureStrategy.kt$CaptureStrategy.Companion$( options: SentryOptions, video: File, currentReplayId: SentryId, segmentTimestamp: Date, segmentId: Int, height: Int, width: Int, frameCount: Int, frameRate: Int, videoDuration: Long, replayType: ReplayType, screenAtStart: String?, breadcrumbs: List<Breadcrumb>, events: Deque<RRWebEvent>, traceIds: List<String>, segmentNames: List<String>, ) + LongParameterList:CaptureStrategy.kt$CaptureStrategy.Companion$( scopes: IScopes?, options: SentryOptions, duration: Long, currentSegmentTimestamp: Date, replayId: SentryId, segmentId: Int, height: Int, width: Int, replayType: ReplayType, cache: ReplayCache?, frameRate: Int, bitRate: Int, screenAtStart: String?, breadcrumbs: List<Breadcrumb>?, events: Deque<RRWebEvent>, traceIds: List<String> = emptyList(), segmentNames: List<String> = emptyList(), ) + LongParameterList:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$( node: LayoutNode, parent: ViewHierarchyNode?, distance: Int, isComposeRoot: Boolean, options: SentryMaskingOptions, logger: ILogger, ) + LongParameterList:PixelCopyStrategy.kt$( destCanvas: Canvas, destPaint: Paint, tmpSrc: Rect, tmpDst: RectF, sourceBitmap: Bitmap, sourceX: Int, sourceY: Int, windowX: Int, windowY: Int, scaleFactorX: Float, scaleFactorY: Float, ) + LongParameterList:PixelCopyStrategy.kt$PixelCopyStrategy$( root: View, captures: Array<SurfaceViewCapture?>, viewHierarchy: ViewHierarchyNode, windowX: Int, windowY: Int, resetUnstableCaptures: Boolean, ) + LongParameterList:ReplayCache.kt$ReplayCache$( duration: Long, from: Long, segmentId: Int, height: Int, width: Int, frameRate: Int, bitRate: Int, videoFile: File = File(replayCacheDir, "$segmentId.mp4"), ) + LongParameterList:ReplayIntegration.kt$ReplayIntegration$( context: Context, dateProvider: ICurrentDateProvider, recorderProvider: (() -> Recorder)?, replayCacheProvider: ((replayId: SentryId) -> ReplayCache)?, replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null, mainLooperHandler: MainLooperHandler? = null, gestureRecorderProvider: (() -> GestureRecorder)? = null, ) + LongParameterList:ReplayIntegrationTest.kt$ReplayIntegrationTest.Fixture$( context: Context, sessionSampleRate: Double = 1.0, onErrorSampleRate: Double = 1.0, isRateLimited: Boolean = false, recorderProvider: (() -> Recorder)? = null, replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null, gestureRecorderProvider: (() -> GestureRecorder)? = null, dateProvider: ICurrentDateProvider = CurrentDateProvider.getInstance(), mainLooperHandler: MainLooperHandler = mock { doAnswer { (it.arguments[0] as Runnable).run() true } .whenever(mock) .post(any()) }, ) + LongParameterList:ViewHierarchyNode.kt$ViewHierarchyNode$( val x: Float, val y: Float, val width: Int, val height: Int, // Elevation (in px) val elevation: Float, // Distance to the parent (index) val distance: Int, val parent: ViewHierarchyNode? = null, val shouldMask: Boolean = false, // Whether the node is important for content capture (=non-empty container) var isImportantForContentCapture: Boolean = false, val isVisible: Boolean = false, val visibleRect: Rect? = null, ) + LongParameterList:ViewHierarchyNode.kt$ViewHierarchyNode.GenericViewHierarchyNode$( x: Float, y: Float, width: Int, height: Int, elevation: Float, distance: Int, parent: ViewHierarchyNode? = null, shouldMask: Boolean = false, isImportantForContentCapture: Boolean = false, isVisible: Boolean = false, visibleRect: Rect? = null, ) + LongParameterList:ViewHierarchyNode.kt$ViewHierarchyNode.ImageViewHierarchyNode$( x: Float, y: Float, width: Int, height: Int, elevation: Float, distance: Int, parent: ViewHierarchyNode? = null, shouldMask: Boolean = false, isImportantForContentCapture: Boolean = false, isVisible: Boolean = false, visibleRect: Rect? = null, ) + LongParameterList:ViewHierarchyNode.kt$ViewHierarchyNode.SurfaceViewHierarchyNode$( val surfaceViewRef: WeakReference<SurfaceView>, x: Float, y: Float, width: Int, height: Int, elevation: Float, distance: Int, parent: ViewHierarchyNode? = null, shouldMask: Boolean = false, isImportantForContentCapture: Boolean = false, isVisible: Boolean = false, visibleRect: Rect? = null, ) + LongParameterList:ViewHierarchyNode.kt$ViewHierarchyNode.TextViewHierarchyNode$( val layout: TextLayout? = null, val dominantColor: Int? = null, val paddingLeft: Int = 0, val paddingTop: Int = 0, x: Float, y: Float, width: Int, height: Int, elevation: Float, distance: Int, parent: ViewHierarchyNode? = null, shouldMask: Boolean = false, isImportantForContentCapture: Boolean = false, isVisible: Boolean = false, visibleRect: Rect? = null, ) + LoopWithTooManyJumpStatements:PixelCopyStrategy.kt$PixelCopyStrategy$for + LoopWithTooManyJumpStatements:ReplayCache.kt$ReplayCache$while + LoopWithTooManyJumpStatements:SimpleVideoEncoder.kt$SimpleVideoEncoder$while + MagicNumber:BaseCaptureStrategy.kt$BaseCaptureStrategy$26 + MagicNumber:BufferCaptureStrategy.kt$BufferCaptureStrategy$26 + MagicNumber:CanvasStrategy.kt$TextIgnoringDelegateCanvas$100 + MagicNumber:CanvasStrategy.kt$TextIgnoringDelegateCanvas$10f + MagicNumber:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$26 + MagicNumber:DebugOverlayDrawable.kt$DebugOverlayDrawable$6f + MagicNumber:DefaultReplayBreadcrumbConverter.kt$DefaultReplayBreadcrumbConverter$1000.0 + MagicNumber:Persistable.kt$PersistableLinkedList$26 + MagicNumber:ReplayCache.kt$ReplayCache$1000 + MagicNumber:ReplayCache.kt$ReplayCache.Companion$1000 + MagicNumber:ReplayGestureConverter.kt$ReplayGestureConverter$10 + MagicNumber:ScreenshotRecorder.kt$ScreenshotRecorder$26 + MagicNumber:ScreenshotRecorder.kt$ScreenshotRecorderConfig.Companion$16 + MagicNumber:ScreenshotRecorder.kt$ScreenshotRecorderConfig.Companion$8 + MagicNumber:SimpleVideoEncoder.kt$MuxerConfig$24 + MagicNumber:SimpleVideoEncoder.kt$SimpleVideoEncoder$26 + MagicNumber:SimpleVideoEncoder.kt$SimpleVideoEncoder$6 + MagicNumber:ViewHierarchyNode.kt$ViewHierarchyNode$26 + MagicNumber:Views.kt$10 + MagicNumber:Views.kt$21 + MagicNumber:WindowRecorder.kt$WindowRecorder$100L + MagicNumber:WindowRecorder.kt$WindowRecorder$26 + MagicNumber:WindowRecorder.kt$WindowRecorder.Capturer$1000L + MatchingDeclarationName:ModifierExtensions.kt$SentryReplayModifiers + MatchingDeclarationName:Nodes.kt$ComposeTextLayout : TextLayout + MatchingDeclarationName:Persistable.kt$PersistableLinkedList : ConcurrentLinkedDeque + MatchingDeclarationName:ReplayLifecycle.kt$ReplayLifecycleState + MaxLineLength:ComposeMaskingOptionsTest.kt$ComposeMaskingOptionsTest$"Flaky: Robolectric intermittently reports zero bounds for nodes, causing isVisible=false and making the assertion non-deterministic" + NestedBlockDepth:CaptureStrategy.kt$CaptureStrategy.Companion$@Suppress("NewApi") private fun buildReplay( options: SentryOptions, video: File, currentReplayId: SentryId, segmentTimestamp: Date, segmentId: Int, height: Int, width: Int, frameCount: Int, frameRate: Int, videoDuration: Long, replayType: ReplayType, screenAtStart: String?, breadcrumbs: List<Breadcrumb>, events: Deque<RRWebEvent>, traceIds: List<String>, segmentNames: List<String>, ): ReplaySegment + NestedBlockDepth:DefaultReplayBreadcrumbConverter.kt$DefaultReplayBreadcrumbConverter$private fun Breadcrumb.toRRWebSpanEvent(): RRWebSpanEvent + NestedBlockDepth:Nodes.kt$internal fun LayoutNode.findPainter(): Painter? + NestedBlockDepth:Nodes.kt$internal fun LayoutNode.findTextColor(): Color? + NestedBlockDepth:ReplayGestureConverter.kt$ReplayGestureConverter$fun convert( event: MotionEvent, recorderConfig: ScreenshotRecorderConfig, ): List<RRWebIncrementalSnapshotEvent>? + NestedBlockDepth:SimpleVideoEncoder.kt$SimpleVideoEncoder$private fun drainCodec(endOfStream: Boolean) + ReturnCount:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$fun fromView( view: View, parent: ViewHierarchyNode?, options: SentryMaskingOptions, logger: ILogger, ): Boolean + ReturnCount:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$private fun SemanticsConfiguration?.shouldMask( isImage: Boolean, options: SentryMaskingOptions, ): Boolean + ReturnCount:DefaultReplayBreadcrumbConverter.kt$DefaultReplayBreadcrumbConverter$override fun convert(breadcrumb: Breadcrumb): RRWebEvent? + ReturnCount:ReplayCache.kt$ReplayCache.Companion$internal fun fromDisk( options: SentryOptions, replayId: SentryId, replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null, ): LastSegmentData? + ReturnCount:ReplayGestureConverter.kt$ReplayGestureConverter$fun convert( event: MotionEvent, recorderConfig: ScreenshotRecorderConfig, ): List<RRWebIncrementalSnapshotEvent>? + ReturnCount:ReplayIntegration.kt$ReplayIntegration$override fun captureReplay(isTerminating: Boolean?): SentryId + ReturnCount:ScreenshotRecorder.kt$ScreenshotRecorder$fun capture() + ReturnCount:SentryLayoutNodeHelper.kt$SentryLayoutNodeHelper$@Suppress("UNCHECKED_CAST") fun getChildren(node: LayoutNode): List<LayoutNode> + ReturnCount:SentryLayoutNodeHelper.kt$SentryLayoutNodeHelper$fun isTransparent(node: LayoutNode): Boolean + ReturnCount:ViewHierarchyNode.kt$ViewHierarchyNode.Companion$@JvmStatic fun fromView( view: View, parent: ViewHierarchyNode?, distance: Int, options: SentryMaskingOptions, ): ViewHierarchyNode + ReturnCount:ViewHierarchyNode.kt$ViewHierarchyNode.Companion$private fun View.shouldMask(options: SentryMaskingOptions): Boolean + ReturnCount:Views.kt$internal fun View.isVisibleToUser(): Pair<Boolean, Rect?> + SwallowedException:Nodes.kt$e: Throwable + SwallowedException:ReplayCache.kt$ReplayCache.Companion$e: Throwable + SwallowedException:ReplaySmokeTest.kt$ReplaySmokeTest$e: ConditionTimeoutException + SwallowedException:Views.kt$e: NullPointerException + ThrowsCount:SimpleVideoEncoder.kt$SimpleVideoEncoder$private fun drainCodec(endOfStream: Boolean) + TooGenericExceptionCaught:BaseCaptureStrategy.kt$BaseCaptureStrategy.<no name provided>$e: Throwable + TooGenericExceptionCaught:BufferCaptureStrategy.kt$BufferCaptureStrategy$e: Throwable + TooGenericExceptionCaught:CanvasStrategy.kt$CanvasStrategy$t: Throwable + TooGenericExceptionCaught:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$e: Throwable + TooGenericExceptionCaught:ComposeViewHierarchyNode.kt$ComposeViewHierarchyNode$t: Throwable + TooGenericExceptionCaught:Executors.kt$e: Throwable + TooGenericExceptionCaught:GestureRecorder.kt$GestureRecorder.SentryReplayGestureRecorder$e: Throwable + TooGenericExceptionCaught:Nodes.kt$e: Throwable + TooGenericExceptionCaught:PixelCopyStrategy.kt$PixelCopyStrategy$e: RuntimeException + TooGenericExceptionCaught:PixelCopyStrategy.kt$PixelCopyStrategy$e: Throwable + TooGenericExceptionCaught:ReplayCache.kt$ReplayCache$e: Throwable + TooGenericExceptionCaught:ReplayCache.kt$ReplayCache$t: Throwable + TooGenericExceptionCaught:ReplayCache.kt$ReplayCache.Companion$e: Throwable + TooGenericExceptionCaught:ReplayExecutorService.kt$ReplayExecutorService$e: Throwable + TooGenericExceptionCaught:ReplayIntegration.kt$ReplayIntegration$e: Throwable + TooGenericExceptionCaught:ScreenshotRecorder.kt$ScreenshotRecorder$e: Throwable + TooGenericExceptionCaught:SimpleVideoEncoder.kt$SimpleVideoEncoder$e: RuntimeException + TooGenericExceptionCaught:SimpleVideoEncoder.kt$SimpleVideoEncoder$e: Throwable + TooGenericExceptionCaught:Views.kt$e: NullPointerException + TooGenericExceptionCaught:WindowRecorder.kt$WindowRecorder.Capturer$e: Throwable + TooGenericExceptionThrown:ReplayIntegrationTest.kt$ReplayIntegrationTest$throw RuntimeException("test") + TooGenericExceptionThrown:SimpleVideoEncoder.kt$SimpleVideoEncoder$throw RuntimeException("encoderOutputBuffer $encoderStatus was null") + TooGenericExceptionThrown:SimpleVideoEncoder.kt$SimpleVideoEncoder$throw RuntimeException("format changed twice") + TooGenericExceptionThrown:SimpleVideoEncoder.kt$SimpleVideoEncoder$throw RuntimeException("muxer hasn't started") + TooManyFunctions:BaseCaptureStrategy.kt$BaseCaptureStrategy : CaptureStrategy + TooManyFunctions:BufferCaptureStrategy.kt$BufferCaptureStrategy : BaseCaptureStrategy + TooManyFunctions:CanvasStrategy.kt$TextIgnoringDelegateCanvas : Canvas + TooManyFunctions:CaptureStrategy.kt$CaptureStrategy + TooManyFunctions:PixelCopyStrategy.kt$PixelCopyStrategy : ScreenshotStrategy + TooManyFunctions:ReplayIntegration.kt$ReplayIntegration : IntegrationCloseableScreenshotRecorderCallbackTouchRecorderCallbackReplayControllerIConnectionStatusObserverIRateLimitObserverWindowCallback + TooManyFunctions:WindowRecorder.kt$WindowRecorder : RecorderOnRootViewsChangedListenerExecutorProvider + UnusedPrivateProperty:DebugOverlayDrawable.kt$DebugOverlayDrawable$private val padding = 6f + UseCheckOrError:ReplayShadowMediaCodec.kt$ReplayShadowMediaCodec$throw IllegalStateException("Simulated codec start failure") + UtilityClassWithPublicConstructor:PixelCopyStrategyTest.kt$DeferredWindowPixelCopyShadow + + diff --git a/sentry-apollo-3/build.gradle.kts b/sentry-apollo-3/build.gradle.kts index 70f43d946ef..357d5224495 100644 --- a/sentry-apollo-3/build.gradle.kts +++ b/sentry-apollo-3/build.gradle.kts @@ -5,6 +5,7 @@ plugins { `java-library` id("io.sentry.javadoc") alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.detekt) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) alias(libs.plugins.buildconfig) diff --git a/sentry-apollo-3/detekt-baseline.xml b/sentry-apollo-3/detekt-baseline.xml new file mode 100644 index 00000000000..6bb8edffb36 --- /dev/null +++ b/sentry-apollo-3/detekt-baseline.xml @@ -0,0 +1,33 @@ + + + + + ClassNaming:LaunchDetailsQuery_ResponseAdapter.kt$LaunchDetailsQuery_ResponseAdapter + ClassNaming:LaunchDetailsQuery_VariablesAdapter.kt$LaunchDetailsQuery_VariablesAdapter : Adapter + CyclomaticComplexMethod:SentryApollo3HttpInterceptor.kt$SentryApollo3HttpInterceptor$private fun captureEvent( request: HttpRequest, response: HttpResponse, operationName: String?, operationType: String?, ) + LongMethod:SentryApollo3HttpInterceptor.kt$SentryApollo3HttpInterceptor$private fun captureEvent( request: HttpRequest, response: HttpResponse, operationName: String?, operationType: String?, ) + LongParameterList:SentryApollo3HttpInterceptor.kt$SentryApollo3HttpInterceptor$( span: ISpan?, request: HttpRequest, response: HttpResponse?, statusCode: Int?, operationName: String?, operationType: String?, operationId: String?, ) + LongParameterList:SentryApollo3InterceptorClientErrors.kt$SentryApollo3InterceptorClientErrors.Fixture$( captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, failedRequestTargets: List<String> = listOf(DEFAULT_PROPAGATION_TARGETS), httpStatusCode: Int = 200, responseBody: String = responseBodyOk, sendDefaultPii: Boolean = false, includeCookies: Boolean = sendDefaultPii, socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN, configureOptions: SentryOptions.() -> Unit = {}, ) + MaxLineLength:LaunchDetailsQuery.kt$LaunchDetailsQuery.Companion$"query LaunchDetails(${'$'}id: ID!) { launch(id: ${'$'}id) { id site mission { name missionPatch(size: LARGE) } rocket { name type } } }" + MaxLineLength:SentryApollo3InterceptorTest.kt$SentryApollo3InterceptorTest$"thirdPartyBaggage=someValue,secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue" + MaxLineLength:SentryApollo3InterceptorTest.kt$SentryApollo3InterceptorTest.Fixture$"secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue" + NestedBlockDepth:SentryApollo3HttpInterceptor.kt$SentryApollo3HttpInterceptor$private fun captureEvent( request: HttpRequest, response: HttpResponse, operationName: String?, operationType: String?, ) + NestedBlockDepth:SentryApollo3HttpInterceptor.kt$SentryApollo3HttpInterceptor$private fun finish( span: ISpan?, request: HttpRequest, response: HttpResponse?, statusCode: Int?, operationName: String?, operationType: String?, operationId: String?, ) + NestedBlockDepth:SentryApollo3HttpInterceptor.kt$SentryApollo3HttpInterceptor$private fun maybeAddTracingHeaders( scopes: IScopes, request: HttpRequest, span: ISpan?, ): HttpRequest + ReturnCount:SentryApollo3HttpInterceptor.kt$SentryApollo3HttpInterceptor$private fun captureEvent( request: HttpRequest, response: HttpResponse, operationName: String?, operationType: String?, ) + SwallowedException:SentryApollo3InterceptorClientErrors.kt$SentryApollo3InterceptorClientErrors$e: ApolloException + SwallowedException:SentryApollo3InterceptorTest.kt$SentryApollo3InterceptorTest$e: ApolloException + SwallowedException:SentryApollo3InterceptorWithVariablesTest.kt$SentryApollo3InterceptorWithVariablesTest$e: ApolloException + TooGenericExceptionCaught:SentryApollo3HttpInterceptor.kt$SentryApollo3HttpInterceptor$e: Throwable + TooGenericExceptionThrown:SentryApollo3InterceptorTest.kt$SentryApollo3InterceptorTest$throw RuntimeException() + TooManyFunctions:SentryApollo3HttpInterceptor.kt$SentryApollo3HttpInterceptor : HttpInterceptor + UseCheckOrError:LaunchDetailsQuery_VariablesAdapter.kt$LaunchDetailsQuery_VariablesAdapter$throw IllegalStateException("Input type used in output position") + UtilityClassWithPublicConstructor:GraphQLBoolean.kt$GraphQLBoolean + UtilityClassWithPublicConstructor:GraphQLID.kt$GraphQLID + UtilityClassWithPublicConstructor:GraphQLString.kt$GraphQLString + UtilityClassWithPublicConstructor:Launch.kt$Launch + UtilityClassWithPublicConstructor:Mission.kt$Mission + UtilityClassWithPublicConstructor:Query.kt$Query + UtilityClassWithPublicConstructor:Rocket.kt$Rocket + + diff --git a/sentry-apollo-4/build.gradle.kts b/sentry-apollo-4/build.gradle.kts index 4f1276f0bf4..078d26b8ff8 100644 --- a/sentry-apollo-4/build.gradle.kts +++ b/sentry-apollo-4/build.gradle.kts @@ -5,6 +5,7 @@ plugins { `java-library` id("io.sentry.javadoc") alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.detekt) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) alias(libs.plugins.buildconfig) diff --git a/sentry-apollo-4/detekt-baseline.xml b/sentry-apollo-4/detekt-baseline.xml new file mode 100644 index 00000000000..371c726cb9f --- /dev/null +++ b/sentry-apollo-4/detekt-baseline.xml @@ -0,0 +1,34 @@ + + + + + ClassNaming:LaunchDetailsQuery_ResponseAdapter.kt$LaunchDetailsQuery_ResponseAdapter + ClassNaming:LaunchDetailsQuery_VariablesAdapter.kt$LaunchDetailsQuery_VariablesAdapter : Adapter + CyclomaticComplexMethod:SentryApollo4HttpInterceptor.kt$SentryApollo4HttpInterceptor$private fun captureEvent( request: HttpRequest, response: HttpResponse, operationName: String?, operationType: String?, ) + LongMethod:SentryApollo4HttpInterceptor.kt$SentryApollo4HttpInterceptor$private fun captureEvent( request: HttpRequest, response: HttpResponse, operationName: String?, operationType: String?, ) + LongParameterList:SentryApollo4BuilderExtensionsClientErrorsTest.kt$SentryApollo4BuilderExtensionsClientErrorsTest.Fixture$( captureFailedRequests: Boolean = DEFAULT_CAPTURE_FAILED_REQUESTS, failedRequestTargets: List<String> = listOf(DEFAULT_PROPAGATION_TARGETS), httpStatusCode: Int = 200, responseBody: String = responseBodyOk, sendDefaultPii: Boolean = false, includeCookies: Boolean = sendDefaultPii, socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN, configureOptions: SentryOptions.() -> Unit = {}, ) + LongParameterList:SentryApollo4HttpInterceptor.kt$SentryApollo4HttpInterceptor$( span: ISpan?, request: HttpRequest, response: HttpResponse?, statusCode: Int?, operationName: String?, operationType: String?, operationId: String?, ) + MaxLineLength:LaunchDetailsQuery.kt$LaunchDetailsQuery.Companion$"query LaunchDetails(${'$'}id: ID!) { launch(id: ${'$'}id) { id site mission { name missionPatch(size: LARGE) } rocket { name type } } }" + MaxLineLength:LaunchDetailsQuerySelections.kt$LaunchDetailsQuerySelections$CompiledArgument.Builder(CompiledArgumentDefinition.Builder("size").build()).value("LARGE").build() + MaxLineLength:SentryApollo4HttpInterceptorTest.kt$SentryApollo4HttpInterceptorTest$"thirdPartyBaggage=someValue,secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue" + MaxLineLength:SentryApollo4HttpInterceptorTest.kt$SentryApollo4HttpInterceptorTest.Fixture$"secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue" + NestedBlockDepth:SentryApollo4HttpInterceptor.kt$SentryApollo4HttpInterceptor$private fun captureEvent( request: HttpRequest, response: HttpResponse, operationName: String?, operationType: String?, ) + NestedBlockDepth:SentryApollo4HttpInterceptor.kt$SentryApollo4HttpInterceptor$private fun finish( span: ISpan?, request: HttpRequest, response: HttpResponse?, statusCode: Int?, operationName: String?, operationType: String?, operationId: String?, ) + NestedBlockDepth:SentryApollo4HttpInterceptor.kt$SentryApollo4HttpInterceptor$private fun maybeAddTracingHeaders( scopes: IScopes, request: HttpRequest, span: ISpan?, ): HttpRequest + ReturnCount:SentryApollo4HttpInterceptor.kt$SentryApollo4HttpInterceptor$private fun captureEvent( request: HttpRequest, response: HttpResponse, operationName: String?, operationType: String?, ) + SwallowedException:SentryApollo4BuilderExtensionsClientErrorsTest.kt$SentryApollo4BuilderExtensionsClientErrorsTest$e: ApolloException + SwallowedException:SentryApollo4BuilderExtensionsTest.kt$SentryApollo4BuilderExtensionsTest$e: ApolloException + SwallowedException:SentryApollo4HttpInterceptorTest.kt$SentryApollo4HttpInterceptorTest$e: ApolloException + TooGenericExceptionCaught:SentryApollo4HttpInterceptor.kt$SentryApollo4HttpInterceptor$e: Throwable + TooGenericExceptionThrown:SentryApollo4HttpInterceptorTest.kt$SentryApollo4HttpInterceptorTest$throw RuntimeException() + TooManyFunctions:SentryApollo4HttpInterceptor.kt$SentryApollo4HttpInterceptor : HttpInterceptor + UseCheckOrError:LaunchDetailsQuery_VariablesAdapter.kt$LaunchDetailsQuery_VariablesAdapter$throw IllegalStateException("Input type used in output position") + UtilityClassWithPublicConstructor:GraphQLBoolean.kt$GraphQLBoolean + UtilityClassWithPublicConstructor:GraphQLID.kt$GraphQLID + UtilityClassWithPublicConstructor:GraphQLString.kt$GraphQLString + UtilityClassWithPublicConstructor:Launch.kt$Launch + UtilityClassWithPublicConstructor:Mission.kt$Mission + UtilityClassWithPublicConstructor:Query.kt$Query + UtilityClassWithPublicConstructor:Rocket.kt$Rocket + + diff --git a/sentry-apollo/build.gradle.kts b/sentry-apollo/build.gradle.kts index 2da8d8b20c1..570214e60b8 100644 --- a/sentry-apollo/build.gradle.kts +++ b/sentry-apollo/build.gradle.kts @@ -5,6 +5,7 @@ plugins { `java-library` id("io.sentry.javadoc") alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.detekt) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) alias(libs.plugins.buildconfig) diff --git a/sentry-apollo/detekt-baseline.xml b/sentry-apollo/detekt-baseline.xml new file mode 100644 index 00000000000..d010e1a4336 --- /dev/null +++ b/sentry-apollo/detekt-baseline.xml @@ -0,0 +1,10 @@ + + + + + EmptyFunctionBlock:SentryApolloInterceptor.kt$SentryApolloInterceptor${} + SwallowedException:SentryApolloInterceptorTest.kt$SentryApolloInterceptorTest$e: ApolloException + TooGenericExceptionCaught:SentryApolloInterceptor.kt$SentryApolloInterceptor$e: Exception + TooGenericExceptionThrown:SentryApolloInterceptorTest.kt$SentryApolloInterceptorTest$throw RuntimeException() + + diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index c5bb5715830..394885df1b8 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -121,6 +121,9 @@ android { } } +// The default detekt source set is src/{main,test}, which doesn't exist in a multiplatform module. +detekt { source.setFrom("src/androidMain/kotlin", "src/androidUnitTest/kotlin") } + tasks.withType().configureEach { // Target version of the generated JVM bytecode. It is used for type resolution. jvmTarget = JavaVersion.VERSION_1_8.toString() diff --git a/sentry-compose/detekt-baseline.xml b/sentry-compose/detekt-baseline.xml new file mode 100644 index 00000000000..b59189497e9 --- /dev/null +++ b/sentry-compose/detekt-baseline.xml @@ -0,0 +1,20 @@ + + + + + CyclomaticComplexMethod:ComposeGestureTargetLocator.kt$ComposeGestureTargetLocator$override fun locate(root: Any?, x: Float, y: Float, targetType: UiElement.Type): UiElement? + FunctionOnlyReturningConstant:SentryModifier.kt$SentryModifier.SentryTagModifierNode$@Suppress("unused") fun isImportantForBounds(): Boolean + LongMethod:ComposeGestureTargetLocator.kt$ComposeGestureTargetLocator$override fun locate(root: Any?, x: Float, y: Float, targetType: UiElement.Type): UiElement? + LongParameterList:ComposeGestureTargetLocatorTest.kt$ComposeGestureTargetLocatorTest$( isPlaced: Boolean, tag: String?, width: Int, height: Int, children: List<LayoutNode> = emptyList(), semanticsKeys: List<String> = emptyList(), nodeModifierClassName: String? = null, left: Float = 0f, top: Float = 0f, ) + LongParameterList:SentryComposeTracing.kt$BucketSpans$( ownerSpan: ISpan, startTimestamp: SentryDate, cached: WeakReference<ISpan>?, operation: String, description: String, setCached: (WeakReference<ISpan>) -> Unit, ) + NestedBlockDepth:ComposeGestureTargetLocator.kt$ComposeGestureTargetLocator$override fun locate(root: Any?, x: Float, y: Float, targetType: UiElement.Type): UiElement? + NestedBlockDepth:SentryComposeHelper.kt$SentryComposeHelper$fun extractTag(modifier: Modifier): String? + ReturnCount:ComposeGestureTargetLocator.kt$ComposeGestureTargetLocator$override fun locate(root: Any?, x: Float, y: Float, targetType: UiElement.Type): UiElement? + ReturnCount:ComposeIntegrationTests.kt$ComposeIntegrationTests$private fun locateAndroidComposeView(root: View?): Any? + ReturnCount:SentryComposeHelper.kt$SentryComposeHelper$fun extractTag(modifier: Modifier): String? + SwallowedException:SentryComposeHelper.kt$SentryComposeHelper$e: Throwable + SwallowedException:SentryComposeHelper.kt$SentryComposeHelper.Companion$e: Exception + TooGenericExceptionCaught:SentryComposeHelper.kt$SentryComposeHelper$e: Throwable + TooGenericExceptionCaught:SentryComposeHelper.kt$SentryComposeHelper.Companion$e: Exception + + diff --git a/sentry-ktor-client/build.gradle.kts b/sentry-ktor-client/build.gradle.kts index fefcdbfebaf..a1eb9150d6f 100644 --- a/sentry-ktor-client/build.gradle.kts +++ b/sentry-ktor-client/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.detekt) id("io.sentry.javadoc") alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) diff --git a/sentry-ktor-client/detekt-baseline.xml b/sentry-ktor-client/detekt-baseline.xml new file mode 100644 index 00000000000..23405d6fda1 --- /dev/null +++ b/sentry-ktor-client/detekt-baseline.xml @@ -0,0 +1,15 @@ + + + + + MagicNumber:SentryKtorClientUtils.kt$SentryKtorClientUtils$1000L + MaxLineLength:SentryKtorClientPluginTest.kt$SentryKtorClientPluginTest$fun + TooGenericExceptionCaught:SentryKtorClientPlugin.kt$t: Throwable + UnusedParameter:SentryKtorClientPluginTest.kt$SentryKtorClientPluginTest.Fixture$beforeSpan: SentryKtorClientPluginConfig.BeforeSpanCallback? = null + WildcardImport:SentryKtorClientPlugin.kt$import io.ktor.client.plugins.api.* + WildcardImport:SentryKtorClientPlugin.kt$import io.ktor.client.request.* + WildcardImport:SentryKtorClientPlugin.kt$import io.ktor.client.statement.* + WildcardImport:SentryKtorClientPlugin.kt$import io.ktor.util.* + WildcardImport:SentryKtorClientPlugin.kt$import io.ktor.util.pipeline.* + + diff --git a/sentry-okhttp/build.gradle.kts b/sentry-okhttp/build.gradle.kts index 94575f54984..5abd1cf26b9 100644 --- a/sentry-okhttp/build.gradle.kts +++ b/sentry-okhttp/build.gradle.kts @@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.detekt) id("io.sentry.javadoc") alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) diff --git a/sentry-okhttp/detekt-baseline.xml b/sentry-okhttp/detekt-baseline.xml new file mode 100644 index 00000000000..b0d75302aa0 --- /dev/null +++ b/sentry-okhttp/detekt-baseline.xml @@ -0,0 +1,10 @@ + + + + + CyclomaticComplexMethod:SentryOkHttpInterceptor.kt$SentryOkHttpInterceptor$@Suppress("LongMethod") @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response + NestedBlockDepth:SentryOkHttpInterceptor.kt$SentryOkHttpInterceptor$@Suppress("LongMethod") @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response + ReturnCount:SentryOkHttpInterceptor.kt$SentryOkHttpInterceptor$private fun shouldCaptureClientError(request: Request, response: Response): Boolean + TooGenericExceptionCaught:SentryOkHttpInterceptor.kt$SentryOkHttpInterceptor$e: Exception + +