Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it on by default on AOT then?

How do I turn it on?

Would be nice to have a mention of that in the changelog too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps something we could also document in Troubleshooting when released?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah when @Flash0ver and I discussed we were thinking we wouldn't promote this too widely, since it's quite experimental. However there are some additional docs on SentryOptions.UseStackTraceFactory (which is how you'd add this):

/// <para>
/// By default, Sentry uses the <see cref="SentryStackTraceFactory"/> to create stack traces and this implementation
/// offers the most comprehensive functionality. However, full stack traces are not available in AOT compiled
/// applications. If you are compiling your applications AOT and the stack traces that you see in Sentry are not
/// informative enough, you could consider using the StringStackTraceFactory instead. This is not as functional but
/// is guaranteed to provide at least _something_ useful in AOT compiled applications.
/// </para>

And we figured we'd add something to our troubleshooting docs as well...

I'll add a blurb in the description of this PR as well.

- 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))

### Fixes

- Native AOT: don't load SentryNative on unsupported platforms ([#4347](https://github.com/getsentry/sentry-dotnet/pull/4347))
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry/Extensibility/ISentryStackTraceFactory.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Sentry.Extensibility;

/// <summary>
/// Factory to <see cref="SentryStackTrace" /> from an <see cref="Exception" />.
/// Factory to create a <see cref="SentryStackTrace" /> from an <see cref="Exception" />.
/// </summary>
public interface ISentryStackTraceFactory
{
Expand Down
6 changes: 1 addition & 5 deletions src/Sentry/Extensibility/SentryStackTraceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ public sealed class SentryStackTraceFactory : ISentryStackTraceFactory
/// </summary>
public SentryStackTraceFactory(SentryOptions 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>
/// <inheritdoc />
public SentryStackTrace? Create(Exception? exception = null)
{
if (exception == null && !_options.AttachStacktrace)
Expand Down
106 changes: 106 additions & 0 deletions src/Sentry/Extensibility/StringStackTraceFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using Sentry.Infrastructure;

namespace Sentry.Extensibility;

#if NET8_0_OR_GREATER

/// <summary>
/// A rudimentary implementation of <see cref="ISentryStackTraceFactory"/> that simply parses the
/// string representation of the stack trace from an exception. This lacks many of the features
/// off the full <see cref="SentryStackTraceFactory"/>. However, it may be useful in AOT compiled
/// applications where the full factory is not returning a useful stack trace.
/// <remarks>
/// <para>
/// This class is currently EXPERIMENTAL
/// </para>
/// <para>
/// This factory is designed for AOT scenarios, so only available for net8.0+
/// </para>
/// </remarks>
/// </summary>
[Experimental(DiagnosticId.ExperimentalFeature)]
public partial class StringStackTraceFactory : ISentryStackTraceFactory
{
private readonly SentryOptions _options;
private const string FullStackTraceLinePattern = @"at (?<Module>[^\.]+)\.(?<Function>.*?) in (?<FileName>.*?):line (?<LineNo>\d+)";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this regex coming from another file in the repo or it's a new regex? curious how tested/vetted this is

Copy link
Collaborator Author

@jamescrosswell jamescrosswell Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a new regex and the short answer is "not very"... which is one reason this is marked as Experimental.

We do have these unit tests but I'm not sure what this string is likely to look like in all the different real world scenarios.

This one (the fallback) is the regex that @filipnavara is using here:

private const string StackTraceLinePattern = @"at (.+)\.(.+) \+";

I'm hoping that between the two of those we can extract something sensible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have been using this regex in production since October 2024. It may not be fool-proof but it's somewhat battle-tested.

private const string StackTraceLinePattern = @"at (.+)\.(.+) \+";

#if NET9_0_OR_GREATER
[GeneratedRegex(FullStackTraceLinePattern)]
internal static partial Regex FullStackTraceLine { get; }
#else
internal static readonly Regex FullStackTraceLine = FullStackTraceLineRegex();

[GeneratedRegex(FullStackTraceLinePattern)]
private static partial Regex FullStackTraceLineRegex();
#endif

#if NET9_0_OR_GREATER
[GeneratedRegex(StackTraceLinePattern)]
private static partial Regex StackTraceLine { get; }
#else
private static readonly Regex StackTraceLine = StackTraceLineRegex();

[GeneratedRegex(StackTraceLinePattern)]
private static partial Regex StackTraceLineRegex();
#endif

/// <summary>
/// Creates a new instance of <see cref="StringStackTraceFactory"/>.
/// </summary>
/// <param name="options">The sentry options</param>
public StringStackTraceFactory(SentryOptions options)
{
_options = options;
}

/// <inheritdoc />
public SentryStackTrace? Create(Exception? exception = null)
{
_options.LogDebug("Source Stack Trace: {0}", exception?.StackTrace);

var trace = new SentryStackTrace();
var frames = new List<SentryStackFrame>();

var lines = exception?.StackTrace?.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) ?? [];
foreach (var line in lines)
{
var fullMatch = FullStackTraceLine.Match(line);
if (fullMatch.Success)
{
frames.Add(new SentryStackFrame()
{
Module = fullMatch.Groups[1].Value,
Function = fullMatch.Groups[2].Value,
FileName = fullMatch.Groups[3].Value,
LineNumber = int.Parse(fullMatch.Groups[4].Value),
});
continue;
}

_options.LogDebug("Full stack frame match failed for: {0}", line);
var lineMatch = StackTraceLine.Match(line);
if (lineMatch.Success)
{
frames.Add(new SentryStackFrame()
{
Module = lineMatch.Groups[1].Value,
Function = lineMatch.Groups[2].Value
});
continue;
}

_options.LogDebug("Stack frame match failed for: {0}", line);
frames.Add(new SentryStackFrame()
{
Function = line
});
}

trace.Frames = frames;
_options.LogDebug("Created {0} with {1} frames.", "StringStackTrace", trace.Frames.Count);
return trace.Frames.Count != 0 ? trace : null;
}
}

#endif
11 changes: 10 additions & 1 deletion src/Sentry/SentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1631,7 +1631,16 @@ public IEnumerable<ISentryEventExceptionProcessor> GetAllExceptionProcessors()
=> ExceptionProcessorsProviders.SelectMany(p => p());

/// <summary>
/// Use custom <see cref="ISentryStackTraceFactory" />.
/// <para>
/// Use a custom <see cref="ISentryStackTraceFactory" />.
/// </para>
/// <para>
/// By default, Sentry uses the <see cref="SentryStackTraceFactory"/> to create stack traces and this implementation
/// offers the most comprehensive functionality. However, full stack traces are not available in AOT compiled
/// applications. If you are compiling your applications AOT and the stack traces that you see in Sentry are not
/// informative enough, you could consider using the StringStackTraceFactory instead. This is not as functional but
/// is guaranteed to provide at least _something_ useful in AOT compiled applications.
/// </para>
/// </summary>
/// <param name="sentryStackTraceFactory">The stack trace factory.</param>
public SentryOptions UseStackTraceFactory(ISentryStackTraceFactory sentryStackTraceFactory)
Expand Down
3 changes: 2 additions & 1 deletion test/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<Using Include="NSubstitute.ReturnsExtensions" />
<Using Include="Xunit" />
<Using Include="Xunit.Abstractions" />
<Using Condition="'$(TargetPlatformIdentifier)'==''" Include="VerifyXunit" />

<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
Expand All @@ -59,7 +60,7 @@

<!-- only non-platform-specific projects should include these packages -->
<ItemGroup Condition="'$(TargetPlatformIdentifier)'==''">
<PackageReference Include="Verify.Xunit" Version="30.3.1" />
<PackageReference Include="Verify.Xunit" Version="30.5.0" />
<PackageReference Include="Verify.DiffPlex" Version="3.1.2" />
<PackageReference Include="PublicApiGenerator" Version="11.1.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1" PrivateAssets="All" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1516,6 +1516,12 @@ namespace Sentry.Extensibility
public SentryStackTraceFactory(Sentry.SentryOptions options) { }
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
}
[System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public class StringStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory
{
public StringStackTraceFactory(Sentry.SentryOptions options) { }
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
}
}
namespace Sentry.Http
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1516,6 +1516,12 @@ namespace Sentry.Extensibility
public SentryStackTraceFactory(Sentry.SentryOptions options) { }
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
}
[System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")]
public class StringStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory
{
public StringStackTraceFactory(Sentry.SentryOptions options) { }
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
}
}
namespace Sentry.Http
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
FileName: {ProjectDirectory}Internals/StringStackTraceFactoryTests.cs,
Function: Tests.Internals.StringStackTraceFactoryTests.GenericMethodThatThrows[T](T value),
Module: Other
}
66 changes: 66 additions & 0 deletions test/Sentry.Tests/Internals/StringStackTraceFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// ReSharper disable once CheckNamespace
// Stack trace filters out Sentry frames by namespace
namespace Other.Tests.Internals;

#if PLATFORM_NEUTRAL && NET8_0_OR_GREATER

public class StringStackTraceFactoryTests
{
[Fact]
public Task MethodGeneric()
{
// Arrange
const int i = 5;
var exception = Record.Exception(() => GenericMethodThatThrows(i));

var options = new SentryOptions
{
AttachStacktrace = true
};
var factory = new StringStackTraceFactory(options);

// Act
var stackTrace = factory.Create(exception);

// Assert;
var frame = stackTrace!.Frames.Single(x => x.Function!.Contains("GenericMethodThatThrows"));
return Verify(frame)
.IgnoreMembers<SentryStackFrame>(
x => x.Package,
x => x.LineNumber,
x => x.ColumnNumber,
x => x.InstructionAddress,
x => x.FunctionId)
.AddScrubber(x => x.Replace(@"\", @"/"));
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static void GenericMethodThatThrows<T>(T value) =>
throw new Exception();

[Theory]
[InlineData("at MyNamespace.MyClass.MyMethod in /path/to/file.cs:line 42", "MyNamespace", "MyClass.MyMethod", "/path/to/file.cs", "42")]
[InlineData("at Foo.Bar.Baz in C:\\code\\foo.cs:line 123", "Foo", "Bar.Baz", "C:\\code\\foo.cs", "123")]
public void FullStackTraceLine_ValidInput_Matches(
string input, string expectedModule, string expectedFunction, string expectedFile, string expectedLine)
{
var match = StringStackTraceFactory.FullStackTraceLine.Match(input);
Assert.True(match.Success);
Assert.Equal(expectedModule, match.Groups["Module"].Value);
Assert.Equal(expectedFunction, match.Groups["Function"].Value);
Assert.Equal(expectedFile, match.Groups["FileName"].Value);
Assert.Equal(expectedLine, match.Groups["LineNo"].Value);
}

[Theory]
[InlineData("at MyNamespace.MyClass.MyMethod +")]
[InlineData("random text")]
[InlineData("at . in :line ")]
public void FullStackTraceLine_InvalidInput_DoesNotMatch(string input)
{
var match = StringStackTraceFactory.FullStackTraceLine.Match(input);
Assert.False(match.Success);
}
}

#endif
Loading