diff --git a/CHANGELOG.md b/CHANGELOG.md index ee836469d..45040a5d0 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 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/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/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 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); }