From 2dff37fcf12df3d9af7f8171b859321fb1e3fdd2 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 17 Jul 2025 18:45:28 +1200 Subject: [PATCH 01/14] Added SimpleStackTraceFactory Resolves #3439 - https://github.com/getsentry/sentry-dotnet/issues/3439 --- .../Extensibility/ISentryStackTraceFactory.cs | 2 +- .../Extensibility/SimpleStackTraceFactory.cs | 73 +++++++++++++++++++ src/Sentry/SentryOptions.cs | 9 +++ ...piApprovalTests.Run.DotNet8_0.verified.txt | 5 ++ ...piApprovalTests.Run.DotNet9_0.verified.txt | 5 ++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/Sentry/Extensibility/SimpleStackTraceFactory.cs 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/SimpleStackTraceFactory.cs b/src/Sentry/Extensibility/SimpleStackTraceFactory.cs new file mode 100644 index 0000000000..f2d5c66d78 --- /dev/null +++ b/src/Sentry/Extensibility/SimpleStackTraceFactory.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; + +namespace Sentry.Extensibility; + +/// +/// 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. +/// SimpleStackTraceFactory is currently EXPERIMENTAL. +/// +public partial class SimpleStackTraceFactory : ISentryStackTraceFactory +{ + private readonly SentryOptions _options; + private const string StackTraceLinePattern = @"at (.+)\.(.+) \+"; + +#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 + + /// + /// Creates a new instance of . + /// + /// The sentry options + public SimpleStackTraceFactory(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 newlines = new[] { Environment.NewLine }; + var lines = exception?.StackTrace?.Split(newlines, StringSplitOptions.RemoveEmptyEntries) ?? []; + foreach (var line in lines) + { + var match = StackTraceLine.Match(line); + if (match.Success) + { + frames.Add(new SentryStackFrame() + { + Module = match.Groups[1].Value, + Function = match.Groups[2].Value + }); + } + else + { + _options.LogDebug("Regex match failed for: {0}", line); + frames.Add(new SentryStackFrame() + { + Function = line + }); + } + } + + trace.Frames = frames; + return trace; + } +} diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index ceab5113bc..ae818e2fc5 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1631,7 +1631,16 @@ public IEnumerable GetAllExceptionProcessors() => ExceptionProcessorsProviders.SelectMany(p => p()); /// + /// /// Use 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 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/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index b3e2244f66..2e77463e0a 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -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 { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index b3e2244f66..2e77463e0a 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -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 { From 02e0a9921142575799584a7cd09ad1d934ccfeb5 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 17 Jul 2025 19:24:31 +1200 Subject: [PATCH 02/14] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e04f22bc4f..457470818e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- 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)) + ### Dependencies - Bump CLI from v2.47.0 to v2.47.1 ([#4348](https://github.com/getsentry/sentry-dotnet/pull/4348)) From d9dd0aa99de88b99806f1db4ac85784ed56da019 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 17 Jul 2025 21:45:33 +1200 Subject: [PATCH 03/14] Update ApiApprovalTests.Run.Net4_8.verified.txt --- test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 8b32fbc30f..0cfebd5d59 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -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 { From d52b2ade06222471ad563b64bc7f1c915b4ae90c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 21 Jul 2025 14:50:36 +1200 Subject: [PATCH 04/14] Added a simple verify test --- ...aceFactoryTests.MethodGeneric.verified.txt | 7 +++ .../Internals/SimpleStackTraceFactoryTests.cs | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt create mode 100644 test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs diff --git a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt new file mode 100644 index 0000000000..59797b3b18 --- /dev/null +++ b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt @@ -0,0 +1,7 @@ +{ + FileName: Internals/SimpleStackTraceFactoryTests.cs, + Function: void SimpleStackTraceFactoryTests.GenericMethodThatThrows(T value), + AbsolutePath: {ProjectDirectory}Internals/SimpleStackTraceFactoryTests.cs, + InApp: true, + AddressMode: rel:0 +} \ No newline at end of file diff --git a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs new file mode 100644 index 0000000000..f3307cd56f --- /dev/null +++ b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs @@ -0,0 +1,49 @@ +using Sentry.PlatformAbstractions; + +// ReSharper disable once CheckNamespace +// Stack trace filters out Sentry frames by namespace +namespace Other.Tests.Internals; + +// TODO: Create integration test to test this behaviour when publishing AOT apps +// See https://github.com/getsentry/sentry-dotnet/pull/2732#discussion_r1371006441 +public class SimpleStackTraceFactoryTests +{ + private class Fixture + { + public SentryOptions SentryOptions { get; } = new(); + public SentryStackTraceFactory GetSut() => new(SentryOptions); + } + + private readonly Fixture _fixture = new(); + + [Fact] + public Task MethodGeneric() + { + _fixture.SentryOptions.UseStackTraceFactory(new SimpleStackTraceFactory(_fixture.SentryOptions)); + + // Arrange + var i = 5; + var exception = Record.Exception(() => GenericMethodThatThrows(i)); + + _fixture.SentryOptions.AttachStacktrace = true; + var factory = _fixture.GetSut(); + + // 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(); +} From 703c19fef42e3c23de1fbff885b9f8b76b2d9425 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 21 Jul 2025 15:19:47 +1200 Subject: [PATCH 05/14] Update SimpleStackTraceFactory.cs --- .../Extensibility/SimpleStackTraceFactory.cs | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/Sentry/Extensibility/SimpleStackTraceFactory.cs b/src/Sentry/Extensibility/SimpleStackTraceFactory.cs index f2d5c66d78..7089861547 100644 --- a/src/Sentry/Extensibility/SimpleStackTraceFactory.cs +++ b/src/Sentry/Extensibility/SimpleStackTraceFactory.cs @@ -13,8 +13,21 @@ namespace Sentry.Extensibility; public partial class SimpleStackTraceFactory : 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)] + 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; } @@ -48,23 +61,36 @@ public SimpleStackTraceFactory(SentryOptions options) var lines = exception?.StackTrace?.Split(newlines, StringSplitOptions.RemoveEmptyEntries) ?? []; foreach (var line in lines) { - var match = StackTraceLine.Match(line); - if (match.Success) + var fullMatch = FullStackTraceLine.Match(line); + if (fullMatch.Success) { frames.Add(new SentryStackFrame() { - Module = match.Groups[1].Value, - Function = match.Groups[2].Value + Module = fullMatch.Groups[1].Value, + Function = fullMatch.Groups[2].Value, + FileName = fullMatch.Groups[3].Value, + LineNumber = int.Parse(fullMatch.Groups[4].Value), }); + continue; } - else + + _options.LogDebug("Full stack frame match failed for: {0}", line); + var lineMatch = StackTraceLine.Match(line); + if (lineMatch.Success) { - _options.LogDebug("Regex match failed for: {0}", line); frames.Add(new SentryStackFrame() { - Function = line + 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; From 357ad201b2784500ff4c8c99ce17d6021feb00a5 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 21 Jul 2025 15:27:56 +1200 Subject: [PATCH 06/14] Update SimpleStackTraceFactoryTests.cs --- test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs index f3307cd56f..353bbab27b 100644 --- a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs +++ b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs @@ -1,5 +1,3 @@ -using Sentry.PlatformAbstractions; - // ReSharper disable once CheckNamespace // Stack trace filters out Sentry frames by namespace namespace Other.Tests.Internals; @@ -33,7 +31,7 @@ public Task MethodGeneric() // Assert; var frame = stackTrace!.Frames.Single(x => x.Function!.Contains("GenericMethodThatThrows")); - return Verify(frame) + return Verifier.Verify(frame) .IgnoreMembers( x => x.Package, x => x.LineNumber, From 9a83f63561d4cdca3973e4a9fc0e21d57eac35d1 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 21 Jul 2025 15:55:35 +1200 Subject: [PATCH 07/14] . --- GlobalUsings.cs | 4 ++++ test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/GlobalUsings.cs b/GlobalUsings.cs index 13162cdda2..65a9155ff5 100644 --- a/GlobalUsings.cs +++ b/GlobalUsings.cs @@ -29,3 +29,7 @@ global using System.Security.Principal; global using System.Text; global using System.Text.RegularExpressions; +global using VerifyTests; +global using VerifyXunit; +global using Xunit; +global using static VerifyXunit.Verifier; diff --git a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs index 353bbab27b..cd93cd5191 100644 --- a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs +++ b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs @@ -31,7 +31,7 @@ public Task MethodGeneric() // Assert; var frame = stackTrace!.Frames.Single(x => x.Function!.Contains("GenericMethodThatThrows")); - return Verifier.Verify(frame) + return Verify(frame) .IgnoreMembers( x => x.Package, x => x.LineNumber, From 4e47a58d20bc14ea1a24f61bb9677d28fffc5058 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 21 Jul 2025 16:04:36 +1200 Subject: [PATCH 08/14] Update GlobalUsings.cs --- GlobalUsings.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/GlobalUsings.cs b/GlobalUsings.cs index 65a9155ff5..4d0b691353 100644 --- a/GlobalUsings.cs +++ b/GlobalUsings.cs @@ -29,7 +29,4 @@ global using System.Security.Principal; global using System.Text; global using System.Text.RegularExpressions; -global using VerifyTests; -global using VerifyXunit; -global using Xunit; global using static VerifyXunit.Verifier; From 16a54eeecda9bd161e23d30c5dc81d2ba1b86895 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 21 Jul 2025 19:27:20 +1200 Subject: [PATCH 09/14] . --- GlobalUsings.cs | 1 - test/Directory.Build.props | 3 ++- ...aceFactoryTests.MethodGeneric.verified.txt | 8 +++--- .../Internals/SimpleStackTraceFactoryTests.cs | 25 ++++++++----------- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/GlobalUsings.cs b/GlobalUsings.cs index 4d0b691353..13162cdda2 100644 --- a/GlobalUsings.cs +++ b/GlobalUsings.cs @@ -29,4 +29,3 @@ global using System.Security.Principal; global using System.Text; global using System.Text.RegularExpressions; -global using static VerifyXunit.Verifier; 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/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt index 59797b3b18..6a3c4199b3 100644 --- a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt +++ b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt @@ -1,7 +1,5 @@ { - FileName: Internals/SimpleStackTraceFactoryTests.cs, - Function: void SimpleStackTraceFactoryTests.GenericMethodThatThrows(T value), - AbsolutePath: {ProjectDirectory}Internals/SimpleStackTraceFactoryTests.cs, - InApp: true, - AddressMode: rel:0 + FileName: {ProjectDirectory}Internals/SimpleStackTraceFactoryTests.cs, + Function: Tests.Internals.SimpleStackTraceFactoryTests.GenericMethodThatThrows[T](T value), + Module: Other } \ No newline at end of file diff --git a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs index cd93cd5191..8045c70e5f 100644 --- a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs +++ b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs @@ -2,29 +2,22 @@ // Stack trace filters out Sentry frames by namespace namespace Other.Tests.Internals; -// TODO: Create integration test to test this behaviour when publishing AOT apps -// See https://github.com/getsentry/sentry-dotnet/pull/2732#discussion_r1371006441 +#if PLATFORM_NEUTRAL + public class SimpleStackTraceFactoryTests { - private class Fixture - { - public SentryOptions SentryOptions { get; } = new(); - public SentryStackTraceFactory GetSut() => new(SentryOptions); - } - - private readonly Fixture _fixture = new(); - [Fact] public Task MethodGeneric() { - _fixture.SentryOptions.UseStackTraceFactory(new SimpleStackTraceFactory(_fixture.SentryOptions)); - // Arrange - var i = 5; + const int i = 5; var exception = Record.Exception(() => GenericMethodThatThrows(i)); - _fixture.SentryOptions.AttachStacktrace = true; - var factory = _fixture.GetSut(); + var options = new SentryOptions + { + AttachStacktrace = true + }; + var factory = new SimpleStackTraceFactory(options); // Act var stackTrace = factory.Create(exception); @@ -45,3 +38,5 @@ public Task MethodGeneric() private static void GenericMethodThatThrows(T value) => throw new Exception(); } + +#endif From 400f5ba62e6995d427b8ef609b3d4de2f9655153 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 23 Jul 2025 16:24:30 +1200 Subject: [PATCH 10/14] Review feedback --- CHANGELOG.md | 2 +- .../Extensibility/SentryStackTraceFactory.cs | 6 +--- ...eFactory.cs => StringStackTraceFactory.cs} | 33 ++++++++++++------- src/Sentry/SentryOptions.cs | 2 +- ...aceFactoryTests.MethodGeneric.verified.txt | 5 --- ...aceFactoryTests.MethodGeneric.verified.txt | 5 +++ ...sts.cs => StringStackTraceFactoryTests.cs} | 4 +-- 7 files changed, 32 insertions(+), 25 deletions(-) rename src/Sentry/Extensibility/{SimpleStackTraceFactory.cs => StringStackTraceFactory.cs} (77%) delete mode 100644 test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt create mode 100644 test/Sentry.Tests/Internals/StringStackTraceFactoryTests.MethodGeneric.verified.txt rename test/Sentry.Tests/Internals/{SimpleStackTraceFactoryTests.cs => StringStackTraceFactoryTests.cs} (91%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b22e787e9..c5fc0e69f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- 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)) +- Sentry now includes an EXPERIMENTAL StringStackTraceFactory. 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)) 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/SimpleStackTraceFactory.cs b/src/Sentry/Extensibility/StringStackTraceFactory.cs similarity index 77% rename from src/Sentry/Extensibility/SimpleStackTraceFactory.cs rename to src/Sentry/Extensibility/StringStackTraceFactory.cs index 7089861547..7bcd68389f 100644 --- a/src/Sentry/Extensibility/SimpleStackTraceFactory.cs +++ b/src/Sentry/Extensibility/StringStackTraceFactory.cs @@ -1,16 +1,25 @@ -using System; -using System.Collections.Generic; +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 +/// off the full . However, it may be useful in AOT compiled /// applications where the full factory is not returning a useful stack trace. -/// SimpleStackTraceFactory is currently EXPERIMENTAL. +/// +/// +/// This class is currently EXPERIMENTAL +/// +/// +/// This factory is designed for AOT scenarios, so only available for net8.0+ +/// +/// /// -public partial class SimpleStackTraceFactory : ISentryStackTraceFactory +[Experimental(DiagnosticId.ExperimentalFeature)] +public partial class StringStackTraceFactory : ISentryStackTraceFactory { private readonly SentryOptions _options; private const string FullStackTraceLinePattern = @"at (?[^\.]+)\.(?.*?) in (?.*?):line (?\d+)"; @@ -41,15 +50,15 @@ public partial class SimpleStackTraceFactory : ISentryStackTraceFactory #endif /// - /// Creates a new instance of . + /// Creates a new instance of . /// /// The sentry options - public SimpleStackTraceFactory(SentryOptions options) + public StringStackTraceFactory(SentryOptions options) { _options = options; } - /// + /// public SentryStackTrace? Create(Exception? exception = null) { _options.LogDebug("Source Stack Trace: {0}", exception?.StackTrace); @@ -57,8 +66,7 @@ public SimpleStackTraceFactory(SentryOptions options) var trace = new SentryStackTrace(); var frames = new List(); - var newlines = new[] { Environment.NewLine }; - var lines = exception?.StackTrace?.Split(newlines, StringSplitOptions.RemoveEmptyEntries) ?? []; + var lines = exception?.StackTrace?.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) ?? []; foreach (var line in lines) { var fullMatch = FullStackTraceLine.Match(line); @@ -94,6 +102,9 @@ public SimpleStackTraceFactory(SentryOptions options) } trace.Frames = frames; - return trace; + _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 ae818e2fc5..e8ed064441 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1638,7 +1638,7 @@ public IEnumerable GetAllExceptionProcessors() /// 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 instead. This is not as + /// informative enough, you could consider using the instead. This is not as /// functional but is guaranteed to provide at least _something_ useful in AOT compiled applications. /// /// diff --git a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt b/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt deleted file mode 100644 index 6a3c4199b3..0000000000 --- a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.MethodGeneric.verified.txt +++ /dev/null @@ -1,5 +0,0 @@ -{ - FileName: {ProjectDirectory}Internals/SimpleStackTraceFactoryTests.cs, - Function: Tests.Internals.SimpleStackTraceFactoryTests.GenericMethodThatThrows[T](T value), - Module: Other -} \ No newline at end of file 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/SimpleStackTraceFactoryTests.cs b/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.cs similarity index 91% rename from test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs rename to test/Sentry.Tests/Internals/StringStackTraceFactoryTests.cs index 8045c70e5f..c4c876c66c 100644 --- a/test/Sentry.Tests/Internals/SimpleStackTraceFactoryTests.cs +++ b/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.cs @@ -4,7 +4,7 @@ namespace Other.Tests.Internals; #if PLATFORM_NEUTRAL -public class SimpleStackTraceFactoryTests +public class StringStackTraceFactoryTests { [Fact] public Task MethodGeneric() @@ -17,7 +17,7 @@ public Task MethodGeneric() { AttachStacktrace = true }; - var factory = new SimpleStackTraceFactory(options); + var factory = new StringStackTraceFactory(options); // Act var stackTrace = factory.Create(exception); From da2f702e940a528e73f513f3b30f5b7da2eb89ca Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 23 Jul 2025 16:29:08 +1200 Subject: [PATCH 11/14] . --- src/Sentry/SentryOptions.cs | 6 +++--- .../ApiApprovalTests.Run.DotNet8_0.verified.txt | 5 +++-- .../ApiApprovalTests.Run.DotNet9_0.verified.txt | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index e8ed064441..b39fc6df19 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1632,14 +1632,14 @@ public IEnumerable GetAllExceptionProcessors() /// /// - /// 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 instead. This is not as - /// functional but is guaranteed to provide at least _something_ useful in AOT compiled applications. + /// 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. diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 2e77463e0a..71d6bfb677 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -1516,9 +1516,10 @@ namespace Sentry.Extensibility public SentryStackTraceFactory(Sentry.SentryOptions options) { } public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } } - public class SimpleStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public class StringStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory { - public SimpleStackTraceFactory(Sentry.SentryOptions options) { } + public StringStackTraceFactory(Sentry.SentryOptions options) { } public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } } } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 2e77463e0a..71d6bfb677 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -1516,9 +1516,10 @@ namespace Sentry.Extensibility public SentryStackTraceFactory(Sentry.SentryOptions options) { } public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } } - public class SimpleStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory + [System.Diagnostics.CodeAnalysis.Experimental("SENTRY0001")] + public class StringStackTraceFactory : Sentry.Extensibility.ISentryStackTraceFactory { - public SimpleStackTraceFactory(Sentry.SentryOptions options) { } + public StringStackTraceFactory(Sentry.SentryOptions options) { } public Sentry.SentryStackTrace? Create(System.Exception? exception = null) { } } } From 0223cf9716f8fb118dc7c4277ea02c85d36f076c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 23 Jul 2025 16:39:37 +1200 Subject: [PATCH 12/14] Dropped obsolete regexes --- .../Extensibility/StringStackTraceFactory.cs | 22 +++++++--------- .../Internals/StringStackTraceFactoryTests.cs | 26 ++++++++++++++++++- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/Sentry/Extensibility/StringStackTraceFactory.cs b/src/Sentry/Extensibility/StringStackTraceFactory.cs index 7bcd68389f..ac8676b60e 100644 --- a/src/Sentry/Extensibility/StringStackTraceFactory.cs +++ b/src/Sentry/Extensibility/StringStackTraceFactory.cs @@ -27,26 +27,22 @@ public partial class StringStackTraceFactory : ISentryStackTraceFactory #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(); + internal static partial Regex FullStackTraceLine { get; } #else - private static readonly Regex FullStackTraceLine = new (FullStackTraceLinePattern, RegexOptions.Compiled); + 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; } -#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); + private static readonly Regex StackTraceLine = StackTraceLineRegex(); + + [GeneratedRegex(StackTraceLinePattern)] + private static partial Regex StackTraceLineRegex(); #endif /// diff --git a/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.cs b/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.cs index c4c876c66c..bd2c65e6cf 100644 --- a/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.cs +++ b/test/Sentry.Tests/Internals/StringStackTraceFactoryTests.cs @@ -2,7 +2,7 @@ // Stack trace filters out Sentry frames by namespace namespace Other.Tests.Internals; -#if PLATFORM_NEUTRAL +#if PLATFORM_NEUTRAL && NET8_0_OR_GREATER public class StringStackTraceFactoryTests { @@ -37,6 +37,30 @@ public Task MethodGeneric() [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 From 45b3258c855495f451fa21bea9ddb461c6812f5b Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 23 Jul 2025 16:49:33 +1200 Subject: [PATCH 13/14] Update ApiApprovalTests.Run.Net4_8.verified.txt --- test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 0cfebd5d59..8b32fbc30f 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -1497,11 +1497,6 @@ 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 { From e4120a976a1067c568fe6060cf3ad8158d6c44e6 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 24 Jul 2025 09:06:07 +1200 Subject: [PATCH 14/14] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5fc0e69f2..8c41ce49e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ### Features -- Sentry now includes an EXPERIMENTAL StringStackTraceFactory. 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)) +- 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))