Repository: getsentry/sentry-java
Pull request #4340: fix(transport): Tag sockets traffic originating from Sentry's HttpConnection
Author: login: romtsn
Created: 2025-04-15T20:36:38Z | Merged: 2025-04-17T21:47:21Z

## :scroll: Description
<!--- Describe your changes in detail -->
To get rid of UntaggedSocket strict mode violation

## :bulb: Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
Addresses #909

## :green_heart: How did you test it?
With tests and verified no strict mode violation when running the sample app

## :pencil: Checklist
<!--- Put an `x` in the boxes that apply -->

- [x] I added tests to verify the changes.
- [x] No new PII added or SDK only sends newly added PII if `sendDefaultPII` is enabled.
- [x] I updated the docs if needed.
- [x] I updated the wizard if needed.
- [x] Review from the native team if needed.
- [x] No breaking change or entry added to the changelog.
- [x] No breaking change for hybrid SDKs or communicated to hybrid SDKs.


## :crystal_ball: Next steps



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a11aa2f2a65..b317c287b61 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`
 - Reduce debug logs verbosity ([#4341](https://github.com/getsentry/sentry-java/pull/4341))
 - Fix unregister `SystemEventsBroadcastReceiver` when entering background ([#4338](https://github.com/getsentry/sentry-java/pull/4338))
   - This should reduce ANRs seen with this class in the stack trace for Android 14 and above
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 <init> (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 <init> (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..7c4afd309f2
--- /dev/null
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java
@@ -0,0 +1,30 @@
+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 {
+
+  // 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() {}
+
+  public static AndroidSocketTagger getInstance() {
+    return instance;
+  }
+
+  @Override
+  public void tagSockets() {
+    TrafficStats.setThreadStatsTag(SENTRY_TAG);
+  }
+
+  @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<IModulesLoader>())
             setDebugMetaLoader(mock<IDebugMetaLoader>())
             threadChecker = mock<IThreadChecker>()
+            setSocketTagger(mock<ISocketTagger>())
             compositePerformanceCollector = mock<CompositePerformanceCollector>()
         })
 
@@ -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 ce8e0d7f3f7..e4d5970f6d4 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
@@ -1697,6 +1702,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
@@ -3108,6 +3119,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;
@@ -3244,6 +3256,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/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<RateLimiter>()
         var sslSocketFactory: SSLSocketFactory? = null
         val requestDetails = mock<RequestDetails>()
+        val socketTagger = mock<ISocketTagger>()
         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")
     }


Reviews:
user: login: stefanosiano
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T09:54:06Z
user: login: stefanosiano
body: Just a small comment on the number used to tag, but no strong opinion on it
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T09:55:13Z
user: login: romtsn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T21:32:36Z

Review comments:
diff_hunk: @@ -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);
path: sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java
user: login: stefanosiano
body: Would it be better to have the current thread id as tag?
I see the value in not showing the violation, but if we want to tackle it, we can show the current thread id or a number hardly used by the user, to ensure we don't mess with any report of the app.
Also, we could use `TrafficStats.getAndSetThreadStatsTag()`, so that we can restore previous value, and if it's -1 clear it later. Not sure how much worth it is, as we are the only one using this thread, i think, but why not
created_at: 2025-04-17T09:54:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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);
path: sentry-android-core/src/main/java/io/sentry/android/core/AndroidSocketTagger.java
user: login: romtsn
body: not sure if it'd change much because it's a threadlocal (under-the-hood) and we anyway use only our own single thread for our requests. But I changed it from 1 to a random number, I think it should be fine like that.
created_at: 2025-04-17T21:32:36Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-15T20:37:02Z
body: ### 🚨 Detected changes in high risk code 🚨 
 High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
 - [ ] sentry/src/main/java/io/sentry/transport/HttpConnection.java
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-15T20:38:58Z
body: ### 🚨 Detected changes in high risk code 🚨 
 High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
 - [ ] sentry/src/main/java/io/sentry/transport/HttpConnection.java
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-15T20:47:32Z
body: ##  Performance metrics :rocket:
<table>
  <tr>
    <th>&nbsp;</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr>
  <tr>
    <th align="left">Startup time</th>
    <td align="right">438.37 ms</td>
    <td align="right">484.16 ms</td>
    <td align="right"><strong>45.79 ms</strong></td>
  </tr>
  <tr>
    <th align="left">Size</th>
    <td align="right">1.58 MiB</td>
    <td align="right">2.08 MiB</td>
    <td align="right"><strong>507.25 KiB</strong></td>
  </tr>
</table>
<details><summary><h3>Previous results on branch: rz/fix/tag-httpconnection-sockets</h3></summary>
<h3>Startup times</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">b030608cce92e798ec3a6bf68ad500958b1e64d2</th>
  <td align="right">397.84 ms</td>
  <td align="right">454.45 ms</td>
  <td align="right"><strong>56.61 ms</strong></td>
</tr></table>
<h3>App size</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">b030608cce92e798ec3a6bf68ad500958b1e64d2</th>
  <td align="right">1.58 MiB</td>
  <td align="right">2.08 MiB</td>
  <td align="right"><strong>506.29 KiB</strong></td>
</tr></table>
</details>

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-15T20:49:49Z
body: ### 🚨 Detected changes in high risk code 🚨 
 High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
 - [ ] sentry/src/main/java/io/sentry/transport/HttpConnection.java
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-17T21:33:51Z
body: ### 🚨 Detected changes in high risk code 🚨 
 High-risk code has higher potential to break the SDK and may be hard to test. To prevent severe bugs, apply the rollout process for releasing such changes and be extra careful when changing and reviewing these files:
 - [x] sentry/src/main/java/io/sentry/transport/HttpConnection.java
author_association: CONTRIBUTOR