Skip to content
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

feat: Better Mono stacktraces #578

Merged
merged 4 commits into from
Nov 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## unreleased

* Add a list of .NET Frameworks installed when available. (#531) @lucas-zimerman
* Parse Mono and IL2CPP stacktraces for Unity and Xamarin (#578) @bruno-garcia

## 3.0.0-alpha.4

Expand Down
2 changes: 1 addition & 1 deletion src/Sentry/Extensibility/SentryStackTraceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class SentryStackTraceFactory : ISentryStackTraceFactory
/// </summary>
/// <param name="exception">The exception to create the stacktrace from.</param>
/// <returns>A Sentry stack trace.</returns>
public SentryStackTrace? Create(Exception? exception = null)
public virtual SentryStackTrace? Create(Exception? exception = null)
{
var isCurrentStackTrace = exception == null && _options.AttachStacktrace;

Expand Down
72 changes: 72 additions & 0 deletions src/Sentry/Internal/MonoSentryStackTraceFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Sentry.Extensibility;
using Sentry.Protocol;

namespace Sentry.Internal
{
/// <summary>
/// Mono factory to <see cref="SentryStackTrace" /> from an <see cref="Exception" />.
/// </summary>
internal class MonoSentryStackTraceFactory : SentryStackTraceFactory
{
private readonly SentryOptions _options;

/// <summary>
/// Creates an instance of <see cref="MonoSentryStackTraceFactory"/>.
/// </summary>
public MonoSentryStackTraceFactory(SentryOptions options) : base(options) => _options = options;

/// <summary>
/// Creates a <see cref="SentryStackTrace" /> from the optional <see cref="Exception" />.
/// </summary>
/// <param name="exception">The exception to create the stacktrace from.</param>
/// <returns>A Sentry stack trace.</returns>
public override SentryStackTrace? Create(Exception? exception = null)
{
if (exception == null)
{
_options.DiagnosticLogger?.LogDebug("No Exception to collect Mono stack trace.");
return base.Create(exception);
}

List<StackFrameData>? frames = null;
if (exception.StackTrace is { } stacktrace)
{
foreach (var line in stacktrace.Split('\n'))
{
if (StackFrameData.TryParse(line, out var frame))
{
frames ??= new List<StackFrameData>();
frames.Add(frame);
}
}
}

if (frames == null)
{
_options.DiagnosticLogger?.LogWarning("Couldn't resolve a Mono stacktrace, calling fallback");
return base.Create(exception);
}

return new SentryStackTrace
{
// https://develop.sentry.dev/sdk/event-payloads/stacktrace/
Frames = frames.Select(f => new SentryStackFrame
{
Module = f.TypeFullName,
InstructionOffset = f.Offset != 0 ? f.Offset : (long?)null,
Function = f.MethodSignature,
LineNumber = GetLineNumber(f.Line),
}).Reverse().ToArray()
};

static int? GetLineNumber(string line) =>
// Protocol is uint. Also, Mono AOT / IL2CPP / no pdb means no line number (0) which isn't useful.
line is { } l && int.TryParse(l, out var parsedLine) && parsedLine >= 0
? parsedLine
: (int?)null;
}
}
}
123 changes: 123 additions & 0 deletions src/Sentry/Internal/StackFrameData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.RegularExpressions;

namespace Sentry.Internal
{
// https://github.com/mono/mono/blob/d336d6be307dfea8b7a07268270c6d885db9d399/mcs/tools/mono-symbolicate/StackFrameData.cs
internal class StackFrameData
{
static readonly Regex _regex = new Regex(
@"\w*at (?<Method>.+) *(\[0x(?<IL>.+)\]|<0x.+ \+ 0x(?<NativeOffset>.+)>( (?<MethodIndex>\d+)|)) in <(?<MVID>[^>#]+)(#(?<AOTID>[^>]+)|)>:0");

public string TypeFullName { get; }
public string MethodSignature { get; }
public int Offset { get; }
public bool IsILOffset { get; }
public uint MethodIndex { get; }
public string Line { get; }
public string Mvid { get; }
public string Aotid { get; }

private StackFrameData(
string line,
string typeFullName,
string methodSig,
int offset,
bool isILOffset,
uint methodIndex,
string mvid,
string aotid)
{
Line = line;
TypeFullName = typeFullName;
MethodSignature = methodSig;
Offset = offset;
IsILOffset = isILOffset;
MethodIndex = methodIndex;
Mvid = mvid;
Aotid = aotid;
}

public StackFrameData Relocate(string typeName, string methodName)
=> new StackFrameData(Line, typeName, methodName, Offset, IsILOffset, MethodIndex, Mvid, Aotid);

public static bool TryParse(string line, [NotNullWhen(true)] out StackFrameData? stackFrame)
{
stackFrame = default;

var match = _regex.Match(line);
if (!match.Success)
{
return false;
}

var methodStr = match.Groups["Method"].Value.Trim();
if (!ExtractSignatures(methodStr, out var typeFullName, out var methodSignature))
{
return false;
}

var isILOffset = !string.IsNullOrEmpty(match.Groups["IL"].Value);
var offsetVarName = isILOffset ? "IL" : "NativeOffset";
var offset = int.Parse(
match.Groups[offsetVarName].Value,
NumberStyles.HexNumber,
CultureInfo.InvariantCulture);

uint methodIndex = 0xffffff;
if (!string.IsNullOrEmpty(match.Groups["MethodIndex"].Value))
{
methodIndex = uint.Parse(match.Groups["MethodIndex"].Value, CultureInfo.InvariantCulture);
}

stackFrame = new StackFrameData(
line,
typeFullName,
methodSignature,
offset,
isILOffset,
methodIndex,
match.Groups["MVID"].Value,
match.Groups["AOTID"].Value);

return true;
}

private static bool ExtractSignatures(
string str,
[NotNullWhen(true)] out string? typeFullName,
[NotNullWhen(true)] out string? methodSignature)
{
typeFullName = null;
methodSignature = null;

var methodNameEnd = str.IndexOf('(');
if (methodNameEnd == -1)
{
return false;
}

var typeNameEnd = str.LastIndexOf('.', methodNameEnd);
if (typeNameEnd == -1)
{
return false;
}

// Adjustment for Type..ctor ()
if (typeNameEnd > 0 && str[typeNameEnd - 1] == '.')
{
--typeNameEnd;
}

typeFullName = str.Substring(0, typeNameEnd);
// Remove generic parameters
typeFullName = Regex.Replace(typeFullName, @"\[[^\[\]]*\]$", "");
typeFullName = Regex.Replace(typeFullName, @"\<[^\[\]]*\>$", "");

methodSignature = str.Substring(typeNameEnd + 1);

return true;
}
}
}
9 changes: 8 additions & 1 deletion src/Sentry/SentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
using Sentry.Http;
using Sentry.Integrations;
using Sentry.Internal;
using Sentry.PlatformAbstractions;
using Sentry.Protocol;
using static Sentry.Internal.Constants;
using static Sentry.Protocol.Constants;
using Runtime = Sentry.PlatformAbstractions.Runtime;

namespace Sentry
{
Expand Down Expand Up @@ -373,7 +375,11 @@ public SentryOptions()
() => ExceptionProcessors ?? Enumerable.Empty<ISentryEventExceptionProcessor>()
};

SentryStackTraceFactory = new SentryStackTraceFactory(this);
SentryStackTraceFactory = Runtime.Current.IsMono()
// Also true for IL2CPP
? new MonoSentryStackTraceFactory(this)
: new SentryStackTraceFactory(this);

_sentryStackTraceFactoryAccessor = () => SentryStackTraceFactory;

EventProcessors = new ISentryEventProcessor[] {
Expand All @@ -396,6 +402,7 @@ public SentryOptions()

InAppExclude = new[] {
"System.",
"Mono.",
"Sentry.",
"Microsoft.",
"MS", // MS.Win32, MS.Internal, etc: Desktop apps
Expand Down
Loading