Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: normalize frame in-app resolution for modules & function prefixes #2234

Merged
merged 2 commits into from
Mar 14, 2023
Merged
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
604 changes: 310 additions & 294 deletions CHANGELOG.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
``` ini

BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1265/22H2/2022Update/SunValley2)
12th Gen Intel Core i7-12700K, 1 CPU, 20 logical and 12 physical cores
.NET SDK=7.0.200
[Host] : .NET 6.0.14 (6.0.1423.7309), X64 RyuJIT AVX2
Job-OZOGBN : .NET 6.0.14 (6.0.1423.7309), X64 RyuJIT AVX2

Runtime=.NET 6.0 InvocationCount=1 UnrollFactor=1

```
| Method | N | Mean | Error | StdDev | Allocated |
|------------------ |----- |---------:|---------:|---------:|----------:|
| ConfigureAppFrame | 1000 | 97.30 μs | 1.929 μs | 2.575 μs | 133.44 KB |
3 changes: 1 addition & 2 deletions benchmarks/Sentry.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ private class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default.WithRuntime(CoreRuntime.Core21));
AddJob(Job.Default.WithRuntime(CoreRuntime.Core31));
AddJob(Job.Default.WithRuntime(CoreRuntime.Core60));
AddDiagnoser(MemoryDiagnoser.Default);
AddExporter(MarkdownExporter.GitHub);
AddLogger(DefaultConfig.Instance.GetLoggers().ToArray());
Expand Down
7 changes: 7 additions & 0 deletions benchmarks/Sentry.Benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Benchmarks

To run benchmarks in a single class:

```shell-script
dotnet run -c Release -- --filter *StackFrameBenchmarks*
```
1,016 changes: 1,016 additions & 0 deletions benchmarks/Sentry.Benchmarks/StackFrameBenchmarks.cs

Large diffs are not rendered by default.

69 changes: 35 additions & 34 deletions src/Sentry/Internal/DebugStackTrace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ private SentryStackFrame InternalCreateFrame(StackFrame stackFrame, bool demangl
// In Enhanced mode, Module (which in this case is the Namespace)
// is already prepended to the function, after return type.
// Removing here at the end because this is used to resolve InApp=true/false
// TODO what is this really about? we have already run ConfigureAppFrame() at this time...
frame.Module = null;
}

Expand Down Expand Up @@ -314,7 +315,7 @@ private static void DemangleAsyncFunctionName(SentryStackFrame frame)
// RemotePrinterService in UpdateNotification at line 457:13

var match = RegexAsyncFunctionName.Match(frame.Module);
if (match is {Success: true, Groups.Count: 3})
if (match is { Success: true, Groups.Count: 3 })
{
frame.Module = match.Groups[1].Value;
frame.Function = match.Groups[2].Value;
Expand All @@ -339,7 +340,7 @@ internal static void DemangleAnonymousFunction(SentryStackFrame frame)
// BeginInvokeAsynchronousActionMethod { <lambda> }

var match = RegexAnonymousFunction.Match(frame.Function);
if (match is {Success: true, Groups.Count: 2})
if (match is { Success: true, Groups.Count: 2 })
{
frame.Function = match.Groups[1].Value + " { <lambda> }";
}
Expand All @@ -363,7 +364,7 @@ private static void DemangleLambdaReturnType(SentryStackFrame frame)
// System.Threading.Tasks.Task`1 in InnerInvoke`
// or System.Collections.Generic.List`1 in get_Item
var match = RegexAsyncReturn.Match(frame.Module);
if (match is {Success: true, Groups.Count: 2})
if (match is { Success: true, Groups.Count: 2 })
{
frame.Module = match.Groups[1].Value;
}
Expand Down Expand Up @@ -429,41 +430,41 @@ private static void DemangleLambdaReturnType(SentryStackFrame frame)
switch (entry.Type)
{
case DebugDirectoryEntryType.PdbChecksum:
{
var checksum = peReader.ReadPdbChecksumDebugDirectoryData(entry);
var checksumHex = checksum.Checksum.AsSpan().ToHexString();
debugChecksum = $"{checksum.AlgorithmName}:{checksumHex}";
break;
}

case DebugDirectoryEntryType.CodeView:
{
var codeView = peReader.ReadCodeViewDebugDirectoryData(entry);
debugFile = codeView.Path;

// Specification:
// https://github.com/dotnet/runtime/blob/main/docs/design/specs/PE-COFF.md#codeview-debug-directory-entry-type-2
//
// See also:
// https://learn.microsoft.com/dotnet/csharp/language-reference/compiler-options/code-generation#debugtype
//
// Note: Matching PDB ID is stored in the #Pdb stream of the .pdb file.

if (entry.IsPortableCodeView)
{
// Portable PDB Format
// Version Major=any, Minor=0x504d
debugId = $"{codeView.Guid}-{entry.Stamp:x8}";
var checksum = peReader.ReadPdbChecksumDebugDirectoryData(entry);
var checksumHex = checksum.Checksum.AsSpan().ToHexString();
debugChecksum = $"{checksum.AlgorithmName}:{checksumHex}";
break;
}
else

case DebugDirectoryEntryType.CodeView:
{
// Full PDB Format (Windows only)
// Version Major=0, Minor=0
debugId = $"{codeView.Guid}-{codeView.Age}";
var codeView = peReader.ReadCodeViewDebugDirectoryData(entry);
debugFile = codeView.Path;

// Specification:
// https://github.com/dotnet/runtime/blob/main/docs/design/specs/PE-COFF.md#codeview-debug-directory-entry-type-2
//
// See also:
// https://learn.microsoft.com/dotnet/csharp/language-reference/compiler-options/code-generation#debugtype
//
// Note: Matching PDB ID is stored in the #Pdb stream of the .pdb file.

if (entry.IsPortableCodeView)
{
// Portable PDB Format
// Version Major=any, Minor=0x504d
debugId = $"{codeView.Guid}-{entry.Stamp:x8}";
}
else
{
// Full PDB Format (Windows only)
// Version Major=0, Minor=0
debugId = $"{codeView.Guid}-{codeView.Age}";
}

break;
}

break;
}
}

if (debugId != null && debugChecksum != null)
Expand Down
36 changes: 18 additions & 18 deletions src/Sentry/SentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ public double TracesSampleRate

// The default propagation list will match anything, but adding to the list should clear that.
private IList<TracePropagationTarget> _tracePropagationTargets = new AutoClearingList<TracePropagationTarget>
(new[] {new TracePropagationTarget(".*")}, clearOnNextAdd: true);
(new[] { new TracePropagationTarget(".*") }, clearOnNextAdd: true);

/// <summary>
/// A customizable list of <see cref="TracePropagationTarget"/> objects, each containing either a
Expand Down Expand Up @@ -861,15 +861,15 @@ public SentryOptions()
{
SettingLocator = new SettingLocator(this);

EventProcessorsProviders = new () {
EventProcessorsProviders = new() {
() => EventProcessors ?? Enumerable.Empty<ISentryEventProcessor>()
};

TransactionProcessorsProviders = new () {
TransactionProcessorsProviders = new() {
() => TransactionProcessors ?? Enumerable.Empty<ISentryTransactionProcessor>()
};

ExceptionProcessorsProviders = new () {
ExceptionProcessorsProviders = new() {
() => ExceptionProcessors ?? Enumerable.Empty<ISentryEventExceptionProcessor>()
};

Expand All @@ -879,17 +879,17 @@ public SentryOptions()

ISentryStackTraceFactory SentryStackTraceFactoryAccessor() => SentryStackTraceFactory;

EventProcessors = new (){
EventProcessors = new(){
// De-dupe to be the first to run
new DuplicateEventDetectionEventProcessor(this),
new MainSentryEventProcessor(this, SentryStackTraceFactoryAccessor)
};

ExceptionProcessors = new (){
ExceptionProcessors = new(){
new MainExceptionProcessor(this, SentryStackTraceFactoryAccessor)
};

Integrations = new () {
Integrations = new() {
// Auto-session tracking to be the first to run
new AutoSessionTrackingIntegration(),
new AppDomainUnhandledExceptionIntegration(),
Expand Down Expand Up @@ -920,16 +920,16 @@ public SentryOptions()
iOS = new IosOptions(this);
#endif

InAppExclude = new () {
"System.",
"Mono.",
"Sentry.",
"Microsoft.",
InAppExclude = new() {
"System",
"Mono",
"Sentry",
"Microsoft",
"MS", // MS.Win32, MS.Internal, etc: Desktop apps
"Newtonsoft.Json",
"FSharp.",
"FSharp",
"Serilog",
"Giraffe.",
"Giraffe",
"NLog",
"Npgsql",
"RabbitMQ",
Expand All @@ -947,9 +947,9 @@ public SentryOptions()
"IdentityModel",
"SqlitePclRaw",
"Xamarin",
"Android.", // Ex: Android.Runtime.JNINativeWrapper...
"Google.",
"MongoDB.",
"Android", // Ex: Android.Runtime.JNINativeWrapper...
"Google",
"MongoDB",
"Remotion.Linq",
"AutoMapper",
"Nest",
Expand All @@ -963,7 +963,7 @@ public SentryOptions()
#if DEBUG
InAppInclude = new()
{
"Sentry.Samples."
"Sentry.Samples"
};
#endif
}
Expand Down
30 changes: 25 additions & 5 deletions src/Sentry/SentryStackFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,20 +182,40 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
/// <remarks><see cref="InApp"/> will remain with the same value if previously set.</remarks>
public void ConfigureAppFrame(SentryOptions options)
{
var parameterName = Module ?? Function;
if (InApp != null)
{
return;
}

if (string.IsNullOrEmpty(parameterName))
if (!string.IsNullOrEmpty(Module))
{
ConfigureAppFrame(options, Module, mustIncludeSeparator: false);
}
else if (!string.IsNullOrEmpty(Function))
{
ConfigureAppFrame(options, Function, mustIncludeSeparator: true);
}
else
{
InApp = true;
return;
}
}

InApp = options.InAppInclude?.Any(include => parameterName.StartsWith(include, StringComparison.Ordinal)) == true ||
options.InAppExclude?.Any(exclude => parameterName.StartsWith(exclude, StringComparison.Ordinal)) != true;
private void ConfigureAppFrame(SentryOptions options, string parameter, bool mustIncludeSeparator)
{
var resolver = (string prefix) =>
{
if (parameter.StartsWith(prefix, StringComparison.Ordinal))
{
if (mustIncludeSeparator)
{
return parameter.Length > prefix.Length && parameter[prefix.Length] == '.';
}
return true;
}
return false;
};
InApp = options.InAppInclude?.Any(resolver) == true || options.InAppExclude?.Any(resolver) != true;
}

/// <summary>
Expand Down
36 changes: 36 additions & 0 deletions test/Sentry.Tests/Protocol/Exceptions/SentryStackFrameTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,42 @@ public void ConfigureAppFrame_InAppRuleDoesntMatch_TrueSet()
Assert.True(sut.InApp);
}

[Fact]
public void ConfigureAppFrame_WithDefaultOptions_RecognizesInAppFrame()
{
var options = new SentryOptions();
var sut = new SentryStackFrame()
{
Function = "Program.<Main>() {QuickJitted}",
Module = "Console.Customized"
};

// Act
sut.ConfigureAppFrame(options);

// Assert
Assert.True(sut.InApp);
}

// Sentry internal frame is marked properly with default options.
// This is an actual frame as captured by Sentry.Profiling.
[Fact]
public void ConfigureAppFrame_WithDefaultOptions_RecognizesSentryInternalFrame()
{
var options = new SentryOptions();
var sut = new SentryStackFrame()
{
Function = "Sentry.Internal.Hub.StartTransaction(class Sentry.ITransactionContext,class System.Collections.Generic.IReadOnlyDictionary`2<class System.String,class System.Object>) {QuickJitted}",
Module = "Sentry"
};

// Act
sut.ConfigureAppFrame(options);

// Assert
Assert.False(sut.InApp);
}

[Fact]
public void ConfigureAppFrame_InAppAlreadySet_InAppIgnored()
{
Expand Down
8 changes: 4 additions & 4 deletions test/Sentry.Tests/SentryOptionsExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,10 @@ public void Integrations_Includes_TaskUnobservedTaskExceptionIntegration()
}

[Theory]
[InlineData("Microsoft.")]
[InlineData("System.")]
[InlineData("FSharp.")]
[InlineData("Giraffe.")]
[InlineData("Microsoft")]
[InlineData("System")]
[InlineData("FSharp")]
[InlineData("Giraffe")]
[InlineData("Newtonsoft.Json")]
public void Integrations_Includes_MajorSystemPrefixes(string expected)
{
Expand Down