Skip to content

Commit

Permalink
Feat: Automatic user IDs on native crashes & .NET events (#728)
Browse files Browse the repository at this point in the history
* feat: allow setting default user ID from native integrations

* feat: Synchronize user-ID from android to dotnet

* feat: Synchronize user-ID from Unity to windows native & dotnet

* feat: Synchronize user-ID from Unity to dotnet on WebGL

* test: SmokeTester checks for user ID in dotnet events

* feat: Synchronize user-ID from native to dotnet on iOS/macOS

* chore: improve RunUnity script log output

* chore: improve WebGL test script

* refactor: DefaultUserId setting logging
  • Loading branch information
vaind authored May 16, 2022
1 parent 279acb4 commit 1db2fa4
Show file tree
Hide file tree
Showing 18 changed files with 185 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Linux native crash support ([#734](https://github.com/getsentry/sentry-unity/pull/734))
- Collect context information synchronously during init to capture it for very early events ([#744](https://github.com/getsentry/sentry-unity/pull/744))
- Automatic user IDs on native crashes & .NET events ([#728](https://github.com/getsentry/sentry-unity/pull/728))

## 0.16.0

Expand Down
12 changes: 12 additions & 0 deletions package-dev/Plugins/iOS/SentryNativeBridge.m
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#import <Sentry/PrivateSentrySDKOnly.h>
#import <Sentry/Sentry.h>

NS_ASSUME_NONNULL_BEGIN
Expand Down Expand Up @@ -121,4 +122,15 @@ void SentryNativeBridgeUnsetUser()
[SentrySDK configureScope:^(SentryScope *scope) { [scope setUser:nil]; }];
}

char *SentryNativeBridgeGetInstallationId()
{
// Create a null terminated C string on the heap as expected by marshalling.
// See Tips for iOS in https://docs.unity3d.com/Manual/PluginsForIOS.html
const char *nsStringUtf8 = [[PrivateSentrySDKOnly installationID] UTF8String];
size_t len = strlen(nsStringUtf8) + 1;
char *cString = (char *)malloc(len);
memcpy(cString, nsStringUtf8, len);
return cString;
}

NS_ASSUME_NONNULL_END
14 changes: 14 additions & 0 deletions package-dev/Plugins/macOS/SentryNativeBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
static Class SentryScope;
static Class SentryBreadcrumb;
static Class SentryUser;
static Class PrivateSentrySDKOnly;

#define LOAD_CLASS_OR_BREAK(name) \
name = (__bridge Class)dlsym(dylib, "OBJC_CLASS_$_" #name); \
Expand All @@ -32,6 +33,7 @@ int SentryNativeBridgeLoadLibrary()
LOAD_CLASS_OR_BREAK(SentryScope)
LOAD_CLASS_OR_BREAK(SentryBreadcrumb)
LOAD_CLASS_OR_BREAK(SentryUser)
LOAD_CLASS_OR_BREAK(PrivateSentrySDKOnly)

// everything above passed - mark as successfully loaded
loadStatus = 1;
Expand Down Expand Up @@ -223,3 +225,15 @@ void SentryNativeBridgeUnsetUser()
SentryConfigureScope(
^(id scope) { [scope performSelector:@selector(setUser:) withObject:nil]; });
}

char *SentryNativeBridgeGetInstallationId()
{
// Create a null terminated C string on the heap as expected by marshalling.
// See Tips for iOS in https://docs.unity3d.com/Manual/PluginsForIOS.html
const char *nsStringUtf8 =
[[PrivateSentrySDKOnly performSelector:@selector(installationID)] UTF8String];
size_t len = strlen(nsStringUtf8) + 1;
char *cString = (char *)malloc(len);
memcpy(cString, nsStringUtf8, len);
return cString;
}
2 changes: 1 addition & 1 deletion package-dev/Runtime/SentryInitialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static void Init()
try
{
#if SENTRY_NATIVE_COCOA
SentryNativeCocoa.Configure(options);
SentryNativeCocoa.Configure(options, sentryUnityInfo);
#elif SENTRY_NATIVE_ANDROID
SentryNativeAndroid.Configure(options, sentryUnityInfo);
#elif SENTRY_NATIVE
Expand Down
1 change: 1 addition & 0 deletions samples/unity-of-bugs/Assets/Scripts/SmokeTester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public static void SmokeTest()

t.ExpectMessage(currentMessage, "'type':'event'");
t.ExpectMessage(currentMessage, $"LogError(GUID)={guid}");
t.ExpectMessage(currentMessage, "'user':{'id':'"); // non-null automatic ID
t.ExpectMessage(currentMessage, "'filename':'screenshot.jpg','attachment_type':'event.attachment'");
t.ExpectMessageNot(currentMessage, "'length':0");

Expand Down
4 changes: 4 additions & 0 deletions scripts/smoke-test-webgl.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ def Expect(self, message, result):
raise Exception(info)

def CheckMessage(self, index, substring, negate):
if len(self.__requests) <= index:
raise Exception('HTTP Request #{} not captured.'.format(index))

message = self.__requests[index]["body"]
contains = substring in message or substring.replace(
"'", "\"") in message
Expand Down Expand Up @@ -183,6 +186,7 @@ def waitUntil(condition, interval=0.1, timeout=1):
currentMessage += 1
t.ExpectMessage(currentMessage, "'type':'event'")
t.ExpectMessage(currentMessage, "LogError(GUID)")
t.ExpectMessage(currentMessage, "'user':{'id':'")
# t.ExpectMessage(
# currentMessage, "'filename':'screenshot.jpg','attachment_type':'event.attachment'")
# t.ExpectMessageNot(currentMessage, "'length':0")
Expand Down
4 changes: 3 additions & 1 deletion scripts/unity-utils.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,13 @@ function RunUnity([string] $unityPath, [string[]] $arguments, [switch] $ReturnLo
break
}
} while ($stopwatch.Elapsed.TotalSeconds -lt $RunUnityLicenseRetryTimeoutSeconds)

if ($process.ExitCode -ne 0)
{
Throw "Unity exited with code $($process.ExitCode)"
}

Write-Host -ForegroundColor Green "Unity finished successfully. Time taken: $($stopwatch.Elapsed.ToString('hh\:mm\:ss\.fff'))"
return $ReturnLogOutput ? $stdout : $null
}

Expand Down
37 changes: 30 additions & 7 deletions src/Sentry.Unity.Android/SentryJava.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,28 @@
namespace Sentry.Unity.Android
{
/// <summary>
/// P/Invoke to `sentry-java` methods.
/// JNI access to `sentry-java` methods.
/// </summary>
/// <remarks>
/// The `sentry-java` SDK on Android is brought in through the `sentry-android-core`
/// and `sentry-java` maven packages.
/// </remarks>
/// <see href="https://github.com/getsentry/sentry-java"/>
public static class SentryJava
internal static class SentryJava
{
internal static string? GetInstallationId()
{
if (!Attach())
{
return null;
}

using var sentry = GetSentryJava();
using var hub = sentry.CallStatic<AndroidJavaObject>("getCurrentHub");
using var options = hub?.Call<AndroidJavaObject>("getOptions");
return options?.Call<string>("getDistinctId");
}

/// <summary>
/// Returns whether or not the last run resulted in a crash.
/// </summary>
Expand All @@ -24,15 +37,25 @@ public static class SentryJava
/// </returns>
public static bool? CrashedLastRun()
{
using var jo = new AndroidJavaObject("io.sentry.Sentry");
return jo.CallStatic<AndroidJavaObject>("isCrashedLastRun")
?.Call<bool>("booleanValue");
if (!Attach())
{
return null;
}
using var sentry = GetSentryJava();
using var jo = sentry.CallStatic<AndroidJavaObject>("isCrashedLastRun");
return jo?.Call<bool>("booleanValue");
}

public static void Close()
{
using var jo = new AndroidJavaObject("io.sentry.Sentry");
jo.CallStatic("close");
if (Attach())
{
using var sentry = GetSentryJava();
sentry.CallStatic("close");
}
}

private static bool Attach() => AndroidJNI.AttachCurrentThread() == 0;
private static AndroidJavaObject GetSentryJava() => new AndroidJavaClass("io.sentry.Sentry");
}
}
2 changes: 2 additions & 0 deletions src/Sentry.Unity.Android/SentryNativeAndroid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public static void Configure(SentryUnityOptions options, ISentryUnityInfo sentry
options.DiagnosticLogger?.LogDebug("Closing the sentry-java SDK");
SentryJava.Close();
};

options.DefaultUserId = SentryJava.GetInstallationId();
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/Sentry.Unity.Native/SentryNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Sentry.Unity.Integrations;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Analytics;

namespace Sentry.Unity.Native
{
Expand Down Expand Up @@ -40,6 +41,13 @@ public static void Configure(SentryUnityOptions options)
options.ScopeObserver = new NativeScopeObserver(options);
options.EnableScopeSync = true;

// Use AnalyticsSessionInfo.userId as the default UserID in native & dotnet
options.DefaultUserId = AnalyticsSessionInfo.userId;
if (options.DefaultUserId is not null)
{
options.ScopeObserver.SetUser(new User { Id = options.DefaultUserId });
}

// Note: we must actually call the function now and on every other call use the value we get here.
// Additionally, we cannot call this multiple times for the same directory, because the result changes
// on subsequent runs. Therefore, we cache the value during the whole runtime of the application.
Expand Down
3 changes: 3 additions & 0 deletions src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,8 @@ public static bool Init(SentryUnityOptions options)

[DllImport("__Internal")]
public static extern void SentryNativeBridgeUnsetUser();

[DllImport("__Internal", EntryPoint = "SentryNativeBridgeGetInstallationId")]
public static extern string GetInstallationId();
}
}
9 changes: 7 additions & 2 deletions src/Sentry.Unity.iOS/SentryNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ public static class SentryNativeCocoa
/// Configures the native support.
/// </summary>
/// <param name="options">The Sentry Unity options to use.</param>
public static void Configure(SentryUnityOptions options) => Configure(options, ApplicationAdapter.Instance.Platform);
public static void Configure(SentryUnityOptions options, ISentryUnityInfo sentryUnityInfo) =>
Configure(options, sentryUnityInfo, ApplicationAdapter.Instance.Platform);

internal static void Configure(SentryUnityOptions options, RuntimePlatform platform)
internal static void Configure(SentryUnityOptions options, ISentryUnityInfo sentryUnityInfo, RuntimePlatform platform)
{
switch (platform)
{
Expand Down Expand Up @@ -58,6 +59,10 @@ internal static void Configure(SentryUnityOptions options, RuntimePlatform platf
options.DiagnosticLogger?.LogDebug("Closing the sentry-cocoa SDK");
SentryCocoaBridgeProxy.Close();
};
if (sentryUnityInfo.IL2CPP)
{
options.DefaultUserId = SentryCocoaBridgeProxy.GetInstallationId();
}
}
}
}
5 changes: 5 additions & 0 deletions src/Sentry.Unity/Sentry.Unity.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,9 @@
Overwrite="true"/>
</Target>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>Sentry.Unity.Native</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>
21 changes: 21 additions & 0 deletions src/Sentry.Unity/SentryUnityOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Linq;
using System.Text.RegularExpressions;
using Sentry.Unity.Integrations;
using Sentry.Extensibility;
using CompressionLevel = System.IO.Compression.CompressionLevel;

namespace Sentry.Unity
Expand Down Expand Up @@ -111,6 +112,26 @@ public sealed class SentryUnityOptions : SentryOptions
/// </summary>
public bool LinuxNativeSupportEnabled { get; set; } = true;


// Initialized by native SDK binding code to set the User.ID in .NET (UnityEventProcessor).
internal string? _defaultUserId;
internal string? DefaultUserId
{
get => _defaultUserId;
set
{
_defaultUserId = value;
if (_defaultUserId is null)
{
DiagnosticLogger?.LogWarning("Couldn't set the default user ID - the value is NULL.");
}
else
{
DiagnosticLogger?.LogDebug("Setting '{0}' as the default user ID.", _defaultUserId);
}
}
}

public SentryUnityOptions() : this(ApplicationAdapter.Instance, false)
{
}
Expand Down
16 changes: 14 additions & 2 deletions src/Sentry.Unity/UnityEventProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ internal static class UnitySdkInfo

internal class UnityEventProcessor : ISentryEventProcessor
{
private readonly SentryOptions _sentryOptions;
private readonly SentryUnityOptions _sentryOptions;
private readonly MainThreadData _mainThreadData;
private readonly IApplication _application;


public UnityEventProcessor(SentryOptions sentryOptions, SentryMonoBehaviour sentryMonoBehaviour, IApplication? application = null)
public UnityEventProcessor(SentryUnityOptions sentryOptions, SentryMonoBehaviour sentryMonoBehaviour, IApplication? application = null)
{
_sentryOptions = sentryOptions;
_mainThreadData = sentryMonoBehaviour.MainThreadData;
Expand All @@ -42,6 +42,7 @@ public SentryEvent Process(SentryEvent @event)
PopulateGpu(@event.Contexts.Gpu);
PopulateUnity((Protocol.Unity)@event.Contexts.GetOrAdd(Protocol.Unity.Type, _ => new Protocol.Unity()));
PopulateTags(@event);
PopulateUser(@event);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -211,6 +212,17 @@ private void PopulateTags(SentryEvent @event)
@event.SetTag("unity.is_main_thread", _mainThreadData.IsMainThread().ToTagValue());
}

private void PopulateUser(SentryEvent @event)
{
if (_sentryOptions.DefaultUserId is not null)
{
if (@event.User.Id is null)
{
@event.User.Id = _sentryOptions.DefaultUserId;
}
}
}

/// <summary>
/// - If UI thread, extract the value (can be null)
/// - If non-UI thread, check if value is created, then extract
Expand Down
4 changes: 4 additions & 0 deletions src/Sentry.Unity/WebGL/SentryWebGL.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Sentry.Extensibility;
using UnityEngine.Analytics;

namespace Sentry.Unity.WebGL
{
Expand Down Expand Up @@ -36,6 +37,9 @@ public static void Configure(SentryUnityOptions options)
options.DiagnosticLogger?.LogWarning("Attaching screenshots on WebGL is disabled - " +
"it currently produces blank screenshots mid-frame.");
}

// Use AnalyticsSessionInfo.userId as the default UserID in native & dotnet
options.DefaultUserId = AnalyticsSessionInfo.userId;
}
}
}
Loading

0 comments on commit 1db2fa4

Please sign in to comment.