diff --git a/CHANGELOG.md b/CHANGELOG.md index a65759a2a..a0eafe14a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Added experimental support for Unity as a Library on Android and iOS. With `AndroidNativeInitializationType` or `IosNativeInitializationType` set to `NativeInitializationType.External`, the game attaches to the sentry-android or sentry-cocoa SDK that the app already started instead of bundling and starting its own. On Android, when the game runs in its own process, it starts sentry-android there and continues the app's trace. Native crashes carry the game's context and breadcrumbs, C# events and logs use the app's release, environment, user, and trace, the game's transactions join the app's trace, and the game's debug symbols are still uploaded automatically. ([#2885](https://github.com/getsentry/sentry-unity/pull/2885), [#2895](https://github.com/getsentry/sentry-unity/pull/2895)) - The SDK now supports Linux ARM64 dedicated servers (Unity 6000.3 and newer, IL2CPP). The native SDK for both backends ships for `x86_64` and `ARM64`, and the build picks the one matching the player. ([#2894](https://github.com/getsentry/sentry-unity/pull/2894)) +- 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)) ### Dependencies diff --git a/package-dev/Plugins/Android/proguard-sentry-unity.pro b/package-dev/Plugins/Android/proguard-sentry-unity.pro index 9567b3b69..38aa01486 100644 --- a/package-dev/Plugins/Android/proguard-sentry-unity.pro +++ b/package-dev/Plugins/Android/proguard-sentry-unity.pro @@ -18,4 +18,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 fefb76eee..a28ff3e53 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"]); +} + // Only the app's own SDK provides these, see SentryNativeBridgeExternal.m. char *_Nullable SentryNativeBridgeGetOption(const char *name) { return NULL; } diff --git a/package-dev/Plugins/iOS/SentryNativeBridgeExternal.m b/package-dev/Plugins/iOS/SentryNativeBridgeExternal.m index e6d0e1bd6..bfdd57e4e 100644 --- a/package-dev/Plugins/iOS/SentryNativeBridgeExternal.m +++ b/package-dev/Plugins/iOS/SentryNativeBridgeExternal.m @@ -283,6 +283,16 @@ static id SentryAppOptions(void) return SentryCopyString(SentryCallWith(installation, @selector(idWithCacheDirectoryPath:), cacheDirectoryPath)); } +// The connection the app's SDK reports in its device context. +void SentryNativeBridgeGetConnection(char **type, char **effectiveType) +{ + id container = SentryCall(NSClassFromString(@"Sentry.SentryDependencyContainer"), @selector(sharedInstance)); + NSDictionary *extraContext = SentryCall(SentryCall(container, @selector(extraContextProvider)), @selector(getExtraContext)); + NSDictionary *device = [extraContext isKindOfClass:[NSDictionary class]] ? extraContext[@"device"] : nil; + *type = SentryCopyString(device[@"connection_type"]); + *effectiveType = SentryCopyString(device[@"connection_effective_type"]); +} + // A string option of the app's SDK, e.g. `releaseName`, `environment`, or `dist`. char *SentryNativeBridgeGetOption(const char *name) { diff --git a/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m b/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m index 21dda35de..8a12cd8e4 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; +} + char *_Nullable SentryNativeBridgeGetOption(const char *name) { return NULL; } char *_Nullable SentryNativeBridgeGetTraceHeader() { return NULL; } diff --git a/package-dev/Plugins/macOS/SentryNativeBridge.m b/package-dev/Plugins/macOS/SentryNativeBridge.m index 3d3361e38..d997be670 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"]); +} + // External initialization is iOS only. char *SentryNativeBridgeGetOption(const char *name) { return NULL; } diff --git a/src/Sentry.Unity.Android/SentryJava.cs b/src/Sentry.Unity.Android/SentryJava.cs index 2afdfb24d..1fc5c862e 100644 --- a/src/Sentry.Unity.Android/SentryJava.cs +++ b/src/Sentry.Unity.Android/SentryJava.cs @@ -21,6 +21,7 @@ internal interface ISentryJava public bool TryGetMainProcessUser(out SentryUser? user); public string? GetInstallationId(); public bool? CrashedLastRun(); + public (string? Type, string? EffectiveType) GetConnection(); public void Close(); public void WriteScope( string? AppStartTime, @@ -486,6 +487,31 @@ public bool TryGetMainProcessUser(out SentryUser? user) 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 70e736fc2..ff25c9751 100644 --- a/src/Sentry.Unity.Android/SentryNativeAndroid.cs +++ b/src/Sentry.Unity.Android/SentryNativeAndroid.cs @@ -95,6 +95,7 @@ public static void Configure(SentryUnityOptions options) : new AndroidJavaScopeObserver(options, SentryJava); options.EnableScopeSync = true; options.NativeDebugImageProvider = new Native.NativeDebugImageProvider(); + options.NativeConnection = SentryJava.GetConnection; if (isExternal) { diff --git a/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs b/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs index 1a1b9b902..dced1a8ab 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 = "SentryNativeBridgeGetOption")] public static extern string? GetOption(string name); diff --git a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs index fc4f33a5c..ea913e04d 100644 --- a/src/Sentry.Unity.iOS/SentryNativeCocoa.cs +++ b/src/Sentry.Unity.iOS/SentryNativeCocoa.cs @@ -95,6 +95,7 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf } 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 10ecce1cf..667f02300 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -525,6 +525,11 @@ internal void UseAppReleaseAndEnvironment(Func getRelease, Func 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 7cdbc94a9..00524c5aa 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 32c2d6ba4..aca49333a 100644 --- a/test/Sentry.Unity.Android.Tests/TestSentryJava.cs +++ b/test/Sentry.Unity.Android.Tests/TestSentryJava.cs @@ -11,6 +11,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? MainProcess { get; set; } = true; public bool InitCalled { get; private set; } public bool CloseCalled { get; private set; } @@ -53,6 +54,8 @@ public bool TryGetMainProcessUser(out SentryUser? user) public bool? CrashedLastRun() => IsCrashedLastRun; + public (string? Type, string? EffectiveType) GetConnection() => Connection; + public void Close() => CloseCalled = true; 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); }