Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 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 SimpleStackTraceFactory. As the name suggests, 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
99 changes: 99 additions & 0 deletions src/Sentry/Extensibility/SimpleStackTraceFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;

namespace Sentry.Extensibility;

/// <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>SimpleStackTraceFactory is currently EXPERIMENTAL.</remarks>
/// </summary>
public partial class SimpleStackTraceFactory : ISentryStackTraceFactory
{
private readonly SentryOptions _options;
private const string FullStackTraceLinePattern = @"at (?<Module>[^\.]+)\.(?<Function>.*?) in (?<FileName>.*?):line (?<LineNo>\d+)";
private const string StackTraceLinePattern = @"at (.+)\.(.+) \+";

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

[GeneratedRegex(FullStackTraceLinePattern)]
private static partial Regex FullStackTraceLineRegex();
#else
private static readonly Regex FullStackTraceLine = new (FullStackTraceLinePattern, RegexOptions.Compiled);
#endif

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

[GeneratedRegex(StackTraceLinePattern)]
private static partial Regex StackTraceLineRegex();
#else
private static readonly Regex StackTraceLine = new (StackTraceLinePattern, RegexOptions.Compiled);
#endif

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

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

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

var newlines = new[] { Environment.NewLine };
var lines = exception?.StackTrace?.Split(newlines, 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;
return trace;
}
}
9 changes: 9 additions & 0 deletions 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>
/// <para>
/// Use 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 <see cref="SimpleStackTraceFactory" /> 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,11 @@ namespace Sentry.Extensibility
public SentryStackTraceFactory(Sentry.SentryOptions options) { }
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
}
public class SimpleStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory
{
public SimpleStackTraceFactory(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,11 @@ namespace Sentry.Extensibility
public SentryStackTraceFactory(Sentry.SentryOptions options) { }
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
}
public class SimpleStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory
{
public SimpleStackTraceFactory(Sentry.SentryOptions options) { }
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
}
}
namespace Sentry.Http
{
Expand Down
5 changes: 5 additions & 0 deletions test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1497,6 +1497,11 @@ namespace Sentry.Extensibility
public SentryStackTraceFactory(Sentry.SentryOptions options) { }
public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { }
}
public class SimpleStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory
{
public SimpleStackTraceFactory(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/SimpleStackTraceFactoryTests.cs,
Function: Tests.Internals.SimpleStackTraceFactoryTests.GenericMethodThatThrows[T](T value),
Module: Other
}
42 changes: 42 additions & 0 deletions test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// ReSharper disable once CheckNamespace
// Stack trace filters out Sentry frames by namespace
namespace Other.Tests.Internals;

#if PLATFORM_NEUTRAL

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

var options = new SentryOptions
{
AttachStacktrace = true
};
var factory = new SimpleStackTraceFactory(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();
}

#endif
Loading