From 28a11a7925b8e45bec38684f2002192787f13477 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 31 Oct 2024 06:48:34 +0100 Subject: [PATCH 01/14] Use `Random` through `ThreadLocal` (#3835) * Use Random as a ThreadLocal<> * changelog * code review changes --- CHANGELOG.md | 4 ++ .../android/core/AnrV2EventProcessor.java | 16 +------ sentry/api/sentry.api | 5 +++ .../src/main/java/io/sentry/SentryClient.java | 5 +-- .../main/java/io/sentry/TracesSampler.java | 16 +++++-- .../java/io/sentry/util/SentryRandom.java | 37 +++++++++++++++ .../java/io/sentry/util/SentryRandomTest.kt | 45 +++++++++++++++++++ 7 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/util/SentryRandom.java create mode 100644 sentry/src/test/java/io/sentry/util/SentryRandomTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 813e9e65bc..cd68c3158e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Use a separate `Random` instance per thread to improve SDK performance ([#3835](https://github.com/getsentry/sentry-java/pull/3835)) + ### Fixes - Accept manifest integer values when requiring floating values ([#3823](https://github.com/getsentry/sentry-java/pull/3823)) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java index 0399b634fb..e914029c30 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java @@ -54,7 +54,7 @@ import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; import io.sentry.util.HintUtils; -import io.sentry.util.Random; +import io.sentry.util.SentryRandom; import java.io.File; import java.util.ArrayList; import java.util.Arrays; @@ -83,24 +83,13 @@ public final class AnrV2EventProcessor implements BackfillingEventProcessor { private final @NotNull SentryExceptionFactory sentryExceptionFactory; - private final @Nullable Random random; - public AnrV2EventProcessor( final @NotNull Context context, final @NotNull SentryAndroidOptions options, final @NotNull BuildInfoProvider buildInfoProvider) { - this(context, options, buildInfoProvider, null); - } - - AnrV2EventProcessor( - final @NotNull Context context, - final @NotNull SentryAndroidOptions options, - final @NotNull BuildInfoProvider buildInfoProvider, - final @Nullable Random random) { this.context = ContextUtils.getApplicationContext(context); this.options = options; this.buildInfoProvider = buildInfoProvider; - this.random = random; final SentryStackTraceFactory sentryStackTraceFactory = new SentryStackTraceFactory(this.options); @@ -180,9 +169,8 @@ private boolean sampleReplay(final @NotNull SentryEvent event) { try { // we have to sample here with the old sample rate, because it may change between app launches - final @NotNull Random random = this.random != null ? this.random : new Random(); final double replayErrorSampleRateDouble = Double.parseDouble(replayErrorSampleRate); - if (replayErrorSampleRateDouble < random.nextDouble()) { + if (replayErrorSampleRateDouble < SentryRandom.current().nextDouble()) { options .getLogger() .log( diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index ed06b29b35..8e266bcdba 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -5829,6 +5829,11 @@ public final class io/sentry/util/SampleRateUtils { public static fun isValidTracesSampleRate (Ljava/lang/Double;Z)Z } +public final class io/sentry/util/SentryRandom { + public fun ()V + public static fun current ()Lio/sentry/util/Random; +} + public final class io/sentry/util/StringUtils { public static fun byteCountToString (J)Ljava/lang/String; public static fun calculateStringHash (Ljava/lang/String;Lio/sentry/ILogger;)Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 27529d100b..b053230ce5 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -18,6 +18,7 @@ import io.sentry.util.HintUtils; import io.sentry.util.Objects; import io.sentry.util.Random; +import io.sentry.util.SentryRandom; import io.sentry.util.TracingUtils; import java.io.Closeable; import java.io.IOException; @@ -40,7 +41,6 @@ public final class SentryClient implements ISentryClient, IMetricsClient { private final @NotNull SentryOptions options; private final @NotNull ITransport transport; - private final @Nullable Random random; private final @NotNull SortBreadcrumbsByDate sortBreadcrumbsByDate = new SortBreadcrumbsByDate(); private final @NotNull IMetricsAggregator metricsAggregator; @@ -66,8 +66,6 @@ public boolean isEnabled() { options.isEnableMetrics() ? new MetricsAggregator(options, this) : NoopMetricsAggregator.getInstance(); - - this.random = options.getSampleRate() == null ? null : new Random(); } private boolean shouldApplyScopeData( @@ -1183,6 +1181,7 @@ public boolean isHealthy() { } private boolean sample() { + final @Nullable Random random = options.getSampleRate() == null ? null : SentryRandom.current(); // https://docs.sentry.io/development/sdk-dev/features/#event-sampling if (options.getSampleRate() != null && random != null) { final double sampling = options.getSampleRate(); diff --git a/sentry/src/main/java/io/sentry/TracesSampler.java b/sentry/src/main/java/io/sentry/TracesSampler.java index 5e5b808333..ef04cae369 100644 --- a/sentry/src/main/java/io/sentry/TracesSampler.java +++ b/sentry/src/main/java/io/sentry/TracesSampler.java @@ -2,6 +2,7 @@ import io.sentry.util.Objects; import io.sentry.util.Random; +import io.sentry.util.SentryRandom; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -10,14 +11,14 @@ final class TracesSampler { private static final @NotNull Double DEFAULT_TRACES_SAMPLE_RATE = 1.0; private final @NotNull SentryOptions options; - private final @NotNull Random random; + private final @Nullable Random random; public TracesSampler(final @NotNull SentryOptions options) { - this(Objects.requireNonNull(options, "options are required"), new Random()); + this(Objects.requireNonNull(options, "options are required"), null); } @TestOnly - TracesSampler(final @NotNull SentryOptions options, final @NotNull Random random) { + TracesSampler(final @NotNull SentryOptions options, final @Nullable Random random) { this.options = options; this.random = random; } @@ -90,6 +91,13 @@ TracesSamplingDecision sample(final @NotNull SamplingContext samplingContext) { } private boolean sample(final @NotNull Double aDouble) { - return !(aDouble < random.nextDouble()); + return !(aDouble < getRandom().nextDouble()); + } + + private Random getRandom() { + if (random == null) { + return SentryRandom.current(); + } + return random; } } diff --git a/sentry/src/main/java/io/sentry/util/SentryRandom.java b/sentry/src/main/java/io/sentry/util/SentryRandom.java new file mode 100644 index 0000000000..f6e1d0a974 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/SentryRandom.java @@ -0,0 +1,37 @@ +package io.sentry.util; + +import org.jetbrains.annotations.NotNull; + +/** + * This SentryRandom is a compromise used for improving performance of the SDK. + * + *

We did some testing where using Random from multiple threads degrades performance + * significantly. We opted for this approach as it wasn't easily possible to vendor + * ThreadLocalRandom since it's using advanced features that can cause java.lang.IllegalAccessError. + */ +public final class SentryRandom { + + private static final @NotNull SentryRandomThreadLocal instance = new SentryRandomThreadLocal(); + + /** + * Returns the current threads instance of {@link Random}. An instance of {@link Random} will be + * created the first time this is invoked on each thread. + * + *

NOTE: Avoid holding a reference to the returned {@link Random} instance as sharing a + * reference across threads (while being thread-safe) will likely degrade performance + * significantly. + * + * @return random + */ + public static @NotNull Random current() { + return instance.get(); + } + + private static class SentryRandomThreadLocal extends ThreadLocal { + + @Override + protected Random initialValue() { + return new Random(); + } + } +} diff --git a/sentry/src/test/java/io/sentry/util/SentryRandomTest.kt b/sentry/src/test/java/io/sentry/util/SentryRandomTest.kt new file mode 100644 index 0000000000..c812c6cbdc --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/SentryRandomTest.kt @@ -0,0 +1,45 @@ +package io.sentry.util + +import kotlin.test.Test +import kotlin.test.assertNotSame +import kotlin.test.assertSame + +class SentryRandomTest { + + @Test + fun `thread local creates a new instance per thread but keeps re-using it for the same thread`() { + val mainThreadRandom1 = SentryRandom.current() + val mainThreadRandom2 = SentryRandom.current() + assertSame(mainThreadRandom1, mainThreadRandom2) + + var thread1Random1: Random? = null + var thread1Random2: Random? = null + + val thread1 = Thread() { + thread1Random1 = SentryRandom.current() + thread1Random2 = SentryRandom.current() + } + + var thread2Random1: Random? = null + var thread2Random2: Random? = null + + val thread2 = Thread() { + thread2Random1 = SentryRandom.current() + thread2Random2 = SentryRandom.current() + } + + thread1.start() + thread2.start() + thread1.join() + thread2.join() + + assertSame(thread1Random1, thread1Random2) + assertNotSame(mainThreadRandom1, thread1Random1) + + assertSame(thread2Random1, thread2Random2) + assertNotSame(mainThreadRandom1, thread2Random1) + + val mainThreadRandom3 = SentryRandom.current() + assertSame(mainThreadRandom1, mainThreadRandom3) + } +} From 2af8d1ae1af717314bb479e07d163f6461205387 Mon Sep 17 00:00:00 2001 From: LucasZF Date: Mon, 4 Nov 2024 14:54:06 +0000 Subject: [PATCH 02/14] Fix: Allow MaxBreadcrumb 0 / Expose MaxBreadcrumb metadata. (#3836) * expose max-breadcrumbs on meta data and implement disabled queue when maxbreadcrumbs sets to 0 * missing queue class and test * update changelog --------- Co-authored-by: Lucas Co-authored-by: Stefano --- CHANGELOG.md | 2 + .../android/core/ManifestMetadataReader.java | 5 + .../core/ManifestMetadataReaderTest.kt | 25 ++++ .../src/main/AndroidManifest.xml | 3 + .../main/java/io/sentry/DisabledQueue.java | 115 ++++++++++++++++++ sentry/src/main/java/io/sentry/Scope.java | 4 +- .../test/java/io/sentry/DisabledQueueTest.kt | 93 ++++++++++++++ sentry/src/test/java/io/sentry/ScopeTest.kt | 11 ++ 8 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 sentry/src/main/java/io/sentry/DisabledQueue.java create mode 100644 sentry/src/test/java/io/sentry/DisabledQueueTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index cd68c3158e..da49a05efd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,12 @@ ### Features +- Add meta option to set the maximum amount of breadcrumbs to be logged. ([#3836](https://github.com/getsentry/sentry-java/pull/3836)) - Use a separate `Random` instance per thread to improve SDK performance ([#3835](https://github.com/getsentry/sentry-java/pull/3835)) ### Fixes +- Using MaxBreadcrumb with value 0 no longer crashes. ([#3836](https://github.com/getsentry/sentry-java/pull/3836)) - Accept manifest integer values when requiring floating values ([#3823](https://github.com/getsentry/sentry-java/pull/3823)) ## 7.16.0 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 adfd4f22ad..eb60c5d9c4 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 @@ -104,6 +104,8 @@ final class ManifestMetadataReader { static final String ENABLE_METRICS = "io.sentry.enable-metrics"; + static final String MAX_BREADCRUMBS = "io.sentry.max-breadcrumbs"; + static final String REPLAYS_SESSION_SAMPLE_RATE = "io.sentry.session-replay.session-sample-rate"; static final String REPLAYS_ERROR_SAMPLE_RATE = "io.sentry.session-replay.on-error-sample-rate"; @@ -213,6 +215,9 @@ static void applyMetadata( SESSION_TRACKING_TIMEOUT_INTERVAL_MILLIS, options.getSessionTrackingIntervalMillis())); + options.setMaxBreadcrumbs( + (int) readLong(metadata, logger, MAX_BREADCRUMBS, options.getMaxBreadcrumbs())); + options.setEnableActivityLifecycleBreadcrumbs( readBool( metadata, 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 17f0f3950b..ee4b4ae39a 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 @@ -1516,6 +1516,31 @@ class ManifestMetadataReaderTest { assertTrue(fixture.options.experimental.sessionReplay.maskViewClasses.contains(SentryReplayOptions.TEXT_VIEW_CLASS_NAME)) } + @Test + fun `applyMetadata reads maxBreadcrumbs to options and sets the value if found`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.MAX_BREADCRUMBS to 1) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(1, fixture.options.maxBreadcrumbs) + } + + @Test + fun `applyMetadata reads maxBreadcrumbs to options and keeps default if not found`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(100, fixture.options.maxBreadcrumbs) + } + @Test fun `applyMetadata reads integers even when expecting floats`() { // Arrange diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index 2327573a43..d8ae6c709d 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -152,6 +152,9 @@ + + + diff --git a/sentry/src/main/java/io/sentry/DisabledQueue.java b/sentry/src/main/java/io/sentry/DisabledQueue.java new file mode 100644 index 0000000000..afef111af4 --- /dev/null +++ b/sentry/src/main/java/io/sentry/DisabledQueue.java @@ -0,0 +1,115 @@ +package io.sentry; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Queue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class DisabledQueue extends AbstractCollection implements Queue, Serializable { + + /** Serialization version. */ + private static final long serialVersionUID = -8423413834657610417L; + + /** Constructor that creates a queue that does not accept any element. */ + public DisabledQueue() {} + + // ----------------------------------------------------------------------- + /** + * Returns the number of elements stored in the queue. + * + * @return this queue's size + */ + @Override + public int size() { + return 0; + } + + /** + * Returns true if this queue is empty; false otherwise. + * + * @return false + */ + @Override + public boolean isEmpty() { + return false; + } + + /** Does nothing. */ + @Override + public void clear() {} + + /** + * Since the queue is disabled, the element will not be added. + * + * @param element the element to add + * @return false, always + */ + @Override + public boolean add(final @NotNull E element) { + return false; + } + + // ----------------------------------------------------------------------- + + /** + * Receives an element but do nothing with it. + * + * @param element the element to add + * @return false, always + */ + @Override + public boolean offer(@NotNull E element) { + return false; + } + + @Override + public @Nullable E poll() { + return null; + } + + @Override + public @Nullable E element() { + return null; + } + + @Override + public @Nullable E peek() { + return null; + } + + @Override + public @NotNull E remove() { + throw new NoSuchElementException("queue is disabled"); + } + + // ----------------------------------------------------------------------- + + /** + * Returns an iterator over this queue's elements. + * + * @return an iterator over this queue's elements + */ + @Override + public @NotNull Iterator iterator() { + return new Iterator() { + + @Override + public boolean hasNext() { + return false; + } + + @Override + public E next() { + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new IllegalStateException(); + } + }; + } +} diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index f071cb8c5e..c74fabce25 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -754,7 +754,9 @@ public void clearAttachments() { * @return the breadcrumbs queue */ private @NotNull Queue createBreadcrumbsList(final int maxBreadcrumb) { - return SynchronizedQueue.synchronizedQueue(new CircularFifoQueue<>(maxBreadcrumb)); + return maxBreadcrumb > 0 + ? SynchronizedQueue.synchronizedQueue(new CircularFifoQueue<>(maxBreadcrumb)) + : SynchronizedQueue.synchronizedQueue(new DisabledQueue<>()); } /** diff --git a/sentry/src/test/java/io/sentry/DisabledQueueTest.kt b/sentry/src/test/java/io/sentry/DisabledQueueTest.kt new file mode 100644 index 0000000000..351f87eaff --- /dev/null +++ b/sentry/src/test/java/io/sentry/DisabledQueueTest.kt @@ -0,0 +1,93 @@ +package io.sentry +import org.junit.Assert.assertThrows +import java.util.NoSuchElementException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull + +class DisabledQueueTest { + + @Test + fun `size starts empty`() { + val queue = DisabledQueue() + assertEquals(0, queue.size, "Size should always be zero.") + } + + @Test + fun `add does not add elements`() { + val queue = DisabledQueue() + assertFalse(queue.add(1), "add should always return false.") + assertEquals(0, queue.size, "Size should still be zero after attempting to add an element.") + } + + @Test + fun `isEmpty returns false when created`() { + val queue = DisabledQueue() + assertFalse(queue.isEmpty(), "isEmpty should always return false.") + } + + @Test + fun `isEmpty always returns false if add function was called`() { + val queue = DisabledQueue() + queue.add(1) + + assertFalse(queue.isEmpty(), "isEmpty should always return false.") + } + + @Test + fun `offer does not add elements`() { + val queue = DisabledQueue() + assertFalse(queue.offer(1), "offer should always return false.") + assertEquals(0, queue.size, "Size should still be zero after attempting to offer an element.") + } + + @Test + fun `poll returns null`() { + val queue = DisabledQueue() + queue.add(1) + assertNull(queue.poll(), "poll should always return null.") + } + + @Test + fun `peek returns null`() { + val queue = DisabledQueue() + queue.add(1) + + assertNull(queue.peek(), "peek should always return null.") + } + + @Test + fun `element returns null`() { + val queue = DisabledQueue() + assertNull(queue.element(), "element should always return null.") + } + + @Test + fun `remove throws NoSuchElementException`() { + val queue = DisabledQueue() + assertThrows(NoSuchElementException::class.java) { queue.remove() } + } + + @Test + fun `clear does nothing`() { + val queue = DisabledQueue() + queue.clear() // Should not throw an exception + assertEquals(0, queue.size, "Size should remain zero after clear.") + } + + @Test + fun `iterator has no elements`() { + val queue = DisabledQueue() + val iterator = queue.iterator() + assertFalse(iterator.hasNext(), "Iterator should have no elements.") + assertThrows(NoSuchElementException::class.java) { iterator.next() } + } + + @Test + fun `iterator remove throws IllegalStateException`() { + val queue = DisabledQueue() + val iterator = queue.iterator() + assertThrows(IllegalStateException::class.java) { iterator.remove() } + } +} diff --git a/sentry/src/test/java/io/sentry/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index 07b9176de7..a0f54d5205 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -1029,6 +1029,17 @@ class ScopeTest { ) } + @Test + fun `creating a new scope won't crash if max breadcrumbs is set to zero`() { + val options = SentryOptions().apply { + maxBreadcrumbs = 0 + } + val scope = Scope(options) + + // expect no exception to be thrown + // previously was crashing, see https://github.com/getsentry/sentry-java/issues/3313 + } + private fun eventProcessor(): EventProcessor { return object : EventProcessor { override fun process(event: SentryEvent, hint: Hint): SentryEvent? { From fd1151b5c7696d17711c40c2facee553a43d07c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:14:32 +0000 Subject: [PATCH 03/14] Bump gradle/actions (#3842) Bumps [gradle/actions](https://github.com/gradle/actions) from bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b to 707359876a764dbcdb9da0b0ed08291818310c3d. - [Release notes](https://github.com/gradle/actions/releases) - [Commits](https://github.com/gradle/actions/compare/bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b...707359876a764dbcdb9da0b0ed08291818310c3d) --- updated-dependencies: - dependency-name: gradle/actions dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/agp-matrix.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/enforce-license-compliance.yml | 2 +- .github/workflows/generate-javadocs.yml | 2 +- .github/workflows/integration-tests-benchmarks.yml | 4 ++-- .github/workflows/integration-tests-ui-critical.yml | 2 +- .github/workflows/integration-tests-ui.yml | 2 +- .github/workflows/release-build.yml | 2 +- .github/workflows/system-tests-backend.yml | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml index 182c0fa19b..56a4f74fae 100644 --- a/.github/workflows/agp-matrix.yml +++ b/.github/workflows/agp-matrix.yml @@ -38,7 +38,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + uses: gradle/actions/setup-gradle@707359876a764dbcdb9da0b0ed08291818310c3d # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6b885942e9..5988e27888 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + uses: gradle/actions/setup-gradle@707359876a764dbcdb9da0b0ed08291818310c3d # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 68855cbd8d..9ce3d9f9bd 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -36,7 +36,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + uses: gradle/actions/setup-gradle@707359876a764dbcdb9da0b0ed08291818310c3d # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index 2b87c1d278..c4a22a909b 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Gradle - uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + uses: gradle/actions/setup-gradle@707359876a764dbcdb9da0b0ed08291818310c3d # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/generate-javadocs.yml b/.github/workflows/generate-javadocs.yml index b540441fb7..5d358bdf50 100644 --- a/.github/workflows/generate-javadocs.yml +++ b/.github/workflows/generate-javadocs.yml @@ -20,7 +20,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + uses: gradle/actions/setup-gradle@707359876a764dbcdb9da0b0ed08291818310c3d # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/integration-tests-benchmarks.yml b/.github/workflows/integration-tests-benchmarks.yml index 58fc933752..34eea1733c 100644 --- a/.github/workflows/integration-tests-benchmarks.yml +++ b/.github/workflows/integration-tests-benchmarks.yml @@ -37,7 +37,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + uses: gradle/actions/setup-gradle@707359876a764dbcdb9da0b0ed08291818310c3d # pin@v3 with: gradle-home-cache-cleanup: true @@ -86,7 +86,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + uses: gradle/actions/setup-gradle@707359876a764dbcdb9da0b0ed08291818310c3d # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/integration-tests-ui-critical.yml b/.github/workflows/integration-tests-ui-critical.yml index 815adf9b61..a28400e50f 100644 --- a/.github/workflows/integration-tests-ui-critical.yml +++ b/.github/workflows/integration-tests-ui-critical.yml @@ -32,7 +32,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + uses: gradle/actions/setup-gradle@707359876a764dbcdb9da0b0ed08291818310c3d # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/integration-tests-ui.yml b/.github/workflows/integration-tests-ui.yml index 6c4ad6564f..059e3c7424 100644 --- a/.github/workflows/integration-tests-ui.yml +++ b/.github/workflows/integration-tests-ui.yml @@ -32,7 +32,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + uses: gradle/actions/setup-gradle@707359876a764dbcdb9da0b0ed08291818310c3d # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 83a3c82f8c..c625efa8eb 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -26,7 +26,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + uses: gradle/actions/setup-gradle@707359876a764dbcdb9da0b0ed08291818310c3d # pin@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index 2222f910ad..3703e26168 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -40,7 +40,7 @@ jobs: java-version: '17' - name: Setup Gradle - uses: gradle/actions/setup-gradle@bb0c460cbf5354b0cddd15bacdf0d6aaa3e5a32b # pin@v3 + uses: gradle/actions/setup-gradle@707359876a764dbcdb9da0b0ed08291818310c3d # pin@v3 with: gradle-home-cache-cleanup: true From fba10b88355deba03a45d039d8d3103d5455e2a7 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Tue, 12 Nov 2024 09:23:04 +0100 Subject: [PATCH 04/14] Limit emulator size to 4096M (#3875) CI runs recently started to fail due to disk size issues when creating the emulator: > ERROR | Not enough space to create userdata partition. Available: 7329.925781 MB at /home/runner/.android/avd/../avd/test.avd, need 7372.800000 MB. #skip-changelog --- .github/workflows/agp-matrix.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml index 56a4f74fae..b93bc364ac 100644 --- a/.github/workflows/agp-matrix.yml +++ b/.github/workflows/agp-matrix.yml @@ -69,6 +69,7 @@ jobs: target: 'aosp_atd' arch: x86 channel: canary # Necessary for ATDs + disk-size: 4096M script: ./gradlew sentry-android-integration-tests:sentry-uitest-android:connectedReleaseAndroidTest -DtestBuildType=release --daemon - name: Upload test results From 566da7624407dd4df6ea3a128431cc223ecc2035 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 12 Nov 2024 14:12:53 +0100 Subject: [PATCH 05/14] Fix standalone tomcat jndi issue (#3873) * fix standalone tomcat jndi issue * format * add changelog entry * adapt changelog --- CHANGELOG.md | 3 +++ sentry/src/main/java/io/sentry/Baggage.java | 6 +++--- sentry/src/main/java/io/sentry/DsnUtil.java | 2 +- .../src/main/java/io/sentry/RequestDetailsResolver.java | 2 +- sentry/src/main/java/io/sentry/Sentry.java | 2 +- sentry/src/main/java/io/sentry/SentryOptions.java | 8 +++++--- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da49a05efd..34fa731c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ - Using MaxBreadcrumb with value 0 no longer crashes. ([#3836](https://github.com/getsentry/sentry-java/pull/3836)) - Accept manifest integer values when requiring floating values ([#3823](https://github.com/getsentry/sentry-java/pull/3823)) +- Fix standalone tomcat jndi issue ([#3873](https://github.com/getsentry/sentry-java/pull/3873)) + - Using Sentry Spring Boot on a standalone tomcat caused the following error: + - Failed to bind properties under 'sentry.parsed-dsn' to io.sentry.Dsn ## 7.16.0 diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index 0a80b154bf..befdead4a5 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -134,7 +134,7 @@ public static Baggage fromEvent( final Baggage baggage = new Baggage(options.getLogger()); final SpanContext trace = event.getContexts().getTrace(); baggage.setTraceId(trace != null ? trace.getTraceId().toString() : null); - baggage.setPublicKey(options.getParsedDsn().getPublicKey()); + baggage.setPublicKey(options.retrieveParsedDsn().getPublicKey()); baggage.setRelease(event.getRelease()); baggage.setEnvironment(event.getEnvironment()); final User user = event.getUser(); @@ -405,7 +405,7 @@ public void setValuesFromTransaction( final @NotNull SentryOptions sentryOptions, final @Nullable TracesSamplingDecision samplingDecision) { setTraceId(transaction.getSpanContext().getTraceId().toString()); - setPublicKey(sentryOptions.getParsedDsn().getPublicKey()); + setPublicKey(sentryOptions.retrieveParsedDsn().getPublicKey()); setRelease(sentryOptions.getRelease()); setEnvironment(sentryOptions.getEnvironment()); setUserSegment(user != null ? getSegment(user) : null); @@ -427,7 +427,7 @@ public void setValuesFromScope( final @Nullable User user = scope.getUser(); final @NotNull SentryId replayId = scope.getReplayId(); setTraceId(propagationContext.getTraceId().toString()); - setPublicKey(options.getParsedDsn().getPublicKey()); + setPublicKey(options.retrieveParsedDsn().getPublicKey()); setRelease(options.getRelease()); setEnvironment(options.getEnvironment()); if (!SentryId.EMPTY_ID.equals(replayId)) { diff --git a/sentry/src/main/java/io/sentry/DsnUtil.java b/sentry/src/main/java/io/sentry/DsnUtil.java index 6cc0dc360b..b6902ad274 100644 --- a/sentry/src/main/java/io/sentry/DsnUtil.java +++ b/sentry/src/main/java/io/sentry/DsnUtil.java @@ -23,7 +23,7 @@ public static boolean urlContainsDsnHost(@Nullable SentryOptions options, @Nulla return false; } - final @NotNull Dsn dsn = options.getParsedDsn(); + final @NotNull Dsn dsn = options.retrieveParsedDsn(); final @NotNull URI sentryUri = dsn.getSentryUri(); final @Nullable String dsnHost = sentryUri.getHost(); diff --git a/sentry/src/main/java/io/sentry/RequestDetailsResolver.java b/sentry/src/main/java/io/sentry/RequestDetailsResolver.java index 6083c69e99..bba4dc19ac 100644 --- a/sentry/src/main/java/io/sentry/RequestDetailsResolver.java +++ b/sentry/src/main/java/io/sentry/RequestDetailsResolver.java @@ -21,7 +21,7 @@ public RequestDetailsResolver(final @NotNull SentryOptions options) { @NotNull RequestDetails resolve() { - final Dsn dsn = options.getParsedDsn(); + final Dsn dsn = options.retrieveParsedDsn(); final URI sentryUri = dsn.getSentryUri(); final String envelopeUrl = sentryUri.resolve(sentryUri.getPath() + "/envelope/").toString(); diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 6e4a2530a7..f075876325 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -398,7 +398,7 @@ private static boolean initConfigurations(final @NotNull SentryOptions options) } // This creates the DSN object and performs some checks - options.getParsedDsn(); + options.retrieveParsedDsn(); ILogger logger = options.getLogger(); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 3c286f2bff..8614abc287 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -541,13 +541,15 @@ public void addIntegration(@NotNull Integration integration) { } /** - * Evaluates and parses the DSN. May throw an exception if the DSN is invalid. + * Evaluates and parses the DSN. May throw an exception if the DSN is invalid. Renamed from + * `getParsedDsn` as this would cause an error when deploying as WAR to Tomcat due to `JNDI` + * property binding. * * @return the parsed DSN or throws if dsn is invalid */ @ApiStatus.Internal @NotNull - Dsn getParsedDsn() throws IllegalArgumentException { + Dsn retrieveParsedDsn() throws IllegalArgumentException { return parsedDsn.getValue(); } @@ -2457,7 +2459,7 @@ public void setEnableScreenTracking(final boolean enableScreenTracking) { */ void loadLazyFields() { getSerializer(); - getParsedDsn(); + retrieveParsedDsn(); getEnvelopeReader(); getDateProvider(); } From 4bd1aa38243dd26abd3c26af70868c2fe5e94048 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 12 Nov 2024 14:05:40 +0000 Subject: [PATCH 06/14] release: 7.17.0 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34fa731c2b..d7a41e65dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 7.17.0 ### Features diff --git a/gradle.properties b/gradle.properties index 4268e10785..f9cfa874ad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ android.useAndroidX=true android.defaults.buildfeatures.buildconfig=true # Release information -versionName=7.16.0 +versionName=7.17.0 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android From 17a41c14c3928588cb52b7d8546e787c473d8850 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 13 Nov 2024 21:43:41 +0100 Subject: [PATCH 07/14] Ensure android initialization process continues even if options configuration block throws an exception (#3887) * Ensure android event processors are added even if options configuration block throws * Changelog --- CHANGELOG.md | 6 ++++++ .../java/io/sentry/android/core/SentryAndroid.java | 12 +++++++++++- .../io/sentry/android/core/SentryAndroidTest.kt | 13 +++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a41e65dc..98595beaa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Ensure android initialization process continues even if options configuration block throws an exception ([#3887](https://github.com/getsentry/sentry-java/pull/3887)) + ## 7.17.0 ### Features diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java index e6e677334c..adeb451332 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java @@ -129,7 +129,17 @@ public static synchronized void init( isTimberAvailable, isReplayAvailable); - configuration.configure(options); + try { + configuration.configure(options); + } catch (Throwable t) { + // let it slip, but log it + options + .getLogger() + .log( + SentryLevel.ERROR, + "Error in the 'OptionsConfiguration.configure' callback.", + t); + } // if SentryPerformanceProvider was disabled or removed, // we set the app start / sdk init time here instead diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index c31076d1ff..17c11475c9 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -517,6 +517,19 @@ class SentryAndroidTest { assertEquals(99, AppStartMetrics.getInstance().appStartTimeSpan.startUptimeMs) } + @Test + fun `if the config options block throws still intializes android event processors`() { + lateinit var optionsRef: SentryOptions + fixture.initSut(context = mock()) { options -> + optionsRef = options + options.dsn = "https://key@sentry.io/123" + throw RuntimeException("Boom!") + } + + assertTrue(optionsRef.eventProcessors.any { it is DefaultAndroidEventProcessor }) + assertTrue(optionsRef.eventProcessors.any { it is AnrV2EventProcessor }) + } + private fun prefillScopeCache(cacheDir: String) { val scopeDir = File(cacheDir, SCOPE_CACHE).also { it.mkdirs() } File(scopeDir, BREADCRUMBS_FILENAME).writeText( From 091f84ef1280c98a0e9ad85f142135458ad91a18 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 14 Nov 2024 10:21:00 +0100 Subject: [PATCH 08/14] Add support for 16KB page sizes (Android 15) (#3620) * Add support for 16KB page sizes (Android 15) * Update Changelog * Revert NDK/min API level bump, properly apply link options instead * Fix Changelog * Exclude sentry-native Java code from being spotless-checked * Bump sentry-native to 0.7.8 * Update Changelog * Update CHANGELOG.md * release: 7.17.0-alpha.1 --------- Co-authored-by: getsentry-bot Co-authored-by: getsentry-bot --- CHANGELOG.md | 11 +++++++++++ build.gradle.kts | 2 +- sentry-android-ndk/CMakeLists.txt | 5 +++++ sentry-android-ndk/build.gradle.kts | 7 +++++++ sentry-android-ndk/sentry-native | 2 +- sentry-samples/sentry-samples-android/CMakeLists.txt | 5 +++++ .../sentry-samples-android/build.gradle.kts | 7 +++++++ 7 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98595beaa8..00744b33f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,21 @@ ## Unreleased +### Features + +- Android 15: Add support for 16KB page sizes ([#3620](https://github.com/getsentry/sentry-java/pull/3620)) + - See https://developer.android.com/guide/practices/page-sizes for more details + ### Fixes - Ensure android initialization process continues even if options configuration block throws an exception ([#3887](https://github.com/getsentry/sentry-java/pull/3887)) +### Dependencies + +- Bump Native SDK from v0.7.2 to v0.7.8 ([#3620](https://github.com/getsentry/sentry-java/pull/3620)) + - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#078) + - [diff](https://github.com/getsentry/sentry-native/compare/0.7.2...0.7.8) + ## 7.17.0 ### Features diff --git a/build.gradle.kts b/build.gradle.kts index 9d53252562..86cd98d54a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -226,7 +226,7 @@ spotless { target("**/*.java") removeUnusedImports() googleJavaFormat() - targetExclude("**/generated/**", "**/vendor/**") + targetExclude("**/generated/**", "**/vendor/**", "**/sentry-native/**") } kotlin { target("**/*.kt") diff --git a/sentry-android-ndk/CMakeLists.txt b/sentry-android-ndk/CMakeLists.txt index c9a0181935..ff5fc2540b 100644 --- a/sentry-android-ndk/CMakeLists.txt +++ b/sentry-android-ndk/CMakeLists.txt @@ -15,3 +15,8 @@ add_subdirectory(${SENTRY_NATIVE_SRC} sentry_build) target_link_libraries(sentry-android PRIVATE $ ) + +# Android 15: Support 16KB page sizes +# see https://developer.android.com/guide/practices/page-sizes +target_link_options(sentry PRIVATE "-Wl,-z,max-page-size=16384") +target_link_options(sentry-android PRIVATE "-Wl,-z,max-page-size=16384") diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index f1c4873053..ee0819eb83 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -95,6 +95,13 @@ android { ignore = true } } + + @Suppress("UnstableApiUsage") + packagingOptions { + jniLibs { + useLegacyPackaging = true + } + } } dependencies { diff --git a/sentry-android-ndk/sentry-native b/sentry-android-ndk/sentry-native index 0f1d664759..f44ab0be7c 160000 --- a/sentry-android-ndk/sentry-native +++ b/sentry-android-ndk/sentry-native @@ -1 +1 @@ -Subproject commit 0f1d664759cba187a846a562f9d55f3c62dffaa3 +Subproject commit f44ab0be7c9d46bbaf24536fb15e7d55b98d6716 diff --git a/sentry-samples/sentry-samples-android/CMakeLists.txt b/sentry-samples/sentry-samples-android/CMakeLists.txt index ad170fe404..03a2da3f66 100644 --- a/sentry-samples/sentry-samples-android/CMakeLists.txt +++ b/sentry-samples/sentry-samples-android/CMakeLists.txt @@ -15,3 +15,8 @@ target_link_libraries(native-sample PRIVATE ${LOG_LIB} $ ) + +# Android 15: Support 16KB page sizes +# see https://developer.android.com/guide/practices/page-sizes +target_link_options(native-sample PRIVATE "-Wl,-z,max-page-size=16384") + diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index 204ef83fc2..90c71b8289 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -99,6 +99,13 @@ android { ignore = true } } + + @Suppress("UnstableApiUsage") + packagingOptions { + jniLibs { + useLegacyPackaging = true + } + } } dependencies { From a1831635b97fd324c127bc4857fc307171d8394b Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 14 Nov 2024 10:59:58 +0100 Subject: [PATCH 09/14] Do not report parsing ANR error when there are no threads (#3888) * Ensure android event processors are added even if options configuration block throws * Changelog * Do not report parsing ANR error when there are no threads * Changelog * Fix tests --- CHANGELOG.md | 2 + .../sentry/android/core/AnrV2Integration.java | 7 +- .../android/core/AnrV2IntegrationTest.kt | 50 +- .../threaddump/ThreadDumpParserTest.kt | 12 + .../test/resources/thread_dump_bad_data.txt | 1029 +++++++++++++++++ 5 files changed, 1093 insertions(+), 7 deletions(-) create mode 100644 sentry-android-core/src/test/resources/thread_dump_bad_data.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 00744b33f4..92d7d2ef4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ### Fixes - Ensure android initialization process continues even if options configuration block throws an exception ([#3887](https://github.com/getsentry/sentry-java/pull/3887)) +- Do not report parsing ANR error when there are no threads ([#3888](https://github.com/getsentry/sentry-java/pull/3888)) + - This should significantly reduce the number of events with message "Sentry Android SDK failed to parse system thread dump..." reported ### Dependencies diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java index 618f53554f..c19c3aeac6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2Integration.java @@ -313,8 +313,11 @@ private void reportAsSentryEvent( final ThreadDumpParser threadDumpParser = new ThreadDumpParser(options, isBackground); final List threads = threadDumpParser.parse(lines); if (threads.isEmpty()) { - // if the list is empty this means our regex matching is garbage and this is still error - return new ParseResult(ParseResult.Type.ERROR, dump); + // if the list is empty this means the system failed to capture a proper thread dump of + // the android threads, and only contains kernel-level threads and statuses, those ANRs + // are not actionable and neither they are reported by Google Play Console, so we just + // fall back to not reporting them + return new ParseResult(ParseResult.Type.NO_DUMP); } return new ParseResult(ParseResult.Type.DUMP, dump, threads); } catch (Throwable e) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt index 885ad22c8f..a658a24505 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrV2IntegrationTest.kt @@ -101,7 +101,8 @@ class AnrV2IntegrationTest { reason: Int? = ApplicationExitInfo.REASON_ANR, timestamp: Long? = null, importance: Int? = null, - addTrace: Boolean = true + addTrace: Boolean = true, + addBadTrace: Boolean = false ) { val builder = ApplicationExitInfoBuilder.newBuilder() if (reason != null) { @@ -117,8 +118,36 @@ class AnrV2IntegrationTest { if (!addTrace) { return } - whenever(mock.traceInputStream).thenReturn( - """ + if (addBadTrace) { + whenever(mock.traceInputStream).thenReturn( + """ + Subject: Input dispatching timed out (7985007 com.example.app/com.example.app.ui.MainActivity (server) is not responding. Waited 5000ms for FocusEvent(hasFocus=false)) + Here are no Binder-related exception messages available. + Pid(12233) have D state thread(tid:12236 name:Signal Catcher) + + + RssHwmKb: 823716 + RssKb: 548348 + RssAnonKb: 382156 + RssShmemKb: 13304 + VmSwapKb: 82484 + + + --- CriticalEventLog --- + capacity: 20 + timestamp_ms: 1731507490032 + window_ms: 300000 + + ----- dumping pid: 12233 at 313446151 + libdebuggerd_client: unexpected registration response: 0 + + ----- Waiting Channels: pid 12233 at 2024-11-13 19:48:09.980104540+0530 ----- + Cmd line: com.example.app:mainProcess + """.trimIndent().byteInputStream() + ) + } else { + whenever(mock.traceInputStream).thenReturn( + """ "main" prio=5 tid=1 Blocked | group="main" sCount=1 ucsCount=0 flags=1 obj=0x72a985e0 self=0xb400007cabc57380 | sysTid=28941 nice=-10 cgrp=top-app sched=0/0 handle=0x7deceb74f8 @@ -147,8 +176,9 @@ class AnrV2IntegrationTest { native: #02 pc 00000000000b63b0 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+208) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) native: #03 pc 00000000000530b8 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64) (BuildId: 01331f74b0bb2cb958bdc15282b8ec7b) (no managed stack frames) - """.trimIndent().byteInputStream() - ) + """.trimIndent().byteInputStream() + ) + } } shadowActivityManager.addApplicationExitInfo(exitInfo) } @@ -551,4 +581,14 @@ class AnrV2IntegrationTest { verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) } + + @Test + fun `when traceInputStream has bad data, does not report ANR`() { + val integration = fixture.getSut(tmpDir, lastReportedAnrTimestamp = oldTimestamp) + fixture.addAppExitInfo(timestamp = newTimestamp, addBadTrace = true) + + integration.register(fixture.hub, fixture.options) + + verify(fixture.hub, never()).captureEvent(any(), anyOrNull()) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt index 9ef22d6a13..19de2e4935 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/threaddump/ThreadDumpParserTest.kt @@ -7,6 +7,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue class ThreadDumpParserTest { @@ -95,4 +96,15 @@ class ThreadDumpParserTest { assertEquals(28, lastFrame.lineno) assertNull(lastFrame.isInApp) } + + @Test + fun `thread dump garbage`() { + val lines = Lines.readLines(File("src/test/resources/thread_dump_bad_data.txt")) + val parser = ThreadDumpParser( + SentryOptions().apply { addInAppInclude("io.sentry.samples") }, + false + ) + val threads = parser.parse(lines) + assertTrue(threads.isEmpty()) + } } diff --git a/sentry-android-core/src/test/resources/thread_dump_bad_data.txt b/sentry-android-core/src/test/resources/thread_dump_bad_data.txt new file mode 100644 index 0000000000..abbe042fce --- /dev/null +++ b/sentry-android-core/src/test/resources/thread_dump_bad_data.txt @@ -0,0 +1,1029 @@ +Subject: Input dispatching timed out (7985007 com.example.app/com.example.app.ui.MainActivity (server) is not responding. Waited 5000ms for FocusEvent(hasFocus=false)) +Here are no Binder-related exception messages available. +Pid(12233) have D state thread(tid:12236 name:Signal Catcher) + + +RssHwmKb: 823716 +RssKb: 548348 +RssAnonKb: 382156 +RssShmemKb: 13304 +VmSwapKb: 82484 + + +--- CriticalEventLog --- +capacity: 20 +timestamp_ms: 1731507490032 +window_ms: 300000 + +----- dumping pid: 12233 at 313446151 +libdebuggerd_client: unexpected registration response: 0 + +----- Waiting Channels: pid 12233 at 2024-11-13 19:48:09.980104540+0530 ----- +Cmd line: com.example.app:mainProcess + +sysTid=12233 state=R 0 +sysTid=12236 state=S do_sigtimedwait +sysTid=12237 state=S futex_wait_queue_me +sysTid=12238 state=S futex_wait_queue_me +sysTid=12239 state=S futex_wait_queue_me +sysTid=12240 state=S futex_wait_queue_me +sysTid=12241 state=S futex_wait_queue_me +sysTid=12242 state=S binder_wait_for_work +sysTid=12243 state=S binder_wait_for_work +sysTid=12245 state=S binder_wait_for_work +sysTid=12252 state=S futex_wait_queue_me +sysTid=12254 state=S inotify_read +sysTid=12257 state=S __arm64_sys_nanosleep +sysTid=12259 state=R 0 +sysTid=12260 state=S __arm64_sys_nanosleep +sysTid=12268 state=R 0 +sysTid=12269 state=S __arm64_sys_nanosleep +sysTid=12270 state=S __arm64_sys_nanosleep +sysTid=12278 state=S futex_wait_queue_me +sysTid=12279 state=S futex_wait_queue_me +sysTid=12280 state=S futex_wait_queue_me +sysTid=12283 state=S futex_wait_queue_me +sysTid=12287 state=S futex_wait_queue_me +sysTid=12290 state=S futex_wait_queue_me +sysTid=12291 state=S futex_wait_queue_me +sysTid=12292 state=S futex_wait_queue_me +sysTid=12295 state=S futex_wait_queue_me +sysTid=12296 state=S futex_wait_queue_me +sysTid=12297 state=S futex_wait_queue_me +sysTid=12298 state=S futex_wait_queue_me +sysTid=12304 state=S futex_wait_queue_me +sysTid=12310 state=S futex_wait_queue_me +sysTid=12311 state=S futex_wait_queue_me +sysTid=12312 state=S do_epoll_wait +sysTid=12314 state=S futex_wait_queue_me +sysTid=12315 state=S futex_wait_queue_me +sysTid=12316 state=S futex_wait_queue_me +sysTid=12317 state=S futex_wait_queue_me +sysTid=12319 state=S futex_wait_queue_me +sysTid=12320 state=S do_epoll_wait +sysTid=12321 state=S futex_wait_queue_me +sysTid=12323 state=S futex_wait_queue_me +sysTid=12324 state=S futex_wait_queue_me +sysTid=12325 state=S futex_wait_queue_me +sysTid=12328 state=S do_epoll_wait +sysTid=12341 state=S futex_wait_queue_me +sysTid=12343 state=S futex_wait_queue_me +sysTid=12344 state=S futex_wait_queue_me +sysTid=12349 state=S futex_wait_queue_me +sysTid=12350 state=S futex_wait_queue_me +sysTid=12351 state=S futex_wait_queue_me +sysTid=12352 state=S futex_wait_queue_me +sysTid=12353 state=S binder_wait_for_work +sysTid=12354 state=S futex_wait_queue_me +sysTid=12355 state=S futex_wait_queue_me +sysTid=12358 state=R lock_page_maybe_drop_mmap +sysTid=12362 state=S futex_wait_queue_me +sysTid=12363 state=S futex_wait_queue_me +sysTid=12365 state=S do_epoll_wait +sysTid=12366 state=S futex_wait_queue_me +sysTid=12367 state=S futex_wait_queue_me +sysTid=12368 state=S futex_wait_queue_me +sysTid=12370 state=S futex_wait_queue_me +sysTid=12371 state=S futex_wait_queue_me +sysTid=12373 state=S futex_wait_queue_me +sysTid=12384 state=S binder_wait_for_work +sysTid=12391 state=S futex_wait_queue_me +sysTid=12399 state=S do_epoll_wait +sysTid=12401 state=S futex_wait_queue_me +sysTid=12402 state=S futex_wait_queue_me +sysTid=12404 state=S futex_wait_queue_me +sysTid=12405 state=S do_epoll_wait +sysTid=12407 state=S futex_wait_queue_me +sysTid=12408 state=S futex_wait_queue_me +sysTid=12409 state=S do_wait +sysTid=12410 state=S futex_wait_queue_me +sysTid=12412 state=S do_epoll_wait +sysTid=12435 state=S do_epoll_wait +sysTid=12468 state=S do_epoll_wait +sysTid=12514 state=S futex_wait_queue_me +sysTid=12550 state=S futex_wait_queue_me +sysTid=12561 state=S binder_wait_for_work +sysTid=12567 state=S binder_wait_for_work +sysTid=12580 state=S futex_wait_queue_me +sysTid=12619 state=S futex_wait_queue_me +sysTid=12627 state=S futex_wait_queue_me +sysTid=12644 state=S futex_wait_queue_me +sysTid=12887 state=S futex_wait_queue_me +sysTid=13430 state=S futex_wait_queue_me +sysTid=13438 state=S futex_wait_queue_me +sysTid=13443 state=S futex_wait_queue_me +sysTid=13454 state=S futex_wait_queue_me +sysTid=13455 state=S futex_wait_queue_me +sysTid=13564 state=S binder_wait_for_work +sysTid=13576 state=S binder_wait_for_work +sysTid=13579 state=S binder_wait_for_work +sysTid=13616 state=S binder_wait_for_work +sysTid=13624 state=S futex_wait_queue_me +sysTid=13706 state=S futex_wait_queue_me +sysTid=13722 state=S futex_wait_queue_me +sysTid=13724 state=S futex_wait_queue_me +sysTid=13730 state=S futex_wait_queue_me +sysTid=13740 state=S futex_wait_queue_me +sysTid=13744 state=S futex_wait_queue_me +sysTid=13745 state=S futex_wait_queue_me +sysTid=13748 state=S futex_wait_queue_me +sysTid=13754 state=S futex_wait_queue_me +sysTid=13756 state=S futex_wait_queue_me +sysTid=13757 state=S futex_wait_queue_me +sysTid=13758 state=S futex_wait_queue_me +sysTid=13759 state=S futex_wait_queue_me +sysTid=13763 state=S futex_wait_queue_me +sysTid=13767 state=S futex_wait_queue_me +sysTid=13768 state=S futex_wait_queue_me +sysTid=13769 state=S futex_wait_queue_me +sysTid=13773 state=S futex_wait_queue_me +sysTid=13776 state=S futex_wait_queue_me +sysTid=13781 state=S futex_wait_queue_me +sysTid=13782 state=S futex_wait_queue_me +sysTid=13783 state=S futex_wait_queue_me +sysTid=13784 state=S futex_wait_queue_me +sysTid=13786 state=S futex_wait_queue_me +sysTid=13791 state=S futex_wait_queue_me +sysTid=13792 state=S futex_wait_queue_me +sysTid=13793 state=S futex_wait_queue_me +sysTid=13794 state=S futex_wait_queue_me +sysTid=13795 state=S futex_wait_queue_me +sysTid=13796 state=S futex_wait_queue_me +sysTid=13797 state=S futex_wait_queue_me +sysTid=13798 state=S futex_wait_queue_me +sysTid=13799 state=S futex_wait_queue_me +sysTid=13800 state=S futex_wait_queue_me +sysTid=13806 state=S futex_wait_queue_me +sysTid=13809 state=S futex_wait_queue_me +sysTid=13814 state=S futex_wait_queue_me +sysTid=13815 state=S futex_wait_queue_me +sysTid=13816 state=S futex_wait_queue_me +sysTid=13817 state=S futex_wait_queue_me +sysTid=13818 state=S futex_wait_queue_me +sysTid=13820 state=S futex_wait_queue_me +sysTid=13825 state=S futex_wait_queue_me +sysTid=13830 state=S futex_wait_queue_me +sysTid=13831 state=S futex_wait_queue_me +sysTid=13832 state=S futex_wait_queue_me +sysTid=13833 state=S futex_wait_queue_me +sysTid=13834 state=S futex_wait_queue_me +sysTid=13835 state=S futex_wait_queue_me +sysTid=13836 state=S futex_wait_queue_me +sysTid=13841 state=S futex_wait_queue_me +sysTid=13847 state=S futex_wait_queue_me +sysTid=13848 state=S futex_wait_queue_me +sysTid=13849 state=S futex_wait_queue_me +sysTid=13850 state=S futex_wait_queue_me +sysTid=13851 state=S futex_wait_queue_me +sysTid=13852 state=S futex_wait_queue_me +sysTid=13853 state=S futex_wait_queue_me +sysTid=13854 state=S futex_wait_queue_me +sysTid=13857 state=S futex_wait_queue_me +sysTid=13863 state=S futex_wait_queue_me +sysTid=13867 state=S futex_wait_queue_me +sysTid=13880 state=S futex_wait_queue_me +sysTid=13920 state=S futex_wait_queue_me +sysTid=13949 state=S futex_wait_queue_me +sysTid=13953 state=S futex_wait_queue_me +sysTid=13954 state=S futex_wait_queue_me +sysTid=13955 state=S futex_wait_queue_me +sysTid=13958 state=S futex_wait_queue_me +sysTid=13959 state=S futex_wait_queue_me +sysTid=13967 state=S futex_wait_queue_me +sysTid=13980 state=S futex_wait_queue_me +sysTid=13981 state=S futex_wait_queue_me +sysTid=13982 state=S futex_wait_queue_me +sysTid=13983 state=S futex_wait_queue_me +sysTid=13984 state=S futex_wait_queue_me +sysTid=13986 state=S futex_wait_queue_me +sysTid=13987 state=S futex_wait_queue_me +sysTid=13991 state=S futex_wait_queue_me +sysTid=13998 state=S futex_wait_queue_me +sysTid=13999 state=S futex_wait_queue_me +sysTid=14000 state=S futex_wait_queue_me +sysTid=14001 state=S futex_wait_queue_me +sysTid=14002 state=S futex_wait_queue_me +sysTid=14003 state=S futex_wait_queue_me +sysTid=14004 state=S futex_wait_queue_me +sysTid=14005 state=S futex_wait_queue_me +sysTid=14006 state=S futex_wait_queue_me +sysTid=14007 state=S futex_wait_queue_me +sysTid=14026 state=S futex_wait_queue_me +sysTid=14052 state=S futex_wait_queue_me +sysTid=14057 state=S futex_wait_queue_me +sysTid=14060 state=S futex_wait_queue_me +sysTid=14063 state=S futex_wait_queue_me +sysTid=14069 state=S futex_wait_queue_me +sysTid=14072 state=S futex_wait_queue_me +sysTid=14075 state=S futex_wait_queue_me +sysTid=14081 state=S futex_wait_queue_me +sysTid=14084 state=S futex_wait_queue_me +sysTid=14089 state=S futex_wait_queue_me +sysTid=14090 state=S futex_wait_queue_me +sysTid=14091 state=S futex_wait_queue_me +sysTid=14092 state=S futex_wait_queue_me +sysTid=14093 state=S futex_wait_queue_me +sysTid=14094 state=S futex_wait_queue_me +sysTid=14095 state=S futex_wait_queue_me +sysTid=14096 state=S futex_wait_queue_me +sysTid=14097 state=S futex_wait_queue_me +sysTid=14098 state=S futex_wait_queue_me +sysTid=14099 state=S futex_wait_queue_me +sysTid=14100 state=S futex_wait_queue_me +sysTid=14101 state=S futex_wait_queue_me +sysTid=14102 state=S futex_wait_queue_me +sysTid=14103 state=S futex_wait_queue_me +sysTid=14104 state=S futex_wait_queue_me +sysTid=14106 state=S futex_wait_queue_me +sysTid=14111 state=S futex_wait_queue_me +sysTid=14117 state=S futex_wait_queue_me +sysTid=14120 state=S futex_wait_queue_me +sysTid=14121 state=S futex_wait_queue_me +sysTid=14122 state=S futex_wait_queue_me +sysTid=14123 state=S futex_wait_queue_me +sysTid=14124 state=S futex_wait_queue_me +sysTid=14129 state=S futex_wait_queue_me +sysTid=14130 state=S futex_wait_queue_me +sysTid=14131 state=S futex_wait_queue_me +sysTid=14132 state=S futex_wait_queue_me +sysTid=14136 state=S futex_wait_queue_me +sysTid=14144 state=S futex_wait_queue_me +sysTid=14148 state=S futex_wait_queue_me +sysTid=14154 state=S futex_wait_queue_me +sysTid=14158 state=S futex_wait_queue_me +sysTid=14164 state=S futex_wait_queue_me +sysTid=14167 state=S futex_wait_queue_me +sysTid=14168 state=S futex_wait_queue_me +sysTid=14169 state=S futex_wait_queue_me +sysTid=14170 state=S futex_wait_queue_me +sysTid=14171 state=S futex_wait_queue_me +sysTid=14172 state=S futex_wait_queue_me +sysTid=14173 state=S futex_wait_queue_me +sysTid=14174 state=S futex_wait_queue_me +sysTid=14175 state=S futex_wait_queue_me +sysTid=14176 state=S futex_wait_queue_me +sysTid=14177 state=S futex_wait_queue_me +sysTid=14178 state=S futex_wait_queue_me +sysTid=14179 state=S futex_wait_queue_me +sysTid=14180 state=S futex_wait_queue_me +sysTid=14181 state=S futex_wait_queue_me +sysTid=14182 state=S futex_wait_queue_me +sysTid=14190 state=S futex_wait_queue_me +sysTid=14195 state=S futex_wait_queue_me +sysTid=14198 state=S futex_wait_queue_me +sysTid=14207 state=S futex_wait_queue_me +sysTid=14209 state=S futex_wait_queue_me +sysTid=14210 state=S futex_wait_queue_me +sysTid=14214 state=S futex_wait_queue_me +sysTid=14220 state=S futex_wait_queue_me +sysTid=14223 state=S futex_wait_queue_me +sysTid=14227 state=S futex_wait_queue_me +sysTid=14235 state=S futex_wait_queue_me +sysTid=14242 state=S futex_wait_queue_me +sysTid=14243 state=S futex_wait_queue_me +sysTid=14244 state=S futex_wait_queue_me +sysTid=14245 state=S futex_wait_queue_me +sysTid=14246 state=S futex_wait_queue_me +sysTid=14247 state=S futex_wait_queue_me +sysTid=14248 state=S futex_wait_queue_me +sysTid=14249 state=S futex_wait_queue_me +sysTid=14250 state=S futex_wait_queue_me +sysTid=14251 state=S futex_wait_queue_me +sysTid=14253 state=S futex_wait_queue_me +sysTid=14259 state=S futex_wait_queue_me +sysTid=14264 state=S futex_wait_queue_me +sysTid=14269 state=S futex_wait_queue_me +sysTid=14272 state=S futex_wait_queue_me +sysTid=14277 state=S futex_wait_queue_me +sysTid=14282 state=S futex_wait_queue_me +sysTid=14296 state=S futex_wait_queue_me +sysTid=14302 state=S futex_wait_queue_me +sysTid=14309 state=S futex_wait_queue_me +sysTid=14314 state=S futex_wait_queue_me +sysTid=14319 state=S futex_wait_queue_me +sysTid=14324 state=S futex_wait_queue_me +sysTid=14325 state=S futex_wait_queue_me +sysTid=14327 state=S futex_wait_queue_me +sysTid=14328 state=S futex_wait_queue_me +sysTid=14329 state=S futex_wait_queue_me +sysTid=14331 state=S futex_wait_queue_me +sysTid=14348 state=S futex_wait_queue_me +sysTid=14349 state=S futex_wait_queue_me +sysTid=14350 state=S futex_wait_queue_me +sysTid=14351 state=S futex_wait_queue_me +sysTid=14352 state=S futex_wait_queue_me +sysTid=14353 state=S futex_wait_queue_me +sysTid=14357 state=S futex_wait_queue_me +sysTid=14358 state=S futex_wait_queue_me +sysTid=14359 state=S futex_wait_queue_me +sysTid=14360 state=S futex_wait_queue_me +sysTid=14361 state=S futex_wait_queue_me +sysTid=14363 state=S futex_wait_queue_me +sysTid=14364 state=S futex_wait_queue_me +sysTid=14365 state=S futex_wait_queue_me +sysTid=14366 state=S futex_wait_queue_me +sysTid=14367 state=S futex_wait_queue_me +sysTid=14368 state=S futex_wait_queue_me +sysTid=14369 state=S futex_wait_queue_me +sysTid=14380 state=S futex_wait_queue_me +sysTid=14400 state=S futex_wait_queue_me +sysTid=14414 state=S futex_wait_queue_me +sysTid=14423 state=S futex_wait_queue_me +sysTid=14431 state=S futex_wait_queue_me +sysTid=14439 state=S futex_wait_queue_me +sysTid=14442 state=S futex_wait_queue_me +sysTid=14451 state=S futex_wait_queue_me +sysTid=14453 state=S futex_wait_queue_me +sysTid=14454 state=S futex_wait_queue_me +sysTid=14456 state=S futex_wait_queue_me +sysTid=14457 state=S futex_wait_queue_me +sysTid=14459 state=S futex_wait_queue_me +sysTid=14460 state=S futex_wait_queue_me +sysTid=14461 state=S futex_wait_queue_me +sysTid=14462 state=S futex_wait_queue_me +sysTid=14465 state=S futex_wait_queue_me +sysTid=14466 state=S futex_wait_queue_me +sysTid=14467 state=S futex_wait_queue_me +sysTid=14473 state=S futex_wait_queue_me +sysTid=14485 state=S futex_wait_queue_me +sysTid=14491 state=S futex_wait_queue_me +sysTid=14493 state=S futex_wait_queue_me +sysTid=14500 state=S futex_wait_queue_me +sysTid=14514 state=S futex_wait_queue_me +sysTid=14522 state=S futex_wait_queue_me +sysTid=14529 state=S futex_wait_queue_me +sysTid=14531 state=S futex_wait_queue_me +sysTid=14538 state=S futex_wait_queue_me +sysTid=14542 state=S futex_wait_queue_me +sysTid=14550 state=S futex_wait_queue_me +sysTid=14551 state=S futex_wait_queue_me +sysTid=14552 state=S futex_wait_queue_me +sysTid=14554 state=S futex_wait_queue_me +sysTid=14555 state=S futex_wait_queue_me +sysTid=14556 state=S futex_wait_queue_me +sysTid=14557 state=S futex_wait_queue_me +sysTid=14558 state=S futex_wait_queue_me +sysTid=14559 state=S futex_wait_queue_me +sysTid=14560 state=S futex_wait_queue_me +sysTid=14561 state=S futex_wait_queue_me +sysTid=14562 state=S futex_wait_queue_me +sysTid=14563 state=S futex_wait_queue_me +sysTid=14564 state=S futex_wait_queue_me +sysTid=14565 state=S futex_wait_queue_me +sysTid=14566 state=S futex_wait_queue_me +sysTid=14567 state=S futex_wait_queue_me +sysTid=14568 state=S futex_wait_queue_me +sysTid=14570 state=S futex_wait_queue_me +sysTid=14573 state=S futex_wait_queue_me +sysTid=14580 state=S futex_wait_queue_me +sysTid=14585 state=S futex_wait_queue_me +sysTid=14594 state=S futex_wait_queue_me +sysTid=14606 state=S futex_wait_queue_me +sysTid=14608 state=S futex_wait_queue_me +sysTid=14622 state=S futex_wait_queue_me +sysTid=14646 state=S futex_wait_queue_me +sysTid=14660 state=S futex_wait_queue_me +sysTid=14664 state=S futex_wait_queue_me +sysTid=14673 state=S futex_wait_queue_me +sysTid=14676 state=S futex_wait_queue_me +sysTid=14691 state=S futex_wait_queue_me +sysTid=14694 state=S futex_wait_queue_me +sysTid=14695 state=S futex_wait_queue_me +sysTid=14696 state=S futex_wait_queue_me +sysTid=14697 state=S futex_wait_queue_me +sysTid=14698 state=S futex_wait_queue_me +sysTid=14699 state=S futex_wait_queue_me +sysTid=14700 state=S futex_wait_queue_me +sysTid=14701 state=S futex_wait_queue_me +sysTid=14702 state=S futex_wait_queue_me +sysTid=14703 state=S futex_wait_queue_me +sysTid=14704 state=S futex_wait_queue_me +sysTid=14705 state=S futex_wait_queue_me +sysTid=14706 state=S futex_wait_queue_me +sysTid=14707 state=S futex_wait_queue_me +sysTid=14708 state=S futex_wait_queue_me +sysTid=14709 state=S futex_wait_queue_me +sysTid=14710 state=S futex_wait_queue_me +sysTid=14711 state=S futex_wait_queue_me +sysTid=14712 state=S futex_wait_queue_me +sysTid=14713 state=S futex_wait_queue_me +sysTid=14714 state=S futex_wait_queue_me +sysTid=14715 state=S futex_wait_queue_me +sysTid=14716 state=S futex_wait_queue_me +sysTid=14717 state=S futex_wait_queue_me +sysTid=14718 state=S futex_wait_queue_me +sysTid=14719 state=S futex_wait_queue_me +sysTid=14720 state=S futex_wait_queue_me +sysTid=14721 state=S futex_wait_queue_me +sysTid=14722 state=S futex_wait_queue_me +sysTid=14723 state=S futex_wait_queue_me +sysTid=14724 state=S futex_wait_queue_me +sysTid=14725 state=S futex_wait_queue_me +sysTid=14726 state=S futex_wait_queue_me +sysTid=14727 state=S futex_wait_queue_me +sysTid=14728 state=S futex_wait_queue_me +sysTid=14731 state=S futex_wait_queue_me +sysTid=14737 state=S futex_wait_queue_me +sysTid=14744 state=S futex_wait_queue_me +sysTid=14749 state=S futex_wait_queue_me +sysTid=14756 state=S futex_wait_queue_me +sysTid=14764 state=S futex_wait_queue_me +sysTid=14766 state=S futex_wait_queue_me +sysTid=14770 state=S futex_wait_queue_me +sysTid=14780 state=S futex_wait_queue_me +sysTid=14783 state=S futex_wait_queue_me +sysTid=14787 state=S futex_wait_queue_me +sysTid=14794 state=S futex_wait_queue_me +sysTid=14799 state=S futex_wait_queue_me +sysTid=14807 state=S futex_wait_queue_me +sysTid=14813 state=S futex_wait_queue_me +sysTid=14817 state=S futex_wait_queue_me +sysTid=14818 state=S futex_wait_queue_me +sysTid=14819 state=S futex_wait_queue_me +sysTid=14820 state=S futex_wait_queue_me +sysTid=14824 state=S futex_wait_queue_me +sysTid=14825 state=S futex_wait_queue_me +sysTid=14826 state=S futex_wait_queue_me +sysTid=14827 state=S futex_wait_queue_me +sysTid=14828 state=S futex_wait_queue_me +sysTid=14829 state=S futex_wait_queue_me +sysTid=14830 state=S futex_wait_queue_me +sysTid=14835 state=S futex_wait_queue_me +sysTid=14842 state=S futex_wait_queue_me +sysTid=14852 state=S futex_wait_queue_me +sysTid=14854 state=S futex_wait_queue_me +sysTid=14862 state=S futex_wait_queue_me +sysTid=14868 state=S futex_wait_queue_me +sysTid=14869 state=S futex_wait_queue_me +sysTid=14870 state=S futex_wait_queue_me +sysTid=14871 state=S futex_wait_queue_me +sysTid=14872 state=S futex_wait_queue_me +sysTid=14873 state=S futex_wait_queue_me +sysTid=14874 state=S futex_wait_queue_me +sysTid=14875 state=S futex_wait_queue_me +sysTid=14876 state=S futex_wait_queue_me +sysTid=14877 state=S futex_wait_queue_me +sysTid=14878 state=S futex_wait_queue_me +sysTid=14879 state=S futex_wait_queue_me +sysTid=14880 state=S futex_wait_queue_me +sysTid=14881 state=S futex_wait_queue_me +sysTid=14882 state=S futex_wait_queue_me +sysTid=14883 state=S futex_wait_queue_me +sysTid=14884 state=S futex_wait_queue_me +sysTid=14885 state=S futex_wait_queue_me +sysTid=14887 state=S futex_wait_queue_me +sysTid=14888 state=S futex_wait_queue_me +sysTid=14889 state=S futex_wait_queue_me +sysTid=14890 state=S futex_wait_queue_me +sysTid=14891 state=S futex_wait_queue_me +sysTid=14892 state=S futex_wait_queue_me +sysTid=14893 state=S futex_wait_queue_me +sysTid=14897 state=S futex_wait_queue_me +sysTid=14903 state=S futex_wait_queue_me +sysTid=14911 state=S futex_wait_queue_me +sysTid=14915 state=S futex_wait_queue_me +sysTid=14920 state=S futex_wait_queue_me +sysTid=14924 state=S futex_wait_queue_me +sysTid=14932 state=S futex_wait_queue_me +sysTid=14972 state=S futex_wait_queue_me +sysTid=14974 state=S futex_wait_queue_me +sysTid=15011 state=S futex_wait_queue_me +sysTid=15019 state=S futex_wait_queue_me +sysTid=15032 state=S futex_wait_queue_me +sysTid=15054 state=S futex_wait_queue_me +sysTid=15124 state=S futex_wait_queue_me +sysTid=15177 state=S futex_wait_queue_me +sysTid=15217 state=S futex_wait_queue_me +sysTid=15228 state=S futex_wait_queue_me +sysTid=15236 state=S futex_wait_queue_me +sysTid=15248 state=S futex_wait_queue_me +sysTid=15265 state=S futex_wait_queue_me +sysTid=15272 state=S futex_wait_queue_me +sysTid=15276 state=S futex_wait_queue_me +sysTid=15344 state=S sk_wait_data +sysTid=15400 state=S sk_wait_data +sysTid=15415 state=S sk_wait_data +sysTid=15421 state=S sk_wait_data +sysTid=15449 state=S sk_wait_data +sysTid=15463 state=S sk_wait_data +sysTid=15471 state=S sk_wait_data +sysTid=15479 state=S sk_wait_data +sysTid=15486 state=S sk_wait_data +sysTid=15509 state=S sk_wait_data +sysTid=15515 state=S sk_wait_data +sysTid=15525 state=S sk_wait_data +sysTid=15530 state=S sk_wait_data +sysTid=15536 state=S sk_wait_data +sysTid=15541 state=S sk_wait_data +sysTid=15578 state=S futex_wait_queue_me +sysTid=16256 state=S futex_wait_queue_me +sysTid=16261 state=S futex_wait_queue_me +sysTid=16262 state=S futex_wait_queue_me + +----- end 12233 ----- + +libdebuggerd_client: unexpected registration response: 0 + +----- Waiting Channels: pid 12233 at 2024-11-13 19:48:10.010218499+0530 ----- +Cmd line: com.example.app:gameProcess + +sysTid=12233 state=R 0 +sysTid=12236 state=D swap_readpage +sysTid=12237 state=S futex_wait_queue_me +sysTid=12238 state=S futex_wait_queue_me +sysTid=12239 state=S futex_wait_queue_me +sysTid=12240 state=S futex_wait_queue_me +sysTid=12241 state=S futex_wait_queue_me +sysTid=12242 state=S binder_wait_for_work +sysTid=12243 state=S binder_wait_for_work +sysTid=12245 state=S binder_wait_for_work +sysTid=12252 state=S futex_wait_queue_me +sysTid=12254 state=S inotify_read +sysTid=12257 state=S __arm64_sys_nanosleep +sysTid=12259 state=R 0 +sysTid=12260 state=S __arm64_sys_nanosleep +sysTid=12268 state=S __arm64_sys_nanosleep +sysTid=12269 state=S __arm64_sys_nanosleep +sysTid=12270 state=S __arm64_sys_nanosleep +sysTid=12278 state=S futex_wait_queue_me +sysTid=12279 state=S futex_wait_queue_me +sysTid=12280 state=S futex_wait_queue_me +sysTid=12283 state=S futex_wait_queue_me +sysTid=12287 state=S futex_wait_queue_me +sysTid=12290 state=S futex_wait_queue_me +sysTid=12291 state=S futex_wait_queue_me +sysTid=12292 state=S futex_wait_queue_me +sysTid=12295 state=S futex_wait_queue_me +sysTid=12296 state=S futex_wait_queue_me +sysTid=12297 state=S futex_wait_queue_me +sysTid=12298 state=S futex_wait_queue_me +sysTid=12304 state=S futex_wait_queue_me +sysTid=12310 state=S futex_wait_queue_me +sysTid=12311 state=S futex_wait_queue_me +sysTid=12312 state=S do_epoll_wait +sysTid=12314 state=S futex_wait_queue_me +sysTid=12315 state=S futex_wait_queue_me +sysTid=12316 state=S futex_wait_queue_me +sysTid=12317 state=S futex_wait_queue_me +sysTid=12319 state=S futex_wait_queue_me +sysTid=12320 state=S do_epoll_wait +sysTid=12321 state=S futex_wait_queue_me +sysTid=12323 state=S futex_wait_queue_me +sysTid=12324 state=S futex_wait_queue_me +sysTid=12325 state=S futex_wait_queue_me +sysTid=12328 state=S do_epoll_wait +sysTid=12341 state=S futex_wait_queue_me +sysTid=12343 state=S futex_wait_queue_me +sysTid=12344 state=S futex_wait_queue_me +sysTid=12349 state=S futex_wait_queue_me +sysTid=12350 state=S futex_wait_queue_me +sysTid=12351 state=S futex_wait_queue_me +sysTid=12352 state=S futex_wait_queue_me +sysTid=12353 state=S binder_wait_for_work +sysTid=12354 state=S futex_wait_queue_me +sysTid=12355 state=S futex_wait_queue_me +sysTid=12358 state=R 0 +sysTid=12362 state=S futex_wait_queue_me +sysTid=12363 state=S futex_wait_queue_me +sysTid=12365 state=S do_epoll_wait +sysTid=12366 state=S futex_wait_queue_me +sysTid=12367 state=S futex_wait_queue_me +sysTid=12368 state=S futex_wait_queue_me +sysTid=12370 state=S futex_wait_queue_me +sysTid=12371 state=S futex_wait_queue_me +sysTid=12373 state=S futex_wait_queue_me +sysTid=12384 state=S binder_wait_for_work +sysTid=12391 state=S futex_wait_queue_me +sysTid=12399 state=S do_epoll_wait +sysTid=12401 state=S futex_wait_queue_me +sysTid=12402 state=S futex_wait_queue_me +sysTid=12404 state=S futex_wait_queue_me +sysTid=12405 state=S do_epoll_wait +sysTid=12407 state=S futex_wait_queue_me +sysTid=12408 state=S futex_wait_queue_me +sysTid=12409 state=S do_wait +sysTid=12410 state=S futex_wait_queue_me +sysTid=12412 state=S do_epoll_wait +sysTid=12435 state=S do_epoll_wait +sysTid=12468 state=S futex_wait_queue_me +sysTid=12514 state=S futex_wait_queue_me +sysTid=12550 state=S futex_wait_queue_me +sysTid=12561 state=S binder_wait_for_work +sysTid=12567 state=S binder_wait_for_work +sysTid=12580 state=S futex_wait_queue_me +sysTid=12619 state=S futex_wait_queue_me +sysTid=12627 state=S futex_wait_queue_me +sysTid=12644 state=S futex_wait_queue_me +sysTid=12887 state=S futex_wait_queue_me +sysTid=13430 state=S futex_wait_queue_me +sysTid=13438 state=S futex_wait_queue_me +sysTid=13443 state=S futex_wait_queue_me +sysTid=13454 state=S futex_wait_queue_me +sysTid=13455 state=S futex_wait_queue_me +sysTid=13564 state=S binder_wait_for_work +sysTid=13576 state=S binder_wait_for_work +sysTid=13579 state=S binder_wait_for_work +sysTid=13616 state=S binder_wait_for_work +sysTid=13624 state=S futex_wait_queue_me +sysTid=13706 state=S futex_wait_queue_me +sysTid=13722 state=S futex_wait_queue_me +sysTid=13724 state=S futex_wait_queue_me +sysTid=13730 state=S futex_wait_queue_me +sysTid=13740 state=S futex_wait_queue_me +sysTid=13744 state=S futex_wait_queue_me +sysTid=13745 state=S futex_wait_queue_me +sysTid=13748 state=S futex_wait_queue_me +sysTid=13754 state=S futex_wait_queue_me +sysTid=13756 state=S futex_wait_queue_me +sysTid=13757 state=S futex_wait_queue_me +sysTid=13758 state=S futex_wait_queue_me +sysTid=13759 state=S futex_wait_queue_me +sysTid=13763 state=S futex_wait_queue_me +sysTid=13767 state=S futex_wait_queue_me +sysTid=13768 state=S futex_wait_queue_me +sysTid=13769 state=S futex_wait_queue_me +sysTid=13773 state=S futex_wait_queue_me +sysTid=13776 state=S futex_wait_queue_me +sysTid=13781 state=S futex_wait_queue_me +sysTid=13782 state=S futex_wait_queue_me +sysTid=13783 state=S futex_wait_queue_me +sysTid=13784 state=S futex_wait_queue_me +sysTid=13786 state=S futex_wait_queue_me +sysTid=13791 state=S futex_wait_queue_me +sysTid=13792 state=S futex_wait_queue_me +sysTid=13793 state=S futex_wait_queue_me +sysTid=13794 state=S futex_wait_queue_me +sysTid=13795 state=S futex_wait_queue_me +sysTid=13796 state=S futex_wait_queue_me +sysTid=13797 state=S futex_wait_queue_me +sysTid=13798 state=S futex_wait_queue_me +sysTid=13799 state=S futex_wait_queue_me +sysTid=13800 state=S futex_wait_queue_me +sysTid=13806 state=S futex_wait_queue_me +sysTid=13809 state=S futex_wait_queue_me +sysTid=13814 state=S futex_wait_queue_me +sysTid=13815 state=S futex_wait_queue_me +sysTid=13816 state=S futex_wait_queue_me +sysTid=13817 state=S futex_wait_queue_me +sysTid=13818 state=S futex_wait_queue_me +sysTid=13820 state=S futex_wait_queue_me +sysTid=13825 state=S futex_wait_queue_me +sysTid=13830 state=S futex_wait_queue_me +sysTid=13831 state=S futex_wait_queue_me +sysTid=13832 state=S futex_wait_queue_me +sysTid=13833 state=S futex_wait_queue_me +sysTid=13834 state=S futex_wait_queue_me +sysTid=13835 state=S futex_wait_queue_me +sysTid=13836 state=S futex_wait_queue_me +sysTid=13841 state=S futex_wait_queue_me +sysTid=13847 state=S futex_wait_queue_me +sysTid=13848 state=S futex_wait_queue_me +sysTid=13849 state=S futex_wait_queue_me +sysTid=13850 state=S futex_wait_queue_me +sysTid=13851 state=S futex_wait_queue_me +sysTid=13852 state=S futex_wait_queue_me +sysTid=13853 state=S futex_wait_queue_me +sysTid=13854 state=S futex_wait_queue_me +sysTid=13857 state=S futex_wait_queue_me +sysTid=13863 state=S futex_wait_queue_me +sysTid=13867 state=S futex_wait_queue_me +sysTid=13880 state=S futex_wait_queue_me +sysTid=13920 state=S futex_wait_queue_me +sysTid=13949 state=S futex_wait_queue_me +sysTid=13953 state=S futex_wait_queue_me +sysTid=13954 state=S futex_wait_queue_me +sysTid=13955 state=S futex_wait_queue_me +sysTid=13958 state=S futex_wait_queue_me +sysTid=13959 state=S futex_wait_queue_me +sysTid=13967 state=S futex_wait_queue_me +sysTid=13980 state=S futex_wait_queue_me +sysTid=13981 state=S futex_wait_queue_me +sysTid=13982 state=S futex_wait_queue_me +sysTid=13983 state=S futex_wait_queue_me +sysTid=13984 state=S futex_wait_queue_me +sysTid=13986 state=S futex_wait_queue_me +sysTid=13987 state=S futex_wait_queue_me +sysTid=13991 state=S futex_wait_queue_me +sysTid=13998 state=S futex_wait_queue_me +sysTid=13999 state=S futex_wait_queue_me +sysTid=14000 state=S futex_wait_queue_me +sysTid=14001 state=S futex_wait_queue_me +sysTid=14002 state=S futex_wait_queue_me +sysTid=14003 state=S futex_wait_queue_me +sysTid=14004 state=S futex_wait_queue_me +sysTid=14005 state=S futex_wait_queue_me +sysTid=14006 state=S futex_wait_queue_me +sysTid=14007 state=S futex_wait_queue_me +sysTid=14026 state=S futex_wait_queue_me +sysTid=14052 state=S futex_wait_queue_me +sysTid=14057 state=S futex_wait_queue_me +sysTid=14060 state=S futex_wait_queue_me +sysTid=14063 state=S futex_wait_queue_me +sysTid=14069 state=S futex_wait_queue_me +sysTid=14072 state=S futex_wait_queue_me +sysTid=14075 state=S futex_wait_queue_me +sysTid=14081 state=S futex_wait_queue_me +sysTid=14084 state=S futex_wait_queue_me +sysTid=14089 state=S futex_wait_queue_me +sysTid=14090 state=S futex_wait_queue_me +sysTid=14091 state=S futex_wait_queue_me +sysTid=14092 state=S futex_wait_queue_me +sysTid=14093 state=S futex_wait_queue_me +sysTid=14094 state=S futex_wait_queue_me +sysTid=14095 state=S futex_wait_queue_me +sysTid=14096 state=S futex_wait_queue_me +sysTid=14097 state=S futex_wait_queue_me +sysTid=14098 state=S futex_wait_queue_me +sysTid=14099 state=S futex_wait_queue_me +sysTid=14100 state=S futex_wait_queue_me +sysTid=14101 state=S futex_wait_queue_me +sysTid=14102 state=S futex_wait_queue_me +sysTid=14103 state=S futex_wait_queue_me +sysTid=14104 state=S futex_wait_queue_me +sysTid=14106 state=S futex_wait_queue_me +sysTid=14111 state=S futex_wait_queue_me +sysTid=14117 state=S futex_wait_queue_me +sysTid=14120 state=S futex_wait_queue_me +sysTid=14121 state=S futex_wait_queue_me +sysTid=14122 state=S futex_wait_queue_me +sysTid=14123 state=S futex_wait_queue_me +sysTid=14124 state=S futex_wait_queue_me +sysTid=14129 state=S futex_wait_queue_me +sysTid=14130 state=S futex_wait_queue_me +sysTid=14131 state=S futex_wait_queue_me +sysTid=14132 state=S futex_wait_queue_me +sysTid=14136 state=S futex_wait_queue_me +sysTid=14144 state=S futex_wait_queue_me +sysTid=14148 state=S futex_wait_queue_me +sysTid=14154 state=S futex_wait_queue_me +sysTid=14158 state=S futex_wait_queue_me +sysTid=14164 state=S futex_wait_queue_me +sysTid=14167 state=S futex_wait_queue_me +sysTid=14168 state=S futex_wait_queue_me +sysTid=14169 state=S futex_wait_queue_me +sysTid=14170 state=S futex_wait_queue_me +sysTid=14171 state=S futex_wait_queue_me +sysTid=14172 state=S futex_wait_queue_me +sysTid=14173 state=S futex_wait_queue_me +sysTid=14174 state=S futex_wait_queue_me +sysTid=14175 state=S futex_wait_queue_me +sysTid=14176 state=S futex_wait_queue_me +sysTid=14177 state=S futex_wait_queue_me +sysTid=14178 state=S futex_wait_queue_me +sysTid=14179 state=S futex_wait_queue_me +sysTid=14180 state=S futex_wait_queue_me +sysTid=14181 state=S futex_wait_queue_me +sysTid=14182 state=S futex_wait_queue_me +sysTid=14190 state=S futex_wait_queue_me +sysTid=14195 state=S futex_wait_queue_me +sysTid=14198 state=S futex_wait_queue_me +sysTid=14207 state=S futex_wait_queue_me +sysTid=14209 state=S futex_wait_queue_me +sysTid=14210 state=S futex_wait_queue_me +sysTid=14214 state=S futex_wait_queue_me +sysTid=14220 state=S futex_wait_queue_me +sysTid=14223 state=S futex_wait_queue_me +sysTid=14227 state=S futex_wait_queue_me +sysTid=14235 state=S futex_wait_queue_me +sysTid=14242 state=S futex_wait_queue_me +sysTid=14243 state=S futex_wait_queue_me +sysTid=14244 state=S futex_wait_queue_me +sysTid=14245 state=S futex_wait_queue_me +sysTid=14246 state=S futex_wait_queue_me +sysTid=14247 state=S futex_wait_queue_me +sysTid=14248 state=S futex_wait_queue_me +sysTid=14249 state=S futex_wait_queue_me +sysTid=14250 state=S futex_wait_queue_me +sysTid=14251 state=S futex_wait_queue_me +sysTid=14253 state=S futex_wait_queue_me +sysTid=14259 state=S futex_wait_queue_me +sysTid=14264 state=S futex_wait_queue_me +sysTid=14269 state=S futex_wait_queue_me +sysTid=14272 state=S futex_wait_queue_me +sysTid=14277 state=S futex_wait_queue_me +sysTid=14282 state=S futex_wait_queue_me +sysTid=14296 state=S futex_wait_queue_me +sysTid=14302 state=S futex_wait_queue_me +sysTid=14309 state=S futex_wait_queue_me +sysTid=14314 state=S futex_wait_queue_me +sysTid=14319 state=S futex_wait_queue_me +sysTid=14324 state=S futex_wait_queue_me +sysTid=14325 state=S futex_wait_queue_me +sysTid=14327 state=S futex_wait_queue_me +sysTid=14328 state=S futex_wait_queue_me +sysTid=14329 state=S futex_wait_queue_me +sysTid=14331 state=S futex_wait_queue_me +sysTid=14348 state=S futex_wait_queue_me +sysTid=14349 state=S futex_wait_queue_me +sysTid=14350 state=S futex_wait_queue_me +sysTid=14351 state=S futex_wait_queue_me +sysTid=14352 state=S futex_wait_queue_me +sysTid=14353 state=S futex_wait_queue_me +sysTid=14357 state=S futex_wait_queue_me +sysTid=14358 state=S futex_wait_queue_me +sysTid=14359 state=S futex_wait_queue_me +sysTid=14360 state=S futex_wait_queue_me +sysTid=14361 state=S futex_wait_queue_me +sysTid=14363 state=S futex_wait_queue_me +sysTid=14364 state=S futex_wait_queue_me +sysTid=14365 state=S futex_wait_queue_me +sysTid=14366 state=S futex_wait_queue_me +sysTid=14367 state=S futex_wait_queue_me +sysTid=14368 state=S futex_wait_queue_me +sysTid=14369 state=S futex_wait_queue_me +sysTid=14380 state=S futex_wait_queue_me +sysTid=14400 state=S futex_wait_queue_me +sysTid=14414 state=S futex_wait_queue_me +sysTid=14423 state=S futex_wait_queue_me +sysTid=14431 state=S futex_wait_queue_me +sysTid=14439 state=S futex_wait_queue_me +sysTid=14442 state=S futex_wait_queue_me +sysTid=14451 state=S futex_wait_queue_me +sysTid=14453 state=S futex_wait_queue_me +sysTid=14454 state=S futex_wait_queue_me +sysTid=14456 state=S futex_wait_queue_me +sysTid=14457 state=S futex_wait_queue_me +sysTid=14459 state=S futex_wait_queue_me +sysTid=14460 state=S futex_wait_queue_me +sysTid=14461 state=S futex_wait_queue_me +sysTid=14462 state=S futex_wait_queue_me +sysTid=14465 state=S futex_wait_queue_me +sysTid=14466 state=S futex_wait_queue_me +sysTid=14467 state=S futex_wait_queue_me +sysTid=14473 state=S futex_wait_queue_me +sysTid=14485 state=S futex_wait_queue_me +sysTid=14491 state=S futex_wait_queue_me +sysTid=14493 state=S futex_wait_queue_me +sysTid=14500 state=S futex_wait_queue_me +sysTid=14514 state=S futex_wait_queue_me +sysTid=14522 state=S futex_wait_queue_me +sysTid=14529 state=S futex_wait_queue_me +sysTid=14531 state=S futex_wait_queue_me +sysTid=14538 state=S futex_wait_queue_me +sysTid=14542 state=S futex_wait_queue_me +sysTid=14550 state=S futex_wait_queue_me +sysTid=14551 state=S futex_wait_queue_me +sysTid=14552 state=S futex_wait_queue_me +sysTid=14554 state=S futex_wait_queue_me +sysTid=14555 state=S futex_wait_queue_me +sysTid=14556 state=S futex_wait_queue_me +sysTid=14557 state=S futex_wait_queue_me +sysTid=14558 state=S futex_wait_queue_me +sysTid=14559 state=S futex_wait_queue_me +sysTid=14560 state=S futex_wait_queue_me +sysTid=14561 state=S futex_wait_queue_me +sysTid=14562 state=S futex_wait_queue_me +sysTid=14563 state=S futex_wait_queue_me +sysTid=14564 state=S futex_wait_queue_me +sysTid=14565 state=S futex_wait_queue_me +sysTid=14566 state=S futex_wait_queue_me +sysTid=14567 state=S futex_wait_queue_me +sysTid=14568 state=S futex_wait_queue_me +sysTid=14570 state=S futex_wait_queue_me +sysTid=14573 state=S futex_wait_queue_me +sysTid=14580 state=S futex_wait_queue_me +sysTid=14585 state=S futex_wait_queue_me +sysTid=14594 state=S futex_wait_queue_me +sysTid=14606 state=S futex_wait_queue_me +sysTid=14608 state=S futex_wait_queue_me +sysTid=14622 state=S futex_wait_queue_me +sysTid=14646 state=S futex_wait_queue_me +sysTid=14660 state=S futex_wait_queue_me +sysTid=14664 state=S futex_wait_queue_me +sysTid=14673 state=S futex_wait_queue_me +sysTid=14676 state=S futex_wait_queue_me +sysTid=14691 state=S futex_wait_queue_me +sysTid=14694 state=S futex_wait_queue_me +sysTid=14695 state=S futex_wait_queue_me +sysTid=14696 state=S futex_wait_queue_me +sysTid=14697 state=S futex_wait_queue_me +sysTid=14698 state=S futex_wait_queue_me +sysTid=14699 state=S futex_wait_queue_me +sysTid=14700 state=S futex_wait_queue_me +sysTid=14701 state=S futex_wait_queue_me +sysTid=14702 state=S futex_wait_queue_me +sysTid=14703 state=S futex_wait_queue_me +sysTid=14704 state=S futex_wait_queue_me +sysTid=14705 state=S futex_wait_queue_me +sysTid=14706 state=S futex_wait_queue_me +sysTid=14707 state=S futex_wait_queue_me +sysTid=14708 state=S futex_wait_queue_me +sysTid=14709 state=S futex_wait_queue_me +sysTid=14710 state=S futex_wait_queue_me +sysTid=14711 state=S futex_wait_queue_me +sysTid=14712 state=S futex_wait_queue_me +sysTid=14713 state=S futex_wait_queue_me +sysTid=14714 state=S futex_wait_queue_me +sysTid=14715 state=S futex_wait_queue_me +sysTid=14716 state=S futex_wait_queue_me +sysTid=14717 state=S futex_wait_queue_me +sysTid=14718 state=S futex_wait_queue_me +sysTid=14719 state=S futex_wait_queue_me +sysTid=14720 state=S futex_wait_queue_me +sysTid=14721 state=S futex_wait_queue_me +sysTid=14722 state=S futex_wait_queue_me +sysTid=14723 state=S futex_wait_queue_me +sysTid=14724 state=S futex_wait_queue_me +sysTid=14725 state=S futex_wait_queue_me +sysTid=14726 state=S futex_wait_queue_me +sysTid=14727 state=S futex_wait_queue_me +sysTid=14728 state=S futex_wait_queue_me +sysTid=14731 state=S futex_wait_queue_me +sysTid=14737 state=S futex_wait_queue_me +sysTid=14744 state=S futex_wait_queue_me +sysTid=14749 state=S futex_wait_queue_me +sysTid=14756 state=S futex_wait_queue_me +sysTid=14764 state=S futex_wait_queue_me +sysTid=14766 state=S futex_wait_queue_me +sysTid=14770 state=S futex_wait_queue_me +sysTid=14780 state=S futex_wait_queue_me +sysTid=14783 state=S futex_wait_queue_me +sysTid=14787 state=S futex_wait_queue_me +sysTid=14794 state=S futex_wait_queue_me +sysTid=14799 state=S futex_wait_queue_me +sysTid=14807 state=S futex_wait_queue_me +sysTid=14813 state=S futex_wait_queue_me +sysTid=14817 state=S futex_wait_queue_me +sysTid=14818 state=S futex_wait_queue_me +sysTid=14819 state=S futex_wait_queue_me +sysTid=14820 state=S futex_wait_queue_me +sysTid=14824 state=S futex_wait_queue_me +sysTid=14825 state=S futex_wait_queue_me +sysTid=14826 state=S futex_wait_queue_me +sysTid=14827 state=S futex_wait_queue_me +sysTid=14828 state=S futex_wait_queue_me +sysTid=14829 state=S futex_wait_queue_me +sysTid=14830 state=S futex_wait_queue_me +sysTid=14835 state=S futex_wait_queue_me +sysTid=14842 state=S futex_wait_queue_me +sysTid=14852 state=S futex_wait_queue_me +sysTid=14854 state=S futex_wait_queue_me +sysTid=14862 state=S futex_wait_queue_me +sysTid=14868 state=S futex_wait_queue_me +sysTid=14869 state=S futex_wait_queue_me +sysTid=14870 state=S futex_wait_queue_me +sysTid=14871 state=S futex_wait_queue_me +sysTid=14872 state=S futex_wait_queue_me +sysTid=14873 state=S futex_wait_queue_me +sysTid=14874 state=S futex_wait_queue_me +sysTid=14875 state=S futex_wait_queue_me +sysTid=14876 state=S futex_wait_queue_me +sysTid=14877 state=S futex_wait_queue_me +sysTid=14878 state=S futex_wait_queue_me +sysTid=14879 state=S futex_wait_queue_me +sysTid=14880 state=S futex_wait_queue_me +sysTid=14881 state=S futex_wait_queue_me +sysTid=14882 state=S futex_wait_queue_me +sysTid=14883 state=S futex_wait_queue_me +sysTid=14884 state=S futex_wait_queue_me +sysTid=14885 state=S futex_wait_queue_me +sysTid=14887 state=S futex_wait_queue_me +sysTid=14888 state=S futex_wait_queue_me +sysTid=14889 state=S futex_wait_queue_me +sysTid=14890 state=S futex_wait_queue_me +sysTid=14891 state=S futex_wait_queue_me +sysTid=14892 state=S futex_wait_queue_me +sysTid=14893 state=S futex_wait_queue_me +sysTid=14897 state=S futex_wait_queue_me +sysTid=14903 state=S futex_wait_queue_me +sysTid=14911 state=S futex_wait_queue_me +sysTid=14915 state=S futex_wait_queue_me +sysTid=14920 state=S futex_wait_queue_me +sysTid=14924 state=S futex_wait_queue_me +sysTid=14932 state=S futex_wait_queue_me +sysTid=14972 state=S futex_wait_queue_me +sysTid=14974 state=S futex_wait_queue_me +sysTid=15011 state=S futex_wait_queue_me +sysTid=15019 state=S futex_wait_queue_me +sysTid=15032 state=S futex_wait_queue_me +sysTid=15054 state=S futex_wait_queue_me +sysTid=15124 state=S futex_wait_queue_me +sysTid=15177 state=S futex_wait_queue_me +sysTid=15217 state=S futex_wait_queue_me +sysTid=15228 state=S futex_wait_queue_me +sysTid=15236 state=S futex_wait_queue_me +sysTid=15248 state=S futex_wait_queue_me +sysTid=15265 state=S futex_wait_queue_me +sysTid=15272 state=S futex_wait_queue_me +sysTid=15276 state=S futex_wait_queue_me +sysTid=15344 state=S sk_wait_data +sysTid=15400 state=S sk_wait_data +sysTid=15415 state=S sk_wait_data +sysTid=15421 state=S sk_wait_data +sysTid=15449 state=S sk_wait_data +sysTid=15463 state=S sk_wait_data +sysTid=15471 state=S sk_wait_data +sysTid=15479 state=S sk_wait_data +sysTid=15486 state=S sk_wait_data +sysTid=15509 state=S sk_wait_data +sysTid=15515 state=S sk_wait_data +sysTid=15525 state=S sk_wait_data +sysTid=15530 state=S sk_wait_data +sysTid=15536 state=S sk_wait_data +sysTid=15541 state=S sk_wait_data +sysTid=15578 state=S futex_wait_queue_me +sysTid=16256 state=S futex_wait_queue_me +sysTid=16261 state=S futex_wait_queue_me +sysTid=16262 state=S futex_wait_queue_me + +----- end 12233 ----- From dab52e252d6b38f0d2433637bdb93500decbb967 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 14 Nov 2024 14:48:46 +0100 Subject: [PATCH 10/14] [QA] Avoid collecting normal frames (#3782) * avoid keeping normal frames in memory when collecting slow/frozen frames * renamed SpanFrameMetricsCollector.getTotalFrameCount method to getSlowFrozenFrameCount * confirm no delay in UI test when there are no slow/frozen frames collected --- CHANGELOG.md | 1 + .../android/core/SentryFrameMetrics.java | 23 ++++-------------- .../core/SpanFrameMetricsCollector.java | 24 ++++++++++--------- .../android/core/SentryFrameMetricsTest.kt | 21 +++++----------- .../core/SpanFrameMetricsCollectorTest.kt | 11 +++++---- 5 files changed, 30 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92d7d2ef4b..ba0e84d5d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes +- Avoid collecting normal frames ([#3782](https://github.com/getsentry/sentry-java/pull/3782)) - Ensure android initialization process continues even if options configuration block throws an exception ([#3887](https://github.com/getsentry/sentry-java/pull/3887)) - Do not report parsing ANR error when there are no threads ([#3888](https://github.com/getsentry/sentry-java/pull/3888)) - This should significantly reduce the number of events with message "Sentry Android SDK failed to parse system thread dump..." reported diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryFrameMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryFrameMetrics.java index 23409eadea..cf2241757c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryFrameMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryFrameMetrics.java @@ -6,7 +6,6 @@ @ApiStatus.Internal final class SentryFrameMetrics { - private int normalFrameCount; private int slowFrameCount; private int frozenFrameCount; @@ -18,15 +17,11 @@ final class SentryFrameMetrics { public SentryFrameMetrics() {} public SentryFrameMetrics( - final int normalFrameCount, final int slowFrameCount, final long slowFrameDelayNanos, final int frozenFrameCount, final long frozenFrameDelayNanos, final long totalDurationNanos) { - - this.normalFrameCount = normalFrameCount; - this.slowFrameCount = slowFrameCount; this.slowFrameDelayNanos = slowFrameDelayNanos; @@ -47,15 +42,9 @@ public void addFrame( } else if (isSlow) { slowFrameDelayNanos += delayNanos; slowFrameCount += 1; - } else { - normalFrameCount += 1; } } - public int getNormalFrameCount() { - return normalFrameCount; - } - public int getSlowFrameCount() { return slowFrameCount; } @@ -72,8 +61,9 @@ public long getFrozenFrameDelayNanos() { return frozenFrameDelayNanos; } - public int getTotalFrameCount() { - return normalFrameCount + slowFrameCount + frozenFrameCount; + /** Returns the sum of the slow and frozen frames. */ + public int getSlowFrozenFrameCount() { + return slowFrameCount + frozenFrameCount; } public long getTotalDurationNanos() { @@ -81,8 +71,6 @@ public long getTotalDurationNanos() { } public void clear() { - normalFrameCount = 0; - slowFrameCount = 0; slowFrameDelayNanos = 0; @@ -95,7 +83,6 @@ public void clear() { @NotNull public SentryFrameMetrics duplicate() { return new SentryFrameMetrics( - normalFrameCount, slowFrameCount, slowFrameDelayNanos, frozenFrameCount, @@ -110,7 +97,6 @@ public SentryFrameMetrics duplicate() { @NotNull public SentryFrameMetrics diffTo(final @NotNull SentryFrameMetrics other) { return new SentryFrameMetrics( - normalFrameCount - other.normalFrameCount, slowFrameCount - other.slowFrameCount, slowFrameDelayNanos - other.slowFrameDelayNanos, frozenFrameCount - other.frozenFrameCount, @@ -123,8 +109,7 @@ public SentryFrameMetrics diffTo(final @NotNull SentryFrameMetrics other) { * to 0 */ public boolean containsValidData() { - return normalFrameCount >= 0 - && slowFrameCount >= 0 + return slowFrameCount >= 0 && slowFrameDelayNanos >= 0 && frozenFrameCount >= 0 && frozenFrameDelayNanos >= 0 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java index 5535bccb91..b4279db13f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SpanFrameMetricsCollector.java @@ -192,7 +192,7 @@ private void captureFrameMetrics(@NotNull final ISpan span) { } } - int totalFrameCount = frameMetrics.getTotalFrameCount(); + int totalFrameCount = frameMetrics.getSlowFrozenFrameCount(); final long nextScheduledFrameNanos = frameMetricsCollector.getLastKnownFrameStartTimeNanos(); // nextScheduledFrameNanos might be -1 if no frames have been scheduled for drawing yet @@ -254,15 +254,17 @@ public void onFrameMetricCollected( (long) ((double) ONE_SECOND_NANOS / (double) refreshRate); lastKnownFrameDurationNanos = expectedFrameDurationNanos; - frames.add( - new Frame( - frameStartNanos, - frameEndNanos, - durationNanos, - delayNanos, - isSlow, - isFrozen, - expectedFrameDurationNanos)); + if (isSlow || isFrozen) { + frames.add( + new Frame( + frameStartNanos, + frameEndNanos, + durationNanos, + delayNanos, + isSlow, + isFrozen, + expectedFrameDurationNanos)); + } } private static int interpolateFrameCount( @@ -277,7 +279,7 @@ private static int interpolateFrameCount( final long frameMetricsDurationNanos = frameMetrics.getTotalDurationNanos(); final long nonRenderedDuration = spanDurationNanos - frameMetricsDurationNanos; if (nonRenderedDuration > 0) { - return (int) (nonRenderedDuration / frameDurationNanos); + return (int) Math.ceil((double) nonRenderedDuration / frameDurationNanos); } return 0; } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryFrameMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryFrameMetricsTest.kt index 1e992041b0..a8138b61ff 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryFrameMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryFrameMetricsTest.kt @@ -6,15 +6,6 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class SentryFrameMetricsTest { - @Test - fun addFastFrame() { - val frameMetrics = SentryFrameMetrics() - frameMetrics.addFrame(10, 0, false, false) - assertEquals(1, frameMetrics.normalFrameCount) - - frameMetrics.addFrame(10, 0, false, false) - assertEquals(2, frameMetrics.normalFrameCount) - } @Test fun addSlowFrame() { @@ -43,10 +34,12 @@ class SentryFrameMetricsTest { @Test fun totalFrameCount() { val frameMetrics = SentryFrameMetrics() + // Normal frames are ignored frameMetrics.addFrame(10, 0, false, false) + // Slow and frozen frames are considered frameMetrics.addFrame(116, 100, true, false) frameMetrics.addFrame(1016, 1000, true, true) - assertEquals(3, frameMetrics.totalFrameCount) + assertEquals(2, frameMetrics.slowFrozenFrameCount) } @Test @@ -57,12 +50,11 @@ class SentryFrameMetricsTest { frameMetrics.addFrame(1016, 1000, true, true) val dup = frameMetrics.duplicate() - assertEquals(1, dup.normalFrameCount) assertEquals(1, dup.slowFrameCount) assertEquals(100, dup.slowFrameDelayNanos) assertEquals(1, dup.frozenFrameCount) assertEquals(1000, dup.frozenFrameDelayNanos) - assertEquals(3, dup.totalFrameCount) + assertEquals(2, dup.slowFrozenFrameCount) } @Test @@ -89,7 +81,7 @@ class SentryFrameMetricsTest { assertEquals(1, diff.frozenFrameCount) assertEquals(1000, diff.frozenFrameDelayNanos) - assertEquals(2, diff.totalFrameCount) + assertEquals(2, diff.slowFrozenFrameCount) } @Test @@ -102,12 +94,11 @@ class SentryFrameMetricsTest { frameMetrics.clear() - assertEquals(0, frameMetrics.normalFrameCount) assertEquals(0, frameMetrics.slowFrameCount) assertEquals(0, frameMetrics.slowFrameDelayNanos) assertEquals(0, frameMetrics.frozenFrameCount) assertEquals(0, frameMetrics.frozenFrameDelayNanos) - assertEquals(0, frameMetrics.totalFrameCount) + assertEquals(0, frameMetrics.slowFrozenFrameCount) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt index d8ff8fde2e..0527baf284 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SpanFrameMetricsCollectorTest.kt @@ -192,7 +192,7 @@ class SpanFrameMetricsCollectorTest { sut.onFrameMetricCollected(0, 10, 10, 0, false, false, 60.0f) sut.onFrameMetricCollected(16, 48, 32, 16, true, false, 60.0f) sut.onFrameMetricCollected(60, 92, 32, 16, true, false, 60.0f) - sut.onFrameMetricCollected(100, 800, 800, 784, true, true, 60.0f) + sut.onFrameMetricCollected(100, 800, 700, 784, true, true, 60.0f) // then a second span starts fixture.timeNanos = 800 @@ -337,10 +337,11 @@ class SpanFrameMetricsCollectorTest { fixture.timeNanos = TimeUnit.SECONDS.toNanos(2) sut.onSpanFinished(span) - // then still 60 frames should be reported (1 second at 60fps) - verify(span).setData("frames.total", 60) + // then still 61 frames should be reported (1 second at 60fps with approximation) + verify(span).setData("frames.total", 61) verify(span).setData("frames.slow", 0) verify(span).setData("frames.frozen", 0) + verify(span).setData("frames.delay", 0.0) } @Test @@ -364,9 +365,9 @@ class SpanFrameMetricsCollectorTest { sut.onSpanFinished(span) // then - // still 60 fps should be reported for 1 seconds + // still 61 fps should be reported for 1 seconds (with approximation) // and one frame with frame delay should be reported (1s - 16ms) - verify(span).setData("frames.total", 61) + verify(span).setData("frames.total", 62) verify(span).setData("frames.slow", 0) verify(span).setData("frames.frozen", 1) verify(span).setData(eq("frames.delay"), AdditionalMatchers.eq(0.983333334, 0.01)) From adbc51d948f25ddbe1e47c274b7174fd7ceeefb1 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 15 Nov 2024 16:45:06 +0100 Subject: [PATCH 11/14] [SR] Disable replay in session mode when rate limit is active (#3854) * Do not capture screenshots in session mode when rate limit is active * Changelog * WIP * Format code * Change approach to rate-limit and offline * Clean up * Tests * Api dump * Fix tests * Address PR feedback * Fix tests --- CHANGELOG.md | 1 + .../api/sentry-android-replay.api | 4 +- .../android/replay/ReplayIntegration.kt | 65 ++++++++- .../android/replay/ScreenshotRecorder.kt | 3 - .../android/replay/capture/CaptureStrategy.kt | 6 +- .../replay/capture/SessionCaptureStrategy.kt | 6 - .../android/replay/ReplayIntegrationTest.kt | 133 ++++++++++++++++++ sentry/api/sentry.api | 9 +- .../clientreport/ClientReportRecorder.java | 3 + .../sentry/transport/AsyncHttpTransport.java | 1 + .../java/io/sentry/transport/RateLimiter.java | 66 ++++++++- .../sentry/clientreport/ClientReportTest.kt | 8 +- .../transport/AsyncHttpTransportTest.kt | 8 ++ .../io/sentry/transport/RateLimiterTest.kt | 72 ++++++++++ 14 files changed, 369 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba0e84d5d6..3163749f06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Ensure android initialization process continues even if options configuration block throws an exception ([#3887](https://github.com/getsentry/sentry-java/pull/3887)) - Do not report parsing ANR error when there are no threads ([#3888](https://github.com/getsentry/sentry-java/pull/3888)) - This should significantly reduce the number of events with message "Sentry Android SDK failed to parse system thread dump..." reported +- Session Replay: Disable replay in session mode when rate limit is active ([#3854](https://github.com/getsentry/sentry-java/pull/3854)) ### Dependencies diff --git a/sentry-android-replay/api/sentry-android-replay.api b/sentry-android-replay/api/sentry-android-replay.api index a08fb1dd98..7e2db5248f 100644 --- a/sentry-android-replay/api/sentry-android-replay.api +++ b/sentry-android-replay/api/sentry-android-replay.api @@ -57,7 +57,7 @@ public final class io/sentry/android/replay/ReplayCache$Companion { public final fun makeReplayCacheDir (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;)Ljava/io/File; } -public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, java/io/Closeable { +public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/IConnectionStatusProvider$IConnectionStatusObserver, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, io/sentry/transport/RateLimiter$IRateLimitObserver, java/io/Closeable { public static final field $stable I public fun (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V public fun (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V @@ -69,7 +69,9 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/ public fun getReplayId ()Lio/sentry/protocol/SentryId; public fun isRecording ()Z public fun onConfigurationChanged (Landroid/content/res/Configuration;)V + public fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V public fun onLowMemory ()V + public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V public fun onScreenshotRecorded (Ljava/io/File;J)V public fun onTouchEvent (Landroid/view/MotionEvent;)V diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index 90832585cd..d644ad096a 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -7,6 +7,11 @@ import android.graphics.Bitmap import android.os.Build import android.view.MotionEvent import io.sentry.Breadcrumb +import io.sentry.DataCategory.All +import io.sentry.DataCategory.Replay +import io.sentry.IConnectionStatusProvider.ConnectionStatus +import io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED +import io.sentry.IConnectionStatusProvider.IConnectionStatusObserver import io.sentry.IHub import io.sentry.Integration import io.sentry.NoOpReplayBreadcrumbConverter @@ -32,6 +37,8 @@ import io.sentry.cache.PersistingScopeObserver.REPLAY_FILENAME import io.sentry.hints.Backfillable import io.sentry.protocol.SentryId import io.sentry.transport.ICurrentDateProvider +import io.sentry.transport.RateLimiter +import io.sentry.transport.RateLimiter.IRateLimitObserver import io.sentry.util.FileUtils import io.sentry.util.HintUtils import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion @@ -48,7 +55,14 @@ public class ReplayIntegration( private val recorderProvider: (() -> Recorder)? = null, private val recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)? = null, private val replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null -) : Integration, Closeable, ScreenshotRecorderCallback, TouchRecorderCallback, ReplayController, ComponentCallbacks { +) : Integration, + Closeable, + ScreenshotRecorderCallback, + TouchRecorderCallback, + ReplayController, + ComponentCallbacks, + IConnectionStatusObserver, + IRateLimitObserver { // needed for the Java's call site constructor(context: Context, dateProvider: ICurrentDateProvider) : this( @@ -113,6 +127,8 @@ public class ReplayIntegration( gestureRecorder = gestureRecorderProvider?.invoke() ?: GestureRecorder(options, this) isEnabled.set(true) + options.connectionStatusProvider.addConnectionStatusObserver(this) + hub.rateLimiter?.addRateLimitObserver(this) try { context.registerComponentCallbacks(this) } catch (e: Throwable) { @@ -222,12 +238,14 @@ public class ReplayIntegration( hub?.configureScope { screen = it.screen?.substringAfterLast('.') } captureStrategy?.onScreenshotRecorded(bitmap) { frameTimeStamp -> addFrame(bitmap, frameTimeStamp, screen) + checkCanRecord() } } override fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long) { captureStrategy?.onScreenshotRecorded { _ -> addFrame(screenshot, frameTimestamp) + checkCanRecord() } } @@ -236,6 +254,8 @@ public class ReplayIntegration( return } + options.connectionStatusProvider.removeConnectionStatusObserver(this) + hub?.rateLimiter?.removeRateLimitObserver(this) try { context.unregisterComponentCallbacks(this) } catch (ignored: Throwable) { @@ -259,12 +279,55 @@ public class ReplayIntegration( recorder?.start(recorderConfig) } + override fun onConnectionStatusChanged(status: ConnectionStatus) { + if (captureStrategy !is SessionCaptureStrategy) { + // we only want to stop recording when offline for session mode + return + } + + if (status == DISCONNECTED) { + pause() + } else { + // being positive for other states, even if it's NO_PERMISSION + resume() + } + } + + override fun onRateLimitChanged(rateLimiter: RateLimiter) { + if (captureStrategy !is SessionCaptureStrategy) { + // we only want to stop recording when rate-limited for session mode + return + } + + if (rateLimiter.isActiveForCategory(All) || rateLimiter.isActiveForCategory(Replay)) { + pause() + } else { + resume() + } + } + override fun onLowMemory() = Unit override fun onTouchEvent(event: MotionEvent) { captureStrategy?.onTouchEvent(event) } + /** + * Check if we're offline or rate-limited and pause for session mode to not overflow the + * envelope cache. + */ + private fun checkCanRecord() { + if (captureStrategy is SessionCaptureStrategy && + ( + options.connectionStatusProvider.connectionStatus == DISCONNECTED || + hub?.rateLimiter?.isActiveForCategory(All) == true || + hub?.rateLimiter?.isActiveForCategory(Replay) == true + ) + ) { + pause() + } + } + private fun registerRootViewListeners() { if (recorder is OnRootViewsChangedListener) { rootViewsSpy.listeners += (recorder as OnRootViewsChangedListener) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt index 54f92a8958..60249103d0 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt @@ -35,7 +35,6 @@ import java.lang.ref.WeakReference import java.util.concurrent.Executors import java.util.concurrent.ThreadFactory import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference import kotlin.LazyThreadSafetyMode.NONE import kotlin.math.roundToInt @@ -51,7 +50,6 @@ internal class ScreenshotRecorder( Executors.newSingleThreadScheduledExecutor(RecorderExecutorServiceThreadFactory()) } private var rootView: WeakReference? = null - private val pendingViewHierarchy = AtomicReference() private val maskingPaint by lazy(NONE) { Paint() } private val singlePixelBitmap: Bitmap by lazy(NONE) { Bitmap.createBitmap( @@ -230,7 +228,6 @@ internal class ScreenshotRecorder( unbind(rootView?.get()) rootView?.clear() lastScreenshot?.recycle() - pendingViewHierarchy.set(null) isCapturing.set(false) recorder.gracefullyShutdown(options) } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt index 7e9168df22..53f5c66683 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt @@ -56,6 +56,7 @@ internal interface CaptureStrategy { fun close() companion object { + private const val BREADCRUMB_START_OFFSET = 100L internal val currentEventsLock = Any() fun createSegment( @@ -161,7 +162,10 @@ internal interface CaptureStrategy { val urls = LinkedList() breadcrumbs.forEach { breadcrumb -> - if (breadcrumb.timestamp.time >= segmentTimestamp.time && + // we add some fixed breadcrumb offset to make sure we don't miss any + // breadcrumbs that might be relevant for the current segment, but just happened + // earlier than the current segment (e.g. network connectivity changed) + if ((breadcrumb.timestamp.time + BREADCRUMB_START_OFFSET) >= segmentTimestamp.time && breadcrumb.timestamp.time < endTimestamp.time ) { val rrwebEvent = options diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt index 7b416d18b7..08aec7e9f7 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt @@ -1,7 +1,6 @@ package io.sentry.android.replay.capture import android.graphics.Bitmap -import io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED import io.sentry.IHub import io.sentry.SentryLevel.DEBUG import io.sentry.SentryLevel.INFO @@ -73,11 +72,6 @@ internal class SessionCaptureStrategy( } override fun onScreenshotRecorded(bitmap: Bitmap?, store: ReplayCache.(frameTimestamp: Long) -> Unit) { - if (options.connectionStatusProvider.connectionStatus == DISCONNECTED) { - options.logger.log(DEBUG, "Skipping screenshot recording, no internet connection") - bitmap?.recycle() - return - } // have to do it before submitting, otherwise if the queue is busy, the timestamp won't be // reflecting the exact time of when it was captured val frameTimestamp = dateProvider.currentTimeMillis diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt index f503268dff..ff396d04c1 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt @@ -9,6 +9,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb import io.sentry.DateUtils import io.sentry.Hint +import io.sentry.IConnectionStatusProvider.ConnectionStatus.CONNECTED +import io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED import io.sentry.IHub import io.sentry.Scope import io.sentry.ScopeCallback @@ -26,6 +28,7 @@ import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_REPLAY_TYPE import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_TIMESTAMP import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_WIDTH import io.sentry.android.replay.capture.CaptureStrategy +import io.sentry.android.replay.capture.SessionCaptureStrategy import io.sentry.android.replay.capture.SessionCaptureStrategyTest.Fixture.Companion.VIDEO_DURATION import io.sentry.android.replay.gestures.GestureRecorder import io.sentry.cache.PersistingScopeObserver @@ -38,6 +41,7 @@ import io.sentry.rrweb.RRWebMetaEvent import io.sentry.rrweb.RRWebVideoEvent import io.sentry.transport.CurrentDateProvider import io.sentry.transport.ICurrentDateProvider +import io.sentry.transport.RateLimiter import org.junit.Rule import org.junit.rules.TemporaryFolder import org.junit.runner.RunWith @@ -82,10 +86,12 @@ class ReplayIntegrationTest { } } val scope = Scope(options) + val rateLimiter = mock() val hub = mock { doAnswer { ((it.arguments[0]) as ScopeCallback).run(scope) }.whenever(mock).configureScope(any()) + on { rateLimiter }.thenReturn(rateLimiter) } val replayCache = mock { @@ -98,6 +104,8 @@ class ReplayIntegrationTest { context: Context, sessionSampleRate: Double = 1.0, onErrorSampleRate: Double = 1.0, + isOffline: Boolean = false, + isRateLimited: Boolean = false, recorderProvider: (() -> Recorder)? = null, replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null, recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)? = null, @@ -107,6 +115,12 @@ class ReplayIntegrationTest { options.run { experimental.sessionReplay.onErrorSampleRate = onErrorSampleRate experimental.sessionReplay.sessionSampleRate = sessionSampleRate + connectionStatusProvider = mock { + on { connectionStatus }.thenReturn(if (isOffline) DISCONNECTED else CONNECTED) + } + } + if (isRateLimited) { + whenever(rateLimiter.isActiveForCategory(any())).thenReturn(true) } return ReplayIntegration( context, @@ -567,4 +581,123 @@ class ReplayIntegrationTest { verify(fixture.replayCache).addFrame(any(), any(), eq("MainActivity")) } + + @Test + fun `onScreenshotRecorded pauses replay when offline for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + isOffline = true + ) + + replay.register(fixture.hub, fixture.options) + replay.start() + replay.onScreenshotRecorded(mock()) + + verify(recorder).pause() + } + + @Test + fun `onScreenshotRecorded pauses replay when rate-limited for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + isRateLimited = true + ) + + replay.register(fixture.hub, fixture.options) + replay.start() + replay.onScreenshotRecorded(mock()) + + verify(recorder).pause() + } + + @Test + fun `onConnectionStatusChanged pauses replay when offline for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy } + ) + + replay.register(fixture.hub, fixture.options) + replay.start() + replay.onConnectionStatusChanged(DISCONNECTED) + + verify(recorder).pause() + } + + @Test + fun `onConnectionStatusChanged resumes replay when back-online for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy } + ) + + replay.register(fixture.hub, fixture.options) + replay.start() + replay.onConnectionStatusChanged(CONNECTED) + + verify(recorder).resume() + } + + @Test + fun `onRateLimitChanged pauses replay when rate-limited for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + isRateLimited = true + ) + + replay.register(fixture.hub, fixture.options) + replay.start() + replay.onRateLimitChanged(fixture.rateLimiter) + + verify(recorder).pause() + } + + @Test + fun `onRateLimitChanged resumes replay when rate-limit lifted for sessions`() { + val captureStrategy = getSessionCaptureStrategy(fixture.options) + val recorder = mock() + val replay = fixture.getSut( + context, + recorderProvider = { recorder }, + replayCaptureStrategyProvider = { captureStrategy }, + isRateLimited = false + ) + + replay.register(fixture.hub, fixture.options) + replay.start() + replay.onRateLimitChanged(fixture.rateLimiter) + + verify(recorder).resume() + } + + private fun getSessionCaptureStrategy(options: SentryOptions): SessionCaptureStrategy { + return SessionCaptureStrategy( + options, + null, + CurrentDateProvider.getInstance(), + executor = mock { + doAnswer { + (it.arguments[0] as Runnable).run() + }.whenever(mock).submit(any()) + } + ) + } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 8e266bcdba..c356b298b6 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -5564,15 +5564,22 @@ public final class io/sentry/transport/NoOpTransportGate : io/sentry/transport/I public fun isConnected ()Z } -public final class io/sentry/transport/RateLimiter { +public final class io/sentry/transport/RateLimiter : java/io/Closeable { public fun (Lio/sentry/SentryOptions;)V public fun (Lio/sentry/transport/ICurrentDateProvider;Lio/sentry/SentryOptions;)V + public fun addRateLimitObserver (Lio/sentry/transport/RateLimiter$IRateLimitObserver;)V + public fun close ()V public fun filter (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/SentryEnvelope; public fun isActiveForCategory (Lio/sentry/DataCategory;)Z public fun isAnyRateLimitActive ()Z + public fun removeRateLimitObserver (Lio/sentry/transport/RateLimiter$IRateLimitObserver;)V public fun updateRetryAfterLimits (Ljava/lang/String;Ljava/lang/String;I)V } +public abstract interface class io/sentry/transport/RateLimiter$IRateLimitObserver { + public abstract fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V +} + public final class io/sentry/transport/ReusableCountLatch { public fun ()V public fun (I)V diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index 796a17cb3c..a88df2824f 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -174,6 +174,9 @@ private DataCategory categoryFromItemType(SentryItemType itemType) { if (SentryItemType.CheckIn.equals(itemType)) { return DataCategory.Monitor; } + if (SentryItemType.ReplayVideo.equals(itemType)) { + return DataCategory.Replay; + } return DataCategory.Default; } diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index 985bbcccbb..24f954c0c1 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -170,6 +170,7 @@ public void close() throws IOException { @Override public void close(final boolean isRestarting) throws IOException { + rateLimiter.close(); executor.shutdown(); options.getLogger().log(SentryLevel.DEBUG, "Shutting down"); try { diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index c4598820ab..191e8cbe7e 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -15,16 +15,21 @@ import io.sentry.util.CollectionUtils; import io.sentry.util.HintUtils; import io.sentry.util.StringUtils; +import java.io.Closeable; +import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** Controls retry limits on different category types sent to Sentry. */ -public final class RateLimiter { +public final class RateLimiter implements Closeable { private static final int HTTP_RETRY_AFTER_DEFAULT_DELAY_MILLIS = 60000; @@ -32,6 +37,9 @@ public final class RateLimiter { private final @NotNull SentryOptions options; private final @NotNull Map sentryRetryAfterLimit = new ConcurrentHashMap<>(); + private final @NotNull List rateLimitObservers = new CopyOnWriteArrayList<>(); + private @Nullable Timer timer = null; + private final @NotNull Object timerLock = new Object(); public RateLimiter( final @NotNull ICurrentDateProvider currentDateProvider, @@ -177,6 +185,8 @@ private boolean isRetryAfter(final @NotNull String itemType) { return DataCategory.Transaction; case "check_in": return DataCategory.Monitor; + case "replay_video": + return DataCategory.Replay; default: return DataCategory.Unknown; } @@ -280,6 +290,23 @@ private void applyRetryAfterOnlyIfLonger( // only overwrite its previous date if the limit is even longer if (oldDate == null || date.after(oldDate)) { sentryRetryAfterLimit.put(dataCategory, date); + + notifyRateLimitObservers(); + + synchronized (timerLock) { + if (timer == null) { + timer = new Timer(true); + } + + timer.schedule( + new TimerTask() { + @Override + public void run() { + notifyRateLimitObservers(); + } + }, + date); + } } } @@ -301,4 +328,41 @@ private long parseRetryAfterOrDefault(final @Nullable String retryAfterHeader) { } return retryAfterMillis; } + + private void notifyRateLimitObservers() { + for (IRateLimitObserver observer : rateLimitObservers) { + observer.onRateLimitChanged(this); + } + } + + public void addRateLimitObserver(@NotNull final IRateLimitObserver observer) { + rateLimitObservers.add(observer); + } + + public void removeRateLimitObserver(@NotNull final IRateLimitObserver observer) { + rateLimitObservers.remove(observer); + } + + @Override + public void close() throws IOException { + synchronized (timerLock) { + if (timer != null) { + timer.cancel(); + timer = null; + } + } + rateLimitObservers.clear(); + } + + public interface IRateLimitObserver { + /** + * Invoked whenever the rate limit changed. You should use {@link + * RateLimiter#isActiveForCategory(DataCategory)} to check whether the category you're + * interested in has changed. + * + * @param rateLimiter this {@link RateLimiter} instance which you can use to check if the rate + * limit is active for a specific category + */ + void onRateLimitChanged(@NotNull RateLimiter rateLimiter); + } } diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt index c06e8da1f6..135cce944b 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -10,12 +10,14 @@ import io.sentry.Hint import io.sentry.IHub import io.sentry.NoOpLogger import io.sentry.ProfilingTraceData +import io.sentry.ReplayRecording import io.sentry.Sentry import io.sentry.SentryEnvelope import io.sentry.SentryEnvelopeHeader import io.sentry.SentryEnvelopeItem import io.sentry.SentryEvent import io.sentry.SentryOptions +import io.sentry.SentryReplayEvent import io.sentry.SentryTracer import io.sentry.Session import io.sentry.TracesSamplingDecision @@ -71,13 +73,14 @@ class ClientReportTest { SentryEnvelopeItem.fromAttachment(opts.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000), SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, opts.serializer), SentryEnvelopeItem.fromCheckIn(opts.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)), - SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())) + SentryEnvelopeItem.fromMetrics(EncodedMetrics(emptyMap())), + SentryEnvelopeItem.fromReplay(opts.serializer, opts.logger, SentryReplayEvent(), ReplayRecording(), false) ) clientReportRecorder.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelope) val clientReportAtEnd = clientReportRecorder.resetCountsAndGenerateClientReport() - testHelper.assertTotalCount(15, clientReportAtEnd) + testHelper.assertTotalCount(16, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.SAMPLE_RATE, DataCategory.Error, 3, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.BEFORE_SEND, DataCategory.Error, 2, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.QUEUE_OVERFLOW, DataCategory.Transaction, 1, clientReportAtEnd) @@ -90,6 +93,7 @@ class ClientReportTest { testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Profile, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Monitor, 1, clientReportAtEnd) testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.MetricBucket, 1, clientReportAtEnd) + testHelper.assertCountFor(DiscardReason.NETWORK_ERROR, DataCategory.Replay, 1, clientReportAtEnd) } @Test diff --git a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt index abaa965175..ae479ed6cb 100644 --- a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt +++ b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt @@ -329,6 +329,14 @@ class AsyncHttpTransportTest { ) } + @Test + fun `close closes the rate limiter`() { + val sut = fixture.getSUT() + sut.close() + + verify(fixture.rateLimiter).close() + } + @Test fun `close uses flushTimeoutMillis option to schedule termination`() { fixture.sentryOptions.flushTimeoutMillis = 123 diff --git a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt index 8346ddf102..8d8fb9601e 100644 --- a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt +++ b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt @@ -3,17 +3,21 @@ package io.sentry.transport import io.sentry.Attachment import io.sentry.CheckIn import io.sentry.CheckInStatus +import io.sentry.DataCategory.Replay import io.sentry.Hint import io.sentry.IHub +import io.sentry.ILogger import io.sentry.ISerializer import io.sentry.NoOpLogger import io.sentry.ProfilingTraceData +import io.sentry.ReplayRecording import io.sentry.SentryEnvelope import io.sentry.SentryEnvelopeHeader import io.sentry.SentryEnvelopeItem import io.sentry.SentryEvent import io.sentry.SentryOptions import io.sentry.SentryOptionsManipulator +import io.sentry.SentryReplayEvent import io.sentry.SentryTracer import io.sentry.Session import io.sentry.TransactionContext @@ -24,6 +28,7 @@ import io.sentry.metrics.EncodedMetrics import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User +import org.awaitility.kotlin.await import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.same @@ -33,6 +38,7 @@ import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import java.io.File import java.util.UUID +import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -354,4 +360,70 @@ class RateLimiterTest { assertTrue(rateLimiter.isAnyRateLimitActive) } + + @Test + fun `drop replay items as lost`() { + val rateLimiter = fixture.getSUT() + val hub = mock() + whenever(hub.options).thenReturn(SentryOptions()) + + val replayItem = SentryEnvelopeItem.fromReplay(fixture.serializer, mock(), SentryReplayEvent(), ReplayRecording(), false) + val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(replayItem, attachmentItem)) + + rateLimiter.updateRetryAfterLimits("60:replay:key", null, 1) + val result = rateLimiter.filter(envelope, Hint()) + + assertNotNull(result) + assertEquals(1, result.items.toList().size) + + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(replayItem)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + + @Test + fun `apply rate limits notifies observers`() { + val rateLimiter = fixture.getSUT() + + var applied = false + rateLimiter.addRateLimitObserver { + applied = rateLimiter.isActiveForCategory(Replay) + } + rateLimiter.updateRetryAfterLimits("60:replay:key", null, 1) + + assertTrue(applied) + } + + @Test + fun `apply rate limits schedules a timer to notify observers of lifted limits`() { + val rateLimiter = fixture.getSUT() + whenever(fixture.currentDateProvider.currentTimeMillis).thenReturn(0, 1, 2001) + + val applied = AtomicBoolean(true) + rateLimiter.addRateLimitObserver { + applied.set(rateLimiter.isActiveForCategory(Replay)) + } + rateLimiter.updateRetryAfterLimits("1:replay:key", null, 1) + + await.untilFalse(applied) + assertFalse(applied.get()) + } + + @Test + fun `close cancels the timer`() { + val rateLimiter = fixture.getSUT() + whenever(fixture.currentDateProvider.currentTimeMillis).thenReturn(0, 1, 2001) + + val applied = AtomicBoolean(true) + rateLimiter.addRateLimitObserver { + applied.set(rateLimiter.isActiveForCategory(Replay)) + } + + rateLimiter.updateRetryAfterLimits("1:replay:key", null, 1) + rateLimiter.close() + + // wait for 1.5s to ensure the timer has run after 1s + await.untilTrue(applied) + assertTrue(applied.get()) + } } From 0438c6f3581c589a0772ec049e5bc374588cbeba Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Fri, 15 Nov 2024 17:56:24 +0100 Subject: [PATCH 12/14] [SR] Add beforeSendReplay callback (#3855) * Do not capture screenshots in session mode when rate limit is active * Changelog * Add beforeSendReplay callback * Changelog * Remove excessive log * Update SessionCaptureStrategyTest.kt --- CHANGELOG.md | 1 + sentry/api/sentry.api | 6 ++ .../src/main/java/io/sentry/SentryClient.java | 33 +++++++- .../main/java/io/sentry/SentryOptions.java | 41 ++++++++++ .../test/java/io/sentry/SentryClientTest.kt | 81 +++++++++++++++++++ 5 files changed, 161 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3163749f06..cafb900253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Android 15: Add support for 16KB page sizes ([#3620](https://github.com/getsentry/sentry-java/pull/3620)) - See https://developer.android.com/guide/practices/page-sizes for more details +- Session Replay: Add `beforeSendReplay` callback ([#3855](https://github.com/getsentry/sentry-java/pull/3855)) ### Fixes diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index c356b298b6..89860df8a2 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2372,6 +2372,7 @@ public class io/sentry/SentryOptions { public fun getBeforeEmitMetricCallback ()Lio/sentry/SentryOptions$BeforeEmitMetricCallback; public fun getBeforeEnvelopeCallback ()Lio/sentry/SentryOptions$BeforeEnvelopeCallback; public fun getBeforeSend ()Lio/sentry/SentryOptions$BeforeSendCallback; + public fun getBeforeSendReplay ()Lio/sentry/SentryOptions$BeforeSendReplayCallback; public fun getBeforeSendTransaction ()Lio/sentry/SentryOptions$BeforeSendTransactionCallback; public fun getBundleIds ()Ljava/util/Set; public fun getCacheDirPath ()Ljava/lang/String; @@ -2487,6 +2488,7 @@ public class io/sentry/SentryOptions { public fun setBeforeEmitMetricCallback (Lio/sentry/SentryOptions$BeforeEmitMetricCallback;)V public fun setBeforeEnvelopeCallback (Lio/sentry/SentryOptions$BeforeEnvelopeCallback;)V public fun setBeforeSend (Lio/sentry/SentryOptions$BeforeSendCallback;)V + public fun setBeforeSendReplay (Lio/sentry/SentryOptions$BeforeSendReplayCallback;)V public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V public fun setCacheDirPath (Ljava/lang/String;)V public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V @@ -2593,6 +2595,10 @@ public abstract interface class io/sentry/SentryOptions$BeforeSendCallback { public abstract fun execute (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; } +public abstract interface class io/sentry/SentryOptions$BeforeSendReplayCallback { + public abstract fun execute (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent; +} + public abstract interface class io/sentry/SentryOptions$BeforeSendTransactionCallback { public abstract fun execute (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index b053230ce5..31a5d6f780 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -285,8 +285,18 @@ private void finalizeTransaction(final @NotNull IScope scope, final @NotNull Hin event = processReplayEvent(event, hint, options.getEventProcessors()); + if (event != null) { + event = executeBeforeSendReplay(event, hint); + + if (event == null) { + options.getLogger().log(SentryLevel.DEBUG, "Event was dropped by beforeSendReplay"); + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.Replay); + } + } + if (event == null) { - options.getLogger().log(SentryLevel.DEBUG, "Replay was dropped by Event processors."); return SentryId.EMPTY_ID; } @@ -1126,6 +1136,27 @@ private void sortBreadcrumbsByDate( return transaction; } + private @Nullable SentryReplayEvent executeBeforeSendReplay( + @NotNull SentryReplayEvent event, final @NotNull Hint hint) { + final SentryOptions.BeforeSendReplayCallback beforeSendReplay = options.getBeforeSendReplay(); + if (beforeSendReplay != null) { + try { + event = beforeSendReplay.execute(event, hint); + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.ERROR, + "The BeforeSendReplay callback threw an exception. It will be added as breadcrumb and continue.", + e); + + // drop event in case of an error in beforeSend due to PII concerns + event = null; + } + } + return event; + } + @Override public void close() { close(false); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 8614abc287..22738b4ba1 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -150,6 +150,12 @@ public class SentryOptions { */ private @Nullable BeforeSendTransactionCallback beforeSendTransaction; + /** + * This function is called with an SDK specific replay object and can return a modified replay + * object or nothing to skip reporting the replay + */ + private @Nullable BeforeSendReplayCallback beforeSendReplay; + /** * This function is called with an SDK specific breadcrumb object before the breadcrumb is added * to the scope. When nothing is returned from the function, the breadcrumb is dropped @@ -761,6 +767,24 @@ public void setBeforeSendTransaction( this.beforeSendTransaction = beforeSendTransaction; } + /** + * Returns the BeforeSendReplay callback + * + * @return the beforeSend callback or null if not set + */ + public @Nullable BeforeSendReplayCallback getBeforeSendReplay() { + return beforeSendReplay; + } + + /** + * Sets the beforeSendReplay callback + * + * @param beforeSendReplay the beforeSend callback + */ + public void setBeforeSendReplay(@Nullable BeforeSendReplayCallback beforeSendReplay) { + this.beforeSendReplay = beforeSendReplay; + } + /** * Returns the beforeBreadcrumb callback * @@ -2493,6 +2517,23 @@ public interface BeforeSendTransactionCallback { SentryTransaction execute(@NotNull SentryTransaction transaction, @NotNull Hint hint); } + /** The BeforeSendReplay callback */ + public interface BeforeSendReplayCallback { + + /** + * Mutate or drop a replay event before being sent. Note that there might be many replay events + * for a single replay (i.e. segments), you can check {@link SentryReplayEvent#getReplayId()} to + * identify that the segments belong to the same replay. + * + * @param event the event + * @param hint the hint, contains {@link ReplayRecording}, can be accessed via {@link + * Hint#getReplayRecording()} + * @return the original event or the mutated event or null if event was dropped + */ + @Nullable + SentryReplayEvent execute(@NotNull SentryReplayEvent event, @NotNull Hint hint); + } + /** The BeforeBreadcrumb callback */ public interface BeforeBreadcrumbCallback { diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index f87a148bf1..fb4f5ae873 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -2800,6 +2800,68 @@ class SentryClientTest { assertFalse(called) } + @Test + fun `when beforeSendReplay is set, callback is invoked`() { + var invoked = false + fixture.sentryOptions.setBeforeSendReplay { replay: SentryReplayEvent, _: Hint -> invoked = true; replay } + + fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint()) + + assertTrue(invoked) + } + + @Test + fun `when beforeSendReplay returns null, event is dropped`() { + fixture.sentryOptions.setBeforeSendReplay { replay: SentryReplayEvent, _: Hint -> null } + + fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint()) + + verify(fixture.transport, never()).send(any(), anyOrNull()) + + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf( + DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1) + ) + ) + } + + @Test + fun `when beforeSendReplay returns new instance, new instance is sent`() { + val expected = SentryReplayEvent().apply { tags = mapOf("test" to "test") } + fixture.sentryOptions.setBeforeSendReplay { _, _ -> expected } + + fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint()) + + verify(fixture.transport).send( + check { + val replay = getReplayFromData(it.items.first().data) + assertEquals("test", replay!!.tags!!["test"]) + }, + anyOrNull() + ) + verifyNoMoreInteractions(fixture.transport) + } + + @Test + fun `when beforeSendReplay throws an exception, replay is dropped`() { + val exception = Exception("test") + + exception.stackTrace.toString() + fixture.sentryOptions.setBeforeSendReplay { _, _ -> throw exception } + + val id = fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint()) + + assertEquals(SentryId.EMPTY_ID, id) + + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf( + DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1) + ) + ) + } + private fun givenScopeWithStartedSession(errored: Boolean = false, crashed: Boolean = false): IScope { val scope = createScope(fixture.sentryOptions) scope.startSession() @@ -2977,6 +3039,25 @@ class SentryClientTest { )!! } + private fun getReplayFromData(data: ByteArray): SentryReplayEvent? { + val unpacker = MessagePack.newDefaultUnpacker(data) + val mapSize = unpacker.unpackMapHeader() + for (i in 0 until mapSize) { + val key = unpacker.unpackString() + when (key) { + SentryItemType.ReplayEvent.itemType -> { + val replayEventLength = unpacker.unpackBinaryHeader() + val replayEventBytes = unpacker.readPayload(replayEventLength) + return fixture.sentryOptions.serializer.deserialize( + InputStreamReader(replayEventBytes.inputStream()), + SentryReplayEvent::class.java + )!! + } + } + } + return null + } + private fun verifyAttachmentsInEnvelope(eventId: SentryId?) { verify(fixture.transport).send( check { actual -> From ce09ad4523b10ddaa0df90c12c17fb0b7a89bf78 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:30:26 +0100 Subject: [PATCH 13/14] feat(replay): Add Mask/Unmask Containers for custom masking in hybrid SDKs (#3881) * feat(replay): Add Mask/Unmask Containers for custom masking in hybrid SDKs * Format code * Address PR feedback * Fix changelog --------- Co-authored-by: Sentry Github Bot Co-authored-by: Roman Zavarnitsyn --- CHANGELOG.md | 1 + .../replay/viewhierarchy/ViewHierarchyNode.kt | 20 ++ .../ContainerMaskingOptionsTest.kt | 231 ++++++++++++++++++ sentry/api/sentry.api | 4 + .../java/io/sentry/SentryReplayOptions.java | 27 ++ 5 files changed, 283 insertions(+) create mode 100644 sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ContainerMaskingOptionsTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index cafb900253..5261116397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Android 15: Add support for 16KB page sizes ([#3620](https://github.com/getsentry/sentry-java/pull/3620)) - See https://developer.android.com/guide/practices/page-sizes for more details - Session Replay: Add `beforeSendReplay` callback ([#3855](https://github.com/getsentry/sentry-java/pull/3855)) +- Session Replay: Add support for masking/unmasking view containers ([#3881](https://github.com/getsentry/sentry-java/pull/3881)) ### Fixes diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt index 03cb37ad3e..03bda7cfc6 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt @@ -3,6 +3,7 @@ package io.sentry.android.replay.viewhierarchy import android.annotation.TargetApi import android.graphics.Rect import android.view.View +import android.view.ViewParent import android.widget.ImageView import android.widget.TextView import io.sentry.SentryOptions @@ -261,6 +262,13 @@ sealed class ViewHierarchyNode( return true } + if (!this.isMaskContainer(options) && + this.parent != null && + this.parent.isUnmaskContainer(options) + ) { + return false + } + if (this.javaClass.isAssignableFrom(options.experimental.sessionReplay.unmaskViewClasses)) { return false } @@ -268,6 +276,18 @@ sealed class ViewHierarchyNode( return this.javaClass.isAssignableFrom(options.experimental.sessionReplay.maskViewClasses) } + private fun ViewParent.isUnmaskContainer(options: SentryOptions): Boolean { + val unmaskContainer = + options.experimental.sessionReplay.unmaskViewContainerClass ?: return false + return this.javaClass.name == unmaskContainer + } + + private fun View.isMaskContainer(options: SentryOptions): Boolean { + val maskContainer = + options.experimental.sessionReplay.maskViewContainerClass ?: return false + return this.javaClass.name == maskContainer + } + fun fromView(view: View, parent: ViewHierarchyNode?, distance: Int, options: SentryOptions): ViewHierarchyNode { val (isVisible, visibleRect) = view.isVisibleToUser() val shouldMask = isVisible && view.shouldMask(options) diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ContainerMaskingOptionsTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ContainerMaskingOptionsTest.kt new file mode 100644 index 0000000000..ff9a125d95 --- /dev/null +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/viewhierarchy/ContainerMaskingOptionsTest.kt @@ -0,0 +1,231 @@ +package io.sentry.android.replay.viewhierarchy + +import android.app.Activity +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.LinearLayout.LayoutParams +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.SentryOptions +import io.sentry.android.replay.maskAllImages +import io.sentry.android.replay.maskAllText +import org.junit.runner.RunWith +import org.robolectric.Robolectric.buildActivity +import org.robolectric.annotation.Config +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +@Config(sdk = [30]) +class ContainerMaskingOptionsTest { + + @BeforeTest + fun setup() { + System.setProperty("robolectric.areWindowsMarkedVisible", "true") + } + + @Test + fun `when maskAllText is set TextView in Unmask container is unmasked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllText = true + experimental.sessionReplay.setUnmaskViewContainerClass(CustomUnmask::class.java.name) + } + + val textNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.textViewInUnmask!!, null, 0, options) + assertFalse(textNode.shouldMask) + } + + @Test + fun `when maskAllImages is set ImageView in Unmask container is unmasked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.maskAllImages = true + experimental.sessionReplay.setUnmaskViewContainerClass(CustomUnmask::class.java.name) + } + + val imageNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.imageViewInUnmask!!, null, 0, options) + assertFalse(imageNode.shouldMask) + } + + @Test + fun `MaskContainer is always masked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.setMaskViewContainerClass(CustomMask::class.java.name) + } + + val maskContainer = ViewHierarchyNode.fromView(MaskingOptionsActivity.maskWithChildren!!, null, 0, options) + + assertTrue(maskContainer.shouldMask) + } + + @Test + fun `when Views are in UnmaskContainer only direct children are unmasked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.addMaskViewClass(CustomView::class.java.name) + experimental.sessionReplay.setUnmaskViewContainerClass(CustomUnmask::class.java.name) + } + + val maskContainer = ViewHierarchyNode.fromView(MaskingOptionsActivity.unmaskWithChildren!!, null, 0, options) + val firstChild = ViewHierarchyNode.fromView(MaskingOptionsActivity.customViewInUnmask!!, maskContainer, 0, options) + val secondLevelChild = ViewHierarchyNode.fromView(MaskingOptionsActivity.secondLayerChildInUnmask!!, firstChild, 0, options) + + assertFalse(maskContainer.shouldMask) + assertFalse(firstChild.shouldMask) + assertTrue(secondLevelChild.shouldMask) + } + + @Test + fun `when MaskContainer is direct child of UnmaskContainer all children od Mask are masked`() { + buildActivity(MaskingOptionsActivity::class.java).setup() + + val options = SentryOptions().apply { + experimental.sessionReplay.setMaskViewContainerClass(CustomMask::class.java.name) + experimental.sessionReplay.setUnmaskViewContainerClass(CustomUnmask::class.java.name) + } + + val unmaskNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.unmaskWithMaskChild!!, null, 0, options) + val maskNode = ViewHierarchyNode.fromView(MaskingOptionsActivity.maskAsDirectChildOfUnmask!!, unmaskNode, 0, options) + + assertFalse(unmaskNode.shouldMask) + assertTrue(maskNode.shouldMask) + } + + private class CustomView(context: Context) : View(context) { + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + canvas.drawColor(Color.BLACK) + } + } + + private open class CustomGroup(context: Context) : LinearLayout(context) { + init { + setBackgroundColor(android.R.color.white) + orientation = VERTICAL + layoutParams = LayoutParams(100, 100) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + canvas.drawColor(Color.BLACK) + } + } + + private class CustomMask(context: Context) : CustomGroup(context) + private class CustomUnmask(context: Context) : CustomGroup(context) + + private class MaskingOptionsActivity : Activity() { + + companion object { + var unmaskWithTextView: ViewGroup? = null + var textViewInUnmask: TextView? = null + + var unmaskWithImageView: ViewGroup? = null + var imageViewInUnmask: ImageView? = null + + var unmaskWithChildren: ViewGroup? = null + var customViewInUnmask: ViewGroup? = null + var secondLayerChildInUnmask: View? = null + + var maskWithChildren: ViewGroup? = null + + var unmaskWithMaskChild: ViewGroup? = null + var maskAsDirectChildOfUnmask: ViewGroup? = null + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val linearLayout = LinearLayout(this).apply { + setBackgroundColor(android.R.color.white) + orientation = LinearLayout.VERTICAL + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + } + + val context = this + + linearLayout.addView( + CustomUnmask(context).apply { + unmaskWithTextView = this + this.addView( + TextView(context).apply { + textViewInUnmask = this + text = "Hello, World!" + layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + } + ) + } + ) + + linearLayout.addView( + CustomUnmask(context).apply { + unmaskWithImageView = this + this.addView( + ImageView(context).apply { + imageViewInUnmask = this + val image = this::class.java.classLoader.getResource("Tongariro.jpg")!! + setImageDrawable(Drawable.createFromPath(image.path)) + layoutParams = LayoutParams(50, 50).apply { + setMargins(0, 16, 0, 0) + } + } + ) + } + ) + + linearLayout.addView( + CustomUnmask(context).apply { + unmaskWithChildren = this + this.addView( + CustomGroup(context).apply { + customViewInUnmask = this + this.addView( + CustomView(context).apply { + secondLayerChildInUnmask = this + } + ) + } + ) + } + ) + + linearLayout.addView( + CustomMask(context).apply { + maskWithChildren = this + this.addView( + CustomGroup(context).apply { + this.addView(CustomView(context)) + } + ) + } + ) + + linearLayout.addView( + CustomUnmask(context).apply { + unmaskWithMaskChild = this + this.addView( + CustomMask(context).apply { + maskAsDirectChildOfUnmask = this + } + ) + } + ) + + setContentView(linearLayout) + } + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 89860df8a2..fc2f4ba51e 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2731,19 +2731,23 @@ public final class io/sentry/SentryReplayOptions { public fun getErrorReplayDuration ()J public fun getFrameRate ()I public fun getMaskViewClasses ()Ljava/util/Set; + public fun getMaskViewContainerClass ()Ljava/lang/String; public fun getOnErrorSampleRate ()Ljava/lang/Double; public fun getQuality ()Lio/sentry/SentryReplayOptions$SentryReplayQuality; public fun getSessionDuration ()J public fun getSessionSampleRate ()Ljava/lang/Double; public fun getSessionSegmentDuration ()J public fun getUnmaskViewClasses ()Ljava/util/Set; + public fun getUnmaskViewContainerClass ()Ljava/lang/String; public fun isSessionReplayEnabled ()Z public fun isSessionReplayForErrorsEnabled ()Z public fun setMaskAllImages (Z)V public fun setMaskAllText (Z)V + public fun setMaskViewContainerClass (Ljava/lang/String;)V public fun setOnErrorSampleRate (Ljava/lang/Double;)V public fun setQuality (Lio/sentry/SentryReplayOptions$SentryReplayQuality;)V public fun setSessionSampleRate (Ljava/lang/Double;)V + public fun setUnmaskViewContainerClass (Ljava/lang/String;)V } public final class io/sentry/SentryReplayOptions$SentryReplayQuality : java/lang/Enum { diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index 0c99085726..fd492213ac 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -81,6 +81,12 @@ public enum SentryReplayQuality { */ private Set unmaskViewClasses = new CopyOnWriteArraySet<>(); + /** The class name of the view container that masks all of its children. */ + private @Nullable String maskViewContainerClass = null; + + /** The class name of the view container that unmasks its direct children. */ + private @Nullable String unmaskViewContainerClass = null; + /** * Defines the quality of the session replay. The higher the quality, the more accurate the replay * will be, but also more data to transfer and more CPU load, defaults to MEDIUM. @@ -239,4 +245,25 @@ public long getSessionSegmentDuration() { public long getSessionDuration() { return sessionDuration; } + + @ApiStatus.Internal + public void setMaskViewContainerClass(@NotNull String containerClass) { + addMaskViewClass(containerClass); + maskViewContainerClass = containerClass; + } + + @ApiStatus.Internal + public void setUnmaskViewContainerClass(@NotNull String containerClass) { + unmaskViewContainerClass = containerClass; + } + + @ApiStatus.Internal + public @Nullable String getMaskViewContainerClass() { + return maskViewContainerClass; + } + + @ApiStatus.Internal + public @Nullable String getUnmaskViewContainerClass() { + return unmaskViewContainerClass; + } } From 78573048c32cd2a7aa5a6cca47b753dfbb49e3a4 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 18 Nov 2024 09:46:53 +0000 Subject: [PATCH 14/14] release: 7.18.0 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5261116397..8e98673f03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 7.18.0 ### Features diff --git a/gradle.properties b/gradle.properties index f9cfa874ad..52a0fad6f9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ android.useAndroidX=true android.defaults.buildfeatures.buildconfig=true # Release information -versionName=7.17.0 +versionName=7.18.0 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android