Skip to content

Unity Sentry SDK programmatic setup #130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
May 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
- https://github.com/getsentry/sentry-dotnet/blob/main/CHANGELOG.md#334
- Bug fixes for performance monitoring
- Ability to keep failed envelopes for troubleshooting when they are too large
- Unity Sentry SDK programmatic setup (#130)
- SentryWindow updated

## 0.0.8

Expand Down
2 changes: 1 addition & 1 deletion samples/unity-of-bugs/Packages/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"dependencies": {
"com.unity.ide.rider": "1.1.4",
"com.unity.ide.vscode": "1.2.3",
"com.unity.test-framework": "1.1.22",
"com.unity.test-framework": "1.1.24",
"com.unity.textmeshpro": "2.1.1",
"com.unity.toolchain.macos-x86_64-linux-x86_64": "0.1.21-preview",
"com.unity.ugui": "1.0.0",
Expand Down
2 changes: 1 addition & 1 deletion samples/unity-of-bugs/Packages/packages-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"url": "https://packages.unity.com"
},
"com.unity.test-framework": {
"version": "1.1.22",
"version": "1.1.24",
"depth": 0,
"source": "registry",
"dependencies": {
Expand Down
19 changes: 13 additions & 6 deletions src/Sentry.Unity.Editor/SentryWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using UnityEditor;
using UnityEngine;

using CompressionLevel = System.IO.Compression.CompressionLevel;

namespace Sentry.Unity.Editor
{
public class SentryWindow : EditorWindow
Expand All @@ -26,7 +28,7 @@ private void OnEnable()

Options = LoadUnitySentryOptions();

TryCopyLinkXml(Options.Logger);
TryCopyLinkXml(Options.DiagnosticLogger);
}

private UnitySentryOptions LoadUnitySentryOptions()
Expand Down Expand Up @@ -105,6 +107,7 @@ private void OnGUI()
Options.Enabled = EditorGUILayout.BeginToggleGroup(
new GUIContent("Enable", "Controls enabling Sentry by initializing the SDK or not."),
Options.Enabled);

Options.CaptureInEditor = EditorGUILayout.Toggle(
new GUIContent("Capture In Editor", "Capture errors while running in the Editor."),
Options.CaptureInEditor);
Expand All @@ -116,14 +119,16 @@ private void OnGUI()
Options.Dsn);
Options.SampleRate = EditorGUILayout.Slider(
new GUIContent("Event Sample Rate", "What random sample rate to apply. 1.0 captures everything, 0.7 captures 70%."),
Options.SampleRate, 0.01f, 1);
Options.RequestBodyCompressionLevel = (SentryUnityCompression)EditorGUILayout.EnumPopup(
new GUIContent("Compress Payload", "The compression level to use on the data sent to Sentry. " +
"Some platforms don't support GZip, 'auto' attempts to disable compression in those cases."),
Options.SampleRate ?? 1.0f, 0.01f, 1);
Options.EnableAutoPayloadCompression = EditorGUILayout.Toggle(
new GUIContent("Compress Payload (Auto)", "The level of which to compress the Sentry event before sending to Sentry (Auto)."),
Options.EnableAutoPayloadCompression);
Options.RequestBodyCompressionLevel = (CompressionLevel)EditorGUILayout.EnumPopup(
new GUIContent("Compress Payload", "The level of which to compress the Sentry event before sending to Sentry."),
Options.RequestBodyCompressionLevel);
Options.AttachStacktrace = EditorGUILayout.Toggle(
new GUIContent("Stacktrace For Logs", "Whether to include a stack trace for non error events like logs. " +
"Even when Unity didn't include and no Exception was thrown.."),
"Even when Unity didn't include and no Exception was thrown.."),
Options.AttachStacktrace);
Options.Release = EditorGUILayout.TextField(
new GUIContent("Override Release", "By default release is taken from 'Application.version'. " +
Expand All @@ -134,6 +139,8 @@ private void OnGUI()
"If not set, auto detects such as 'development', 'production' or 'editor'."),
Options.Environment);

GUILayout.Label(new GUIContent(GUIContent.none), EditorStyles.boldLabel);

GUILayout.Label(new GUIContent(GUIContent.none), EditorStyles.boldLabel);
Options.Debug = EditorGUILayout.BeginToggleGroup(
new GUIContent("Debug Mode", "Whether the Sentry SDK should print its diagnostic logs to the console."),
Expand Down
7 changes: 7 additions & 0 deletions src/Sentry.Unity/EventCapture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Sentry.Unity
{
internal interface IEventCapture
{
SentryId Capture(SentryEvent sentryEvent);
}
}
47 changes: 47 additions & 0 deletions src/Sentry.Unity/Extensions/JsonExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Text.Json;

namespace Sentry.Unity.Extensions
{
internal static class JsonExtensions
{
// From Sentry.Internal.Extensions.JsonExtensions
public static JsonElement? GetPropertyOrNull(this JsonElement json, string name)
{
if (json.ValueKind != JsonValueKind.Object)
{
return null;
}

if (json.TryGetProperty(name, out var result))
{
if (json.ValueKind == JsonValueKind.Undefined ||
json.ValueKind == JsonValueKind.Null)
{
return null;
}

return result;
}

return null;
}

public static TEnum? GetEnumOrNull<TEnum>(this JsonElement json, string name)
where TEnum : struct
{
var enumString = json.GetPropertyOrNull(name)?.ToString();
if (string.IsNullOrWhiteSpace(enumString))
{
return null;
}

if (!Enum.TryParse(enumString, true, out TEnum value))
{
return null;
}

return value;
}
}
}
36 changes: 36 additions & 0 deletions src/Sentry.Unity/Integrations/IApplication.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace Sentry.Unity.Integrations
{
internal interface IApplication
{
event Application.LogCallback LogMessageReceived;
event Action Quitting;
string ActiveSceneName { get; }
}

internal sealed class ApplicationAdapter : IApplication
{
public static readonly ApplicationAdapter Instance = new();

private ApplicationAdapter()
{
Application.logMessageReceived += OnLogMessageReceived;
Application.quitting += OnQuitting;
}

public event Application.LogCallback? LogMessageReceived;

public event Action? Quitting;

public string ActiveSceneName => SceneManager.GetActiveScene().name;

private void OnLogMessageReceived(string condition, string stackTrace, LogType type)
=> LogMessageReceived?.Invoke(condition, stackTrace, type);

private void OnQuitting()
=> Quitting?.Invoke();
}
}
103 changes: 103 additions & 0 deletions src/Sentry.Unity/Integrations/UnityApplicationLoggingIntegration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using Sentry.Integrations;
using UnityEngine;

namespace Sentry.Unity.Integrations
{
internal sealed class UnityApplicationLoggingIntegration : ISdkIntegration
{
internal readonly ErrorTimeDebounce ErrorTimeDebounce = new(TimeSpan.FromSeconds(1));
internal readonly LogTimeDebounce LogTimeDebounce = new(TimeSpan.FromSeconds(1));
internal readonly WarningTimeDebounce WarningTimeDebounce = new(TimeSpan.FromSeconds(1));

// TODO: remove 'IEventCapture' in further iteration
private readonly IEventCapture? _eventCapture;
private readonly IApplication _application;

private IHub? _hub;
private SentryOptions? _sentryOptions;

public UnityApplicationLoggingIntegration(IApplication? appDomain = null, IEventCapture? eventCapture = null)
{
_application = appDomain ?? ApplicationAdapter.Instance;
_eventCapture = eventCapture;
}

public void Register(IHub hub, SentryOptions sentryOptions)
{
_hub = hub;
_sentryOptions = sentryOptions;

_application.LogMessageReceived += OnLogMessageReceived;
_application.Quitting += OnQuitting;
}

// Internal for testability
internal void OnLogMessageReceived(string condition, string stackTrace, LogType type)
{
var debounced = type switch
{
LogType.Error or LogType.Exception or LogType.Assert => ErrorTimeDebounce.Debounced(),
LogType.Log => LogTimeDebounce.Debounced(),
LogType.Warning => WarningTimeDebounce.Debounced(),
_ => true
};
if (!debounced || _hub is null)
{
return;
}

// TODO: to check against 'MinBreadcrumbLevel'
if (type != LogType.Error && type != LogType.Exception && type != LogType.Assert)
{
// TODO: MinBreadcrumbLevel
// options.MinBreadcrumbLevel
_hub.AddBreadcrumb(condition, level: ToBreadcrumbLevel(type));
return;
}

var sentryEvent = new SentryEvent(new UnityLogException(condition, stackTrace))
{
Level = ToEventTagType(type)
};

_eventCapture?.Capture(sentryEvent); // TODO: remove, for current integration tests compatibility
_hub.CaptureEvent(sentryEvent);

// So the next event includes this error as a breadcrumb:
_hub.AddBreadcrumb(condition, level: ToBreadcrumbLevel(type));
}

private void OnQuitting()
{
// Note: iOS applications are usually suspended and do not quit. You should tick "Exit on Suspend" in Player settings for iOS builds to cause the game to quit and not suspend, otherwise you may not see this call.
// If "Exit on Suspend" is not ticked then you will see calls to OnApplicationPause instead.
// Note: On Windows Store Apps and Windows Phone 8.1 there is no application quit event. Consider using OnApplicationFocus event when focusStatus equals false.
// Note: On WebGL it is not possible to implement OnApplicationQuit due to nature of the browser tabs closing.
_application.LogMessageReceived -= OnLogMessageReceived;
_hub?.FlushAsync(_sentryOptions?.ShutdownTimeout ?? TimeSpan.FromSeconds(1)).GetAwaiter().GetResult();
}

private static SentryLevel ToEventTagType(LogType logType)
=> logType switch
{
LogType.Assert => SentryLevel.Error,
LogType.Error => SentryLevel.Error,
LogType.Exception => SentryLevel.Error,
LogType.Log => SentryLevel.Info,
LogType.Warning => SentryLevel.Warning,
_ => SentryLevel.Fatal
};

private static BreadcrumbLevel ToBreadcrumbLevel(LogType logType)
=> logType switch
{
LogType.Assert => BreadcrumbLevel.Error,
LogType.Error => BreadcrumbLevel.Error,
LogType.Exception => BreadcrumbLevel.Error,
LogType.Log => BreadcrumbLevel.Info,
LogType.Warning => BreadcrumbLevel.Warning,
_ => BreadcrumbLevel.Info
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using Sentry.Integrations;

namespace Sentry.Unity.Integrations
{
internal sealed class UnityBeforeSceneLoadIntegration : ISdkIntegration
{
private readonly IApplication _application;

public UnityBeforeSceneLoadIntegration(IApplication? appDomain = null)
=> _application = appDomain ?? ApplicationAdapter.Instance;

public void Register(IHub hub, SentryOptions options)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is called by Sentry when the SDK is initialized.

THe integrations are supposed to call into Sentry whenever something happens. Like when the game changes scenes, we could add a new breadcrumb. And even change a tag. That would be an integration

{
var data = _application.ActiveSceneName is { } name
? new Dictionary<string, string> {{"scene", name}}
: null;

hub.AddBreadcrumb("BeforeSceneLoad", data: data);

options.DiagnosticLogger?.Log(SentryLevel.Debug, "Registered BeforeSceneLoad integration.");
}
}
}
Loading