Skip to content

Commit 94afdbb

Browse files
committed
Fix async exception stack trace frames
1 parent 190c6d3 commit 94afdbb

3 files changed

Lines changed: 208 additions & 3 deletions

File tree

.changeset/async-stack-traces.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"PostHog": patch
3+
---
4+
5+
Fix async exception stack trace frames.

src/PostHog/ErrorTracking/ExceptionPropertiesBuilder.cs

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using PostHog.Library;
22
using PostHog.Versioning;
3+
using System.Reflection;
4+
using System.Runtime.CompilerServices;
35
using System.Runtime.InteropServices;
46
using System.Text;
57

@@ -102,6 +104,17 @@ private static List<Dictionary<string, object>> BuildStackFrameList(Exception ex
102104
}
103105

104106
var method = frame.GetMethod();
107+
if (IsStackTraceHidden(method))
108+
{
109+
continue;
110+
}
111+
112+
var displayMethod = GetDisplayMethod(method);
113+
if (displayMethod != method && IsStackTraceHidden(displayMethod))
114+
{
115+
continue;
116+
}
117+
105118
var fileName = frame.GetFileName();
106119
var lineNumber = frame.GetFileLineNumber();
107120
var columnNumber = frame.GetFileColumnNumber();
@@ -112,8 +125,8 @@ private static List<Dictionary<string, object>> BuildStackFrameList(Exception ex
112125
["lang"] = "dotnet",
113126
["filename"] = Path.GetFileName(fileName) ?? "",
114127
["abs_path"] = fileName ?? "",
115-
["function"] = method?.Name ?? "",
116-
["module"] = method?.DeclaringType?.FullName ?? "",
128+
["function"] = displayMethod?.Name ?? "",
129+
["module"] = displayMethod?.DeclaringType?.FullName ?? "",
117130
["lineno"] = lineNumber,
118131
["colno"] = columnNumber
119132
};
@@ -137,6 +150,80 @@ private static List<Dictionary<string, object>> BuildStackFrameList(Exception ex
137150
return stackFrames;
138151
}
139152

153+
private static MethodBase? GetDisplayMethod(MethodBase? method)
154+
{
155+
if (method?.Name != nameof(IAsyncStateMachine.MoveNext))
156+
{
157+
return method;
158+
}
159+
160+
var stateMachineType = method?.DeclaringType;
161+
if (stateMachineType is null ||
162+
!stateMachineType.IsDefined(typeof(CompilerGeneratedAttribute), inherit: false))
163+
{
164+
return method;
165+
}
166+
167+
var isAsyncStateMachine = typeof(IAsyncStateMachine).IsAssignableFrom(stateMachineType);
168+
var declaringType = stateMachineType.DeclaringType;
169+
if (declaringType is null)
170+
{
171+
return method;
172+
}
173+
174+
foreach (var sourceMethod in declaringType.GetMethods(
175+
BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
176+
BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
177+
{
178+
var asyncStateMachine = sourceMethod.GetCustomAttribute<AsyncStateMachineAttribute>();
179+
if (isAsyncStateMachine && IsStateMachineType(asyncStateMachine?.StateMachineType, stateMachineType))
180+
{
181+
return sourceMethod;
182+
}
183+
184+
if (HasAsyncIteratorStateMachineAttribute(sourceMethod, stateMachineType))
185+
{
186+
return sourceMethod;
187+
}
188+
189+
var iteratorStateMachine = sourceMethod.GetCustomAttribute<IteratorStateMachineAttribute>();
190+
if (IsStateMachineType(iteratorStateMachine?.StateMachineType, stateMachineType))
191+
{
192+
return sourceMethod;
193+
}
194+
}
195+
196+
return method;
197+
}
198+
199+
private static bool HasAsyncIteratorStateMachineAttribute(MethodInfo sourceMethod, Type stateMachineType)
200+
{
201+
foreach (var attribute in sourceMethod.GetCustomAttributesData())
202+
{
203+
if (attribute.AttributeType.FullName == "System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute" &&
204+
attribute.ConstructorArguments.FirstOrDefault().Value is Type attributedType &&
205+
IsStateMachineType(attributedType, stateMachineType))
206+
{
207+
return true;
208+
}
209+
}
210+
211+
return false;
212+
}
213+
214+
private static bool IsStateMachineType(Type? attributedType, Type stateMachineType)
215+
=> attributedType == stateMachineType ||
216+
(attributedType?.IsGenericType == true && stateMachineType.IsGenericType &&
217+
attributedType.GetGenericTypeDefinition() == stateMachineType.GetGenericTypeDefinition());
218+
219+
private static bool IsStackTraceHidden(MethodBase? method)
220+
=> method is not null &&
221+
(HasStackTraceHiddenAttribute(method) || HasStackTraceHiddenAttribute(method.DeclaringType));
222+
223+
private static bool HasStackTraceHiddenAttribute(MemberInfo? member)
224+
=> member?.GetCustomAttributesData().Any(attribute =>
225+
attribute.AttributeType.FullName == "System.Diagnostics.StackTraceHiddenAttribute") == true;
226+
140227
private static SourceCodeContext BuildSourceCodeContext(
141228
string absolutePath,
142229
int lineNumber,

tests/UnitTests/PostHogClientTests.cs

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,60 @@ public async Task CaptureExceptionWithDivideByZeroException() // based on PostHo
13131313
}
13141314
}
13151315

1316+
[Fact]
1317+
public async Task CaptureExceptionUsesLogicalAsyncMethodName()
1318+
{
1319+
var (_, requestHandler, client) = CreateClient();
1320+
var exception = await CreateExceptionAfterAwaitAsync();
1321+
1322+
client.CaptureException(exception, "some-distinct-id");
1323+
await client.FlushAsync();
1324+
1325+
var (_, _, properties) = ParseSingleEvent(requestHandler.GetReceivedRequestBody(indented: false));
1326+
var frames = GetStackFrames(GetFirstException(properties));
1327+
1328+
Assert.Contains(frames, frame =>
1329+
frame.GetProperty("function").GetString() == nameof(CreateExceptionAfterAwaitAsync) &&
1330+
frame.GetProperty("module").GetString() == typeof(TheCaptureExceptionMethod).FullName);
1331+
Assert.DoesNotContain(frames, frame => frame.GetProperty("function").GetString() == "MoveNext");
1332+
}
1333+
1334+
[Fact]
1335+
public async Task CaptureExceptionUsesLogicalAsyncIteratorMethodName()
1336+
{
1337+
var (_, requestHandler, client) = CreateClient();
1338+
var exception = await CreateExceptionFromAsyncIteratorAsync();
1339+
1340+
client.CaptureException(exception, "some-distinct-id");
1341+
await client.FlushAsync();
1342+
1343+
var (_, _, properties) = ParseSingleEvent(requestHandler.GetReceivedRequestBody(indented: false));
1344+
var frames = GetStackFrames(GetFirstException(properties));
1345+
1346+
Assert.Contains(frames, frame =>
1347+
frame.GetProperty("function").GetString() == nameof(ThrowFromAsyncIteratorAfterAwait) &&
1348+
frame.GetProperty("module").GetString() == typeof(TheCaptureExceptionMethod).FullName);
1349+
Assert.DoesNotContain(frames, frame => frame.GetProperty("function").GetString() == "MoveNext");
1350+
}
1351+
1352+
#if NET8_0_OR_GREATER
1353+
[Fact]
1354+
public async Task CaptureExceptionOmitsStackTraceHiddenFrames()
1355+
{
1356+
var (_, requestHandler, client) = CreateClient();
1357+
var exception = CreateExceptionThroughHiddenMethod();
1358+
1359+
client.CaptureException(exception, "some-distinct-id");
1360+
await client.FlushAsync();
1361+
1362+
var (_, _, properties) = ParseSingleEvent(requestHandler.GetReceivedRequestBody(indented: false));
1363+
var frames = GetStackFrames(GetFirstException(properties));
1364+
1365+
Assert.DoesNotContain(frames, frame =>
1366+
frame.GetProperty("function").GetString() == nameof(ThrowFromHiddenMethod));
1367+
}
1368+
#endif
1369+
13161370
[Fact]
13171371
public async Task CaptureExceptionWithAggregateException()
13181372
{
@@ -1571,6 +1625,65 @@ public static void Boom()
15711625
}
15721626
}
15731627

1628+
private static async Task<InvalidOperationException> CreateExceptionAfterAwaitAsync()
1629+
{
1630+
try
1631+
{
1632+
await Task.Yield();
1633+
throw new InvalidOperationException("Async exception");
1634+
}
1635+
catch (InvalidOperationException exception)
1636+
{
1637+
return exception;
1638+
}
1639+
}
1640+
1641+
private static async Task<InvalidOperationException> CreateExceptionFromAsyncIteratorAsync()
1642+
{
1643+
try
1644+
{
1645+
await foreach (var _ in ThrowFromAsyncIteratorAfterAwait())
1646+
{
1647+
}
1648+
1649+
throw new InvalidOperationException("Unreachable");
1650+
}
1651+
catch (InvalidOperationException exception)
1652+
{
1653+
return exception;
1654+
}
1655+
}
1656+
1657+
private static async IAsyncEnumerable<int> ThrowFromAsyncIteratorAfterAwait()
1658+
{
1659+
await Task.Yield();
1660+
if (DateTime.UtcNow.Year == 1)
1661+
{
1662+
yield return 0;
1663+
}
1664+
1665+
throw new InvalidOperationException("Async iterator exception");
1666+
}
1667+
1668+
#if NET8_0_OR_GREATER
1669+
private static InvalidOperationException CreateExceptionThroughHiddenMethod()
1670+
{
1671+
try
1672+
{
1673+
ThrowFromHiddenMethod();
1674+
throw new InvalidOperationException("Unreachable");
1675+
}
1676+
catch (InvalidOperationException exception)
1677+
{
1678+
return exception;
1679+
}
1680+
}
1681+
1682+
[System.Diagnostics.StackTraceHidden]
1683+
private static void ThrowFromHiddenMethod()
1684+
=> throw new InvalidOperationException("Hidden exception");
1685+
#endif
1686+
15741687
// This test is pretty expensive because it dynamically compiles and loads an assembly.
15751688
// Consider alternatives.
15761689
[Fact]
@@ -2322,4 +2435,4 @@ await Assert.ThrowsAsync<OperationCanceledException>(() =>
23222435
Assert.DoesNotContain(errorLogs, log =>
23232436
log.Message?.Contains("Failed to load feature flags", StringComparison.Ordinal) == true);
23242437
}
2325-
}
2438+
}

0 commit comments

Comments
 (0)