From 576789acb8ae44ed19635a764a55607441584393 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 15 Apr 2025 22:34:24 +0200 Subject: [PATCH 1/4] Tag sockets traffic originating from Sentry's HttpConnection --- .../api/sentry-android-core.api | 6 ++ .../core/AndroidOptionsInitializer.java | 4 ++ .../android/core/AndroidSocketTagger.java | 27 +++++++++ .../core/AndroidOptionsInitializerTest.kt | 10 ++++ sentry/api/sentry.api | 13 ++++ .../benchmark/StringAfterDotBenchmark.java | 60 +++++++++++++++++++ .../main/java/io/sentry/ISocketTagger.java | 9 +++ .../main/java/io/sentry/NoOpSocketTagger.java | 24 ++++++++ .../main/java/io/sentry/SentryOptions.java | 20 +++++++ .../io/sentry/transport/HttpConnection.java | 2 + .../io/sentry/transport/HttpConnectionTest.kt | 13 ++++ 11 files changed, 188 insertions(+) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java create mode 100644 sentry/src/jmh/java/io/sentry/benchmark/StringAfterDotBenchmark.java create mode 100644 sentry/src/main/java/io/sentry/ISocketTagger.java create mode 100644 sentry/src/main/java/io/sentry/NoOpSocketTagger.java diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index fe36d2c1746..fd3b2619257 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -111,6 +111,12 @@ public class io/sentry/android/core/AndroidProfiler$ProfileStartData { public fun (JJLjava/util/Date;)V } +public final class io/sentry/android/core/AndroidSocketTagger : io/sentry/ISocketTagger { + public static fun getInstance ()Lio/sentry/android/core/AndroidSocketTagger; + public fun tagSockets ()V + public fun untagSockets ()V +} + public final class io/sentry/android/core/AnrIntegration : io/sentry/Integration, java/io/Closeable { public fun (Landroid/content/Context;)V public fun close ()V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 657e6d369ae..b5cc3ddc933 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -15,6 +15,7 @@ import io.sentry.NoOpCompositePerformanceCollector; import io.sentry.NoOpConnectionStatusProvider; import io.sentry.NoOpContinuousProfiler; +import io.sentry.NoOpSocketTagger; import io.sentry.NoOpTransactionProfiler; import io.sentry.NoopVersionDetector; import io.sentry.ScopeType; @@ -238,6 +239,9 @@ static void initializeIntegrationsAndProcessors( if (options.getThreadChecker() instanceof NoOpThreadChecker) { options.setThreadChecker(AndroidThreadChecker.getInstance()); } + if (options.getSocketTagger() instanceof NoOpSocketTagger) { + options.setSocketTagger(AndroidSocketTagger.getInstance()); + } if (options.getPerformanceCollectors().isEmpty()) { options.addPerformanceCollector(new AndroidMemoryCollector()); options.addPerformanceCollector(new AndroidCpuCollector(options.getLogger())); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java new file mode 100644 index 00000000000..e9333f9acd5 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java @@ -0,0 +1,27 @@ +package io.sentry.android.core; + +import android.net.TrafficStats; +import io.sentry.ISocketTagger; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public final class AndroidSocketTagger implements ISocketTagger { + + private static final AndroidSocketTagger instance = new AndroidSocketTagger(); + + private AndroidSocketTagger() {} + + public static AndroidSocketTagger getInstance() { + return instance; + } + + @Override + public void tagSockets() { + TrafficStats.setThreadStatsTag(1); + } + + @Override + public void untagSockets() { + TrafficStats.clearThreadStatsTag(); + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index e5a2395feb3..504d2bda837 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -11,6 +11,7 @@ import io.sentry.DefaultCompositePerformanceCollector import io.sentry.IConnectionStatusProvider import io.sentry.IContinuousProfiler import io.sentry.ILogger +import io.sentry.ISocketTagger import io.sentry.ITransactionProfiler import io.sentry.MainEventProcessor import io.sentry.NoOpContinuousProfiler @@ -746,6 +747,13 @@ class AndroidOptionsInitializerTest { assertTrue { fixture.sentryOptions.threadChecker is AndroidThreadChecker } } + @Test + fun `AndroidSocketTagger is set to options`() { + fixture.initSut() + + assertTrue { fixture.sentryOptions.socketTagger is AndroidSocketTagger } + } + @Test fun `does not install ComposeGestureTargetLocator, if sentry-compose is not available`() { fixture.initSutWithClassLoader() @@ -859,6 +867,7 @@ class AndroidOptionsInitializerTest { setModulesLoader(mock()) setDebugMetaLoader(mock()) threadChecker = mock() + setSocketTagger(mock()) compositePerformanceCollector = mock() }) @@ -868,6 +877,7 @@ class AndroidOptionsInitializerTest { assertFalse { fixture.sentryOptions.modulesLoader is AssetsModulesLoader } assertFalse { fixture.sentryOptions.debugMetaLoader is AssetsDebugMetaLoader } assertFalse { fixture.sentryOptions.threadChecker is AndroidThreadChecker } + assertFalse { fixture.sentryOptions.socketTagger is AndroidSocketTagger } assertFalse { fixture.sentryOptions.compositePerformanceCollector is DefaultCompositePerformanceCollector } } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index c908c56f99a..b44ec176475 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1028,6 +1028,11 @@ public abstract interface class io/sentry/ISerializer { public abstract fun serialize (Ljava/util/Map;)Ljava/lang/String; } +public abstract interface class io/sentry/ISocketTagger { + public abstract fun tagSockets ()V + public abstract fun untagSockets ()V +} + public abstract interface class io/sentry/ISpan { public abstract fun finish ()V public abstract fun finish (Lio/sentry/SpanStatus;)V @@ -1696,6 +1701,12 @@ public final class io/sentry/NoOpScopesStorage : io/sentry/IScopesStorage { public fun set (Lio/sentry/IScopes;)Lio/sentry/ISentryLifecycleToken; } +public final class io/sentry/NoOpSocketTagger : io/sentry/ISocketTagger { + public static fun getInstance ()Lio/sentry/ISocketTagger; + public fun tagSockets ()V + public fun untagSockets ()V +} + public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V @@ -3105,6 +3116,7 @@ public class io/sentry/SentryOptions { public fun getSessionReplay ()Lio/sentry/SentryReplayOptions; public fun getSessionTrackingIntervalMillis ()J public fun getShutdownTimeoutMillis ()J + public fun getSocketTagger ()Lio/sentry/ISocketTagger; public fun getSpanFactory ()Lio/sentry/ISpanFactory; public fun getSpotlightConnectionUrl ()Ljava/lang/String; public fun getSslSocketFactory ()Ljavax/net/ssl/SSLSocketFactory; @@ -3241,6 +3253,7 @@ public class io/sentry/SentryOptions { public fun setSessionReplay (Lio/sentry/SentryReplayOptions;)V public fun setSessionTrackingIntervalMillis (J)V public fun setShutdownTimeoutMillis (J)V + public fun setSocketTagger (Lio/sentry/ISocketTagger;)V public fun setSpanFactory (Lio/sentry/ISpanFactory;)V public fun setSpotlightConnectionUrl (Ljava/lang/String;)V public fun setSslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;)V diff --git a/sentry/src/jmh/java/io/sentry/benchmark/StringAfterDotBenchmark.java b/sentry/src/jmh/java/io/sentry/benchmark/StringAfterDotBenchmark.java new file mode 100644 index 00000000000..240706138f7 --- /dev/null +++ b/sentry/src/jmh/java/io/sentry/benchmark/StringAfterDotBenchmark.java @@ -0,0 +1,60 @@ +package io.sentry.benchmark; + +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.NotNull; +import org.openjdk.jmh.annotations.*; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Thread) +public class StringAfterDotBenchmark { + + @NotNull private String testString = ""; + + private char[] buf = new char[64]; + + @Setup + public void setup() { + testString = "com.example.deep.package.ClassName"; + } + // + // @Benchmark + // public String usingSubstring() { + // int idx = testString.lastIndexOf('.'); + // return (idx >= 0 && idx + 1 < testString.length()) + // ? testString.substring(idx + 1) + // : testString; + // } + // + // @Benchmark + // public String usingManualCharCopy() { + // int len = testString.length(); + // for (int i = len - 1; i >= 0; i--) { + // if (testString.charAt(i) == '.') { + // int newLen = len - i - 1; + // char[] buf = new char[newLen]; + // testString.getChars(i + 1, len, buf, 0); + // return new String(buf); + // } + // } + // return testString; + // } + + @Benchmark + public String usingThreadLocalBuffer() { + int len = testString.length(); + int bufIndex = buf.length; + + for (int i = len - 1; i >= 0; i--) { + char c = testString.charAt(i); + if (c == '.') { + int suffixLen = buf.length - bufIndex; + return new String(buf, bufIndex, suffixLen); + } + buf[--bufIndex] = c; + } + + // No dot found — return original + return testString; + } +} diff --git a/sentry/src/main/java/io/sentry/ISocketTagger.java b/sentry/src/main/java/io/sentry/ISocketTagger.java new file mode 100644 index 00000000000..8f1d2711599 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ISocketTagger.java @@ -0,0 +1,9 @@ +package io.sentry; + +public interface ISocketTagger { + /** Tags the sockets traffic originating from the Sentry HttpConnection thread. */ + void tagSockets(); + + /** Untags the sockets traffic originating from the Sentry HttpConnection thread. */ + void untagSockets(); +} diff --git a/sentry/src/main/java/io/sentry/NoOpSocketTagger.java b/sentry/src/main/java/io/sentry/NoOpSocketTagger.java new file mode 100644 index 00000000000..756e80ce2ea --- /dev/null +++ b/sentry/src/main/java/io/sentry/NoOpSocketTagger.java @@ -0,0 +1,24 @@ +package io.sentry; + +import org.jetbrains.annotations.NotNull; + +public final class NoOpSocketTagger implements ISocketTagger { + + private static final NoOpSocketTagger instance = new NoOpSocketTagger(); + + private NoOpSocketTagger() {} + + public static @NotNull ISocketTagger getInstance() { + return instance; + } + + @Override + public void tagSockets() { + // No operation + } + + @Override + public void untagSockets() { + // No operation + } +} diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index d5623d44f26..54e37a405ca 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -566,6 +566,8 @@ public class SentryOptions { */ private boolean startProfilerOnAppStart = false; + private @NotNull ISocketTagger socketTagger = NoOpSocketTagger.getInstance(); + /** * Adds an event processor * @@ -2827,6 +2829,24 @@ public boolean isCaptureOpenTelemetryEvents() { return captureOpenTelemetryEvents; } + /** + * Returns the SocketTagger + * + * @return the socket tagger + */ + public @NotNull ISocketTagger getSocketTagger() { + return socketTagger; + } + + /** + * Sets the SocketTagger + * + * @param socketTagger the socket tagger + */ + public void setSocketTagger(final @Nullable ISocketTagger socketTagger) { + this.socketTagger = socketTagger != null ? socketTagger : NoOpSocketTagger.getInstance(); + } + /** * Load the lazy fields. Useful to load in the background, so that results are already cached. DO * NOT CALL THIS METHOD ON THE MAIN THREAD. diff --git a/sentry/src/main/java/io/sentry/transport/HttpConnection.java b/sentry/src/main/java/io/sentry/transport/HttpConnection.java index 5105c74af53..3256804201a 100644 --- a/sentry/src/main/java/io/sentry/transport/HttpConnection.java +++ b/sentry/src/main/java/io/sentry/transport/HttpConnection.java @@ -146,6 +146,7 @@ HttpURLConnection open() throws IOException { } public @NotNull TransportResult send(final @NotNull SentryEnvelope envelope) throws IOException { + options.getSocketTagger().tagSockets(); final HttpURLConnection connection = createConnection(); TransportResult result; @@ -161,6 +162,7 @@ HttpURLConnection open() throws IOException { "An exception occurred while submitting the envelope to the Sentry server."); } finally { result = readAndLog(connection); + options.getSocketTagger().untagSockets(); } return result; } diff --git a/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt b/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt index 47bc34de48b..ea50bbc9db4 100644 --- a/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt +++ b/sentry/src/test/java/io/sentry/transport/HttpConnectionTest.kt @@ -2,6 +2,7 @@ package io.sentry.transport import io.sentry.ILogger import io.sentry.ISerializer +import io.sentry.ISocketTagger import io.sentry.RequestDetails import io.sentry.SentryEnvelope import io.sentry.SentryEvent @@ -41,6 +42,7 @@ class HttpConnectionTest { val rateLimiter = mock() var sslSocketFactory: SSLSocketFactory? = null val requestDetails = mock() + val socketTagger = mock() val options = SentryOptions() init { @@ -58,6 +60,7 @@ class HttpConnectionTest { options.setSerializer(serializer) options.proxy = proxy options.sslSocketFactory = sslSocketFactory + options.setSocketTagger(socketTagger) return HttpConnection(options, requestDetails, authenticatorWrapper, rateLimiter) } @@ -262,6 +265,16 @@ class HttpConnectionTest { verify(fixture.requestDetails.url).openConnection() } + @Test + fun `tags sockets`() { + val transport = fixture.getSUT() + + transport.send(createEnvelope()) + + verify(fixture.socketTagger).tagSockets() + verify(fixture.socketTagger).untagSockets() + } + private fun createSession(): Session { return Session("123", User(), "env", "release") } From 11437912da76b49945d0dc8e162d353c5dfb58b2 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 15 Apr 2025 22:37:20 +0200 Subject: [PATCH 2/4] Remove leftovers --- .../benchmark/StringAfterDotBenchmark.java | 60 ------------------- 1 file changed, 60 deletions(-) delete mode 100644 sentry/src/jmh/java/io/sentry/benchmark/StringAfterDotBenchmark.java diff --git a/sentry/src/jmh/java/io/sentry/benchmark/StringAfterDotBenchmark.java b/sentry/src/jmh/java/io/sentry/benchmark/StringAfterDotBenchmark.java deleted file mode 100644 index 240706138f7..00000000000 --- a/sentry/src/jmh/java/io/sentry/benchmark/StringAfterDotBenchmark.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.sentry.benchmark; - -import java.util.concurrent.TimeUnit; -import org.jetbrains.annotations.NotNull; -import org.openjdk.jmh.annotations.*; - -@BenchmarkMode(Mode.Throughput) -@OutputTimeUnit(TimeUnit.MILLISECONDS) -@State(Scope.Thread) -public class StringAfterDotBenchmark { - - @NotNull private String testString = ""; - - private char[] buf = new char[64]; - - @Setup - public void setup() { - testString = "com.example.deep.package.ClassName"; - } - // - // @Benchmark - // public String usingSubstring() { - // int idx = testString.lastIndexOf('.'); - // return (idx >= 0 && idx + 1 < testString.length()) - // ? testString.substring(idx + 1) - // : testString; - // } - // - // @Benchmark - // public String usingManualCharCopy() { - // int len = testString.length(); - // for (int i = len - 1; i >= 0; i--) { - // if (testString.charAt(i) == '.') { - // int newLen = len - i - 1; - // char[] buf = new char[newLen]; - // testString.getChars(i + 1, len, buf, 0); - // return new String(buf); - // } - // } - // return testString; - // } - - @Benchmark - public String usingThreadLocalBuffer() { - int len = testString.length(); - int bufIndex = buf.length; - - for (int i = len - 1; i >= 0; i--) { - char c = testString.charAt(i); - if (c == '.') { - int suffixLen = buf.length - bufIndex; - return new String(buf, bufIndex, suffixLen); - } - buf[--bufIndex] = c; - } - - // No dot found — return original - return testString; - } -} From 5068888d8e840c09f418a4e7f7bbde68272a3b5f Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 15 Apr 2025 22:49:26 +0200 Subject: [PATCH 3/4] Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f20efc0e3cf..f2ecf3e6852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ ### Fixes - Fix TTFD measurement when API called too early ([#4297](https://github.com/getsentry/sentry-java/pull/4297)) +- Tag sockets traffic originating from Sentry's HttpConnection ([#4340](https://github.com/getsentry/sentry-java/pull/4340)) + - This should suppress the StrictMode's `UntaggedSocketViolation` ## 8.8.0 From e20c6cb597b52a8f1a6475d1d5fd7abbde17cb7c Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 17 Apr 2025 23:31:06 +0200 Subject: [PATCH 4/4] Use a sentry-specific tag --- .../java/io/sentry/android/core/AndroidSocketTagger.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java index e9333f9acd5..7c4afd309f2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java @@ -7,6 +7,9 @@ @ApiStatus.Internal public final class AndroidSocketTagger implements ISocketTagger { + // just a random number to tag outgoing traffic from the Sentry SDK + private static final int SENTRY_TAG = 0xF001; + private static final AndroidSocketTagger instance = new AndroidSocketTagger(); private AndroidSocketTagger() {} @@ -17,7 +20,7 @@ public static AndroidSocketTagger getInstance() { @Override public void tagSockets() { - TrafficStats.setThreadStatsTag(1); + TrafficStats.setThreadStatsTag(SENTRY_TAG); } @Override