Skip to content

Commit 3947066

Browse files
Added StringStackTraceFactory (#4362)
Resolves #3439 - #3439
1 parent a276045 commit 3947066

10 files changed

+207
-8
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+
- Sentry now includes an EXPERIMENTAL StringStackTraceFactory. This factory isn't as feature rich as the full `SentryStackTraceFactory`. However, it may provide better results if you are compiling your application AOT and not getting useful stack traces from the full stack trace factory. ([#4362](https://github.com/getsentry/sentry-dotnet/pull/4362))
8+
59
### Fixes
610

711
- Source context for class libraries when running on Android in Release mode ([#4294](https://github.com/getsentry/sentry-dotnet/pull/4294))

src/Sentry/Extensibility/ISentryStackTraceFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
namespace Sentry.Extensibility;
22

33
/// <summary>
4-
/// Factory to <see cref="SentryStackTrace" /> from an <see cref="Exception" />.
4+
/// Factory to create a <see cref="SentryStackTrace" /> from an <see cref="Exception" />.
55
/// </summary>
66
public interface ISentryStackTraceFactory
77
{

src/Sentry/Extensibility/SentryStackTraceFactory.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@ public sealed class SentryStackTraceFactory : ISentryStackTraceFactory
1414
/// </summary>
1515
public SentryStackTraceFactory(SentryOptions options) => _options = options;
1616

17-
/// <summary>
18-
/// Creates a <see cref="SentryStackTrace" /> from the optional <see cref="Exception" />.
19-
/// </summary>
20-
/// <param name="exception">The exception to create the stacktrace from.</param>
21-
/// <returns>A Sentry stack trace.</returns>
17+
/// <inheritdoc />
2218
public SentryStackTrace? Create(Exception? exception = null)
2319
{
2420
if (exception == null && !_options.AttachStacktrace)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using Sentry.Infrastructure;
2+
3+
namespace Sentry.Extensibility;
4+
5+
#if NET8_0_OR_GREATER
6+
7+
/// <summary>
8+
/// A rudimentary implementation of <see cref="ISentryStackTraceFactory"/> that simply parses the
9+
/// string representation of the stack trace from an exception. This lacks many of the features
10+
/// off the full <see cref="SentryStackTraceFactory"/>. However, it may be useful in AOT compiled
11+
/// applications where the full factory is not returning a useful stack trace.
12+
/// <remarks>
13+
/// <para>
14+
/// This class is currently EXPERIMENTAL
15+
/// </para>
16+
/// <para>
17+
/// This factory is designed for AOT scenarios, so only available for net8.0+
18+
/// </para>
19+
/// </remarks>
20+
/// </summary>
21+
[Experimental(DiagnosticId.ExperimentalFeature)]
22+
public partial class StringStackTraceFactory : ISentryStackTraceFactory
23+
{
24+
private readonly SentryOptions _options;
25+
private const string FullStackTraceLinePattern = @"at (?<Module>[^\.]+)\.(?<Function>.*?) in (?<FileName>.*?):line (?<LineNo>\d+)";
26+
private const string StackTraceLinePattern = @"at (.+)\.(.+) \+";
27+
28+
#if NET9_0_OR_GREATER
29+
[GeneratedRegex(FullStackTraceLinePattern)]
30+
internal static partial Regex FullStackTraceLine { get; }
31+
#else
32+
internal static readonly Regex FullStackTraceLine = FullStackTraceLineRegex();
33+
34+
[GeneratedRegex(FullStackTraceLinePattern)]
35+
private static partial Regex FullStackTraceLineRegex();
36+
#endif
37+
38+
#if NET9_0_OR_GREATER
39+
[GeneratedRegex(StackTraceLinePattern)]
40+
private static partial Regex StackTraceLine { get; }
41+
#else
42+
private static readonly Regex StackTraceLine = StackTraceLineRegex();
43+
44+
[GeneratedRegex(StackTraceLinePattern)]
45+
private static partial Regex StackTraceLineRegex();
46+
#endif
47+
48+
/// <summary>
49+
/// Creates a new instance of <see cref="StringStackTraceFactory"/>.
50+
/// </summary>
51+
/// <param name="options">The sentry options</param>
52+
public StringStackTraceFactory(SentryOptions options)
53+
{
54+
_options = options;
55+
}
56+
57+
/// <inheritdoc />
58+
public SentryStackTrace? Create(Exception? exception = null)
59+
{
60+
_options.LogDebug("Source Stack Trace: {0}", exception?.StackTrace);
61+
62+
var trace = new SentryStackTrace();
63+
var frames = new List<SentryStackFrame>();
64+
65+
var lines = exception?.StackTrace?.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) ?? [];
66+
foreach (var line in lines)
67+
{
68+
var fullMatch = FullStackTraceLine.Match(line);
69+
if (fullMatch.Success)
70+
{
71+
frames.Add(new SentryStackFrame()
72+
{
73+
Module = fullMatch.Groups[1].Value,
74+
Function = fullMatch.Groups[2].Value,
75+
FileName = fullMatch.Groups[3].Value,
76+
LineNumber = int.Parse(fullMatch.Groups[4].Value),
77+
});
78+
continue;
79+
}
80+
81+
_options.LogDebug("Full stack frame match failed for: {0}", line);
82+
var lineMatch = StackTraceLine.Match(line);
83+
if (lineMatch.Success)
84+
{
85+
frames.Add(new SentryStackFrame()
86+
{
87+
Module = lineMatch.Groups[1].Value,
88+
Function = lineMatch.Groups[2].Value
89+
});
90+
continue;
91+
}
92+
93+
_options.LogDebug("Stack frame match failed for: {0}", line);
94+
frames.Add(new SentryStackFrame()
95+
{
96+
Function = line
97+
});
98+
}
99+
100+
trace.Frames = frames;
101+
_options.LogDebug("Created {0} with {1} frames.", "StringStackTrace", trace.Frames.Count);
102+
return trace.Frames.Count != 0 ? trace : null;
103+
}
104+
}
105+
106+
#endif

src/Sentry/SentryOptions.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1631,7 +1631,16 @@ public IEnumerable<ISentryEventExceptionProcessor> GetAllExceptionProcessors()
16311631
=> ExceptionProcessorsProviders.SelectMany(p => p());
16321632

16331633
/// <summary>
1634-
/// Use custom <see cref="ISentryStackTraceFactory" />.
1634+
/// <para>
1635+
/// Use a custom <see cref="ISentryStackTraceFactory" />.
1636+
/// </para>
1637+
/// <para>
1638+
/// By default, Sentry uses the <see cref="SentryStackTraceFactory"/> to create stack traces and this implementation
1639+
/// offers the most comprehensive functionality. However, full stack traces are not available in AOT compiled
1640+
/// applications. If you are compiling your applications AOT and the stack traces that you see in Sentry are not
1641+
/// informative enough, you could consider using the StringStackTraceFactory instead. This is not as functional but
1642+
/// is guaranteed to provide at least _something_ useful in AOT compiled applications.
1643+
/// </para>
16351644
/// </summary>
16361645
/// <param name="sentryStackTraceFactory">The stack trace factory.</param>
16371646
public SentryOptions UseStackTraceFactory(ISentryStackTraceFactory sentryStackTraceFactory)

test/Directory.Build.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
<Using Include="NSubstitute.ReturnsExtensions" />
4949
<Using Include="Xunit" />
5050
<Using Include="Xunit.Abstractions" />
51+
<Using Condition="'$(TargetPlatformIdentifier)'==''" Include="VerifyXunit" />
5152

5253
<PackageReference Include="NSubstitute" Version="5.3.0" />
5354
<PackageReference Include="FluentAssertions" Version="6.12.0" />
@@ -59,7 +60,7 @@
5960

6061
<!-- only non-platform-specific projects should include these packages -->
6162
<ItemGroup Condition="'$(TargetPlatformIdentifier)'==''">
62-
<PackageReference Include="Verify.Xunit" Version="30.3.1" />
63+
<PackageReference Include="Verify.Xunit" Version="30.5.0" />
6364
<PackageReference Include="Verify.DiffPlex" Version="3.1.2" />
6465
<PackageReference Include="PublicApiGenerator" Version="11.1.0" />
6566
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1" PrivateAssets="All" />

test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,12 @@ namespace Sentry.Extensibility
15161516
public SentryStackTraceFactory(Sentry.SentryOptions options) { }
15171517
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
15181518
}
1519+
[System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
1520+
public class StringStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory
1521+
{
1522+
public StringStackTraceFactory(Sentry.SentryOptions options) { }
1523+
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
1524+
}
15191525
}
15201526
namespace Sentry.Http
15211527
{

test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,6 +1516,12 @@ namespace Sentry.Extensibility
15161516
public SentryStackTraceFactory(Sentry.SentryOptions options) { }
15171517
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
15181518
}
1519+
[System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
1520+
public class StringStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory
1521+
{
1522+
public StringStackTraceFactory(Sentry.SentryOptions options) { }
1523+
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
1524+
}
15191525
}
15201526
namespace Sentry.Http
15211527
{
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
FileName: {ProjectDirectory}Internals/StringStackTraceFactoryTests.cs,
3+
Function: Tests.Internals.StringStackTraceFactoryTests.GenericMethodThatThrows[T](T value),
4+
Module: Other
5+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// ReSharper disable once CheckNamespace
2+
// Stack trace filters out Sentry frames by namespace
3+
namespace Other.Tests.Internals;
4+
5+
#if PLATFORM_NEUTRAL && NET8_0_OR_GREATER
6+
7+
public class StringStackTraceFactoryTests
8+
{
9+
[Fact]
10+
public Task MethodGeneric()
11+
{
12+
// Arrange
13+
const int i = 5;
14+
var exception = Record.Exception(() => GenericMethodThatThrows(i));
15+
16+
var options = new SentryOptions
17+
{
18+
AttachStacktrace = true
19+
};
20+
var factory = new StringStackTraceFactory(options);
21+
22+
// Act
23+
var stackTrace = factory.Create(exception);
24+
25+
// Assert;
26+
var frame = stackTrace!.Frames.Single(x => x.Function!.Contains("GenericMethodThatThrows"));
27+
return Verify(frame)
28+
.IgnoreMembers<SentryStackFrame>(
29+
x => x.Package,
30+
x => x.LineNumber,
31+
x => x.ColumnNumber,
32+
x => x.InstructionAddress,
33+
x => x.FunctionId)
34+
.AddScrubber(x => x.Replace(@"\", @"/"));
35+
}
36+
37+
[MethodImpl(MethodImplOptions.NoInlining)]
38+
private static void GenericMethodThatThrows<T>(T value) =>
39+
throw new Exception();
40+
41+
[Theory]
42+
[InlineData("at MyNamespace.MyClass.MyMethod in /path/to/file.cs:line 42", "MyNamespace", "MyClass.MyMethod", "/path/to/file.cs", "42")]
43+
[InlineData("at Foo.Bar.Baz in C:\\code\\foo.cs:line 123", "Foo", "Bar.Baz", "C:\\code\\foo.cs", "123")]
44+
public void FullStackTraceLine_ValidInput_Matches(
45+
string input, string expectedModule, string expectedFunction, string expectedFile, string expectedLine)
46+
{
47+
var match = StringStackTraceFactory.FullStackTraceLine.Match(input);
48+
Assert.True(match.Success);
49+
Assert.Equal(expectedModule, match.Groups["Module"].Value);
50+
Assert.Equal(expectedFunction, match.Groups["Function"].Value);
51+
Assert.Equal(expectedFile, match.Groups["FileName"].Value);
52+
Assert.Equal(expectedLine, match.Groups["LineNo"].Value);
53+
}
54+
55+
[Theory]
56+
[InlineData("at MyNamespace.MyClass.MyMethod +")]
57+
[InlineData("random text")]
58+
[InlineData("at . in :line ")]
59+
public void FullStackTraceLine_InvalidInput_DoesNotMatch(string input)
60+
{
61+
var match = StringStackTraceFactory.FullStackTraceLine.Match(input);
62+
Assert.False(match.Success);
63+
}
64+
}
65+
66+
#endif

0 commit comments

Comments
 (0)