Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions package-dev/Plugins/Android/proguard-sentry-unity.pro
Original file line number Diff line number Diff line change
Expand Up @@ -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.**
13 changes: 13 additions & 0 deletions package-dev/Plugins/iOS/SentryNativeBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
10 changes: 10 additions & 0 deletions package-dev/Plugins/iOS/SentryNativeBridgeExternal.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
6 changes: 6 additions & 0 deletions package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
13 changes: 13 additions & 0 deletions package-dev/Plugins/macOS/SentryNativeBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
26 changes: 26 additions & 0 deletions src/Sentry.Unity.Android/SentryJava.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<AndroidJavaObject>("getCurrentHub");
using var options = hub?.Call<AndroidJavaObject>("getOptions");
using var provider = options?.Call<AndroidJavaObject>("getConnectionStatusProvider");
using var connection = provider?.Call<AndroidJavaObject>("getConnection");
return (connection?.Get<string>("type"), connection?.Get<string>("effectiveType"));
}
catch (Exception e)
{
_logger?.LogError(e, "Calling 'SentryJava.GetConnection' failed.");
}

return (null, null);
}

public void WriteScope(
string? AppStartTime,
string? AppBuildType,
Expand Down
1 change: 1 addition & 0 deletions src/Sentry.Unity.Android/SentryNativeAndroid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
9 changes: 9 additions & 0 deletions src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
1 change: 1 addition & 0 deletions src/Sentry.Unity.iOS/SentryNativeCocoa.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () =>
{
Expand Down
5 changes: 5 additions & 0 deletions src/Sentry.Unity/SentryUnityOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,11 @@ internal void UseAppReleaseAndEnvironment(Func<string?> getRelease, Func<string?
/// </summary>
internal INativeDebugImageProvider? NativeDebugImageProvider { get; set; } = null;

/// <summary>
/// Reads the connection type and the cellular network generation from the native SDK. Called on the main thread.
/// </summary>
internal Func<(string? Type, string? EffectiveType)>? NativeConnection { get; set; } = null;

/// <summary>
/// Used to close down the native SDK
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/Sentry.Unity/UnityEventProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions test/Sentry.Unity.Android.Tests/SentryNativeAndroidTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
3 changes: 3 additions & 0 deletions test/Sentry.Unity.Android.Tests/TestSentryJava.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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(
Expand Down
43 changes: 43 additions & 0 deletions test/Sentry.Unity.Tests/UnityEventScopeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
1 change: 1 addition & 0 deletions test/Sentry.Unity.iOS.Tests/SentryNativeIosTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading