-
-
Notifications
You must be signed in to change notification settings - Fork 50
/
UnityLogHandlerIntegration.cs
188 lines (160 loc) · 7.72 KB
/
UnityLogHandlerIntegration.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
using System;
using Sentry.Extensibility;
using Sentry.Integrations;
using Sentry.Protocol;
using UnityEngine;
namespace Sentry.Unity.Integrations
{
internal sealed class UnityLogHandlerIntegration : ISdkIntegration, ILogHandler
{
internal readonly ErrorTimeDebounce ErrorTimeDebounce;
internal readonly LogTimeDebounce LogTimeDebounce;
internal readonly WarningTimeDebounce WarningTimeDebounce;
private readonly IApplication _application;
private IHub? _hub;
private SentryUnityOptions? _sentryOptions;
private ILogHandler _unityLogHandler = null!; // Set during register
public UnityLogHandlerIntegration(SentryUnityOptions options, IApplication? application = null)
{
_application = application ?? ApplicationAdapter.Instance;
LogTimeDebounce = new LogTimeDebounce(options.DebounceTimeLog);
WarningTimeDebounce = new WarningTimeDebounce(options.DebounceTimeWarning);
ErrorTimeDebounce = new ErrorTimeDebounce(options.DebounceTimeError);
}
public void Register(IHub hub, SentryOptions sentryOptions)
{
_hub = hub;
_sentryOptions = sentryOptions as SentryUnityOptions;
if (_sentryOptions is null)
{
return;
}
// If called twice (i.e. init with the same options object) the integration will reference itself as the
// original handler loghandler and endlessly forward to itself
if (Debug.unityLogger.logHandler == this)
{
_sentryOptions.DiagnosticLogger?.LogWarning("UnityLogHandlerIntegration has already been registered.");
return;
}
_unityLogHandler = Debug.unityLogger.logHandler;
Debug.unityLogger.logHandler = this;
_application.Quitting += OnQuitting;
}
public void LogException(Exception exception, UnityEngine.Object context)
{
try
{
CaptureException(exception, context);
}
finally
{
// Always pass the exception back to Unity
_unityLogHandler.LogException(exception, context);
}
}
internal void CaptureException(Exception exception, UnityEngine.Object? context)
{
if (_hub?.IsEnabled is not true)
{
return;
}
// TODO: Capture the context (i.e. grab the name if != null and set it as context)
// NOTE: This might not be entirely true, as a user could as well call `Debug.LogException`
// and expect a handled exception but it is not possible for us to differentiate
// https://docs.sentry.io/platforms/unity/troubleshooting/#unhandled-exceptions---debuglogexception
exception.Data[Mechanism.HandledKey] = false;
exception.Data[Mechanism.MechanismKey] = "Unity.LogException";
_ = _hub.CaptureException(exception);
if (_sentryOptions?.AddBreadcrumbsForLogType[LogType.Exception] is true)
{
// So the next event includes this error as a breadcrumb
_hub.AddBreadcrumb(message: $"{exception.GetType()}: {exception.Message}", category: "unity.logger", level: BreadcrumbLevel.Error);
}
}
public void LogFormat(LogType logType, UnityEngine.Object? context, string format, params object[] args)
{
try
{
CaptureLogFormat(logType, context, format, args);
}
finally
{
// Always pass the log back to Unity
_unityLogHandler.LogFormat(logType, context, format, args);
}
}
internal void CaptureLogFormat(LogType logType, UnityEngine.Object? context, string format, params object[] args)
{
if (_hub?.IsEnabled is not true)
{
return;
}
// The SDK sets "Sentry" as tag when logging and we're not capturing SDK internal logs. Expected format: "{0}: {1}"
if (args.Length > 1 && "Sentry".Equals(args[0])) // Checking it this way around because `args[0]` could be null
{
return;
}
if (_sentryOptions?.EnableLogDebouncing is true)
{
var debounced = logType switch
{
LogType.Error or LogType.Exception or LogType.Assert => ErrorTimeDebounce.Debounced(),
LogType.Log => LogTimeDebounce.Debounced(),
LogType.Warning => WarningTimeDebounce.Debounced(),
_ => true
};
if (!debounced)
{
return;
}
}
var logMessage = args.Length == 0 ? format : string.Format(format, args);
if (logType is LogType.Error or LogType.Assert)
{
// TODO: Capture the context (i.e. grab the name if != null and set it as context)
_hub.CaptureMessage(logMessage, ToEventTagType(logType));
}
if (_sentryOptions?.AddBreadcrumbsForLogType[logType] is true)
{
// So the next event includes this as a breadcrumb
_hub.AddBreadcrumb(message: logMessage, category: "unity.logger", level: ToBreadcrumbLevel(logType));
}
}
private void OnQuitting()
{
_sentryOptions?.DiagnosticLogger?.LogInfo("OnQuitting was invoked. Unhooking log callback and pausing session.");
// 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.
// 'OnQuitting' is invoked even when an uncaught exception happens in the ART. To make sure the .NET
// SDK checks with the native layer on restart if the previous run crashed (through the CrashedLastRun callback)
// we'll just pause sessions on shutdown. On restart they can be closed with the right timestamp and as 'exited'.
if (_sentryOptions?.AutoSessionTracking is true)
{
_hub?.PauseSession();
}
_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
};
}
}