From 94afdbbd6784c952787525786e2a445e64cd7531 Mon Sep 17 00:00:00 2001 From: kumpelstachu <24489866+kumpelstachu@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:51:44 +0200 Subject: [PATCH 1/2] Fix async exception stack trace frames --- .changeset/async-stack-traces.md | 5 + .../ExceptionPropertiesBuilder.cs | 91 +++++++++++++- tests/UnitTests/PostHogClientTests.cs | 115 +++++++++++++++++- 3 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 .changeset/async-stack-traces.md diff --git a/.changeset/async-stack-traces.md b/.changeset/async-stack-traces.md new file mode 100644 index 00000000..81efb1c8 --- /dev/null +++ b/.changeset/async-stack-traces.md @@ -0,0 +1,5 @@ +--- +"PostHog": patch +--- + +Fix async exception stack trace frames. diff --git a/src/PostHog/ErrorTracking/ExceptionPropertiesBuilder.cs b/src/PostHog/ErrorTracking/ExceptionPropertiesBuilder.cs index c1cc453a..e15dafa2 100644 --- a/src/PostHog/ErrorTracking/ExceptionPropertiesBuilder.cs +++ b/src/PostHog/ErrorTracking/ExceptionPropertiesBuilder.cs @@ -1,5 +1,7 @@ using PostHog.Library; using PostHog.Versioning; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -102,6 +104,17 @@ private static List> BuildStackFrameList(Exception ex } var method = frame.GetMethod(); + if (IsStackTraceHidden(method)) + { + continue; + } + + var displayMethod = GetDisplayMethod(method); + if (displayMethod != method && IsStackTraceHidden(displayMethod)) + { + continue; + } + var fileName = frame.GetFileName(); var lineNumber = frame.GetFileLineNumber(); var columnNumber = frame.GetFileColumnNumber(); @@ -112,8 +125,8 @@ private static List> BuildStackFrameList(Exception ex ["lang"] = "dotnet", ["filename"] = Path.GetFileName(fileName) ?? "", ["abs_path"] = fileName ?? "", - ["function"] = method?.Name ?? "", - ["module"] = method?.DeclaringType?.FullName ?? "", + ["function"] = displayMethod?.Name ?? "", + ["module"] = displayMethod?.DeclaringType?.FullName ?? "", ["lineno"] = lineNumber, ["colno"] = columnNumber }; @@ -137,6 +150,80 @@ private static List> BuildStackFrameList(Exception ex return stackFrames; } + private static MethodBase? GetDisplayMethod(MethodBase? method) + { + if (method?.Name != nameof(IAsyncStateMachine.MoveNext)) + { + return method; + } + + var stateMachineType = method?.DeclaringType; + if (stateMachineType is null || + !stateMachineType.IsDefined(typeof(CompilerGeneratedAttribute), inherit: false)) + { + return method; + } + + var isAsyncStateMachine = typeof(IAsyncStateMachine).IsAssignableFrom(stateMachineType); + var declaringType = stateMachineType.DeclaringType; + if (declaringType is null) + { + return method; + } + + foreach (var sourceMethod in declaringType.GetMethods( + BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | + BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) + { + var asyncStateMachine = sourceMethod.GetCustomAttribute(); + if (isAsyncStateMachine && IsStateMachineType(asyncStateMachine?.StateMachineType, stateMachineType)) + { + return sourceMethod; + } + + if (HasAsyncIteratorStateMachineAttribute(sourceMethod, stateMachineType)) + { + return sourceMethod; + } + + var iteratorStateMachine = sourceMethod.GetCustomAttribute(); + if (IsStateMachineType(iteratorStateMachine?.StateMachineType, stateMachineType)) + { + return sourceMethod; + } + } + + return method; + } + + private static bool HasAsyncIteratorStateMachineAttribute(MethodInfo sourceMethod, Type stateMachineType) + { + foreach (var attribute in sourceMethod.GetCustomAttributesData()) + { + if (attribute.AttributeType.FullName == "System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute" && + attribute.ConstructorArguments.FirstOrDefault().Value is Type attributedType && + IsStateMachineType(attributedType, stateMachineType)) + { + return true; + } + } + + return false; + } + + private static bool IsStateMachineType(Type? attributedType, Type stateMachineType) + => attributedType == stateMachineType || + (attributedType?.IsGenericType == true && stateMachineType.IsGenericType && + attributedType.GetGenericTypeDefinition() == stateMachineType.GetGenericTypeDefinition()); + + private static bool IsStackTraceHidden(MethodBase? method) + => method is not null && + (HasStackTraceHiddenAttribute(method) || HasStackTraceHiddenAttribute(method.DeclaringType)); + + private static bool HasStackTraceHiddenAttribute(MemberInfo? member) + => member?.GetCustomAttributesData().Any(attribute => + attribute.AttributeType.FullName == "System.Diagnostics.StackTraceHiddenAttribute") == true; + private static SourceCodeContext BuildSourceCodeContext( string absolutePath, int lineNumber, diff --git a/tests/UnitTests/PostHogClientTests.cs b/tests/UnitTests/PostHogClientTests.cs index d997eaa7..14b4183f 100644 --- a/tests/UnitTests/PostHogClientTests.cs +++ b/tests/UnitTests/PostHogClientTests.cs @@ -1313,6 +1313,60 @@ public async Task CaptureExceptionWithDivideByZeroException() // based on PostHo } } + [Fact] + public async Task CaptureExceptionUsesLogicalAsyncMethodName() + { + var (_, requestHandler, client) = CreateClient(); + var exception = await CreateExceptionAfterAwaitAsync(); + + client.CaptureException(exception, "some-distinct-id"); + await client.FlushAsync(); + + var (_, _, properties) = ParseSingleEvent(requestHandler.GetReceivedRequestBody(indented: false)); + var frames = GetStackFrames(GetFirstException(properties)); + + Assert.Contains(frames, frame => + frame.GetProperty("function").GetString() == nameof(CreateExceptionAfterAwaitAsync) && + frame.GetProperty("module").GetString() == typeof(TheCaptureExceptionMethod).FullName); + Assert.DoesNotContain(frames, frame => frame.GetProperty("function").GetString() == "MoveNext"); + } + + [Fact] + public async Task CaptureExceptionUsesLogicalAsyncIteratorMethodName() + { + var (_, requestHandler, client) = CreateClient(); + var exception = await CreateExceptionFromAsyncIteratorAsync(); + + client.CaptureException(exception, "some-distinct-id"); + await client.FlushAsync(); + + var (_, _, properties) = ParseSingleEvent(requestHandler.GetReceivedRequestBody(indented: false)); + var frames = GetStackFrames(GetFirstException(properties)); + + Assert.Contains(frames, frame => + frame.GetProperty("function").GetString() == nameof(ThrowFromAsyncIteratorAfterAwait) && + frame.GetProperty("module").GetString() == typeof(TheCaptureExceptionMethod).FullName); + Assert.DoesNotContain(frames, frame => frame.GetProperty("function").GetString() == "MoveNext"); + } + +#if NET8_0_OR_GREATER + [Fact] + public async Task CaptureExceptionOmitsStackTraceHiddenFrames() + { + var (_, requestHandler, client) = CreateClient(); + var exception = CreateExceptionThroughHiddenMethod(); + + client.CaptureException(exception, "some-distinct-id"); + await client.FlushAsync(); + + var (_, _, properties) = ParseSingleEvent(requestHandler.GetReceivedRequestBody(indented: false)); + var frames = GetStackFrames(GetFirstException(properties)); + + Assert.DoesNotContain(frames, frame => + frame.GetProperty("function").GetString() == nameof(ThrowFromHiddenMethod)); + } +#endif + [Fact] public async Task CaptureExceptionWithAggregateException() { @@ -1571,6 +1625,65 @@ public static void Boom() } } + private static async Task CreateExceptionAfterAwaitAsync() + { + try + { + await Task.Yield(); + throw new InvalidOperationException("Async exception"); + } + catch (InvalidOperationException exception) + { + return exception; + } + } + + private static async Task CreateExceptionFromAsyncIteratorAsync() + { + try + { + await foreach (var _ in ThrowFromAsyncIteratorAfterAwait()) + { + } + + throw new InvalidOperationException("Unreachable"); + } + catch (InvalidOperationException exception) + { + return exception; + } + } + + private static async IAsyncEnumerable ThrowFromAsyncIteratorAfterAwait() + { + await Task.Yield(); + if (DateTime.UtcNow.Year == 1) + { + yield return 0; + } + + throw new InvalidOperationException("Async iterator exception"); + } + +#if NET8_0_OR_GREATER + private static InvalidOperationException CreateExceptionThroughHiddenMethod() + { + try + { + ThrowFromHiddenMethod(); + throw new InvalidOperationException("Unreachable"); + } + catch (InvalidOperationException exception) + { + return exception; + } + } + + [System.Diagnostics.StackTraceHidden] + private static void ThrowFromHiddenMethod() + => throw new InvalidOperationException("Hidden exception"); +#endif + // This test is pretty expensive because it dynamically compiles and loads an assembly. // Consider alternatives. [Fact] @@ -2322,4 +2435,4 @@ await Assert.ThrowsAsync(() => Assert.DoesNotContain(errorLogs, log => log.Message?.Contains("Failed to load feature flags", StringComparison.Ordinal) == true); } -} \ No newline at end of file +} From 1c8ba5f91276a57b32bc90c7670e3ae3d9b89a87 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 1 Sep 2026 13:52:32 +0200 Subject: [PATCH 2/2] chore: release ASP.NET Core stack trace fix --- .changeset/async-stack-traces.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/async-stack-traces.md b/.changeset/async-stack-traces.md index 81efb1c8..eb1b37de 100644 --- a/.changeset/async-stack-traces.md +++ b/.changeset/async-stack-traces.md @@ -1,5 +1,6 @@ --- "PostHog": patch +"PostHog.AspNetCore": patch --- Fix async exception stack trace frames.