From 0902ba9584b276a97515419fe59f19972018d526 Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Mon, 19 Oct 2026 15:05:57 +0200 Subject: [PATCH 1/2] Report connection type and cellular generation on C# events C# events do not pass through the native SDK, so they did not get device.connection_type or device.connection_effective_type, which sentry-java 8.60.0 and sentry-cocoa 9.30.0 report on native events. UnityEventProcessor now reads both values from the native SDK on the main thread. On Android, SentryJava reads the cached connection from AndroidConnectionStatusProvider through JNI, and new ProGuard rules keep the class names. On iOS and macOS, a new bridge function reads the device entry of the Cocoa extra context. This needs the Device fields from getsentry/sentry-dotnet#5745. --- CHANGELOG.md | 7 +++ .../Plugins/Android/proguard-sentry-unity.pro | 2 + package-dev/Plugins/iOS/SentryNativeBridge.m | 13 ++++++ .../Plugins/iOS/SentryNativeBridgeNoOp.m | 6 +++ .../Plugins/macOS/SentryNativeBridge.m | 13 ++++++ src/Sentry.Unity.Android/SentryJava.cs | 26 +++++++++++ .../SentryNativeAndroid.cs | 1 + .../SentryCocoaBridgeProxy.cs | 9 ++++ src/Sentry.Unity.iOS/SentryNativeCocoa.cs | 1 + src/Sentry.Unity/SentryUnityOptions.cs | 5 +++ src/Sentry.Unity/UnityEventProcessor.cs | 7 +++ .../SentryNativeAndroidTests.cs | 12 ++++++ .../TestSentryJava.cs | 3 ++ .../UnityEventScopeTests.cs | 43 +++++++++++++++++++ .../SentryNativeIosTests.cs | 1 + 15 files changed, 149 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee836469d..6a1547c24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- On Android, iOS, and macOS, C# events now report the connection type in `device.connection_type` and the cellular network generation in `device.connection_effective_type`, for example `4g` or `5g`. On Android 11 and earlier, the generation requires the `READ_PHONE_STATE` permission. ([#2889](https://github.com/getsentry/sentry-unity/pull/2889)) + ## 4.11.0 ### Fixes @@ -11,6 +17,7 @@ ### Features +- On Android, iOS, and macOS, C# events now report the connection type in `device.connection_type` and the cellular network generation in `device.connection_effective_type`, for example `4g` or `5g`. On Android 11 and earlier, the generation requires the `READ_PHONE_STATE` permission. ([#PLACEHOLDER](https://github.com/getsentry/sentry-unity/pull/PLACEHOLDER)) - Added the `NativeHandlerPath` option. It sets `handler_path` on the native SDK, so the out-of-process crash handler can be placed somewhere other than next to the player executable. Left unset, sentry-native keeps looking for the handler next to the executable as before. ([#2881](https://github.com/getsentry/sentry-unity/pull/2881)) ### Dependencies diff --git a/package-dev/Plugins/Android/proguard-sentry-unity.pro b/package-dev/Plugins/Android/proguard-sentry-unity.pro index 2d60bf651..957bbe462 100644 --- a/package-dev/Plugins/Android/proguard-sentry-unity.pro +++ b/package-dev/Plugins/Android/proguard-sentry-unity.pro @@ -10,4 +10,6 @@ -keep class io.sentry.Scope { *; } -keep class io.sentry.ScopeCallback { *; } -keep class io.sentry.protocol.** { *; } +-keep class io.sentry.android.core.internal.util.AndroidConnectionStatusProvider { *; } +-keep class io.sentry.android.core.internal.util.NetworkConnection { *; } -dontwarn io.sentry.** diff --git a/package-dev/Plugins/iOS/SentryNativeBridge.m b/package-dev/Plugins/iOS/SentryNativeBridge.m index 6631804da..c2d534133 100644 --- a/package-dev/Plugins/iOS/SentryNativeBridge.m +++ b/package-dev/Plugins/iOS/SentryNativeBridge.m @@ -241,6 +241,19 @@ void SentryNativeBridgeUnsetUser() return cString; } +static char *_Nullable _CStringCopyOrNull(id _Nullable value) +{ + return [value isKindOfClass:[NSString class]] ? strdup([value UTF8String]) : NULL; +} + +void SentryNativeBridgeGetConnection( + char *_Nullable *_Nonnull type, char *_Nullable *_Nonnull effectiveType) +{ + NSDictionary *device = [SentryObjCSDK internal].sdk.extraContext[@"device"]; + *type = _CStringCopyOrNull(device[@"connection_type"]); + *effectiveType = _CStringCopyOrNull(device[@"connection_effective_type"]); +} + void SentryNativeBridgeSetTrace(const char *traceId, const char *spanId) { if (traceId == NULL || spanId == NULL) { diff --git a/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m b/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m index dc52c16e0..7b08dce69 100644 --- a/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m +++ b/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m @@ -32,6 +32,12 @@ void SentryNativeBridgeUnsetUser() { } char *SentryNativeBridgeGetInstallationId() { return NULL; } +void SentryNativeBridgeGetConnection(char **type, char **effectiveType) +{ + *type = NULL; + *effectiveType = NULL; +} + void SentryNativeBridgeSetTrace(const char *traceId, const char *spanId) { } void SentryNativeBridgeSetEnvironment(const char *environment) { } diff --git a/package-dev/Plugins/macOS/SentryNativeBridge.m b/package-dev/Plugins/macOS/SentryNativeBridge.m index 0f034de92..f456ade96 100644 --- a/package-dev/Plugins/macOS/SentryNativeBridge.m +++ b/package-dev/Plugins/macOS/SentryNativeBridge.m @@ -357,6 +357,19 @@ void SentryNativeBridgeUnsetUser() return cString; } +static char *_CStringCopyOrNull(id value) +{ + return [value isKindOfClass:[NSString class]] ? strdup([value UTF8String]) : NULL; +} + +void SentryNativeBridgeGetConnection(char **type, char **effectiveType) +{ + id sdkApi = [SentryInternalApi() performSelector:@selector(sdk)]; + NSDictionary *device = [sdkApi performSelector:@selector(extraContext)][@"device"]; + *type = _CStringCopyOrNull(device[@"connection_type"]); + *effectiveType = _CStringCopyOrNull(device[@"connection_effective_type"]); +} + void SentryNativeBridgeSetTrace(const char *traceId, const char *spanId) { if (traceId == NULL || spanId == NULL) { diff --git a/src/Sentry.Unity.Android/SentryJava.cs b/src/Sentry.Unity.Android/SentryJava.cs index a8b0b58cf..0ff71ab6d 100644 --- a/src/Sentry.Unity.Android/SentryJava.cs +++ b/src/Sentry.Unity.Android/SentryJava.cs @@ -13,6 +13,7 @@ internal interface ISentryJava public void Init(SentryUnityOptions options); public string? GetInstallationId(); public bool? CrashedLastRun(); + public (string? Type, string? EffectiveType) GetConnection(); public void Close(); public void WriteScope( string? AppStartTime, @@ -231,6 +232,31 @@ public void Init(SentryUnityOptions options) return null; } + public (string? Type, string? EffectiveType) GetConnection() + { + if (!MainThreadData.IsMainThread()) + { + _logger?.LogError("Calling GetConnection() on Android SDK requires running on MainThread"); + return (null, null); + } + + try + { + using var sentry = GetSentryJava(); + using var hub = sentry.CallStatic("getCurrentHub"); + using var options = hub?.Call("getOptions"); + using var provider = options?.Call("getConnectionStatusProvider"); + using var connection = provider?.Call("getConnection"); + return (connection?.Get("type"), connection?.Get("effectiveType")); + } + catch (Exception e) + { + _logger?.LogError(e, "Calling 'SentryJava.GetConnection' failed."); + } + + return (null, null); + } + public void WriteScope( string? AppStartTime, string? AppBuildType, diff --git a/src/Sentry.Unity.Android/SentryNativeAndroid.cs b/src/Sentry.Unity.Android/SentryNativeAndroid.cs index 005aed753..500ecb797 100644 --- a/src/Sentry.Unity.Android/SentryNativeAndroid.cs +++ b/src/Sentry.Unity.Android/SentryNativeAndroid.cs @@ -73,6 +73,7 @@ public static void Configure(SentryUnityOptions options) options.ScopeObserver = new AndroidJavaScopeObserver(options, SentryJava); options.EnableScopeSync = true; options.NativeDebugImageProvider = new Native.NativeDebugImageProvider(); + options.NativeConnection = SentryJava.GetConnection; if (options.NativeAppHangTrackingEnabled && options.NdkIntegrationEnabled) { diff --git a/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs b/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs index 2f5596a5e..be8fb61a6 100644 --- a/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs +++ b/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs @@ -146,6 +146,15 @@ public static extern void AddBreadcrumb(string timestamp, string? message, strin [DllImport("__Internal", EntryPoint = "SentryNativeBridgeGetInstallationId")] public static extern string GetInstallationId(); + [DllImport("__Internal", EntryPoint = "SentryNativeBridgeGetConnection")] + private static extern void GetConnection(out string? type, out string? effectiveType); + + public static (string? Type, string? EffectiveType) GetConnection() + { + GetConnection(out var type, out var effectiveType); + return (type, effectiveType); + } + [DllImport("__Internal", EntryPoint = "SentryNativeBridgeSetTrace")] public static extern void SetTrace(string traceId, string spanId); diff --git a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs index 3d6293238..867285ef3 100644 --- a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs +++ b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs @@ -66,6 +66,7 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf SentryCocoaBridgeProxy.SetSdkName(); // Since we're not building the SDK we have to overwrite the name here options.NativeContextWriter = new NativeContextWriter(); + options.NativeConnection = SentryCocoaBridgeProxy.GetConnection; options.EnableScopeSync = true; options.CrashedLastRun = () => { diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 95249276a..345fa6180 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -494,6 +494,11 @@ internal string? DefaultUserId /// internal INativeDebugImageProvider? NativeDebugImageProvider { get; set; } = null; + /// + /// Reads the connection type and the cellular network generation from the native SDK. Called on the main thread. + /// + internal Func<(string? Type, string? EffectiveType)>? NativeConnection { get; set; } = null; + /// /// Used to close down the native SDK /// diff --git a/src/Sentry.Unity/UnityEventProcessor.cs b/src/Sentry.Unity/UnityEventProcessor.cs index 0274b8b13..3f19469a5 100644 --- a/src/Sentry.Unity/UnityEventProcessor.cs +++ b/src/Sentry.Unity/UnityEventProcessor.cs @@ -111,6 +111,13 @@ private void PopulateDevice(Device device) // TODO: Add to protocol? break; } + + if (_sentryOptions.NativeConnection is { } nativeConnection) + { + var (connectionType, connectionEffectiveType) = nativeConnection(); + device.ConnectionType = connectionType; + device.ConnectionEffectiveType = connectionEffectiveType; + } } private void PopulateUnity(Protocol.Unity unity) diff --git a/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs b/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs index db5b37d7c..284dc4706 100644 --- a/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs +++ b/test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs @@ -170,6 +170,18 @@ public void Configure_NoInstallationIdReturned_DoesNotSetDefaultUserId() Assert.IsNull(options.DefaultUserId); } + [Test] + public void Configure_DefaultConfiguration_SetsNativeConnection() + { + var options = new SentryUnityOptions(); + _testSentryJava.Connection = ("cellular", "5g"); + + SentryNativeAndroid.Configure(options); + + Assert.IsNotNull(options.NativeConnection); + Assert.AreEqual(("cellular", "5g"), options.NativeConnection!()); + } + [Test] public void Configure_DefaultConfigurationSentryJavaNotPresent_LogsErrorAndReturns() { diff --git a/test/Sentry.Unity.Android.Tests/TestSentryJava.cs b/test/Sentry.Unity.Android.Tests/TestSentryJava.cs index 84c7f3735..6b3e017d4 100644 --- a/test/Sentry.Unity.Android.Tests/TestSentryJava.cs +++ b/test/Sentry.Unity.Android.Tests/TestSentryJava.cs @@ -10,6 +10,7 @@ internal class TestSentryJava : ISentryJava public bool SentryPresent { get; set; } = true; public string? InstallationId { get; set; } public bool? IsCrashedLastRun { get; set; } + public (string? Type, string? EffectiveType) Connection { get; set; } public bool? IsEnabled() => Enabled; @@ -19,6 +20,8 @@ public void Init(SentryUnityOptions options) { } public bool? CrashedLastRun() => IsCrashedLastRun; + public (string? Type, string? EffectiveType) GetConnection() => Connection; + public void Close() { } public void WriteScope( diff --git a/test/Sentry.Unity.Tests/UnityEventScopeTests.cs b/test/Sentry.Unity.Tests/UnityEventScopeTests.cs index b9d341745..31d88ed1f 100644 --- a/test/Sentry.Unity.Tests/UnityEventScopeTests.cs +++ b/test/Sentry.Unity.Tests/UnityEventScopeTests.cs @@ -298,6 +298,49 @@ public void AppMemory_SetByEventProcessor() Assert.IsNotNull(sentryEvent.Contexts.App.Memory); } + [Test] + public void Process_NativeConnectionSet_SetsDeviceConnection() + { + _sentryOptions.NativeConnection = () => ("cellular", "5g"); + var sut = new UnityEventProcessor(_sentryOptions, new TestUnityInfo()); + var sentryEvent = new SentryEvent(); + + sut.Process(sentryEvent); + + Assert.AreEqual("cellular", sentryEvent.Contexts.Device.ConnectionType); + Assert.AreEqual("5g", sentryEvent.Contexts.Device.ConnectionEffectiveType); + } + + [Test] + public void Process_NativeConnectionNotSet_LeavesDeviceConnectionEmpty() + { + var sut = new UnityEventProcessor(_sentryOptions, new TestUnityInfo()); + var sentryEvent = new SentryEvent(); + + sut.Process(sentryEvent); + + Assert.IsNull(sentryEvent.Contexts.Device.ConnectionType); + Assert.IsNull(sentryEvent.Contexts.Device.ConnectionEffectiveType); + } + + [Test] + public void Process_OffMainThread_DoesNotCallNativeConnection() + { + var called = false; + _sentryOptions.NativeConnection = () => + { + called = true; + return ("wifi", null); + }; + var sut = new UnityEventProcessor(_sentryOptions, new TestUnityInfo()); + var sentryEvent = new SentryEvent(); + + Task.Run(() => sut.Process(sentryEvent)).Wait(); + + Assert.IsFalse(called); + Assert.IsNull(sentryEvent.Contexts.Device.ConnectionType); + } + [Test] public void AppMemory_SetByEventProcessorForTransactions() { diff --git a/test/Sentry.Unity.iOS.Tests/SentryNativeIosTests.cs b/test/Sentry.Unity.iOS.Tests/SentryNativeIosTests.cs index 2f55fb00a..eac8b89de 100644 --- a/test/Sentry.Unity.iOS.Tests/SentryNativeIosTests.cs +++ b/test/Sentry.Unity.iOS.Tests/SentryNativeIosTests.cs @@ -25,6 +25,7 @@ public void Configure_NativeSupportDisabled_iOS() SentryNativeCocoa.Configure(options, RuntimePlatform.IPhonePlayer); Assert.Null(options.ScopeObserver); Assert.Null(options.CrashedLastRun); + Assert.Null(options.NativeConnection); Assert.False(options.EnableScopeSync); } From 72053429af09637d33c72aa224047f0e0c5f2f9b Mon Sep 17 00:00:00 2001 From: Alexander Pantiukhov Date: Tue, 20 Oct 2026 13:36:07 +0200 Subject: [PATCH 2/2] Update sentry-dotnet to c9ad2be4 for the device connection fields The feature needs Device.ConnectionType and Device.ConnectionEffectiveType from getsentry/sentry-dotnet#5745, which is merged but not released. sentry-dotnet main needs .NET SDK 10.0.402, so global.json moves too. Replace this commit with the update to the next sentry-dotnet release. --- CHANGELOG.md | 2 +- global.json | 4 ++-- src/sentry-dotnet | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a1547c24..55273d53e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ ### Features -- On Android, iOS, and macOS, C# events now report the connection type in `device.connection_type` and the cellular network generation in `device.connection_effective_type`, for example `4g` or `5g`. On Android 11 and earlier, the generation requires the `READ_PHONE_STATE` permission. ([#PLACEHOLDER](https://github.com/getsentry/sentry-unity/pull/PLACEHOLDER)) +- On Android, iOS, and macOS, C# events now report the connection type in `device.connection_type` and the cellular network generation in `device.connection_effective_type`, for example `4g` or `5g`. On Android 11 and earlier, the generation requires the `READ_PHONE_STATE` permission. ([#2889](https://github.com/getsentry/sentry-unity/pull/2889)) - Added the `NativeHandlerPath` option. It sets `handler_path` on the native SDK, so the out-of-process crash handler can be placed somewhere other than next to the player executable. Left unset, sentry-native keeps looking for the handler next to the executable as before. ([#2881](https://github.com/getsentry/sentry-unity/pull/2881)) ### Dependencies diff --git a/global.json b/global.json index 8327777f8..b10c5e90c 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "10.0.401", - "workloadVersion": "10.0.401", + "version": "10.0.402", + "workloadVersion": "10.0.402", "rollForward": "disable", "allowPrerelease": false } diff --git a/src/sentry-dotnet b/src/sentry-dotnet index 68395d269..c9ad2be49 160000 --- a/src/sentry-dotnet +++ b/src/sentry-dotnet @@ -1 +1 @@ -Subproject commit 68395d269be8b3dc375b15471ff4ca7bbfd1a93a +Subproject commit c9ad2be49332fe414c5161be8ec5545c00320d2c