diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6b899e623..8c41ce49e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Features
+
+- 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))
diff --git a/src/Sentry/Extensibility/ISentryStackTraceFactory.cs b/src/Sentry/Extensibility/ISentryStackTraceFactory.cs
index db572f60a2..9345032b23 100644
--- a/src/Sentry/Extensibility/ISentryStackTraceFactory.cs
+++ b/src/Sentry/Extensibility/ISentryStackTraceFactory.cs
@@ -1,7 +1,7 @@
namespace Sentry.Extensibility;
///
-/// Factory to from an .
+/// Factory to create a from an .
///
public interface ISentryStackTraceFactory
{
diff --git a/src/Sentry/Extensibility/SentryStackTraceFactory.cs b/src/Sentry/Extensibility/SentryStackTraceFactory.cs
index 62bdf6f1f7..9f917e956e 100644
--- a/src/Sentry/Extensibility/SentryStackTraceFactory.cs
+++ b/src/Sentry/Extensibility/SentryStackTraceFactory.cs
@@ -14,11 +14,7 @@ public sealed class SentryStackTraceFactory : ISentryStackTraceFactory
///
public SentryStackTraceFactory(SentryOptions options) => _options = options;
- ///
- /// Creates a from the optional .
- ///
- /// The exception to create the stacktrace from.
- /// A Sentry stack trace.
+ ///
public SentryStackTrace? Create(Exception? exception = null)
{
if (exception == null && !_options.AttachStacktrace)
diff --git a/src/Sentry/Extensibility/StringStackTraceFactory.cs b/src/Sentry/Extensibility/StringStackTraceFactory.cs
new file mode 100644
index 0000000000..ac8676b60e
--- /dev/null
+++ b/src/Sentry/Extensibility/StringStackTraceFactory.cs
@@ -0,0 +1,106 @@
+using Sentry.Infrastructure;
+
+namespace Sentry.Extensibility;
+
+#if NET8_0_OR_GREATER
+
+///
+/// A rudimentary implementation of that simply parses the
+/// string representation of the stack trace from an exception. This lacks many of the features
+/// off the full . However, it may be useful in AOT compiled
+/// applications where the full factory is not returning a useful stack trace.
+///
+///
+/// This class is currently EXPERIMENTAL
+///
+///
+/// This factory is designed for AOT scenarios, so only available for net8.0+
+///
+///
+///
+[Experimental(DiagnosticId.ExperimentalFeature)]
+public partial class StringStackTraceFactory : ISentryStackTraceFactory
+{
+ private readonly SentryOptions _options;
+ private const string FullStackTraceLinePattern = @"at (?[^\.]+)\.(?.*?) in (?.*?):line (?\d+)";
+ 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
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The sentry options
+ public StringStackTraceFactory(SentryOptions options)
+ {
+ _options = options;
+ }
+
+ ///
+ public SentryStackTrace? Create(Exception? exception = null)
+ {
+ _options.LogDebug("Source Stack Trace: {0}", exception?.StackTrace);
+
+ var trace = new SentryStackTrace();
+ var frames = new List();
+
+ 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
diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs
index ceab5113bc..b39fc6df19 100644
--- a/src/Sentry/SentryOptions.cs
+++ b/src/Sentry/SentryOptions.cs
@@ -1631,7 +1631,16 @@ public IEnumerable GetAllExceptionProcessors()
=> ExceptionProcessorsProviders.SelectMany(p => p());
///
- /// Use custom .
+ ///
+ /// Use a custom .
+ ///
+ ///
+ /// By default, Sentry uses the 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.
+ ///
///
/// The stack trace factory.
public SentryOptions UseStackTraceFactory(ISentryStackTraceFactory sentryStackTraceFactory)
diff --git a/test/Directory.Build.props b/test/Directory.Build.props
index 11475f416c..5c1a6a9d47 100644
--- a/test/Directory.Build.props
+++ b/test/Directory.Build.props
@@ -48,6 +48,7 @@
+
@@ -59,7 +60,7 @@
-
+
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
index b3e2244f66..71d6bfb677 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
@@ -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
{
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
index b3e2244f66..71d6bfb677 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
@@ -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
{
diff --git a/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.MethodGeneric.verified.txt b/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.MethodGeneric.verified.txt
new file mode 100644
index 0000000000..23cb07a00b
--- /dev/null
+++ b/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.MethodGeneric.verified.txt
@@ -0,0 +1,5 @@
+{
+ FileName: {ProjectDirectory}Internals/StringStackTraceFactoryTests.cs,
+ Function: Tests.Internals.StringStackTraceFactoryTests.GenericMethodThatThrows[T](T value),
+ Module: Other
+}
\ No newline at end of file
diff --git a/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.cs b/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.cs
new file mode 100644
index 0000000000..bd2c65e6cf
--- /dev/null
+++ b/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.cs
@@ -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(
+ 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 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