Skip to content

Commit 23e43b7

Browse files
authored
Improve Unity WebGL handling for stackless Unity log-callback exception reports. (#282)
* Add capture-mode enum for Unity exception log-handler * Add fallback enum for optional WebGL JS stack capture * Add BacktraceUnityLogCapture helper Add helper for Unity log-callback diagnostics and stackless classification * Add BacktraceUnityLogHandler wrapper ILogHandler that intercepts Debug.LogException * Add config fields for log handler capture and WebGL JS stack fallback * Add BacktraceWebGLJavaScriptStack WebGL JS stack helper * Add Addressables integration options * Add optional Addressables integration * Serialize custom report annotations * Add AddAnnotation API and preserve source code on reports without frames * Update SetThreadInformations to stop synthesizing faulting thread when no managed frames exist * Update BacktraceSync.jslib Add BT_CaptureJavaScriptStack jslib export for stack-at-capture * Update BacktraceClient.cs Capture original Debug.LogException Classify stackless logs Lock background queue * Add tests for stackless log diagnostics and custom annotations * Remove Addressables integration * Add internal no-environment fallback * Add internal CreateFromUnityLogCallback factory * Update BacktraceReport.cs Make AddAnnotation internal Remove chaining Add no-fallback factory * Update Annotations.cs Remove chaining in custom annotation overload * Replace with stack source aware attribute/annotation helpers * Create BacktraceUnityLogExceptionCandidate.cs Add candidate model for original exception capture * Create BacktraceUnityLogExceptionCandidateStore.cs Add candidate store with prefix matching * Create BacktraceUnityLogReportFactory.cs Add factory that picks stack source per Unity log callback * Record original exception only and remove direct send * Update BacktraceClient.cs Wire candidate store and report factory Drop suppression queue * Rewrite for stack-source attributes and no-fallback factory * Create BacktraceUnityLogReportFactoryTests.cs Add factory tests for stack-source * add created classes metadata * Update Tests * Make log-handler and callback capture path dynamic * Add callback capture path to candidate report builders * Update SourceCodeFlowWithLogManagerTests.cs Update source-code flow tests for stackless and frame bearing paths * Update BacktraceUnityLogCapture.cs Restrict candidate prefixes to type+message Fall back to type only when empty * Set backtrace.unity.stack_source on Unity-callback-only reports * Update prefix tests for better matching and empty-message fallback * Add regression test for same type & different message candidate mismatch * Assert stack_source on Unity-callback only paths * Update BacktraceClient.cs Filter capture-path by prefix and fall back to log type * Clear stale stackless reason when frames are recovered Update Test case to assert reports with recovered frames drop stackless reason * Keep original exception path when its stack is unparseable and Unity stack is empty Update Test case to cover original exception unparsed-stack diagnostic * Add candidate thread id to CreateOriginalExceptionAttributes * Forward candidate thread id to original exception attributes * Update BacktraceUnityLogCaptureTests.cs Pass thread id to helper tests and assert it * Update BacktraceUnityLogReportFactoryTests.cs Add Test case for candidate thread id propagation for background-thread captures
1 parent 83ba54c commit 23e43b7

34 files changed

Lines changed: 2240 additions & 117 deletions

Runtime/BacktraceClient.cs

Lines changed: 306 additions & 40 deletions
Large diffs are not rendered by default.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using UnityEngine;
3+
4+
namespace Backtrace.Unity
5+
{
6+
internal sealed class BacktraceUnityLogHandler : ILogHandler
7+
{
8+
private readonly BacktraceClient _client;
9+
private readonly ILogHandler _innerLogHandler;
10+
11+
internal ILogHandler InnerLogHandler
12+
{
13+
get { return _innerLogHandler; }
14+
}
15+
16+
internal BacktraceUnityLogHandler(
17+
BacktraceClient client,
18+
ILogHandler innerLogHandler)
19+
{
20+
_client = client;
21+
_innerLogHandler = innerLogHandler;
22+
}
23+
24+
public void LogException(Exception exception, UnityEngine.Object context)
25+
{
26+
try
27+
{
28+
if (_client != null)
29+
{
30+
_client.RecordUnityLogHandlerException(exception, context);
31+
}
32+
}
33+
catch
34+
{
35+
// Never allow Backtrace instrumentation to interfere with Unity logging.
36+
}
37+
38+
if (_innerLogHandler != null)
39+
{
40+
_innerLogHandler.LogException(exception, context);
41+
}
42+
}
43+
44+
public void LogFormat(
45+
LogType logType,
46+
UnityEngine.Object context,
47+
string format,
48+
params object[] args)
49+
{
50+
if (_innerLogHandler != null)
51+
{
52+
_innerLogHandler.LogFormat(logType, context, format, args);
53+
}
54+
}
55+
}
56+
}

Runtime/BacktraceUnityLogHandler.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/Model/BacktraceConfiguration.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,24 @@ public class BacktraceConfiguration : ScriptableObject
232232
[Tooltip("If exception does not have a stack trace, use a normalized exception message to generate fingerprint.")]
233233
public bool UseNormalizedExceptionMessage = false;
234234

235+
/// <summary>
236+
/// Controls whether the SDK wraps Debug.unityLogger.logHandler to capture
237+
/// original Exception objects before Unity reduces them to log-callback messages.
238+
/// Automatic enables this on WebGL builds and disables it elsewhere.
239+
/// </summary>
240+
[Tooltip("Controls whether Backtrace wraps Debug.unityLogger.logHandler to capture original Exception objects passed through Debug.LogException. Automatic enables this on WebGL and disables it elsewhere.")]
241+
public BacktraceUnityLogHandlerExceptionCaptureMode UnityLogHandlerExceptionCapture =
242+
BacktraceUnityLogHandlerExceptionCaptureMode.Automatic;
243+
244+
/// <summary>
245+
/// WebGL only. Attach a best-effort browser JavaScript stack-at-capture annotation
246+
/// for stackless Unity Error/Exception log callbacks. Disabled by default because
247+
/// this is supplemental context, not the original managed C# throw-site stack.
248+
/// </summary>
249+
[Tooltip("WebGL only. Attach a best-effort browser JavaScript stack-at-capture annotation for stackless Unity Error/Exception log callbacks. Disabled by default.")]
250+
public BacktraceWebGLJavaScriptStackFallbackMode WebGLJavaScriptStackFallback =
251+
BacktraceWebGLJavaScriptStackFallbackMode.Disabled;
252+
235253
/// <summary>
236254
/// Determine minidump type support - minidump generation is supported on Windows.
237255
/// </summary>

Runtime/Model/BacktraceData.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,14 @@ public string ToJson()
175175
/// </summary>
176176
private void SetThreadInformations()
177177
{
178-
var faultingThread = !(Report.Exception is BacktraceUnhandledException
179-
&& string.IsNullOrEmpty(Report.Exception.StackTrace));
178+
var hasManagedFrames = Report.DiagnosticStack != null && Report.DiagnosticStack.Count != 0;
179+
var faultingThread = hasManagedFrames;
180+
if (faultingThread &&
181+
Report.Exception is BacktraceUnhandledException &&
182+
string.IsNullOrEmpty(Report.Exception.StackTrace))
183+
{
184+
faultingThread = false;
185+
}
180186

181187
ThreadData = new ThreadData(Report.DiagnosticStack, faultingThread);
182188
ThreadInformations = ThreadData.ThreadInformations;
@@ -191,7 +197,10 @@ private void SetThreadInformations()
191197
private void SetAttributes(Dictionary<string, string> clientAttributes, int gameObjectDepth)
192198
{
193199
Attributes = new BacktraceAttributes(Report, clientAttributes);
194-
Annotation = new Annotations(Report.ExceptionTypeReport ? Report.Exception : null, gameObjectDepth);
200+
Annotation = new Annotations(
201+
Report.ExceptionTypeReport ? Report.Exception : null,
202+
gameObjectDepth,
203+
Report.CustomAnnotations);
195204
}
196205
}
197206
}

Runtime/Model/BacktraceReport.cs

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ public class BacktraceReport
8181
/// </summary>
8282
public BacktraceSourceCode SourceCode = null;
8383

84+
/// <summary>
85+
/// Custom report annotations serialized under the top-level annotations object.
86+
/// </summary>
87+
private readonly Dictionary<string, Dictionary<string, string>> _customAnnotations =
88+
new Dictionary<string, Dictionary<string, string>>();
89+
90+
internal IDictionary<string, Dictionary<string, string>> CustomAnnotations
91+
{
92+
get { return _customAnnotations; }
93+
}
94+
8495
/// <summary>
8596
/// Create new instance of Backtrace report to sending a report with custom client message
8697
/// </summary>
@@ -91,11 +102,12 @@ public BacktraceReport(
91102
string message,
92103
Dictionary<string, string> attributes = null,
93104
List<string> attachmentPaths = null)
94-
: this(null as Exception, attributes, attachmentPaths)
95105
{
106+
Attributes = attributes ?? new Dictionary<string, string>();
107+
AttachmentPaths = attachmentPaths ?? new List<string>();
108+
Exception = null;
109+
ExceptionTypeReport = false;
96110
Message = message;
97-
// analyse stack trace information in both constructor
98-
// to include error message in both source code properties.
99111
SetStacktraceInformation();
100112
SetDefaultAttributes();
101113
}
@@ -124,6 +136,44 @@ public BacktraceReport(
124136
SetDefaultAttributes();
125137
}
126138

139+
private BacktraceReport(
140+
Exception exception,
141+
Dictionary<string, string> attributes,
142+
List<string> attachmentPaths,
143+
bool allowEnvironmentStackFallback)
144+
{
145+
Attributes = attributes ?? new Dictionary<string, string>();
146+
AttachmentPaths = attachmentPaths ?? new List<string>();
147+
Exception = exception;
148+
ExceptionTypeReport = exception != null;
149+
if (ExceptionTypeReport)
150+
{
151+
Message = exception.Message;
152+
SetClassifierInfo();
153+
if (allowEnvironmentStackFallback)
154+
{
155+
SetStacktraceInformation();
156+
}
157+
else
158+
{
159+
SetStacktraceInformationWithoutEnvironmentFallback();
160+
}
161+
}
162+
SetDefaultAttributes();
163+
}
164+
165+
internal static BacktraceReport CreateWithoutEnvironmentStackFallback(
166+
Exception exception,
167+
Dictionary<string, string> attributes = null,
168+
List<string> attachmentPaths = null)
169+
{
170+
return new BacktraceReport(
171+
exception,
172+
attributes,
173+
attachmentPaths,
174+
allowEnvironmentStackFallback: false);
175+
}
176+
127177

128178
/// <summary>
129179
/// Sets report symbolication type
@@ -149,7 +199,7 @@ private void SetDefaultAttributes()
149199
/// <param name="text"></param>
150200
internal void AssignSourceCodeToReport(string text)
151201
{
152-
if (DiagnosticStack == null || DiagnosticStack.Count == 0)
202+
if (string.IsNullOrEmpty(text))
153203
{
154204
return;
155205
}
@@ -158,7 +208,11 @@ internal void AssignSourceCodeToReport(string text)
158208
{
159209
Text = text
160210
};
161-
// assign log information to first stack frame
211+
212+
if (DiagnosticStack == null || DiagnosticStack.Count == 0)
213+
{
214+
return;
215+
}
162216
foreach (var diagnosticStack in DiagnosticStack)
163217
{
164218
diagnosticStack.SourceCode = BacktraceSourceCode.SOURCE_CODE_PROPERTY;
@@ -237,6 +291,30 @@ internal void SetReportFingerprint(bool generateFingerprint)
237291
}
238292
}
239293

294+
internal void AddAnnotation(
295+
string name,
296+
IDictionary<string, string> values)
297+
{
298+
if (string.IsNullOrEmpty(name) || values == null || values.Count == 0)
299+
{
300+
return;
301+
}
302+
var annotation = new Dictionary<string, string>();
303+
foreach (var value in values)
304+
{
305+
if (string.IsNullOrEmpty(value.Key))
306+
{
307+
continue;
308+
}
309+
annotation[value.Key] = value.Value ?? string.Empty;
310+
}
311+
if (annotation.Count == 0)
312+
{
313+
return;
314+
}
315+
_customAnnotations[name] = annotation;
316+
}
317+
240318
internal BacktraceData ToBacktraceData(Dictionary<string, string> clientAttributes, int gameObjectDepth)
241319
{
242320
return new BacktraceData(this, clientAttributes, gameObjectDepth);
@@ -248,6 +326,13 @@ internal void SetStacktraceInformation()
248326
var stacktrace = new BacktraceStackTrace(Exception);
249327
DiagnosticStack = stacktrace.StackFrames;
250328
}
329+
330+
private void SetStacktraceInformationWithoutEnvironmentFallback()
331+
{
332+
var stacktrace =
333+
BacktraceStackTrace.CreateWithoutEnvironmentFallback(Exception);
334+
DiagnosticStack = stacktrace.StackFrames;
335+
}
251336
/// <summary>
252337
/// create a copy of BacktraceReport for inner exception object inside exception
253338
/// </summary>

Runtime/Model/BacktraceStackTrace.cs

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
44

@@ -14,64 +14,89 @@ internal class BacktraceStackTrace
1414
/// </summary>
1515
public readonly List<BacktraceStackFrame> StackFrames = new List<BacktraceStackFrame>();
1616

17-
18-
/// <summary>
19-
/// Current exception
20-
/// </summary>
2117
private readonly Exception _exception;
18+
private readonly bool _allowEnvironmentStackFallback;
2219

2320
public BacktraceStackTrace(Exception exception)
2421
{
2522
_exception = exception;
23+
_allowEnvironmentStackFallback = true;
24+
Initialize();
25+
}
26+
27+
private BacktraceStackTrace(
28+
Exception exception,
29+
bool allowEnvironmentStackFallback)
30+
{
31+
_exception = exception;
32+
_allowEnvironmentStackFallback = allowEnvironmentStackFallback;
2633
Initialize();
2734
}
2835

36+
internal static BacktraceStackTrace CreateWithoutEnvironmentFallback(
37+
Exception exception)
38+
{
39+
return new BacktraceStackTrace(
40+
exception,
41+
allowEnvironmentStackFallback: false);
42+
}
43+
2944
private void Initialize()
3045
{
31-
bool generateExceptionInformation = _exception != null;
46+
var generateExceptionInformation = _exception != null;
3247
if (_exception != null)
3348
{
34-
if (_exception is BacktraceUnhandledException)
49+
var unhandledException = _exception as BacktraceUnhandledException;
50+
if (unhandledException != null)
3551
{
36-
var current = _exception as BacktraceUnhandledException;
37-
StackFrames.InsertRange(0, current.StackFrames);
52+
StackFrames.InsertRange(0, unhandledException.StackFrames);
53+
return;
3854
}
39-
else
55+
56+
var exceptionStackTrace = new StackTrace(_exception, true);
57+
var exceptionFrames = exceptionStackTrace.GetFrames();
58+
if (exceptionFrames == null || exceptionFrames.Length == 0)
4059
{
41-
var exceptionStackTrace = new StackTrace(_exception, true);
42-
var exceptionFrames = exceptionStackTrace.GetFrames();
43-
if (exceptionFrames == null || exceptionFrames.Length == 0)
60+
if (!_allowEnvironmentStackFallback)
4461
{
45-
exceptionFrames = new StackTrace(true).GetFrames();
62+
return;
4663
}
47-
SetStacktraceInformation(exceptionFrames, true);
64+
exceptionFrames = new StackTrace(true).GetFrames();
4865
}
66+
SetStacktraceInformation(exceptionFrames, true);
67+
return;
4968
}
50-
else
69+
70+
if (!_allowEnvironmentStackFallback)
5171
{
52-
//initialize environment stack trace
53-
var stackTrace = new StackTrace(true);
54-
//reverse frame order
55-
var frames = stackTrace.GetFrames();
56-
SetStacktraceInformation(frames, generateExceptionInformation);
72+
return;
5773
}
74+
75+
var stackTrace = new StackTrace(true);
76+
var frames = stackTrace.GetFrames();
77+
SetStacktraceInformation(frames, generateExceptionInformation);
5878
}
5979

60-
private void SetStacktraceInformation(StackFrame[] frames, bool generatedByException = false)
80+
private void SetStacktraceInformation(
81+
StackFrame[] frames,
82+
bool generatedByException = false)
6183
{
6284
if (frames == null || frames.Length == 0)
6385
{
6486
return;
6587
}
66-
int startingIndex = 0;
88+
var startingIndex = 0;
6789
foreach (var frame in frames)
6890
{
69-
var backtraceFrame = new BacktraceStackFrame(frame, generatedByException);
91+
var backtraceFrame = new BacktraceStackFrame(
92+
frame,
93+
generatedByException);
7094
if (backtraceFrame.InvalidFrame)
7195
{
7296
continue;
7397
}
74-
backtraceFrame.StackFrameType = Types.BacktraceStackFrameType.Dotnet;
98+
backtraceFrame.StackFrameType =
99+
Types.BacktraceStackFrameType.Dotnet;
75100
StackFrames.Insert(startingIndex, backtraceFrame);
76101
startingIndex++;
77102
}

0 commit comments

Comments
 (0)