Skip to content

Commit 123b270

Browse files
authored
fix: Stacktraces for Debug.LogError events (#1965)
1 parent 79491f6 commit 123b270

14 files changed

+762
-244
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- The SDK now provides stacktraces when capturing events created via `Debug.LogError`. Note, that the SDK is currently not able to provide line numbers for these events. ([#1965](https://github.com/getsentry/sentry-unity/pull/1965))
8+
59
### Fixes
610

711
- When targeting iOS and disabling native support, the SDK no longer causes builds to fail with an `Undefined symbol: _SentryNativeBridgeIsEnabled` error. ([#1983](https://github.com/getsentry/sentry-unity/pull/1983))

samples/unity-of-bugs/Assets/Resources/Sentry/SentryOptions.asset

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ MonoBehaviour:
2727
<AutoSessionTrackingInterval>k__BackingField: 30000
2828
<ReleaseOverride>k__BackingField:
2929
<EnvironmentOverride>k__BackingField:
30-
<AttachStacktrace>k__BackingField: 0
30+
<AttachStacktrace>k__BackingField: 1
3131
<AttachScreenshot>k__BackingField: 0
3232
<ScreenshotQuality>k__BackingField: 1
3333
<ScreenshotCompression>k__BackingField: 75

samples/unity-of-bugs/Assets/Scripts/BugFarmButtons.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Globalization;
23
using System.Runtime.CompilerServices;
34
using Sentry;
45
using UnityEngine;
@@ -52,14 +53,38 @@ public void ThrowNullAndCatch()
5253

5354
public void CaptureMessage() => SentrySdk.CaptureMessage("🕷️🕷️🕷️ Spider message 🕷️🕷️🕷️🕷️");
5455

56+
// IL2CPP inlines this anyway :( - so we're adding some fake work to prevent the compiler from optimizing too much
5557
[MethodImpl(MethodImplOptions.NoInlining)]
56-
private void StackTraceExampleB() => throw new InvalidOperationException("Exception from a lady beetle 🐞");
58+
private void StackTraceExampleB()
59+
{
60+
var someWork = DateTime.Now.ToString();
61+
if (someWork.Length > 0) // This condition will always be true but compiler can't be certain
62+
{
63+
throw new InvalidOperationException("Exception from a lady beetle 🐞");
64+
}
65+
}
5766

58-
// IL2CPP inlines this anyway :(
67+
// IL2CPP inlines this anyway :( - so we're adding some fake work to prevent the compiler from optimizing too much
5968
[MethodImpl(MethodImplOptions.NoInlining)]
60-
public void StackTraceExampleA() => StackTraceExampleB();
69+
public void StackTraceExampleA()
70+
{
71+
var someWork = DateTime.Now.ToString();
72+
if (someWork.Length > 0) // This condition will always be true but compiler can't be certain
73+
{
74+
StackTraceExampleB();
75+
}
76+
}
6177

62-
public void LogError() => Debug.LogError("Debug.LogError() called");
78+
// IL2CPP inlines this anyway :( - so we're adding some fake work to prevent the compiler from optimizing too much
79+
[MethodImpl(MethodImplOptions.NoInlining)]
80+
public void LogError()
81+
{
82+
var someWork = DateTime.Now.ToString();
83+
if (someWork.Length > 0) // This condition will always be true but compiler can't be certain
84+
{
85+
Debug.LogError("Debug.LogError() called");
86+
}
87+
}
6388
}
6489

6590
public class CustomException : Exception

src/Sentry.Unity/Il2CppEventProcessor.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Runtime.InteropServices;
55
using Sentry.Extensibility;
66
using Sentry.Protocol;
7+
using Sentry.Unity.Integrations;
78
using Sentry.Unity.NativeUtils;
89
using UnityEngine;
910
using Application = UnityEngine.Application;
@@ -30,6 +31,13 @@ public void Process(Exception incomingException, SentryEvent sentryEvent)
3031
{
3132
Options.DiagnosticLogger?.LogDebug("Running Unity IL2CPP event exception processor on: Event {0}", sentryEvent.EventId);
3233

34+
// UnityLogException is a synthetic exception created by the LoggingIntegration by parsing the stacktrace provided
35+
// to the SDK as a string. It therefore lacks the necessary data to fetch the native stacktrace and go from there
36+
if (incomingException is UnityErrorLogException)
37+
{
38+
return;
39+
}
40+
3341
var sentryExceptions = sentryEvent.SentryExceptions;
3442
if (sentryExceptions == null)
3543
{
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using Sentry.Integrations;
2+
using UnityEngine;
3+
4+
namespace Sentry.Unity.Integrations;
5+
6+
internal class UnityApplicationLoggingIntegration : ISdkIntegration
7+
{
8+
private readonly IApplication _application;
9+
private ErrorTimeDebounce? _errorTimeDebounce;
10+
private LogTimeDebounce? _logTimeDebounce;
11+
private WarningTimeDebounce? _warningTimeDebounce;
12+
13+
private IHub? _hub;
14+
private SentryUnityOptions? _options;
15+
16+
internal UnityApplicationLoggingIntegration(IApplication? application = null)
17+
{
18+
_application = application ?? ApplicationAdapter.Instance;
19+
}
20+
21+
public void Register(IHub hub, SentryOptions sentryOptions)
22+
{
23+
_hub = hub;
24+
_options = sentryOptions as SentryUnityOptions;
25+
if (_options is null)
26+
{
27+
return;
28+
}
29+
30+
_logTimeDebounce = new LogTimeDebounce(_options.DebounceTimeLog);
31+
_warningTimeDebounce = new WarningTimeDebounce(_options.DebounceTimeWarning);
32+
_errorTimeDebounce = new ErrorTimeDebounce(_options.DebounceTimeError);
33+
34+
_application.LogMessageReceived += OnLogMessageReceived;
35+
_application.Quitting += OnQuitting;
36+
}
37+
38+
internal void OnLogMessageReceived(string message, string stacktrace, LogType logType)
39+
{
40+
if (_hub is null)
41+
{
42+
return;
43+
}
44+
45+
// We're not capturing or creating breadcrumbs from SDK logs
46+
if (message.StartsWith(UnityLogger.LogTag))
47+
{
48+
return;
49+
}
50+
51+
// LogType.Exception are getting handled by the UnityLogHandlerIntegration
52+
if (logType is LogType.Exception)
53+
{
54+
return;
55+
}
56+
57+
if (_options?.EnableLogDebouncing is true)
58+
{
59+
var debounced = logType switch
60+
{
61+
LogType.Error or LogType.Assert => _errorTimeDebounce?.Debounced(),
62+
LogType.Log => _logTimeDebounce?.Debounced(),
63+
LogType.Warning => _warningTimeDebounce?.Debounced(),
64+
_ => true
65+
};
66+
67+
if (debounced is not true)
68+
{
69+
return;
70+
}
71+
}
72+
73+
if (logType is LogType.Error)
74+
{
75+
if (_options?.AttachStacktrace is true && !string.IsNullOrEmpty(stacktrace))
76+
{
77+
var ule = new UnityErrorLogException(message, stacktrace, _options);
78+
var evt = new SentryEvent(ule) { Level = SentryLevel.Error };
79+
80+
_hub.CaptureEvent(evt);
81+
}
82+
else
83+
{
84+
_hub.CaptureMessage(message, level: SentryLevel.Error);
85+
}
86+
}
87+
88+
// Capture so the next event includes this error as breadcrumb
89+
if (_options?.AddBreadcrumbsForLogType[logType] is true)
90+
{
91+
_hub.AddBreadcrumb(message: message, category: "unity.logger", level: ToBreadcrumbLevel(logType));
92+
}
93+
}
94+
95+
private void OnQuitting()
96+
{
97+
_application.LogMessageReceived -= OnLogMessageReceived;
98+
}
99+
100+
private static BreadcrumbLevel ToBreadcrumbLevel(LogType logType)
101+
=> logType switch
102+
{
103+
LogType.Assert => BreadcrumbLevel.Error,
104+
LogType.Error => BreadcrumbLevel.Error,
105+
LogType.Exception => BreadcrumbLevel.Error,
106+
LogType.Log => BreadcrumbLevel.Info,
107+
LogType.Warning => BreadcrumbLevel.Warning,
108+
_ => BreadcrumbLevel.Info
109+
};
110+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using Sentry.Protocol;
5+
using UnityEngine;
6+
7+
namespace Sentry.Unity.Integrations
8+
{
9+
/// <summary>
10+
/// An exception raised through the Application Logging Integration
11+
/// </summary>
12+
/// <remarks>
13+
/// <see cref="Application.logMessageReceived"/>
14+
/// </remarks>
15+
internal class UnityErrorLogException : Exception
16+
{
17+
internal static readonly string ExceptionType = "LogError";
18+
19+
private readonly string _logString = string.Empty;
20+
private readonly string _logStackTrace = string.Empty;
21+
22+
private readonly SentryOptions? _options;
23+
24+
public UnityErrorLogException(string logString, string logStackTrace, SentryOptions? options)
25+
{
26+
_logString = logString;
27+
_logStackTrace = logStackTrace;
28+
_options = options;
29+
}
30+
31+
internal UnityErrorLogException(string logString, string logStackTrace)
32+
{
33+
_logString = logString;
34+
_logStackTrace = logStackTrace;
35+
}
36+
37+
internal UnityErrorLogException() : base() { }
38+
39+
private UnityErrorLogException(string message) : base(message) { }
40+
41+
private UnityErrorLogException(string message, Exception innerException) : base(message, innerException) { }
42+
43+
public SentryException ToSentryException()
44+
{
45+
var frames = ParseStackTrace(_logStackTrace);
46+
frames.Reverse();
47+
48+
var stacktrace = new SentryStackTrace { Frames = frames };
49+
50+
return new SentryException
51+
{
52+
Stacktrace = stacktrace,
53+
Type = ExceptionType,
54+
Value = _logString,
55+
Mechanism = new Mechanism
56+
{
57+
Handled = true,
58+
Type = "unity.log"
59+
}
60+
};
61+
}
62+
63+
private const string AtFileMarker = " (at ";
64+
65+
private List<SentryStackFrame> ParseStackTrace(string stackTrace)
66+
{
67+
// Example: Sentry.Unity.Integrations.UnityLogHandlerIntegration:LogFormat (UnityEngine.LogType,UnityEngine.Object,string,object[]) (at UnityLogHandlerIntegration.cs:89)
68+
// This follows the following format:
69+
// Module.Class.Method[.Invoke] (arguments) (at filepath:linenumber)
70+
// The ':linenumber' is optional and will be omitted in builds
71+
72+
var frames = new List<SentryStackFrame>();
73+
var stackList = stackTrace.Split('\n');
74+
75+
foreach (var line in stackList)
76+
{
77+
var item = line.TrimEnd('\r');
78+
if (string.IsNullOrEmpty(item))
79+
{
80+
continue;
81+
}
82+
83+
var frame = ParseStackFrame(item);
84+
if (_options is not null)
85+
{
86+
frame.ConfigureAppFrame(_options);
87+
}
88+
frames.Add(frame);
89+
}
90+
91+
return frames;
92+
}
93+
94+
private static SentryStackFrame ParseStackFrame(string stackFrameLine)
95+
{
96+
var closingParenthesis = stackFrameLine.IndexOf(')');
97+
if (closingParenthesis == -1)
98+
{
99+
return CreateBasicStackFrame(stackFrameLine);
100+
}
101+
102+
try
103+
{
104+
var functionName = stackFrameLine.Substring(0, closingParenthesis + 1);
105+
var remainingText = stackFrameLine.Substring(closingParenthesis + 1);
106+
107+
if (!remainingText.StartsWith(AtFileMarker))
108+
{
109+
// If it does not start with '(at' it's an unknown format. We're falling back to a basic stackframe
110+
return CreateBasicStackFrame(stackFrameLine);
111+
}
112+
113+
var (filename, lineNo) = ParseFileLocation(remainingText);
114+
var filenameWithoutZeroes = StripZeroes(filename);
115+
116+
return new SentryStackFrame
117+
{
118+
FileName = TryResolveFileNameForMono(filenameWithoutZeroes),
119+
AbsolutePath = filenameWithoutZeroes,
120+
Function = functionName,
121+
LineNumber = lineNo == -1 ? null : lineNo
122+
};
123+
}
124+
catch (Exception)
125+
{
126+
// Suppress any errors while parsing and fall back to a basic stackframe
127+
return CreateBasicStackFrame(stackFrameLine);
128+
}
129+
}
130+
131+
private static (string Filename, int LineNo) ParseFileLocation(string location)
132+
{
133+
// Remove " (at " prefix and trailing ")"
134+
var fileInfo = location.Substring(AtFileMarker.Length, location.Length - AtFileMarker.Length - 1);
135+
var lastColon = fileInfo.LastIndexOf(':');
136+
137+
return lastColon == -1
138+
? (fileInfo, -1)
139+
: (fileInfo.Substring(0, lastColon), int.Parse(fileInfo.Substring(lastColon + 1)));
140+
}
141+
142+
private static SentryStackFrame CreateBasicStackFrame(string functionName) => new()
143+
{
144+
Function = functionName,
145+
FileName = null,
146+
AbsolutePath = null,
147+
LineNumber = null
148+
};
149+
150+
// https://github.com/getsentry/sentry-unity/issues/103
151+
private static string StripZeroes(string filename)
152+
=> filename.Replace("0", "").Equals("<>", StringComparison.OrdinalIgnoreCase)
153+
? string.Empty
154+
: filename;
155+
156+
private static string TryResolveFileNameForMono(string fileName)
157+
{
158+
try
159+
{
160+
// throws on Mono for <1231231231> paths
161+
return Path.GetFileName(fileName);
162+
}
163+
catch
164+
{
165+
// mono path
166+
return "Unknown";
167+
}
168+
}
169+
}
170+
}

0 commit comments

Comments
 (0)