Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
20 changes: 18 additions & 2 deletions src/PostHog/ErrorTracking/ExceptionPropertiesBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@

var method = frame.GetMethod();
var fileName = frame.GetFileName();
var moduleName = method?.DeclaringType?.FullName ?? "";
var lineNumber = frame.GetFileLineNumber();
var columnNumber = frame.GetFileColumnNumber();

Expand All @@ -112,8 +113,8 @@
["lang"] = "dotnet",
["filename"] = Path.GetFileName(fileName) ?? "",
["abs_path"] = fileName ?? "",
["function"] = method?.Name ?? "",
["module"] = method?.DeclaringType?.FullName ?? "",
["function"] = BuildFunctionName(method?.Name, moduleName, fileName),
["module"] = moduleName,
["lineno"] = lineNumber,
["colno"] = columnNumber
};
Expand All @@ -137,6 +138,21 @@
return stackFrames;
}

private static string BuildFunctionName(string? methodName, string moduleName, string? fileName)
{
if (string.IsNullOrEmpty(methodName))
{
return "";
}

if (!string.IsNullOrEmpty(fileName) || string.IsNullOrEmpty(moduleName))
{
return methodName;

Check failure on line 150 in src/PostHog/ErrorTracking/ExceptionPropertiesBuilder.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.

Check failure on line 150 in src/PostHog/ErrorTracking/ExceptionPropertiesBuilder.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
}

return $"{moduleName}.{methodName}";
}

private static SourceCodeContext BuildSourceCodeContext(
string absolutePath,
int lineNumber,
Expand Down
74 changes: 71 additions & 3 deletions tests/UnitTests/PostHogClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,71 @@ public static void Boom()

// This test is pretty expensive because it dynamically compiles and loads an assembly.
// Consider alternatives.
[Fact]
public async Task CaptureExceptionWithoutSourceInfoQualifiesFunctionName()
{
var (_, requestHandler, client) = CreateClient();

var code =
"""
using System;
namespace SourceLessFrames;

public static class Thrower
{
public static void Boom()
{
throw new InvalidOperationException("No source info");
}
}
""";

var tree = CSharpSyntaxTree.ParseText(SourceText.From(code, Encoding.UTF8));
var trustedPlatformAssemblies = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator);

var refs = trustedPlatformAssemblies
.Where(p => p.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
.GroupBy(Path.GetFileName, StringComparer.OrdinalIgnoreCase)
.Select(g => MetadataReference.CreateFromFile(g.First()));

var comp = CSharpCompilation.Create(
$"ThrowerAsm{Guid.NewGuid():N}",
[tree],
refs,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release));

using var pe = new MemoryStream();
var emit = comp.Emit(pe);
Assert.True(emit.Success, string.Join(Environment.NewLine, emit.Diagnostics));

pe.Position = 0;

var assemblyLoadContext = new AssemblyLoadContext($"ThrowerCtx{Guid.NewGuid():N}", isCollectible: true);
var assembly = assemblyLoadContext.LoadFromStream(pe);
var boom = assembly.GetType("SourceLessFrames.Thrower")!.GetMethod("Boom", BindingFlags.Public | BindingFlags.Static)!;

try
{
boom.Invoke(null, null);
}
catch (TargetInvocationException tie) when (tie.InnerException is InvalidOperationException ex)
{
client.CaptureException(ex, "some-distinct-id");
await client.FlushAsync();

var (_, batchItem, props) = ParseSingleEvent(requestHandler.GetReceivedRequestBody(indented: true));
var invalidOperationException = GetExceptionOfType(props, "System.InvalidOperationException");
var frames = GetStackFrames(invalidOperationException);

Assert.Equal("$exception", batchItem.GetProperty("event").GetString());
Assert.Equal("", frames[0].GetProperty("filename").GetString());
Assert.Equal("", frames[0].GetProperty("abs_path").GetString());
Assert.Equal("SourceLessFrames.Thrower", frames[0].GetProperty("module").GetString());
Assert.Equal("SourceLessFrames.Thrower.Boom", frames[0].GetProperty("function").GetString());
Assert.Equal(0, frames[0].GetProperty("lineno").GetInt32());
}
}

[Fact]
public async Task CaptureExceptionWithInvalidFilePathInStackFrame()
{
Expand Down Expand Up @@ -1338,10 +1403,13 @@ public static void Boom()

var divideByZeroException = GetExceptionOfType(props, "System.DivideByZeroException");
var frames = GetStackFrames(divideByZeroException);
var sourceFrame = frames.FirstOrDefault(f =>
f.TryGetProperty("filename", out var filename) &&
string.Equals(filename.GetString(), fakePath, StringComparison.Ordinal));

Assert.Equal("$exception", batchItem.GetProperty("event").GetString());
Assert.Equal(fakePath, frames[0].GetProperty("filename").GetString());
AssertContextEmpty(frames[0]);
Assert.NotEqual(JsonValueKind.Undefined, sourceFrame.ValueKind);
AssertContextEmpty(sourceFrame);
}
}

Expand Down Expand Up @@ -2009,4 +2077,4 @@ await Assert.ThrowsAsync<OperationCanceledException>(() =>
Assert.DoesNotContain(errorLogs, log =>
log.Message?.Contains("Failed to load feature flags", StringComparison.Ordinal) == true);
}
}
}
Loading